"""
Clustering Service
Menangani proses:
1. Data Cleaning & Transformation
2. Normalisasi (Z-Score / Min-Max)
3. K-Means Clustering
4. Hierarchical Clustering
5. Evaluasi (Silhouette Score)
6. Kesimpulan & Rekomendasi
"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import io, base64, json

from sklearn.cluster import KMeans, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.metrics import silhouette_score
from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.spatial.distance import pdist


# ─── Mapping kategorikal ke numerik ───────────────────────────────────────────

PENDIDIKAN_MAP = {
    'Tidak Sekolah': 0, 'SD': 1, 'SMP': 2, 'SMA/SMK': 3,
    'D1': 4, 'D2': 5, 'D3': 6, 'S1': 7, 'S2': 8, 'S3': 9
}

STATUS_RUMAH_MAP = {
    'Menumpang': 0, 'Sewa/Kontrak': 1, 'Dinas': 2,
    'Milik Sendiri': 3, 'Lainnya': 1
}

STATUS_PEKERJAAN_MAP = {
    'Tidak Bekerja': 0, 'Ibu Rumah Tangga': 1, 'Pelajar/Mahasiswa': 2,
    'Bekerja': 3, 'Pensiunan': 2
}

DISABILITAS_MAP = {
    'Tidak Ada': 0, 'Fisik': 1, 'Sensorik': 1,
    'Mental': 2, 'Intelektual': 2, 'Ganda': 3
}

JENIS_KELAMIN_MAP = {'L': 0, 'P': 1}

AGAMA_MAP = {
    'Islam': 1, 'Kristen': 2, 'Katholik': 3,
    'Hindu': 4, 'Buddha': 5, 'Konghucu': 6, 'Lainnya': 7
}


def penduduk_to_dataframe(penduduk_list):
    """Konversi list objek Penduduk ke DataFrame."""
    records = []
    for p in penduduk_list:
        records.append({
            'id': p.id,
            'nama': p.nama,
            'rw': p.rw,
            'rt': p.rt,
            'kelurahan': p.kelurahan,
            'kecamatan': p.kecamatan,
            'jenis_kelamin': p.jenis_kelamin,
            'pendidikan': p.pendidikan,
            'status_rumah': p.status_rumah,
            'status_pekerjaan': p.status_pekerjaan,
            'jenis_disabilitas': p.jenis_disabilitas or 'Tidak Ada',
            'tahun_penerima': p.tahun_penerima,
        })
    return pd.DataFrame(records)


def clean_and_transform(df):
    """
    Step 1: Data Cleaning & Transformation
    - Drop duplikat
    - Handle missing values
    - Encode kategorik → numerik
    - Hitung usia dari tahun_penerima (jika ada)
    """
    original_count = len(df)
    df = df.copy()

    # Drop baris duplikat berdasarkan nama + rw + rt
    df.drop_duplicates(subset=['nama', 'rw', 'rt'], keep='first', inplace=True)

    # Encode kategorik
    df['jenis_kelamin_enc'] = df['jenis_kelamin'].map(JENIS_KELAMIN_MAP).fillna(0)
    df['pendidikan_enc'] = df['pendidikan'].map(PENDIDIKAN_MAP).fillna(0)
    df['status_rumah_enc'] = df['status_rumah'].map(STATUS_RUMAH_MAP).fillna(1)
    df['status_pekerjaan_enc'] = df['status_pekerjaan'].map(STATUS_PEKERJAAN_MAP).fillna(0)
    df['disabilitas_enc'] = df['jenis_disabilitas'].map(DISABILITAS_MAP).fillna(0)

    # RW sebagai numerik (untuk clustering per RW)
    df['rw_num'] = pd.to_numeric(df['rw'], errors='coerce').fillna(0)

    cleaned_count = len(df)
    drop_count = original_count - cleaned_count

    return df, {
        'original': original_count,
        'cleaned': cleaned_count,
        'dropped_duplicates': drop_count
    }


def get_feature_matrix(df):
    """Ambil kolom fitur numerik untuk clustering."""
    features = [
        'jenis_kelamin_enc',
        'pendidikan_enc',
        'status_rumah_enc',
        'status_pekerjaan_enc',
        'disabilitas_enc',
    ]
    X = df[features].values.astype(float)
    return X, features


def normalize(X, method='zscore'):
    """
    Step 2: Normalisasi
    method: 'zscore' (StandardScaler) atau 'minmax' (MinMaxScaler)
    """
    if method == 'zscore':
        scaler = StandardScaler()
    else:
        scaler = MinMaxScaler()

    X_scaled = scaler.fit_transform(X)
    return X_scaled, scaler


def run_kmeans(X_scaled, n_clusters, random_state=42):
    """
    Step 3a: K-Means Clustering
    Returns: labels, centroids (dalam ruang ternormalisasi), inertia
    """
    kmeans = KMeans(
        n_clusters=n_clusters,
        init='k-means++',
        n_init=10,
        max_iter=300,
        random_state=random_state
    )
    labels = kmeans.fit_predict(X_scaled)
    centroids = kmeans.cluster_centers_.tolist()
    inertia = float(kmeans.inertia_)
    return labels, centroids, inertia


def run_hierarchical(X_scaled, n_clusters, linkage_method='ward'):
    """
    Step 3b: Hierarchical (Agglomerative) Clustering
    Returns: labels, linkage_matrix (untuk dendrogram)
    """
    agg = AgglomerativeClustering(
        n_clusters=n_clusters,
        linkage=linkage_method
    )
    labels = agg.fit_predict(X_scaled)

    # Hitung linkage matrix untuk dendrogram
    Z = linkage(X_scaled, method=linkage_method)
    return labels, Z


def generate_dendrogram(Z, labels_text=None, n_clusters=3):
    """Generate dendrogram sebagai base64 PNG."""
    fig, ax = plt.subplots(figsize=(12, 5))
    fig.patch.set_facecolor('#0f172a')
    ax.set_facecolor('#0f172a')

    # Warna berdasarkan threshold
    color_threshold = Z[-n_clusters + 1, 2] if len(Z) >= n_clusters else Z[-1, 2] * 0.7

    dendrogram(
        Z,
        ax=ax,
        color_threshold=color_threshold,
        above_threshold_color='#475569',
        no_labels=True,
        leaf_font_size=8,
    )

    ax.set_title('Dendrogram Hierarchical Clustering', color='#f1f5f9', fontsize=13, pad=12)
    ax.set_xlabel('Indeks Data', color='#94a3b8', fontsize=10)
    ax.set_ylabel('Jarak (Distance)', color='#94a3b8', fontsize=10)
    ax.tick_params(colors='#64748b')
    for spine in ax.spines.values():
        spine.set_edgecolor('#334155')

    # Garis threshold
    ax.axhline(y=color_threshold, color='#f59e0b', linestyle='--', linewidth=1.2, alpha=0.8,
               label=f'Threshold ({color_threshold:.3f})')
    ax.legend(facecolor='#1e293b', edgecolor='#334155', labelcolor='#f1f5f9', fontsize=9)

    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=110, bbox_inches='tight', facecolor=fig.get_facecolor())
    plt.close(fig)
    buf.seek(0)
    return base64.b64encode(buf.read()).decode('utf-8')


def compute_silhouette(X_scaled, labels):
    """Hitung Silhouette Score. Return None jika < 2 cluster unik."""
    unique = np.unique(labels)
    if len(unique) < 2:
        return None
    return float(silhouette_score(X_scaled, labels))


def assign_priority(labels, df, features):
    """
    Tentukan prioritas tiap cluster berdasarkan rata-rata skor kerentanan.
    Skor kerentanan: rendah pendidikan + tidak bekerja + menumpang/sewa + disabilitas = PRIORITAS TINGGI
    """
    df = df.copy()
    df['cluster'] = labels

    # Skor kerentanan: pendidikan rendah = buruk, tidak bekerja = buruk, disabilitas ada = buruk
    # Invert pendidikan & status_pekerjaan & status_rumah (nilai rendah = lebih rentan)
    df['skor_rentan'] = (
        (9 - df['pendidikan_enc']) * 2 +          # pendidikan rendah → rentan
        (3 - df['status_pekerjaan_enc']) * 1.5 +  # tidak bekerja → rentan
        (3 - df['status_rumah_enc']) * 1.5 +      # menumpang → rentan
        df['disabilitas_enc'] * 2                  # disabilitas → rentan
    )

    cluster_mean = df.groupby('cluster')['skor_rentan'].mean().sort_values(ascending=False)

    priority_map = {}
    priority_labels = ['Prioritas Tinggi', 'Prioritas Sedang', 'Prioritas Rendah']
    n = len(cluster_mean)
    for i, cluster_id in enumerate(cluster_mean.index):
        if n == 1:
            priority_map[cluster_id] = 'Prioritas Sedang'
        elif n == 2:
            priority_map[cluster_id] = priority_labels[i * 2]  # Tinggi / Rendah
        else:
            idx = min(i, 2)
            priority_map[cluster_id] = priority_labels[idx]

    return priority_map, cluster_mean.to_dict()


def build_kesimpulan(metode, n_clusters, silhouette, priority_map, cluster_stats, normalisasi):
    """Generate teks kesimpulan otomatis."""
    metode_str = 'K-Means' if metode == 'kmeans' else 'Hierarchical (Agglomerative)'
    norm_str = 'Z-Score Standardization' if normalisasi == 'zscore' else 'Min-Max Scaling'

    sil_interp = (
        "sangat baik (≥ 0.71)" if silhouette and silhouette >= 0.71 else
        "baik (0.51–0.70)" if silhouette and silhouette >= 0.51 else
        "cukup (0.26–0.50)" if silhouette and silhouette >= 0.26 else
        "lemah (< 0.26)"
    )

    kesimpulan = (
        f"Analisis clustering menggunakan metode {metode_str} dengan normalisasi {norm_str} "
        f"menghasilkan {n_clusters} cluster. "
        f"Nilai Silhouette Score sebesar {silhouette:.4f} menunjukkan kualitas pemisahan cluster yang {sil_interp}, "
        f"artinya data penduduk berhasil dikelompokkan dengan {'baik' if silhouette and silhouette > 0.5 else 'cukup baik'}."
    )

    rekomendasi_lines = [
        "Berdasarkan hasil clustering dan analisis skor kerentanan, berikut rekomendasi prioritas pembagian bantuan:"
    ]
    for cluster_id, prioritas in priority_map.items():
        skor = cluster_stats.get(cluster_id, 0)
        rekomendasi_lines.append(
            f"• Cluster {cluster_id + 1} ({prioritas}): Skor kerentanan rata-rata {skor:.2f}. "
            + ("Segera mendapatkan bantuan." if prioritas == 'Prioritas Tinggi' else
               "Dalam antrian penerima bantuan." if prioritas == 'Prioritas Sedang' else
               "Dapat ditangani pada periode berikutnya.")
        )

    return kesimpulan, "\n".join(rekomendasi_lines)


def run_clustering_pipeline(penduduk_list, metode, normalisasi, n_clusters, linkage_method='ward'):
    """
    Pipeline lengkap:
    1. Clean & Transform
    2. Normalize
    3. Cluster
    4. Evaluate
    5. Prioritize
    6. Conclude
    """
    # 1. Convert & clean
    df = penduduk_to_dataframe(penduduk_list)
    df_clean, cleaning_info = clean_and_transform(df)

    if len(df_clean) < n_clusters:
        raise ValueError(
            f"Jumlah data setelah cleaning ({len(df_clean)}) lebih kecil dari jumlah cluster ({n_clusters})."
        )

    # 2. Feature matrix
    X, features = get_feature_matrix(df_clean)

    # 3. Normalize
    X_scaled, scaler = normalize(X, method=normalisasi)

    # 4. Cluster
    dendrogram_b64 = None
    centroids = None

    if metode == 'kmeans':
        labels, centroids, inertia = run_kmeans(X_scaled, n_clusters)
    else:
        labels, Z = run_hierarchical(X_scaled, n_clusters, linkage_method)
        dendrogram_b64 = generate_dendrogram(Z, n_clusters=n_clusters)

    # 5. Evaluate
    silhouette = compute_silhouette(X_scaled, labels)

    # 6. Priority
    priority_map, cluster_stats = assign_priority(labels, df_clean, features)

    # 7. Conclude
    kesimpulan, rekomendasi = build_kesimpulan(
        metode, n_clusters, silhouette or 0, priority_map, cluster_stats, normalisasi
    )

    # Gabungkan hasil ke df
    df_clean['cluster_label'] = labels
    df_clean['prioritas'] = df_clean['cluster_label'].map(priority_map)

    return {
        'df_result': df_clean,
        'labels': labels.tolist(),
        'features': features,
        'silhouette_score': silhouette,
        'centroids': centroids,
        'dendrogram_b64': dendrogram_b64,
        'priority_map': priority_map,
        'cluster_stats': cluster_stats,
        'kesimpulan': kesimpulan,
        'rekomendasi': rekomendasi,
        'cleaning_info': cleaning_info,
        'jumlah_data': len(df_clean),
    }
