# interface.py

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image
import io

from bioprocess_model import BioprocessModel
from decorators import gpu_decorator  # Asegúrate de que la ruta es correcta

# Nuevas importaciones para Yi-Coder
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import json

# Inicialización del dispositivo
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model_path = "Qwen/Qwen2.5-7B"#"Qwen/QwQ-32B-Preview"#"Qwen/Qwen2.5-7B-Instruct" #"01-ai/Yi-Coder-9B-Chat"

# Carga del tokenizer y modelo
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(model_path).to(device).eval()

def parse_bounds(bounds_str, num_params):
    try:
        # Reemplazar 'inf' por 'np.inf' si el usuario lo escribió así
        bounds_str = bounds_str.replace('inf', 'np.inf')
        # Evaluar la cadena de límites
        bounds = eval(f"[{bounds_str}]")
        if len(bounds) != num_params:
            raise ValueError("Número de límites no coincide con el número de parámetros.")
        lower_bounds = [b[0] for b in bounds]
        upper_bounds = [b[1] for b in bounds]
        return lower_bounds, upper_bounds
    except Exception as e:
        print(f"Error al parsear los límites: {e}. Usando límites por defecto.")
        lower_bounds = [-np.inf] * num_params
        upper_bounds = [np.inf] * num_params
        return lower_bounds, upper_bounds

def generate_analysis(prompt, max_length=100):  # Reducido a 100
    """
    Genera un análisis utilizando el modelo Yi-Coder-9B-Chat.
    """
    try:
        # Tokenizar el prompt y mover los tensores al dispositivo correcto
        inputs = tokenizer(prompt, return_tensors="pt").to(device)
        
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=max_length,  # Limitar la generación a 100 tokens
                eos_token_id=tokenizer.eos_token_id,
                pad_token_id=tokenizer.eos_token_id
            )
        
        analysis = tokenizer.decode(outputs[0], skip_special_tokens=True)
        return analysis
    except Exception as e:
        print(f"Error al generar el análisis con Yi-Coder: {e}. Usando análisis por defecto.")
        return "Análisis generado por el modelo de lenguaje."

@gpu_decorator(duration=100)  # Reducido de 600 a 100 segundos
def process_and_plot(
    file,
    biomass_eq1, biomass_eq2, biomass_eq3,
    biomass_param1, biomass_param2, biomass_param3,
    biomass_bound1, biomass_bound2, biomass_bound3,
    substrate_eq1, substrate_eq2, substrate_eq3,
    substrate_param1, substrate_param2, substrate_param3,
    substrate_bound1, substrate_bound2, substrate_bound3,
    product_eq1, product_eq2, product_eq3,
    product_param1, product_param2, product_param3,
    product_bound1, product_bound2, product_bound3,
    legend_position,
    show_legend,
    show_params,
    biomass_eq_count,
    substrate_eq_count,
    product_eq_count
):
    # Leer el archivo Excel
    df = pd.read_excel(file.name)
    
    # Verificar que las columnas necesarias estén presentes
    expected_columns = ['Tiempo', 'Biomasa', 'Sustrato', 'Producto']
    for col in expected_columns:
        if col not in df.columns:
            raise KeyError(f"La columna esperada '{col}' no se encuentra en el archivo Excel.")

    # Asignar los datos desde las columnas
    time = df['Tiempo'].values
    biomass_data = df['Biomasa'].values
    substrate_data = df['Sustrato'].values
    product_data = df['Producto'].values

    # Convierte los contadores a enteros
    biomass_eq_count = int(biomass_eq_count)
    substrate_eq_count = int(substrate_eq_count)
    product_eq_count = int(product_eq_count)

    # Recolecta las ecuaciones, parámetros y límites según los contadores
    biomass_eqs = [biomass_eq1, biomass_eq2, biomass_eq3][:biomass_eq_count]
    biomass_params = [biomass_param1, biomass_param2, biomass_param3][:biomass_eq_count]
    biomass_bounds = [biomass_bound1, biomass_bound2, biomass_bound3][:biomass_eq_count]

    substrate_eqs = [substrate_eq1, substrate_eq2, substrate_eq3][:substrate_eq_count]
    substrate_params = [substrate_param1, substrate_param2, substrate_param3][:substrate_eq_count]
    substrate_bounds = [substrate_bound1, substrate_bound2, substrate_bound3][:substrate_eq_count]

    product_eqs = [product_eq1, product_eq2, product_eq3][:product_eq_count]
    product_params = [product_param1, product_param2, product_param3][:product_eq_count]
    product_bounds = [product_bound1, product_bound2, product_bound3][:product_eq_count]

    biomass_results = []
    substrate_results = []
    product_results = []

    # Inicializar el modelo principal
    main_model = BioprocessModel()

    # Ajusta los modelos de Biomasa
    for i in range(len(biomass_eqs)):
        equation = biomass_eqs[i]
        params_str = biomass_params[i]
        bounds_str = biomass_bounds[i]

        try:
            main_model.set_model_biomass(equation, params_str)
        except ValueError as ve:
            raise ValueError(f"Error en la configuración del modelo de biomasa {i+1}: {ve}")

        params = [param.strip() for param in params_str.split(',')]
        lower_bounds, upper_bounds = parse_bounds(bounds_str, len(params))

        try:
            y_pred = main_model.fit_model(
                'biomass', time, biomass_data,
                bounds=(lower_bounds, upper_bounds)
            )
            biomass_results.append({
                'y_pred': y_pred.tolist(),  # Convertir a lista para serialización
                'equation': equation,
                'params': main_model.params['biomass']
            })
        except Exception as e:
            raise RuntimeError(f"Error al ajustar el modelo de biomasa {i+1}: {e}")

    # Ajusta los modelos de Sustrato
    for i in range(len(substrate_eqs)):
        equation = substrate_eqs[i]
        params_str = substrate_params[i]
        bounds_str = substrate_bounds[i]

        try:
            main_model.set_model_substrate(equation, params_str)
        except ValueError as ve:
            raise ValueError(f"Error en la configuración del modelo de sustrato {i+1}: {ve}")

        params = [param.strip() for param in params_str.split(',')]
        lower_bounds, upper_bounds = parse_bounds(bounds_str, len(params))

        try:
            y_pred = main_model.fit_model(
                'substrate', time, substrate_data,
                bounds=(lower_bounds, upper_bounds)
            )
            substrate_results.append({
                'y_pred': y_pred.tolist(),  # Convertir a lista para serialización
                'equation': equation,
                'params': main_model.params['substrate']
            })
        except Exception as e:
            raise RuntimeError(f"Error al ajustar el modelo de sustrato {i+1}: {e}")

    # Ajusta los modelos de Producto
    for i in range(len(product_eqs)):
        equation = product_eqs[i]
        params_str = product_params[i]
        bounds_str = product_bounds[i]

        try:
            main_model.set_model_product(equation, params_str)
        except ValueError as ve:
            raise ValueError(f"Error en la configuración del modelo de producto {i+1}: {ve}")

        params = [param.strip() for param in params_str.split(',')]
        lower_bounds, upper_bounds = parse_bounds(bounds_str, len(params))

        try:
            y_pred = main_model.fit_model(
                'product', time, product_data,
                bounds=(lower_bounds, upper_bounds)
            )
            product_results.append({
                'y_pred': y_pred.tolist(),  # Convertir a lista para serialización
                'equation': equation,
                'params': main_model.params['product']
            })
        except Exception as e:
            raise RuntimeError(f"Error al ajustar el modelo de producto {i+1}: {e}")

    # Genera las gráficas
    fig, axs = plt.subplots(3, 1, figsize=(10, 15))

    # Gráfica de Biomasa
    axs[0].plot(time, biomass_data, 'o', label='Datos de Biomasa')
    for i, result in enumerate(biomass_results):
        axs[0].plot(time, result['y_pred'], '-', label=f'Modelo de Biomasa {i+1}')
    axs[0].set_xlabel('Tiempo')
    axs[0].set_ylabel('Biomasa')
    if show_legend:
        axs[0].legend(loc=legend_position)

    # Gráfica de Sustrato
    axs[1].plot(time, substrate_data, 'o', label='Datos de Sustrato')
    for i, result in enumerate(substrate_results):
        axs[1].plot(time, result['y_pred'], '-', label=f'Modelo de Sustrato {i+1}')
    axs[1].set_xlabel('Tiempo')
    axs[1].set_ylabel('Sustrato')
    if show_legend:
        axs[1].legend(loc=legend_position)

    # Gráfica de Producto
    axs[2].plot(time, product_data, 'o', label='Datos de Producto')
    for i, result in enumerate(product_results):
        axs[2].plot(time, result['y_pred'], '-', label=f'Modelo de Producto {i+1}')
    axs[2].set_xlabel('Tiempo')
    axs[2].set_ylabel('Producto')
    if show_legend:
        axs[2].legend(loc=legend_position)

    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    image = Image.open(buf)

    # Construcción del prompt para Yi-Coder
    prompt = f"""
Analiza estos resultados de cinética y parámetros de bioprocesos, como experto:

Biomasa:
{json.dumps(biomass_results, indent=2)}

Sustrato:
{json.dumps(substrate_results, indent=2)}

Producto:
{json.dumps(product_results, indent=2)}

Para cada cinética (Biomasa, Sustrato, Producto):
1. Evalúa el modelo (ajuste, parámetros)
2. Identifica problemas específicos
3. Sugiere mejoras concretas

Concluye con un veredicto general sobre la calidad del modelado.
"""

    # Generar el análisis utilizando Yi-Coder
    analysis = generate_analysis(prompt, max_length=1500)  # Reducido a 100

    return image, analysis