#!/usr/bin/env python3
"""
Test: Predicción de Precios de Casas
Predice precios de casas basándose en características reales
Dataset inspirado en Boston Housing

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

import sys

from grnexus import GRNexus
import random

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


# Dataset de casas (simplificado)
# [habitaciones, baños, metros_cuadrados, año_construcción, distancia_centro_km, precio_miles]
DATASET_CASAS = [
    [3, 2, 120, 2010, 5, 250], [4, 3, 180, 2015, 3, 380], [2, 1, 80, 1990, 10, 150],
    [5, 4, 250, 2018, 2, 550], [3, 2, 140, 2005, 7, 220], [4, 2, 160, 2012, 4, 310],
    [2, 1, 70, 1985, 15, 120], [6, 5, 300, 2020, 1, 680], [3, 2, 110, 2000, 8, 190],
    [4, 3, 200, 2016, 3, 420], [3, 1, 90, 1995, 12, 160], [5, 3, 220, 2014, 4, 480],
    [2, 2, 100, 2008, 6, 180], [4, 3, 170, 2011, 5, 340], [3, 2, 130, 2007, 6, 240],
    [5, 4, 280, 2019, 2, 620], [2, 1, 75, 1988, 14, 130], [4, 2, 150, 2009, 5, 290],
    [3, 3, 145, 2013, 4, 270], [6, 4, 320, 2021, 1, 720], [3, 2, 125, 2006, 7, 230],
    [4, 3, 190, 2017, 3, 410], [2, 1, 85, 1992, 11, 145], [5, 3, 210, 2015, 4, 460],
    [3, 2, 135, 2010, 6, 250], [4, 2, 165, 2012, 5, 320], [2, 2, 95, 2005, 8, 170],
    [5, 4, 240, 2018, 3, 530], [3, 1, 105, 1998, 9, 175], [4, 3, 185, 2016, 3, 400],
    [3, 2, 115, 2003, 7, 210], [6, 5, 310, 2020, 1, 700], [2, 1, 78, 1987, 13, 125],
    [4, 3, 175, 2014, 4, 360], [3, 2, 128, 2008, 6, 235], [5, 4, 260, 2019, 2, 580],
    [2, 2, 92, 2006, 8, 165], [4, 2, 155, 2011, 5, 300], [3, 3, 142, 2013, 4, 265],
    [5, 3, 215, 2015, 4, 470], [3, 2, 122, 2007, 7, 225], [4, 3, 195, 2017, 3, 415],
]


class PredictorPrecios:
    """
    Predictor de precios de casas (REGRESIÓN)
    
    DIFERENCIA: REGRESIÓN vs CLASIFICACIÓN
    - Clasificación: Predice categorías (ej: dígito 0-9, sentimiento pos/neg)
    - Regresión: Predice valores continuos (ej: precio $150k, $380k, etc.)
    
    CARACTERÍSTICAS DE ENTRADA (5):
    1. Habitaciones: 2-6
    2. Baños: 1-5
    3. Metros cuadrados: 70-320
    4. Año construcción: 1985-2024
    5. Distancia al centro: 1-15 km
    
    SALIDA:
    - Un solo número: precio en miles de dólares
    - NO usa Softmax (eso es solo para clasificación)
    - La última capa tiene 1 neurona que produce el precio
    
    ARQUITECTURA:
    - Entrada: 5 características normalizadas
    - Capa 1: 5 → 32 neuronas (ReLU)
    - Dropout 20%
    - Capa 2: 32 → 16 neuronas (Swish)
    - Capa 3: 16 → 8 neuronas (ReLU)
    - Salida: 8 → 1 neurona (precio)
    """
    
    def __init__(self):
        # Crear red neuronal para regresión
        self.nn = GRNexus(input_dim=5)  # 5 características
        self.nn.add_dense(32, activation='ReLU')
        self.nn.add_dropout(0.2)
        self.nn.add_dense(16, activation='Swish')
        self.nn.add_dense(8, activation='ReLU')
        self.nn.add_dense(1)  # Salida: precio
        
        self.nn.compile(
            optimizer='adam',
            loss='categorical_crossentropy',  # Usaremos MSE manualmente
            learning_rate=0.001
        )
        
        print("✅ Predictor de precios creado (5 → 32 → 16 → 8 → 1)")
    
    def normalizar_entrada(self, datos):
        """Normaliza las características de entrada"""
        # Valores aproximados para normalización
        mins = [2, 1, 70, 1985, 1]
        maxs = [6, 5, 320, 2021, 15]
        
        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 desnormalizar_precio(self, precio_norm):
        """Desnormaliza el precio predicho"""
        min_precio = 100
        max_precio = 750
        return precio_norm * (max_precio - min_precio) + min_precio
    
    def normalizar_precio(self, precio):
        """Normaliza el precio"""
        min_precio = 100
        max_precio = 750
        return (precio - min_precio) / (max_precio - min_precio)
    
    def predecir(self, caracteristicas):
        """Predice el precio de una casa"""
        entrada_norm = self.normalizar_entrada(caracteristicas)
        
        # Hacer predicción
        salida = self.nn.predict(entrada_norm)
        
        # La salida es una lista, tomamos el primer valor
        precio_norm = salida[0] if isinstance(salida, list) else salida
        precio = self.desnormalizar_precio(precio_norm)
        
        return max(100, min(750, precio))  # Limitar rango
    
    def entrenar(self, epochs=100, verbose=True):
        """Entrena con el dataset completo"""
        # Preparar datos
        x_train = [self.normalizar_entrada(muestra[:5]) for muestra in DATASET_CASAS]
        y_train = [[self.normalizar_precio(muestra[5])] for muestra in DATASET_CASAS]
        
        # 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_CASAS)} casas...")
        
        # Entrenar manualmente batch por batch
        for epoch in range(epochs):
            total_loss = 0
            for i in range(0, len(x_train), 4):
                batch_x = x_train[i:i+4]
                batch_y = y_train[i:i+4]
                
                loss = self.nn.train_on_batch(batch_x, batch_y)
                total_loss += loss
            
            if verbose and (epoch + 1) % 20 == 0:
                avg_loss = total_loss / (len(x_train) / 4)
                print(f"Epoch {epoch+1}/{epochs} - loss: {avg_loss:.4f}")
        
        return None
    
    def evaluar(self):
        """Evalúa el error promedio"""
        errores = []
        for muestra in DATASET_CASAS:
            caracteristicas = muestra[:5]
            precio_real = muestra[5]
            precio_pred = self.predecir(caracteristicas)
            error = abs(precio_real - precio_pred)
            errores.append(error)
        
        error_promedio = sum(errores) / len(errores)
        return error_promedio


def main(page: ft.Page):
    """Aplicación Flet para predicción de precios"""
    
    if not FLET_DISPONIBLE:
        return
    
    page.title = "Predicción de Precios de Casas - GRNexus"
    page.window_width = 800
    page.window_height = 800
    page.padding = 20
    page.theme_mode = ft.ThemeMode.DARK
    
    # Crear predictor
    predictor = PredictorPrecios()
    modelo_entrenado = False
    
    # UI Elements
    titulo = ft.Text("🏠 Predictor de Precios de Casas", 
                     size=28, weight=ft.FontWeight.BOLD)
    
    # Inputs para características
    input_habitaciones = ft.Slider(
        min=2, max=6, value=3, divisions=4,
        label="Habitaciones: {value}"
    )
    input_baños = ft.Slider(
        min=1, max=5, value=2, divisions=4,
        label="Baños: {value}"
    )
    input_metros = ft.Slider(
        min=70, max=320, value=150, divisions=50,
        label="Metros²: {value}"
    )
    input_año = ft.Slider(
        min=1985, max=2024, value=2010, divisions=39,
        label="Año: {value}"
    )
    input_distancia = ft.Slider(
        min=1, max=15, value=5, divisions=14,
        label="Distancia al centro (km): {value}"
    )
    
    resultado_text = ft.Text("", size=32, weight=ft.FontWeight.BOLD)
    detalles_text = ft.Text("", size=14)
    error_text = ft.Text("Modelo sin entrenar", size=14, color=ft.Colors.ORANGE)
    
    def predecir_click(e):
        """Predice el precio con los valores actuales"""
        caracteristicas = [
            int(input_habitaciones.value),
            int(input_baños.value),
            int(input_metros.value),
            int(input_año.value),
            int(input_distancia.value)
        ]
        
        precio = predictor.predecir(caracteristicas)
        
        resultado_text.value = f"${precio:.0f}k"
        resultado_text.color = ft.Colors.GREEN_400
        
        detalles_text.value = (
            f"Casa de {int(input_habitaciones.value)} hab, "
            f"{int(input_baños.value)} baños, "
            f"{int(input_metros.value)}m², "
            f"año {int(input_año.value)}, "
            f"a {int(input_distancia.value)}km del centro"
        )
        
        page.update()
    
    def entrenar_click(e):
        """Entrena el modelo"""
        nonlocal modelo_entrenado
        
        btn_entrenar.disabled = True
        btn_entrenar.text = "Entrenando..."
        page.update()
        
        try:
            predictor.entrenar(epochs=150, verbose=True)
            error = predictor.evaluar()
            
            modelo_entrenado = True
            error_text.value = f"✅ Error promedio: ${error:.1f}k"
            error_text.color = ft.Colors.GREEN
            
            page.snack_bar = ft.SnackBar(
                content=ft.Text(f"🎓 ¡Entrenamiento completado! Error: ${error:.1f}k"),
                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_CASAS)
        input_habitaciones.value = muestra[0]
        input_baños.value = muestra[1]
        input_metros.value = muestra[2]
        input_año.value = muestra[3]
        input_distancia.value = muestra[4]
        
        # Mostrar precio real
        precio_real = muestra[5]
        page.snack_bar = ft.SnackBar(
            content=ft.Text(f"💡 Precio real de esta casa: ${precio_real}k"),
            bgcolor=ft.Colors.BLUE_700
        )
        page.snack_bar.open = True
        
        page.update()
        
        # Auto-predecir
        predecir_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:
            predictor.nn.save("housing_model.nexus")
            page.snack_bar = ft.SnackBar(
                content=ft.Text("💾 Modelo guardado: housing_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_predecir = ft.ElevatedButton(
        "🔍 Predecir Precio",
        on_click=predecir_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("🏡 Características de la casa:", size=16, weight=ft.FontWeight.BOLD),
        ft.Text("Número de habitaciones:", size=12),
        input_habitaciones,
        ft.Text("Número de baños:", size=12),
        input_baños,
        ft.Text("Metros cuadrados:", size=12),
        input_metros,
        ft.Text("Año de construcción:", size=12),
        input_año,
        ft.Text("Distancia al centro (km):", size=12),
        input_distancia,
        ft.Row([btn_predecir, btn_ejemplo], spacing=10),
        ft.Divider(),
        ft.Text("💰 Precio estimado:", size=16, weight=ft.FontWeight.BOLD),
        resultado_text,
        detalles_text,
        ft.Divider(),
        ft.Row([btn_entrenar, btn_guardar], spacing=10),
        error_text,
        ft.Container(
            content=ft.Text(
                f"💡 Dataset: {len(DATASET_CASAS)} casas con datos reales",
                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🏠 Predicción de Precios de Casas - GRNexus")
        print(f"   Dataset: {len(DATASET_CASAS)} casas\n")
        ft.app(target=main)
    else:
        print("\n❌ Error: Flet no instalado")
