import os
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image
import numpy as np
import random
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, davies_bouldin_score, calinski_harabasz_score, adjusted_rand_score
import matplotlib.pyplot as plt
from scipy.stats import ttest_ind
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 = 10

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. DATA LOADING
def load_images():
    img_names = sorted([f for f in os.listdir(IMG_DIR) if f.endswith('.png')])
    raw_pixels = []
    mzs = []
    
    print(f"Loading {len(img_names)} images for validation...")
    for name in img_names:
        img_path = os.path.join(IMG_DIR, name)
        img = Image.open(img_path).convert("L")
        raw_pixels.append(np.array(img).flatten())
        
        try:
            mz = float(name.split('_')[1].replace('.png', ''))
            mzs.append(mz)
        except:
            mzs.append(0.0)
            
    return np.array(raw_pixels), np.array(mzs), img_names

def extract_simclr_features(img_names):
    model = MSI_Structural_Encoder()
    if os.path.exists(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 = []
    print(f"Extracting SimCLR features on {DEVICE}...")
    with torch.no_grad():
        for name in 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. VALIDATION TASKS
def baseline_comparison(X_raw, X_simclr):
    print("\n--- TASK 1: BASELINE COMPARISON (PCA vs SimCLR) ---")
    pca_raw = PCA(n_components=50, random_state=42)
    X_pca_raw = pca_raw.fit_transform(X_raw)
    
    pca_simclr = PCA(n_components=50, random_state=42)
    X_pca_simclr = pca_simclr.fit_transform(X_simclr)
    
    km_raw = KMeans(n_clusters=K, random_state=42, n_init=10)
    labels_raw = km_raw.fit_predict(X_pca_raw)
    
    km_sim = KMeans(n_clusters=K, random_state=42, n_init=10)
    labels_sim = km_sim.fit_predict(X_pca_simclr)
    
    metrics = {
        "Method": ["Raw PCA", "SimCLR Encoder"],
        "Silhouette": [silhouette_score(X_pca_raw, labels_raw), silhouette_score(X_pca_simclr, labels_sim)],
        "Davies-Bouldin": [davies_bouldin_score(X_pca_raw, labels_raw), davies_bouldin_score(X_pca_simclr, labels_sim)],
        "Calinski-Harabasz": [calinski_harabasz_score(X_pca_raw, labels_raw), calinski_harabasz_score(X_pca_simclr, labels_sim)]
    }
    
    df_metrics = pd.DataFrame(metrics)
    print(df_metrics.to_string(index=False))
    return df_metrics

def stability_analysis(X_features):
    print("\n--- TASK 2: STABILITY ANALYSIS (Multiple Seeds) ---")
    seeds = [10, 20, 30, 42, 50, 60, 70, 80, 90, 100]
    all_labels = []
    metrics_list = []
    
    pca = PCA(n_components=50, random_state=42)
    X_pca = pca.fit_transform(X_features)

    for seed in seeds:
        km = KMeans(n_clusters=K, random_state=seed, n_init=10)
        labels = km.fit_predict(X_pca)
        all_labels.append(labels)
        metrics_list.append(silhouette_score(X_pca, labels))
    
    ari_scores = []
    for i in range(len(all_labels)):
        for j in range(i + 1, len(all_labels)):
            ari_scores.append(adjusted_rand_score(all_labels[i], all_labels[j]))
            
    print(f"Mean Silhouette Score: {np.mean(metrics_list):.4f} ± {np.std(metrics_list):.4f}")
    print(f"Mean Adjusted Rand Index (ARI) between runs: {np.mean(ari_scores):.4f} (1.0 is perfect stability)")

def isotope_validation(X_features, mzs):
    print("\n--- TASK 3: ISOTOPE CO-LOCALIZATION VALIDATION ---")
    X_norm = X_features / np.linalg.norm(X_features, axis=1, keepdims=True)
    
    isotope_dist = []
    random_dist = []
    
    for i in range(len(mzs)):
        for j in range(i + 1, len(mzs)):
            diff = abs(mzs[i] - mzs[j])
            if 0.95 <= diff <= 1.05:
                dist = np.linalg.norm(X_norm[i] - X_norm[j])
                isotope_dist.append(dist)
    
    if len(isotope_dist) > 0:
        for _ in range(len(isotope_dist)):
            idx1 = random.randint(0, len(mzs)-1)
            idx2 = random.randint(0, len(mzs)-1)
            if idx1 != idx2:
                dist = np.linalg.norm(X_norm[idx1] - X_norm[idx2])
                random_dist.append(dist)
                
        print(f"Number of isotopic pairs found: {len(isotope_dist)}")
        print(f"Mean distance (Isotopes): {np.mean(isotope_dist):.4f}")
        print(f"Mean distance (Random): {np.mean(random_dist):.4f}")
        
        t_stat, p_val = ttest_ind(isotope_dist, random_dist)
        print(f"T-test p-value: {p_val:.4e} (Significant if < 0.05)")
        
        plt.figure(figsize=(8, 6))
        plt.boxplot([isotope_dist, random_dist], tick_labels=['Isotopic Pairs', 'Random Pairs'])
        plt.title('Latent Space Distances: Isotopes vs Random')
        plt.ylabel('Euclidean Distance (Normalized Latent Space)')
        plt.savefig(os.path.join(BASE_DIR, "..", "figures", "isotope_validation_plot.png"))
        print(f"Isotope validation plot saved to figures/isotope_validation_plot.png")

if __name__ == "__main__":
    if not os.path.exists(IMG_DIR):
        print(f"Error: IMG_DIR {IMG_DIR} not found. Generate ion images first.")
    else:
        X_raw, mzs, img_names = load_images()
        X_simclr = extract_simclr_features(img_names)
        
        baseline_comparison(X_raw, X_simclr)
        stability_analysis(X_simclr)
        isotope_validation(X_simclr, mzs)
