#!/usr/bin/env python3
"""
Test: Clasificación de Flores Iris
Clasifica flores iris en 3 especies usando medidas reales
Dataset clásico de Machine Learning

Copyright (C) 2024 GR Code Digital Solutions
Licenciado bajo GPL v3
"""

from grnexus import NeuralNetwork
import random

try:
    import flet as ft
    FLET_DISPONIBLE = True
except ImportError:
    FLET_DISPONIBLE = False
    print("❌ Flet no está instalado")


# Dataset Iris real (simplificado - 50 muestras de cada clase)
# Formato: [sepal_length, sepal_width, petal_length, petal_width, species]
# Species: 0=Setosa, 1=Versicolor, 2=Virginica
DATASET_IRIS = [
    # Setosa (0)
    [5.1, 3.5, 1.4, 0.2, 0], [4.9, 3.0, 1.4, 0.2, 0], [4.7, 3.2, 1.3, 0.2, 0],
    [4.6, 3.1, 1.5, 0.2, 0], [5.0, 3.6, 1.4, 0.2, 0], [5.4, 3.9, 1.7, 0.4, 0],
    [4.6, 3.4, 1.4, 0.3, 0], [5.0, 3.4, 1.5, 0.2, 0], [4.4, 2.9, 1.4, 0.2, 0],
    [4.9, 3.1, 1.5, 0.1, 0], [5.4, 3.7, 1.5, 0.2, 0], [4.8, 3.4, 1.6, 0.2, 0],
    [4.8, 3.0, 1.4, 0.1, 0], [4.3, 3.0, 1.1, 0.1, 0], [5.8, 4.0, 1.2, 0.2, 0],
    [5.7, 4.4, 1.5, 0.4, 0], [5.4, 3.9, 1.3, 0.4, 0], [5.1, 3.5, 1.4, 0.3, 0],
    [5.7, 3.8, 1.7, 0.3, 0], [5.1, 3.8, 1.5, 0.3, 0], [5.4, 3.4, 1.7, 0.2, 0],
    [5.1, 3.7, 1.5, 0.4, 0], [4.6, 3.6, 1.0, 0.2, 0], [5.1, 3.3, 1.7, 0.5, 0],
    [4.8, 3.4, 1.9, 0.2, 0], [5.0, 3.0, 1.6, 0.2, 0], [5.0, 3.4, 1.6, 0.4, 0],
    [5.2, 3.5, 1.5, 0.2, 0], [5.2, 3.4, 1.4, 0.2, 0], [4.7, 3.2, 1.6, 0.2, 0],
    
    # Versicolor (1)
    [7.0, 3.2, 4.7, 1.4, 1], [6.4, 3.2, 4.5, 1.5, 1], [6.9, 3.1, 4.9, 1.5, 1],
    [5.5, 2.3, 4.0, 1.3, 1], [6.5, 2.8, 4.6, 1.5, 1], [5.7, 2.8, 4.5, 1.3, 1],
    [6.3, 3.3, 4.7, 1.6, 1], [4.9, 2.4, 3.3, 1.0, 1], [6.6, 2.9, 4.6, 1.3, 1],
    [5.2, 2.7, 3.9, 1.4, 1], [5.0, 2.0, 3.5, 1.0, 1], [5.9, 3.0, 4.2, 1.5, 1],
    [6.0, 2.2, 4.0, 1.0, 1], [6.1, 2.9, 4.7, 1.4, 1], [5.6, 2.9, 3.6, 1.3, 1],
    [6.7, 3.1, 4.4, 1.4, 1], [5.6, 3.0, 4.5, 1.5, 1], [5.8, 2.7, 4.1, 1.0, 1],
    [6.2, 2.2, 4.5, 1.5, 1], [5.6, 2.5, 3.9, 1.1, 1], [5.9, 3.2, 4.8, 1.8, 1],
    [6.1, 2.8, 4.0, 1.3, 1], [6.3, 2.5, 4.9, 1.5, 1], [6.1, 2.8, 4.7, 1.2, 1],
    [6.4, 2.9, 4.3, 1.3, 1], [6.6, 3.0, 4.4, 1.4, 1], [6.8, 2.8, 4.8, 1.4, 1],
    [6.7, 3.0, 5.0, 1.7, 1], [6.0, 2.9, 4.5, 1.5, 1], [5.7, 2.6, 3.5, 1.0, 1],
    
    # Virginica (2)
    [6.3, 3.3, 6.0, 2.5, 2], [5.8, 2.7, 5.1, 1.9, 2], [7.1, 3.0, 5.9, 2.1, 2],
    [6.3, 2.9, 5.6, 1.8, 2], [6.5, 3.0, 5.8, 2.2, 2], [7.6, 3.0, 6.6, 2.1, 2],
    [4.9, 2.5, 4.5, 1.7, 2], [7.3, 2.9, 6.3, 1.8, 2], [6.7, 2.5, 5.8, 1.8, 2],
    [7.2, 3.6, 6.1, 2.5, 2], [6.5, 3.2, 5.1, 2.0, 2], [6.4, 2.7, 5.3, 1.9, 2],
    [6.8, 3.0, 5.5, 2.1, 2], [5.7, 2.5, 5.0, 2.0, 2], [5.8, 2.8, 5.1, 2.4, 2],
    [6.4, 3.2, 5.3, 2.3, 2], [6.5, 3.0, 5.5, 1.8, 2], [7.7, 3.8, 6.7, 2.2, 2],
    [7.7, 2.6, 6.9, 2.3, 2], [6.0, 2.2, 5.0, 1.5, 2], [6.9, 3.2, 5.7, 2.3, 2],
    [5.6, 2.8, 4.9, 2.0, 2], [7.7, 2.8, 6.7, 2.0, 2], [6.3, 2.7, 4.9, 1.8, 2],
    [6.7, 3.3, 5.7, 2.1, 2], [7.2, 3.2, 6.0, 1.8, 2], [6.2, 2.8, 4.8, 1.8, 2],
    [6.1, 3.0, 4.9, 1.8, 2], [6.4, 2.8, 5.6, 2.1, 2], [7.2, 3.0, 5.8, 1.6, 2],
]

ESPECIES = ["Setosa", "Versicolor", "Virginica"]
COLORES = [ft.Colors.PINK_400, ft.Colors.BLUE_400, ft.Colors.PURPLE_400]


class ClasificadorIris:
    """
    Clasificador de flores Iris
    
    NORMALIZACIÓN DE DATOS:
    Las 4 características tienen diferentes escalas:
    - Largo sépalo: 4.3-7.9 cm
    - Ancho sépalo: 2.0-4.4 cm
    - Largo pétalo: 1.0-6.9 cm
    - Ancho pétalo: 0.1-2.5 cm
    
    Normalizar a rango [0, 1] ayuda a la red a aprender mejor porque:
    - Todas las características tienen igual importancia
    - Los gradientes son más estables
    - El entrenamiento converge más rápido
    
    ARQUITECTURA:
    - Entrada: 4 características normalizadas
    - Capa 1: 4 → 16 neuronas (ReLU)
    - Capa 2: 16 → 8 neuronas (Swish)
    - Salida: 8 → 3 neuronas (3 especies)
    - Softmax: Convierte en probabilidades
    """
    
    def __init__(self):
        # Crear red neuronal
        self.nn = NeuralNetwork()
        # Agregar capas
        from grnexus import DenseLayer, ReLU, Swish, SoftmaxLayer
        self.nn.add(DenseLayer(16, input_dim=4, activation=ReLU()))
        self.nn.add(DenseLayer(8, activation=Swish()))
        self.nn.add(DenseLayer(3))  # 3 especies
        self.nn.add(SoftmaxLayer())
        
        self.nn.compile(
            optimizer='adam',
            loss='cross_entropy',
            learning_rate=0.01
        )
        
        print("✅ Clasificador Iris creado (4 → 16 → 8 → 3)")
    
    def normalizar_datos(self, datos):
        """
        Normaliza las características al rango [0, 1]
        
        Fórmula: normalized = (valor - min) / (max - min)
        
        Ejemplo:
        - Largo sépalo = 5.1 cm
        - Min = 4.3, Max = 7.9
        - Normalizado = (5.1 - 4.3) / (7.9 - 4.3) = 0.22
        
        Esto convierte todas las medidas a escala 0-1
        """
        # Valores aproximados para normalización
        mins = [4.3, 2.0, 1.0, 0.1]
        maxs = [7.9, 4.4, 6.9, 2.5]
        
        normalizados = []
        for val, min_val, max_val in zip(datos, mins, maxs):
            norm = (val - min_val) / (max_val - min_val)
            normalizados.append(norm)
        
        return normalizados
    
    def predecir(self, medidas):
        """Predice la especie"""
        medidas_norm = self.normalizar_datos(medidas)
        probs = self.nn.predict(medidas_norm)
        especie = probs.index(max(probs))
        return especie, probs
    
    def entrenar(self, epochs=50, verbose=True):
        """Entrena con el dataset completo"""
        # Preparar datos
        x_train = [self.normalizar_datos(muestra[:4]) for muestra in DATASET_IRIS]
        y_train = []
        for muestra in DATASET_IRIS:
            label = muestra[4]
            one_hot = [0.0, 0.0, 0.0]
            one_hot[label] = 1.0
            y_train.append(one_hot)
        
        # Mezclar datos
        combined = list(zip(x_train, y_train))
        random.shuffle(combined)
        x_train, y_train = zip(*combined)
        x_train, y_train = list(x_train), list(y_train)
        
        if verbose:
            print(f"\n🎓 Entrenando con {len(DATASET_IRIS)} muestras...")
        
        history = self.nn.train(
            x_train, y_train,
            epochs=epochs,
            batch_size=8,
            verbose=verbose
        )
        
        return history
    
    def evaluar(self):
        """Evalúa precisión"""
        correctos = 0
        for muestra in DATASET_IRIS:
            medidas = muestra[:4]
            label_real = muestra[4]
            especie, _ = self.predecir(medidas)
            if especie == label_real:
                correctos += 1
        
        return correctos / len(DATASET_IRIS)


def main(page: ft.Page):
    """Aplicación Flet para clasificación de Iris"""
    
    if not FLET_DISPONIBLE:
        return
    
    page.title = "Clasificación de Flores Iris - GRNexus"
    page.window_width = 800
    page.window_height = 750
    page.padding = 20
    page.theme_mode = ft.ThemeMode.DARK
    
    # Crear clasificador
    clasificador = ClasificadorIris()
    modelo_entrenado = False
    
    # UI Elements
    titulo = ft.Text("🌸 Clasificador de Flores Iris", 
                     size=28, weight=ft.FontWeight.BOLD)
    
    # Sliders para características
    slider_sepal_length = ft.Slider(
        min=4.0, max=8.0, value=5.0, divisions=40,
        label="Largo Sépalo: {value} cm"
    )
    slider_sepal_width = ft.Slider(
        min=2.0, max=4.5, value=3.0, divisions=25,
        label="Ancho Sépalo: {value} cm"
    )
    slider_petal_length = ft.Slider(
        min=1.0, max=7.0, value=4.0, divisions=60,
        label="Largo Pétalo: {value} cm"
    )
    slider_petal_width = ft.Slider(
        min=0.1, max=2.5, value=1.0, divisions=24,
        label="Ancho Pétalo: {value} cm"
    )
    
    resultado_text = ft.Text("", size=24, weight=ft.FontWeight.BOLD)
    confianza_text = ft.Text("", size=16)
    
    # Barras de probabilidad
    prob_bars = ft.Column(spacing=5)
    
    accuracy_text = ft.Text("Modelo sin entrenar", size=14, color=ft.Colors.ORANGE)
    
    def actualizar_barras(probs):
        """Actualiza barras de probabilidad"""
        prob_bars.controls.clear()
        for i, (especie, prob) in enumerate(zip(ESPECIES, probs)):
            prob_bars.controls.append(
                ft.Row([
                    ft.Text(especie, size=12, width=80),
                    ft.ProgressBar(value=prob, color=COLORES[i], height=15),
                    ft.Text(f"{prob:.0%}", size=12, width=40)
                ], spacing=10)
            )
        page.update()
    
    def clasificar_click(e):
        """Clasifica con los valores actuales"""
        medidas = [
            slider_sepal_length.value,
            slider_sepal_width.value,
            slider_petal_length.value,
            slider_petal_width.value
        ]
        
        especie, probs = clasificador.predecir(medidas)
        
        resultado_text.value = f"🌸 {ESPECIES[especie]}"
        resultado_text.color = COLORES[especie]
        confianza_text.value = f"Confianza: {probs[especie]:.1%}"
        
        actualizar_barras(probs)
    
    def entrenar_click(e):
        """Entrena el modelo"""
        nonlocal modelo_entrenado
        
        btn_entrenar.disabled = True
        btn_entrenar.text = "Entrenando..."
        page.update()
        
        try:
            history = clasificador.entrenar(epochs=100, verbose=True)
            precision = clasificador.evaluar()
            
            modelo_entrenado = True
            accuracy_text.value = f"✅ Precisión: {precision:.1%} ({int(precision * len(DATASET_IRIS))}/{len(DATASET_IRIS)} correctas)"
            accuracy_text.color = ft.Colors.GREEN
            
            page.snack_bar = ft.SnackBar(
                content=ft.Text(f"🎓 ¡Entrenamiento completado! Precisión: {precision:.1%}"),
                bgcolor=ft.Colors.GREEN_700
            )
            page.snack_bar.open = True
            
        except Exception as ex:
            page.snack_bar = ft.SnackBar(
                content=ft.Text(f"❌ Error: {str(ex)}"),
                bgcolor=ft.Colors.RED_700
            )
            page.snack_bar.open = True
        
        finally:
            btn_entrenar.disabled = False
            btn_entrenar.text = "🎓 Entrenar Modelo"
            page.update()
    
    def ejemplo_aleatorio_click(e):
        """Carga un ejemplo aleatorio del dataset"""
        muestra = random.choice(DATASET_IRIS)
        slider_sepal_length.value = muestra[0]
        slider_sepal_width.value = muestra[1]
        slider_petal_length.value = muestra[2]
        slider_petal_width.value = muestra[3]
        page.update()
        
        # Auto-clasificar
        clasificar_click(None)
    
    def guardar_click(e):
        """Guarda el modelo"""
        if not modelo_entrenado:
            page.snack_bar = ft.SnackBar(content=ft.Text("⚠️ Entrena el modelo primero"))
            page.snack_bar.open = True
            page.update()
            return
        
        try:
            clasificador.nn.save("iris_model.nexus")
            page.snack_bar = ft.SnackBar(
                content=ft.Text("💾 Modelo guardado: iris_model.nexus"),
                bgcolor=ft.Colors.GREEN_700
            )
            page.snack_bar.open = True
            page.update()
        except Exception as ex:
            page.snack_bar = ft.SnackBar(
                content=ft.Text(f"❌ Error: {str(ex)}"),
                bgcolor=ft.Colors.RED_700
            )
            page.snack_bar.open = True
            page.update()
    
    # Botones
    btn_clasificar = ft.ElevatedButton(
        "🔍 Clasificar",
        on_click=clasificar_click,
        style=ft.ButtonStyle(bgcolor=ft.Colors.BLUE_700, color=ft.Colors.WHITE),
        expand=True
    )
    
    btn_ejemplo = ft.ElevatedButton(
        "🎲 Ejemplo Aleatorio",
        on_click=ejemplo_aleatorio_click,
        style=ft.ButtonStyle(bgcolor=ft.Colors.TEAL_700, color=ft.Colors.WHITE),
        expand=True
    )
    
    btn_entrenar = ft.ElevatedButton(
        "🎓 Entrenar Modelo",
        on_click=entrenar_click,
        style=ft.ButtonStyle(bgcolor=ft.Colors.PURPLE_700, color=ft.Colors.WHITE)
    )
    
    btn_guardar = ft.ElevatedButton(
        "💾 Guardar",
        on_click=guardar_click,
        style=ft.ButtonStyle(bgcolor=ft.Colors.GREEN_700, color=ft.Colors.WHITE)
    )
    
    # Layout
    page.add(
        titulo,
        ft.Divider(),
        ft.Text("📏 Ajusta las medidas de la flor:", size=16, weight=ft.FontWeight.BOLD),
        ft.Text("Largo del Sépalo (cm):", size=12),
        slider_sepal_length,
        ft.Text("Ancho del Sépalo (cm):", size=12),
        slider_sepal_width,
        ft.Text("Largo del Pétalo (cm):", size=12),
        slider_petal_length,
        ft.Text("Ancho del Pétalo (cm):", size=12),
        slider_petal_width,
        ft.Row([btn_clasificar, btn_ejemplo], spacing=10),
        ft.Divider(),
        ft.Text("📊 Resultado:", size=16, weight=ft.FontWeight.BOLD),
        resultado_text,
        confianza_text,
        prob_bars,
        ft.Divider(),
        ft.Row([btn_entrenar, btn_guardar], spacing=10),
        accuracy_text,
        ft.Container(
            content=ft.Text(
                f"💡 Dataset: {len(DATASET_IRIS)} muestras reales de flores Iris",
                size=12,
                color=ft.Colors.GREY_400
            ),
            padding=10,
            bgcolor=ft.Colors.GREY_900,
            border_radius=5
        )
    )


if __name__ == "__main__":
    if FLET_DISPONIBLE:
        print("\n🌸 Clasificación de Flores Iris - GRNexus")
        print(f"   Dataset: {len(DATASET_IRIS)} muestras reales\n")
        ft.app(target=main)
    else:
        print("\n❌ Error: Flet no instalado")
