from fastapi import FastAPI, Form, Request, Response, HTTPException
from fastapi.responses import JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
import logging
import valormas
import valormas_ws
import os
from datetime import datetime
import base64
import binascii  # Importante para capturar errores base64
import mimetypes
from typing import Optional, List
from dotenv import load_dotenv
import valormas_gemini
import valormas_anthropic_v2
from down_models_hf import download_models
from asistente_n8n import predios_de_propietario, propietarios_del_predio, detalle_predio, all_predios, detalles_predios
import asyncio
import time
import torch
import json

# Rate Limiting
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from limit.rate_limiter import (
    limiter,
    RATE_LIMIT_DEFAULT,
    RATE_LIMIT_CHAT_IA,
    RATE_LIMIT_HEAVY_MODELS,
    RATE_LIMIT_MODELS,
    RATE_LIMIT_DATABASE,
    RATE_LIMIT_SENSITIVE
)

# Cargar configuración desde .env
class ConfigManager:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._load_config()
        return cls._instance
    
    def _load_config(self):
        # Primero intenta cargar desde variables de entorno del sistema
        self.CARPETA_DESTINO = os.getenv('CARPETA_DESTINO')
        
        # Si no está definida en las variables de entorno del sistema, busca en .env
        if not self.CARPETA_DESTINO:
            load_dotenv()  # Carga el .env en el directorio actual
            self.CARPETA_DESTINO = os.getenv('CARPETA_DESTINO')
        
        # Si aún no tenemos la variable, usa un valor por defecto
        if not self.CARPETA_DESTINO:
            self.CARPETA_DESTINO = "/var/www/dev.catia.catastroantioquia-mas.com/valormas/archivos_ciudadanos"
        
        # Verificar si la carpeta existe
        if not os.path.exists(self.CARPETA_DESTINO):
            os.makedirs(self.CARPETA_DESTINO, exist_ok=True)

try:
    config = ConfigManager()
except Exception as e:
    logging.error(f"Error crítico de configuración: {str(e)}")
    raise SystemExit(1)

app = FastAPI(debug=True,
    docs_url=None,
    redoc_url=None,
    openapi_url=None)

# Configurar rate limiter
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# Configurar CORS para permitir solicitudes desde los orígenes necesarios
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:4200", 
        "http://10.51.20.5",
        "https://pre.crm.ayudacatastro.co",
        "https://pre.ayudacatastro.co",
        "https://pre.miperfil.ayudacatastro.co",
        "https://evolution.api.cic-ware.com/message/sendContact/t",
        "https://demo.agencycic.com",
        "https://www.agencycic.com",        
        "https://agencycic.com",
        "https://dev.miperfil.catastroantioquia-mas.com",
        "https://dev.crm.catastroantioquia-mas.com",
        "https://dev.ayuda.catastroantioquia-mas.com",
        "https://n8n.cic-ware.com",
        "https://pru.n8n.agencycic.com",
        "https://n8n.catastroantioquia-mas.com",
        "https://dev.n8n.catastroantioquia-mas.com",
        "https://agencycic.app.n8n.cloud",
        "https://dev.tasa-chat.valormas.gov.co",
        "https://crm.catastroantioquia-mas.com"
 

    ],  # Asegura que la IP del frontend esté permitida
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Configurar logging para capturar y mostrar errores
logging.basicConfig(level=logging.INFO)

@app.get("/model-dataset/{model_id}")
@limiter.limit(RATE_LIMIT_MODELS)
async def get_model_dataset(request: Request, response: Response, model_id: int):
    """
    Descarga el archivo de dataset desde el modelo almacenado
    """
    try:
        # Mapear ID del modelo a su carpeta
        model_files = {
            2: "llama-model-base",
            5: "llama-model-base-crm"
        }
        
        if model_id not in model_files:
            raise HTTPException(
                status_code=404, 
                detail=f"Modelo {model_id} no encontrado"
            )
        
        model_folder = model_files[model_id]
        model_path = f"/data/{model_folder}"
        
        # Buscar el dataset en la carpeta training_data
        dataset_dir = os.path.join(model_path, "training_data")
        
        if not os.path.exists(dataset_dir):
            raise HTTPException(
                status_code=404,
                detail="Carpeta de training_data no encontrada en el modelo"
            )
        
        # Buscar archivos JSON en la carpeta
        dataset_files = [f for f in os.listdir(dataset_dir) if f.endswith('.json')]
        
        if not dataset_files:
            raise HTTPException(
                status_code=404,
                detail="No se encontraron archivos de dataset en el modelo"
            )
        
        # Tomar el primer archivo JSON encontrado
        dataset_filename = dataset_files[0]
        dataset_path = os.path.join(dataset_dir, dataset_filename)
        
        return FileResponse(
            path=dataset_path,
            media_type="application/json",
            filename=dataset_filename
        )
        
    except HTTPException:
        raise
    except Exception as e:
        logging.error(f"Error sirviendo dataset del modelo {model_id}: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Error interno: {str(e)}")

@app.get("/model-dataset-info/{model_id}")
@limiter.limit(RATE_LIMIT_MODELS)
async def get_model_dataset_info(request: Request, response: Response, model_id: int):
    """
    Obtiene información del dataset incluido en el modelo
    """
    try:
        model_files = {
            2: "llama-model-base",
            5: "llama-model-base-crm"
        }
        
        if model_id not in model_files:
            return {"status": "error", "message": f"Modelo {model_id} no encontrado"}
        
        model_folder = model_files[model_id]
        model_path = f"/data/{model_folder}"
        
        # Buscar metadata de entrenamiento
        metadata_path = os.path.join(model_path, "training_metadata.json")
        training_metadata = None
        
        if os.path.exists(metadata_path):
            with open(metadata_path, 'r', encoding='utf-8') as f:
                training_metadata = json.load(f)
        
        # Buscar dataset
        dataset_dir = os.path.join(model_path, "training_data")
        dataset_info = None
        
        if os.path.exists(dataset_dir):
            dataset_files = [f for f in os.listdir(dataset_dir) if f.endswith('.json')]
            
            if dataset_files:
                dataset_filename = dataset_files[0]
                dataset_path = os.path.join(dataset_dir, dataset_filename)
                
                # Información del archivo
                stat_info = os.stat(dataset_path)
                
                # Leer contenido del JSON para obtener información
                try:
                    with open(dataset_path, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                        total_entries = len(data) if isinstance(data, list) else "N/A"
                except:
                    total_entries = "Error al leer"
                
                def format_file_size(size_bytes):
                    if size_bytes == 0:
                        return "0 B"
                    size_names = ["B", "KB", "MB", "GB"]
                    import math
                    i = int(math.floor(math.log(size_bytes, 1024)))
                    p = math.pow(1024, i)
                    s = round(size_bytes / p, 2)
                    return f"{s} {size_names[i]}"
                
                dataset_info = {
                    "filename": dataset_filename,
                    "file_size": format_file_size(stat_info.st_size),
                    "file_size_bytes": stat_info.st_size,
                    "modification_date": datetime.fromtimestamp(stat_info.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
                    "total_entries": total_entries,
                    "download_available": True
                }
        
        return {
            "status": "success",
            "data": {
                "dataset_info": dataset_info,
                "training_metadata": training_metadata,
                "has_dataset": dataset_info is not None
            }
        }
        
    except Exception as e:
        logging.error(f"Error obteniendo info del dataset {model_id}: {str(e)}")
        return {"status": "error", "message": f"Error interno: {str(e)}"}
    
@app.get("/model-info/{model_id}")
@limiter.limit(RATE_LIMIT_MODELS)
async def get_model_info(request: Request, response: Response, model_id: int):
    """
    Obtiene información básica del modelo entrenado basado en su ID
    model_id 2: llama-model-base
    model_id 5: llama-model-base-crm
    """
    try:
        # Mapear ID del modelo a nombre del archivo/carpeta
        model_files = {
            2: "llama-model-base",
            5: "llama-model-base-crm"
        }
        
        if model_id not in model_files:
            return {
                "status": "error", 
                "message": f"Modelo con ID {model_id} no encontrado"
            }
        
        model_filename = model_files[model_id]
        model_path = f"/data/{model_filename}"
        
        # Verificar si el archivo/carpeta existe
        if not os.path.exists(model_path):
            return {
                "status": "error",
                "message": f"Archivo del modelo no encontrado en {model_path}"
            }
        
        # Función para calcular el tamaño total (archivo o directorio)
        def get_total_size(path):
            if os.path.isfile(path):
                return os.path.getsize(path)
            elif os.path.isdir(path):
                total_size = 0
                for dirpath, dirnames, filenames in os.walk(path):
                    for filename in filenames:
                        filepath = os.path.join(dirpath, filename)
                        try:
                            total_size += os.path.getsize(filepath)
                        except (OSError, IOError):
                            continue
                return total_size
            return 0
        
        # Obtener información del archivo/directorio principal
        stat_info = os.stat(model_path)
        modification_time = datetime.fromtimestamp(stat_info.st_mtime)
        creation_time = datetime.fromtimestamp(stat_info.st_ctime)
        
        # Calcular el tamaño total
        total_size = get_total_size(model_path)
        
        # Formatear el tamaño del archivo
        def format_file_size(size_bytes):
            if size_bytes == 0:
                return "0 B"
            size_names = ["B", "KB", "MB", "GB", "TB"]
            import math
            i = int(math.floor(math.log(size_bytes, 1024)))
            p = math.pow(1024, i)
            s = round(size_bytes / p, 2)
            return f"{s} {size_names[i]}"
        
        # Determinar si es archivo o directorio
        model_type = "Directorio" if os.path.isdir(model_path) else "Archivo"
        
        return {
            "status": "success",
            "data": {
                "model_id": model_id,
                "model_name": model_filename,
                "model_type": model_type,
                "modification_date": modification_time.strftime("%Y-%m-%d %H:%M:%S"),
                "creation_date": creation_time.strftime("%Y-%m-%d %H:%M:%S"),
                "file_size": format_file_size(total_size),
                "file_size_bytes": total_size,
                "path": model_path
            }
        }
        
    except Exception as e:
        logging.error(f"Error obteniendo información del modelo {model_id}: {str(e)}")
        return {
            "status": "error",
            "message": f"Error interno: {str(e)}"
        }
    
# Nuevo endpoint en `/catia`
@app.post("/catia")
@limiter.limit(RATE_LIMIT_CHAT_IA)
def catia_endpoint(
    request: Request,
    response: Response,
    asistente: str = Form(...),
    pregunta: str = Form(...),
    idcliente: str = Form(...),
    volume_up: str = Form(...),
    human_on: str = Form(...),
    thread_id: str = Form(...),
    instrucciones: str = Form(...),
    id_asistente: str = Form(...),
    archivo: Optional[str] = Form(None)  # ✅ Ahora es opcional
):
    try:
        archivo_path = "no"

        if archivo and archivo != "no":
            try:
                tipo_mime = "application/octet-stream"  # Valor por defecto

                if "," in archivo:
                    partes = archivo.split(",", 1)
                    encabezado, contenido_base64 = partes[0], partes[1]
                    
                    if "data:" in encabezado and ";base64" in encabezado:
                        tipo_mime = encabezado.split(":")[1].split(";")[0]
                    archivo = contenido_base64
                else:
                    archivo = archivo

                # Limpiar base64
                archivo = archivo.strip().replace("\n", "").replace("\r", "")
                archivo += '=' * (-len(archivo) % 4)

                try:
                    contenido = base64.b64decode(archivo)
                except (binascii.Error, ValueError) as e:
                    logging.error(f"Base64 malformado: {e}")
                    return {"status": "error", "message": "El archivo enviado no tiene un formato base64 válido."}

                # Obtener extensión desde el tipo MIME
                extension = mimetypes.guess_extension(tipo_mime) or ".bin"

                # Crear nombre del archivo con extensión
                timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
                nombre_archivo = f"{idcliente}-{timestamp}{extension}"

                # Guardar el archivo
                carpeta_destino = config.CARPETA_DESTINO
                os.makedirs(carpeta_destino, exist_ok=True)
                archivo_path = os.path.join(carpeta_destino, nombre_archivo)

                with open(archivo_path, "wb") as f:
                    f.write(contenido)

                logging.info(f"Archivo guardado exitosamente en: {archivo_path}")

            except Exception as e:
                logging.error(f"Error procesando archivo base64: {str(e)}")
                return {"status": "error", "message": f"Error al procesar el archivo: {str(e)}"}

        # Ejecutar función principal
        resultado = valormas.main(
            asistente, thread_id, pregunta, archivo_path,
            idcliente, instrucciones, id_asistente, "no",
            human_on, volume_up
        )

        logging.info(f"Resultado de ValorPlus.main(): {resultado}")
        return resultado

    except Exception as e:
        logging.error(f"Error ejecutando ValorPlus.main(): {str(e)}")
        return {"status": "error", "message": str(e)}

@app.post("/catia_ws")
@limiter.limit(RATE_LIMIT_CHAT_IA)
async def catia_endpoint(request: Request, response: Response):
    try:
        data = await request.json()
        
        asistente = data.get("asistente", "")
        pregunta = data.get("pregunta", "")
        volume_up = data.get("volume_up", "")
        instrucciones = data.get("instrucciones", "")
        id_asistente = data.get("id_asistente", "")
        telefono = data.get("telefono", "")
                
        # Llamamos a la función `main()` con los parámetros extraídos
        resultado = valormas_ws.main_telefono(
            asistente, pregunta, instrucciones, id_asistente, volume_up, telefono
        )
        
        # logging.info(f"Resultado de main(): {resultado}")
        return resultado
    except Exception as e:
        logging.error(f"Error ejecutando main(): {str(e)}")
        return {"status": "error", "message": str(e), "resultado": resultado}


@app.post("/catia-llama")
@limiter.limit(RATE_LIMIT_HEAVY_MODELS)
async def catia_llama_endpoint(
    request: Request,
    response: Response,
    thread_id: str = Form(...),
    pregunta: str = Form(...),
    idcliente: str = Form(...),
    id_asistente: str = Form(...),
    volume_up: str = Form(...),
):
    try:
        # Importar la versión optimizada
        from valormas_llama import generar_respuesta, request_queue
        
        # Si la cola está disponible, usarla; sino, ejecutar directamente
        if request_queue:
            respuesta = await request_queue.process_request(
                generar_respuesta(
                    hilo_conversacion=thread_id,
                    prompt=pregunta,
                    idcliente=int(idcliente),
                    id_asistente=id_asistente,
                    volume_up=volume_up,
                )
            )
        else:
            # Fallback si la optimización falla
            respuesta = await generar_respuesta(
                hilo_conversacion=thread_id,
                prompt=pregunta,
                idcliente=int(idcliente),
                id_asistente=id_asistente,
                volume_up=volume_up,
            )
        
        return respuesta
        
    except Exception as e:
        logging.error(f"Error ejecutando Valormas Llama optimizado: {str(e)}")
        
        # Si es error de cola llena, devolver status específico
        if "Cola de requests llena" in str(e):
            raise HTTPException(
                status_code=429, 
                detail="Servidor sobrecargado. Intenta más tarde."
            )
        
        return {"status": "error", "message": str(e)}
    
    
@app.post("/catia-llama-crm")
@limiter.limit(RATE_LIMIT_HEAVY_MODELS)
async def catia_llama_crm_endpoint(
    request: Request,
    response: Response,
    thread_id: str = Form(...),
    pregunta: str = Form(...),
    idcliente: str = Form(...),
    id_asistente: str = Form(...),
    volume_up: str = Form(...),
):
    try:
        # Importar la versión optimizada
        from valormas_llama_crm import generar_respuesta, request_queue
        
        # Si la cola está disponible, usarla; sino, ejecutar directamente
        if request_queue:
            respuesta = await request_queue.process_request(
                generar_respuesta(
                    hilo_conversacion=thread_id,
                    prompt=pregunta,
                    idcliente=int(idcliente),
                    id_asistente=id_asistente,
                    volume_up=volume_up,
                )
            )
        else:
            # Fallback si la optimización falla
            respuesta = await generar_respuesta(
                hilo_conversacion=thread_id,
                prompt=pregunta,
                idcliente=int(idcliente),
                id_asistente=id_asistente,
                volume_up=volume_up,
            )
        
        return respuesta
        
    except Exception as e:
        logging.error(f"Error ejecutando Valormas Llama optimizado: {str(e)}")
        
        # Si es error de cola llena, devolver status específico
        if "Cola de requests llena" in str(e):
            raise HTTPException(
                status_code=429, 
                detail="Servidor sobrecargado. Intenta más tarde."
            )
        
        return {"status": "error", "message": str(e)}
    
@app.post("/catia-gemini")
@limiter.limit(RATE_LIMIT_CHAT_IA)
async def catia_gemini_endpoint(
    request: Request,
    response: Response,
    thread_id: str = Form(...),
    pregunta: str = Form(...),
    idcliente: int = Form(...),
    asistente: str = Form(...),
    volume_up: str = Form(...),
    id_asistente: int = Form(...),
):
    try:
        resultado = valormas_gemini.main(
            thread_id=thread_id,
            pregunta=pregunta,
            idcliente=idcliente,
            asistente=asistente,
            volume_up=volume_up,
            id_asistente=id_asistente
        )
        return JSONResponse(content=resultado)

    except Exception as e:
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})

@app.post("/catia-anthropic-v2")
@limiter.limit(RATE_LIMIT_CHAT_IA)
async def catia_anthropic_endpoint(
    request: Request,
    response: Response,
    thread_id: str = Form(...),
    pregunta: str = Form(...),
    idcliente: int = Form(...),
    asistente: str = Form(...),
    volume_up: str = Form(...),
    id_asistente: int = Form(...),
):
    try:
        # Solo llamamos a la función main pasándole estas variables
        resultado = valormas_anthropic_v2.main(
            thread_id=thread_id,
            pregunta=pregunta,
            idcliente=idcliente,
            asistente=asistente,
            volume_up=volume_up,
            id_asistente=id_asistente
        )
        return JSONResponse(content=resultado)

    except Exception as e:
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@app.post("/descargar_modelos")
@limiter.limit(RATE_LIMIT_SENSITIVE)
async def descargar_modelos(
    request: Request,
    response: Response,
    model_repos: List[str] = Form(...)
):
    try:
        # Validación implícita: si llega no es lista o está vacía, FastAPI lanza error 422
        
        downloaded, errors = download_models(model_repos)

        status = "ok" if not errors else "partial_success"
        return {
            "status": status,
            "downloaded_models": downloaded,
            "errors": errors
        }
    except Exception as e:
        logging.error(f"Error en descargar_modelos: {str(e)}")
        return {"status": "error", "message": f"Error inesperado: {str(e)}"}
    
# Endpoint de consultas para n8n
@app.post("/catia-predios")
@limiter.limit(RATE_LIMIT_DATABASE)
async def catia_predios_endpoint(request: Request, response: Response, idcliente: str = Form(...)):
    try:
        resultado = predios_de_propietario(idcliente)
        return JSONResponse(content=resultado)
    except Exception as e:
        logging.error(f"Error en /catia-predios: {str(e)}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
    
@app.post("/catia-propietarios")
@limiter.limit(RATE_LIMIT_DATABASE)
async def catia_propietarios_endpoint(request: Request, response: Response, idcliente: str = Form(...)):
    try:
        resultado = propietarios_del_predio(idcliente)
        return JSONResponse(content={"status": "success", "data": resultado} if isinstance(resultado, str) else resultado)
    except Exception as e:
        logging.error(f"Error en /catia-propietarios: {str(e)}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})

@app.post("/catia-detalle-predio")
@limiter.limit(RATE_LIMIT_DATABASE)
async def detalle_predio_endpoint(request: Request, response: Response, idcliente: str = Form(...)):
    try:
        resultado = detalle_predio(idcliente)  # esta es la función de lógica
        return JSONResponse(
            content={"status": "success", "data": resultado} if isinstance(resultado, str) else resultado
        )
    except Exception as e:
        logging.error(f"Error en /catia-detalle-predio: {str(e)}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})    

#Enpoints actualizados de predios
@app.post("/all-predios-user")
@limiter.limit(RATE_LIMIT_DATABASE)
async def all_predios_endpoint(request: Request, response: Response, documento: str = Form(...), tipo_consulta: str = Form(...)):
    try:
        resultado = all_predios(documento, tipo_consulta)
        return JSONResponse(content=resultado)
    except Exception as e:
        logging.error(f"Error en /all-predios-user: {str(e)}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
    
@app.post("/detalle-predio")
@limiter.limit(RATE_LIMIT_DATABASE)
async def detalles_predios_endpoint(
    request: Request,
    response: Response,
    ficha: str = Form(...),
    npn: str = Form(...)
):
    try:
        resultado = detalles_predios(ficha, npn)  # función lógica
        return JSONResponse(content=resultado)
    except Exception as e:
        logging.error(f"Error en /detalle-predio: {str(e)}")
        return JSONResponse(
            status_code=500, 
            content={"status": "error", "message": str(e)}
        )

    
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=5000)