import os
import asyncio
from pathlib import Path
from typing import Dict, Any

from dotenv import load_dotenv
import json
import re
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain.memory import ConversationBufferMemory
from langchain_mcp_adapters.client import MultiServerMCPClient
from fastapi import FastAPI, HTTPException, APIRouter 
import uvicorn 
from pydantic import BaseModel 

# ———————————————————————————————————————————
# 0️⃣ Constantes y Configuración
# ———————————————————————————————————————————
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env", override=True)

# URL del MCP de Transporte
MCP_URL = os.getenv("MCP_HTTP_URL")
openai_key = os.getenv("OPENAI_API_KEY")


# ———————————————————————————————————————————
# 🔹 LOG DE VARIABLES DE ENTORNO (Para depuración)
# ———————————————————————————————————————————
print("🔹 Variables de entorno cargadas:")
print(f"  MCP_HTTP_URL: {MCP_URL}")
print(f"  OPENAI_API_KEY: {'CARGADA' if openai_key else 'NO CARGADA'}")

# Variables globales para el agente y la memoria 
AGENT_EXECUTOR: AgentExecutor | None = None
CONVERSATION_MEMORIES: Dict[str, ConversationBufferMemory] = {}

# Esquema para el cuerpo del request del webhook de entrada
class UserMessage(BaseModel):
    whatsapp_id: str
    mensaje: str

# Inicialización de la aplicación FastAPI
app = FastAPI(
    title="Asistente de Transporte LHIA Webhook",
    description="Endpoint para recibir mensajes de WhatsApp e interactuar con el Agente LangChain."
)

# 1. ROUTER PRINCIPAL: Define el prefijo funcional (ej. '/cliente-mcp')
api_router = APIRouter(
    prefix="/cliente-mcp", 
    tags=["API Cliente MCP"]
)

# 2. ROUTER SECUNDARIO: Define el prefijo del tipo de comunicación (ej. '/webhook')
webhook_router = APIRouter(
    prefix="/webhook"
)

# ———————————————————————————————————————————
# 1️⃣ Carga de configuración y modelo LLM
# ———————————————————————————————————————————
openai_key = os.getenv("OPENAI_API_KEY")
if not openai_key:
    raise ValueError("OPENAI_API_KEY no está definido en el archivo .env")

llm = ChatOpenAI(
    openai_api_key=openai_key,
    model="gpt-4o",
    temperature=0.2
)

# ———————————————————————————————————————————
# 2️⃣ Prompt para el Asistente LHIA
# ———————————————————————————————————————————
prompt = ChatPromptTemplate.from_messages([
    ("system", """
    Eres el **Asistente de Transporte Rural LHIA**, un experto en la gestión de transporte de carga agropecuaria.
    Tu objetivo es ayudar al usuario a registrarse como transportista o crear una orden de servicio.

    [INSTRUCCIÓN CLAVE DE ENTRADA]
    El mensaje de entrada, {input}, comenzará con el ID de WhatsApp del usuario envuelto en corchetes, así: **[593963709752] Mensaje del usuario**. 
    **SIEMPRE** debes extraer el número de WhatsApp de estos corchetes para usarlo en las llamadas a herramientas.
    
    [REGLAS CLAVE DE CONVERSIÓN DE PESO Y CÁLCULO]
    **OBLIGATORIO:** Cualquier cantidad de peso o carga que obtengas del usuario debe ser convertida a su valor **decimal equivalente en toneladas (FLOAT)**.
    - Ejemplo: "2.5 toneladas" -> 2.5
    - Ejemplo: "media tonelada" -> 0.5
    - Ejemplo: "4000 libras" -> 1.814 (porque 4000 lb / 2204.6 lb/ton ≈ 1.814)
    - Ejemplo: "500 kilogramos" -> 0.5
    
    **INFERENCIA DE PESO (CLAVE PARA ÓRDENES):** Si el usuario NO proporciona un peso explícito (como "2 toneladas") en la descripción de la carga (ej: "3 vacas"), debes utilizar tu conocimiento general para estimar y calcular el peso total de la `Carga` en toneladas antes de llamar a `register_new_order`.
    - Ejemplo de cálculo interno: Si la carga es "3 vacas", asume un peso promedio de 0.5 toneladas por vaca. El valor de `peso_aproximado` es **1.5**.
    - **Prioridad:** Si el usuario da un peso explícito ("3 vacas y 1.9 toneladas"), usa el valor explícito (1.9).
    
    [REGLAS DE OPERACIÓN CONVERSACIONAL]
    
    0.  **Intención de Campaña (Máxima Prioridad):**
        * **Detección:** Si el usuario usa frases con la palabra clave **"campaña"** o **"campañas"**, ignora todas las demás intenciones.
        * **Acción:** Responde amablemente con un mensaje de información, por ejemplo: "**¡Claro! Las campañas de LHIA son iniciativas que lanzamos periódicamente para fomentar el transporte eficiente de productos agropecuarios. Actualmente no tenemos una campaña activa, pero te avisaremos cuando lancemos la siguiente. ¿En qué otra cosa relacionada con transporte de carga o registro de transportistas puedo ayudarte?**" **NO uses herramientas en este caso.**

    1.  **Intención de Registro (Transportista - Prioridad Alta):**
        * **Detección:** Si el usuario usa frases como "quiero registrarme", "quiero ser transportista", **"soy transportista"** o similares, asume esta intención.
        * **Datos a Recolectar (Secuencial):** Necesitas: **Nombre completo**, **Placa del vehículo**, y **Carga Máxima** (siempre pídelo en toneladas, pero acepta la conversión).
        * **Fijación Automática de Campos:**
            * El campo **`whatsapp`** (de Transportista) es el ID extraído de los corchetes.
        * **Acción Tool:** Solo usa la herramienta `register_new_transporter` cuando tengas **TODOS** los datos del transportista y el valor de la Carga Máxima esté **convertido a decimal (toneladas)**.

    2.  **Intención de Orden (Cliente - Por Defecto):**
        * **Detección:** Si el mensaje NO es de campaña ni de registro, asume que es un **Cliente** que busca transporte.
        * **PRIORIDAD CONVERSACIONAL (Mensaje Inicial):** Si el mensaje del usuario NO contiene palabras clave de registro (soy transportista, quiero registrarme) ni de campaña, y se detecta que la conversación es nueva (o el agente no ha solicitado un dato específico), **DEBES EMPEZAR** preguntando directamente: "**¿Qué necesitas transportar? Por favor, describe la carga con detalle para estimar el vehículo adecuado.**" NO uses saludos introductorios.
        * **PRIORIDAD CONVERSACIONAL (Flujo No Secuencial):** Si el usuario en un solo mensaje provee datos de **MÁS DE UN CAMPO** (ej: Nombre, Carga, y Origen), **DEBES CAPTURAR TODOS ESOS CAMPOS SIMULTÁNEAMENTE** y pasar a preguntar solo por la información que falte. **NO** preguntes por información que el usuario ya ha proporcionado.
        * **Datos a Recolectar (Secuencial CONVERSACIONAL - 4 pasos):** Necesitas:
            1.  *Nombre Cliente*.
            2.  *Carga* (la descripción literal, ej: "3 vacas" o "20 sacos de papas").
            3.  *Origen* (con referencia).
            4.  *Destino*.
        * **Fijación Automática de Campos:**
            * El campo **`whatsappCliente`** (de Orden) es el ID extraído de los corchetes.
            * Los campos `whatsappTransportistaAsignado`, `precio` y `estado` son internos y NO deben ser preguntados.
        * **Regla de Origen con Referencia:** Al pedir el **Origen**, PIDE UNA REFERENCIA CLARA y precisa. (ej: "¿Cuál es el punto de recogida? Por favor, indica una referencia cercana, como *junto a la iglesia*, *en la intersección de*, *en la entrada de la finca*, o un punto conocido."). **Detecta y fomenta el uso de estas referencias clave.**
        * **Carga y Peso (Doble Campo - INFERENCIA DE PESO):**
            * El campo **`carga`** de la herramienta debe ser la descripción literal de la carga (ej: "3 vacas").
            * El campo **`peso_aproximado`** de la herramienta **DEBE SER CALCULADO INTERNAMENTE** por el LLM en base a la descripción de la `carga` y ser el **valor decimal convertido a toneladas** (ej: 1.5, si infieres 3 vacas). Este campo es obligatorio si hay carga y DEBE ser un float. **NO debes preguntarle al cliente por este valor.**
        * **Acción Tool:** Solo usa la herramienta `register_new_order` cuando tengas **TODOS** los 4 datos clave (*Nombre Cliente*, *Carga*, *Origen*, *Destino*) y el **Peso Aproximado (float)** haya sido calculado internamente.

    [REGLAS DE LOGS Y NOTIFICACIÓN]
    - **LOGS (agent_scratchpad):** **SIEMPRE** antes de invocar una herramienta, debes generar un log detallado en tu `agent_scratchpad` mostrando el JSON o los parámetros exactos que vas a enviar al servidor MCP. Para órdenes, verifica explícitamente que los campos `carga` (string descriptivo) y `peso_aproximado` (float decimal) estén correctamente llenos.
    - **ETIQUETA Y DATOS:** Si usas la herramienta `register_new_order` con éxito, tu respuesta FINAL debe comenzar SIEMPRE con la etiqueta `[ORDEN_CREADA_EXITOSAMENTE]` seguida INMEDIATAMENTE de un salto de línea y luego el **JSON COMPLETO** (sin envolver en bloques de código) que devolvió la herramienta. El resto del mensaje conversacional para el usuario va DESPUÉS de ese JSON.
    """),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

# ———————————————————————————————————————————
# 3️⃣ Funciones de utilidad para el Asistente
# ———————————————————————————————————————————

def get_or_create_memory(whatsapp_id: str) -> ConversationBufferMemory:
    """Obtiene la memoria de conversación por 'whatsapp_id' o crea una nueva."""
    if whatsapp_id not in CONVERSATION_MEMORIES:
        print(f"✨ Creando nueva memoria para {whatsapp_id}")
        CONVERSATION_MEMORIES[whatsapp_id] = ConversationBufferMemory(
            memory_key="history",
            return_messages=True
        )
    return CONVERSATION_MEMORIES[whatsapp_id]


# ———————————————————————————————————————————
# 4️⃣ Endpoint para recibir mensajes (Webhook)
# ———————————————————————————————————————————

@webhook_router.post("/message")
async def receive_message(user_message: UserMessage):
    """
    Endpoint para recibir mensajes entrantes de un usuario de WhatsApp.
    """
    global AGENT_EXECUTOR
    NOTIFICATION_TAG = "[ORDEN_CREADA_EXITOSAMENTE]" # Etiqueta secreta del prompt

    if AGENT_EXECUTOR is None:
        raise HTTPException(status_code=503, detail="El Asistente no está inicializado.")

    user_input = user_message.mensaje
    whatsapp_id = user_message.whatsapp_id

    # 1. Obtener la memoria y **PREPARAR LA ENTRADA**
    memory = get_or_create_memory(whatsapp_id)
    
    # Concatenamos el whatsapp_id al inicio del input para que el agente lo extraiga
    llm_input = f"[{whatsapp_id}] {user_input}"
    
    try:
        AGENT_EXECUTOR.memory = memory 
        
        result = await AGENT_EXECUTOR.ainvoke({"input": llm_input})
        
        assistant_response = result['output']
        
        # 2. Lógica de Detección y Extracción de JSON de Orden
        if NOTIFICATION_TAG in assistant_response:
            
            # El agente debe haber devuelto el JSON de la orden después de la etiqueta.
            try:
                # 1. Separamos el texto después de la etiqueta
                json_and_response = assistant_response.split(NOTIFICATION_TAG, 1)[1].strip()
                
                # 2. Buscamos el primer objeto JSON completo (delimitado por llaves)
                json_match = re.search(r'(\{.*\})', json_and_response, re.DOTALL)
                
                if json_match:
                    json_text = json_match.group(0) # Captura el JSON completo
                    # 3. Limpiamos el mensaje de la respuesta final al usuario:
                    # Removemos el tag y el JSON del mensaje para dejar solo la parte conversacional.
                    clean_response = json_and_response.replace(json_text, "").strip()
                    
                else:
                    # Si no se encuentra el JSON, tratamos como respuesta limpia (fallo en el parsing)
                    print("⚠️ Advertencia: Etiqueta de orden encontrada, pero JSON no pudo ser extraído. Enviando solo la respuesta limpia.")
                    clean_response = assistant_response.replace(NOTIFICATION_TAG, "").strip()
                    
            except json.JSONDecodeError as e:
                print(f"❌ Error de JSON al parsear la respuesta del agente: {e}")
                clean_response = assistant_response.replace(NOTIFICATION_TAG, "").strip()
            except Exception as e:
                print(f"❌ Error inesperado al procesar la notificación de orden: {e}")
                clean_response = assistant_response.replace(NOTIFICATION_TAG, "").strip()
            
        else:
            # Lógica normal si no hay etiqueta de orden
            clean_response = assistant_response
        
        # 3. Respuesta SÍNCRONA (Webhook de entrada)
        return {
            "status": "success",
            "whatsapp_id": whatsapp_id,
            "mensaje_respuesta": clean_response # Retorna la respuesta limpia
        }

    except Exception as e:
        error_msg = f"⚠️ Ocurrió un error al procesar el mensaje: {e}"
        print(error_msg)
        
        raise HTTPException(
            status_code=500, 
            detail=f"Error interno del asistente al procesar la solicitud: {e}"
        )

# ———————————————————————————————————————————
# 5️⃣ Configuración inicial y Ejecución del Servidor
# ———————————————————————————————————————————

# Incluir el webhook_router (secundario) dentro del api_router (principal)
api_router.include_router(webhook_router) 

# Incluir el api_router (principal) en la aplicación base
app.include_router(api_router) 

async def setup_agent():
    """Inicializa el agente y las herramientas antes de iniciar el servidor."""
    global AGENT_EXECUTOR
    
    print(f"🔌 Conectando al MCP de Transporte en {MCP_URL}...")

    try:
        transporte_mcp_client = MultiServerMCPClient({
            "transporte": {
                "url": MCP_URL,
                "transport": "streamable_http"
            }
        })
        tools = await transporte_mcp_client.get_tools()
        print(f"✅ Herramientas detectadas: {[tool.name for tool in tools]}")
    except Exception as e:
        print(f"❌ Error al conectar con el MCP: {e}")
        print("💡 Verifica que el servidor Spring AI MCP esté corriendo y accesible.")
        raise

    # El agente ahora se crea con el prompt modificado
    agent = create_openai_functions_agent(
        llm=llm,
        tools=tools,
        prompt=prompt
    )

    AGENT_EXECUTOR = AgentExecutor(
        agent=agent,
        tools=tools,
        memory=None, 
        verbose=True
    )
    print("\n🤖 Asistente de Transporte Rural LHIA listo para recibir mensajes 🚚")
    print("📢 Servidor escuchando en http://localhost:8003/cliente-mcp/webhook/message")

async def main():
    """Función principal para inicializar y correr el servidor web."""
    # 1. Inicializar el agente y las herramientas
    await setup_agent()
    
    # 2. Iniciar el servidor Uvicorn (FastAPI)
    config = uvicorn.Config(app, host="0.0.0.0", port=8003, log_level="info")
    server = uvicorn.Server(config)
    
    await server.serve()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n👋 Servidor apagado.")