import os
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image
import numpy as np
import random
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, davies_bouldin_score, calinski_harabasz_score
import matplotlib.pyplot as plt
import torch.nn.functional as F

# 0. REPRODUCIBILITY
def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

set_seed(42)

# CONFIGURATION
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai")
MODEL_PATH = os.path.join(BASE_DIR, "..", "models", "msi_encoder_trained.pth")
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
K_RANGE = range(2, 21) # Testing from 2 to 20 clusters

class MSI_Structural_Encoder(nn.Module):
    def __init__(self):
        super(MSI_Structural_Encoder, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
            nn.GroupNorm(4, 32),
            nn.ReLU(),
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
            nn.GroupNorm(8, 64),
            nn.ReLU(),
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
            nn.GroupNorm(16, 128),
            nn.ReLU(),
            nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=False),
            nn.GroupNorm(32, 256),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((1, 1))
        )

    def forward(self, x):
        return self.features(x).squeeze(-1).squeeze(-1)

# 1. LOAD MODEL AND EXTRACT FEATURES
def extract_features():
    model = MSI_Structural_Encoder()
    if not os.path.exists(MODEL_PATH):
        raise FileNotFoundError(f"No se encontró el modelo en {MODEL_PATH}")
    model.features.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
    model.to(DEVICE)
    model.eval()

    transform = transforms.Compose([
        transforms.Resize((256, 256)),
        transforms.ToTensor(),
    ])

    features = []
    if not os.path.exists(IMG_DIR):
        raise FileNotFoundError(f"No se encontró el directorio de imágenes en {IMG_DIR}")
    img_names = sorted([f for f in os.listdir(IMG_DIR) if f.endswith('.png')])
    
    print(f"Extracting features from {len(img_names)} images on {DEVICE}...")
    with torch.no_grad():
        for i, name in enumerate(img_names):
            if i % 100 == 0: print(f"Processing image {i}/{len(img_names)}...")
            img_path = os.path.join(IMG_DIR, name)
            img = Image.open(img_path).convert("L")
            img_t = transform(img).unsqueeze(0).to(DEVICE)
            feat = model(img_t)
            # CRITICAL FIX: Project explicitly onto the unit hypersphere
            feat_norm = F.normalize(feat, p=2, dim=1)
            features.append(feat_norm.cpu().numpy().flatten())
    
    return np.array(features)

# 2. EVALUATE K-MEANS
if __name__ == "__main__":
    X = extract_features()
    
    pca = PCA(n_components=min(50, X.shape[1]), random_state=42) 
    X_pca = pca.fit_transform(X)
    # CRITICAL FIX: Re-normalize after PCA to restore unit hypersphere geometry for Spherical K-Means equivalent
    X_pca = X_pca / np.linalg.norm(X_pca, axis=1, keepdims=True)

    inertias = []
    silhouettes = []
    davies_bouldin = []
    calinski_harabasz = []

    print("Evaluating different K values...")
    for k in K_RANGE:
        print(f"Calculating for K={k}...")
        kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
        labels = kmeans.fit_predict(X_pca)
        
        inertias.append(kmeans.inertia_)
        # CRITICAL FIX: Evaluate using cosine distance on the un-reduced features
        silhouettes.append(silhouette_score(X, labels, metric='cosine'))
        davies_bouldin.append(davies_bouldin_score(X_pca, labels))
        calinski_harabasz.append(calinski_harabasz_score(X_pca, labels))

    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))

    ax1.plot(K_RANGE, inertias, 'o-', color='tab:red')
    ax1.set_title('Elbow Method (Inertia)')
    ax1.set_xlabel('Number of Clusters (k)')
    ax1.set_ylabel('Sum of Squared Errors')
    ax1.grid(True)

    ax2.plot(K_RANGE, silhouettes, 's-', color='tab:blue')
    ax2.set_title('Silhouette Score (Higher is better)')
    ax2.set_xlabel('Number of Clusters (k)')
    ax2.set_ylabel('Score')
    ax2.grid(True)

    ax3.plot(K_RANGE, davies_bouldin, '^-', color='tab:green')
    ax3.set_title('Davies-Bouldin Index (Lower is better)')
    ax3.set_xlabel('Number of Clusters (k)')
    ax3.set_ylabel('Score')
    ax3.grid(True)

    ax4.plot(K_RANGE, calinski_harabasz, 'd-', color='tab:orange')
    ax4.set_title('Calinski-Harabasz Index (Higher is better)')
    ax4.set_xlabel('Number of Clusters (k)')
    ax4.set_ylabel('Score')
    ax4.grid(True)

    plt.tight_layout()
    output_plot = os.path.join(BASE_DIR, "..", "figures", "cluster_validation_detailed_plot.png")
    os.makedirs(os.path.dirname(output_plot), exist_ok=True)
    plt.savefig(output_plot)
    print(f"Plot saved as '{output_plot}'")

    idx_10 = list(K_RANGE).index(10)
    print(f"\n--- RESULTS FOR K=10 ---")
    print(f"Silhouette Score: {silhouettes[idx_10]:.4f}")
    print(f"Davies-Bouldin Index: {davies_bouldin[idx_10]:.4f}")
    print(f"Calinski-Harabasz Index: {calinski_harabasz[idx_10]:.4f}")
    
    print(f"\n--- MATHEMATICAL SUGGESTIONS ---")
    print(f"Best K (Silhouette): {K_RANGE[np.argmax(silhouettes)]}")
    print(f"Best K (Davies-Bouldin): {K_RANGE[np.argmin(davies_bouldin)]}")
    print(f"Best K (Calinski-Harabasz): {K_RANGE[np.argmax(calinski_harabasz)]}")
