"""Smart Orchestrator - El cerebro que genera planes de acción."""

from typing import Dict, Any, List, Optional
import json
import re
import inspect
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from sonika_langchain_bot.planner.nodes.base_node import BaseNode


class SmartOrchestrator(BaseNode):
    """
    Orquestador inteligente que analiza el mensaje del usuario y genera
    un plan de acciones a ejecutar.
    
    NO elige un agente. Genera un PLAN con múltiples acciones ordenadas
    por prioridad.
    """

    # Patrones para detectar datos de contacto
    CONTACT_PATTERNS = {
        'name': [
            r'(?:me llamo|soy|mi nombre es)\s+([A-ZÁÉÍÓÚÑ][a-záéíóúñ]+(?:\s+[A-ZÁÉÍÓÚÑ][a-záéíóúñ]+)?)',
            r'(?:my name is|i\'m|i am)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)',
        ],
        'email': [
            r'[\w\.-]+@[\w\.-]+\.\w+',
        ],
        'phone': [
            r'(?:mi (?:cel|celular|teléfono|telefono|número|numero) es\s*)?(\+?[\d\s\-\(\)]{7,15})',
            r'(?:my (?:phone|cell|number) is\s*)?(\+?[\d\s\-\(\)]{7,15})',
        ]
    }

    # Palabras clave para confirmación de políticas
    POLICY_CONFIRMATION_KEYWORDS = [
        'sí', 'si', 'yes', 'ok', 'acepto', 'accept', 'agree', 'claro', 
        'dale', 'está bien', 'esta bien', 'de acuerdo', 'confirmo'
    ]

    def __init__(self, model, tools: List[Any] = None, logger=None):
        super().__init__(logger)
        self.model = model
        self.tools = tools or []
        self.tool_names = {t.name for t in self.tools}
        self.tool_schemas = self._extract_tool_schemas()

    def _extract_tool_schemas(self) -> Dict[str, Dict[str, Any]]:
        """
        Extrae los schemas de las tools para conocer los parámetros exactos.
        
        Returns:
            Dict con nombre de tool -> {description, parameters: {name: type}}
        """
        schemas = {}
        
        # Parámetros internos a ignorar
        internal_params = {'self', 'args', 'kwargs', 'run_manager', 'config'}
        
        for tool in self.tools:
            tool_name = tool.name
            tool_desc = tool.description
            
            # Intentar obtener el schema de diferentes formas
            params = {}
            
            # Método 1: args_schema (Pydantic model)
            if hasattr(tool, 'args_schema') and tool.args_schema:
                try:
                    schema = tool.args_schema.model_json_schema()
                    properties = schema.get('properties', {})
                    required = schema.get('required', [])
                    for param_name, param_info in properties.items():
                        if param_name in internal_params:
                            continue
                        params[param_name] = {
                            'type': param_info.get('type', 'string'),
                            'description': param_info.get('description', ''),
                            'required': param_name in required
                        }
                except Exception:
                    pass
            
            # Método 2: get_input_schema() para tools de LangChain
            if not params and hasattr(tool, 'get_input_schema'):
                try:
                    input_schema = tool.get_input_schema()
                    if hasattr(input_schema, 'model_json_schema'):
                        schema = input_schema.model_json_schema()
                        properties = schema.get('properties', {})
                        required = schema.get('required', [])
                        for param_name, param_info in properties.items():
                            if param_name in internal_params:
                                continue
                            params[param_name] = {
                                'type': param_info.get('type', 'string'),
                                'description': param_info.get('description', ''),
                                'required': param_name in required
                            }
                except Exception:
                    pass
            
            # Método 3: Inspeccionar _run method
            if not params and hasattr(tool, '_run'):
                try:
                    sig = inspect.signature(tool._run)
                    for param_name, param in sig.parameters.items():
                        if param_name in internal_params:
                            continue
                        params[param_name] = {
                            'type': 'string',
                            'description': '',
                            'required': param.default == inspect.Parameter.empty
                        }
                except Exception:
                    pass
            
            # Método 4: Para tools creadas con @tool decorator
            if not params and hasattr(tool, 'func'):
                try:
                    sig = inspect.signature(tool.func)
                    for param_name, param in sig.parameters.items():
                        if param_name in internal_params:
                            continue
                        params[param_name] = {
                            'type': 'string',
                            'description': '',
                            'required': param.default == inspect.Parameter.empty
                        }
                except Exception:
                    pass
            
            schemas[tool_name] = {
                'description': tool_desc,
                'parameters': params
            }
        
        return schemas

    def _detect_contact_data(self, text: str) -> Dict[str, Any]:
        """Detecta datos de contacto en el texto del usuario."""
        detected = {}
        
        # Detectar nombre
        for pattern in self.CONTACT_PATTERNS['name']:
            match = re.search(pattern, text, re.IGNORECASE)
            if match:
                detected['name'] = match.group(1).strip()
                break
        
        # Detectar email
        for pattern in self.CONTACT_PATTERNS['email']:
            match = re.search(pattern, text, re.IGNORECASE)
            if match:
                detected['email'] = match.group(0).strip()
                break
        
        # Detectar teléfono
        for pattern in self.CONTACT_PATTERNS['phone']:
            match = re.search(pattern, text, re.IGNORECASE)
            if match:
                phone = match.group(1) if match.lastindex else match.group(0)
                # Limpiar el teléfono
                phone_clean = re.sub(r'[^\d+]', '', phone)
                if len(phone_clean) >= 7:
                    detected['phone'] = phone_clean
                break
        
        return detected

    def _is_policy_confirmation(self, text: str, dynamic_info: str) -> bool:
        """Verifica si el usuario está confirmando aceptación de políticas."""
        # Si ya aceptó, no es confirmación
        if 'Policies accepted: Yes' in dynamic_info:
            return False
        
        text_lower = text.lower().strip()
        
        # Verificar si es una confirmación simple
        for keyword in self.POLICY_CONFIRMATION_KEYWORDS:
            if text_lower == keyword or text_lower.startswith(keyword + ' ') or text_lower.startswith(keyword + ','):
                return True
        
        return False

    def _policies_already_accepted(self, dynamic_info: str) -> bool:
        """Verifica si las políticas ya fueron aceptadas."""
        return 'Policies accepted: Yes' in dynamic_info

    def _is_first_message(self, messages: List[Any]) -> bool:
        """Verifica si es el primer mensaje de la conversación."""
        return len(messages) == 0

    def _find_contact_tool(self) -> Optional[str]:
        """Encuentra la tool de contacto por nombre."""
        contact_tool_names = ['create_or_update_contact', 'SaveContact', 'save_contact']
        for name in contact_tool_names:
            if name in self.tool_names:
                return name
        return None

    def _find_policy_tool(self) -> Optional[str]:
        """Encuentra la tool de políticas por nombre."""
        policy_tool_names = ['accept_policies', 'AcceptPolicies', 'accept_policy']
        for name in policy_tool_names:
            if name in self.tool_names:
                return name
        return None

    def _map_contact_data_to_tool_params(self, detected_contact: Dict[str, Any], tool_name: str) -> Dict[str, Any]:
        """
        Mapea los datos de contacto detectados a los parámetros reales de la tool.
        
        Esto es necesario porque diferentes tools pueden usar diferentes nombres:
        - name -> nombre, first_name, name
        - email -> correo, email, email_address
        - phone -> telefono, phone, phone_number
        """
        schema = self.tool_schemas.get(tool_name, {})
        params = schema.get('parameters', {})
        param_names = set(params.keys())
        
        mapped = {}
        
        # Mapeo de name
        if 'name' in detected_contact:
            name_candidates = ['nombre', 'name', 'first_name', 'full_name', 'contact_name']
            for candidate in name_candidates:
                if candidate in param_names:
                    mapped[candidate] = detected_contact['name']
                    break
        
        # Mapeo de email
        if 'email' in detected_contact:
            email_candidates = ['correo', 'email', 'email_address', 'contact_email']
            for candidate in email_candidates:
                if candidate in param_names:
                    mapped[candidate] = detected_contact['email']
                    break
        
        # Mapeo de phone
        if 'phone' in detected_contact:
            phone_candidates = ['telefono', 'phone', 'phone_number', 'celular', 'cel']
            for candidate in phone_candidates:
                if candidate in param_names:
                    mapped[candidate] = detected_contact['phone']
                    break
        
        # Si no encontramos mapeo, intentar pasar los valores con valores vacíos para campos requeridos
        if not mapped and detected_contact:
            # Rellenar todos los parámetros requeridos
            for param_name, param_info in params.items():
                if param_info.get('required', True):
                    # Intentar adivinar qué valor usar
                    if any(n in param_name.lower() for n in ['name', 'nombre']):
                        mapped[param_name] = detected_contact.get('name', '')
                    elif any(e in param_name.lower() for e in ['email', 'correo', 'mail']):
                        mapped[param_name] = detected_contact.get('email', '')
                    elif any(p in param_name.lower() for p in ['phone', 'telefono', 'cel']):
                        mapped[param_name] = detected_contact.get('phone', '')
                    else:
                        mapped[param_name] = ''
        
        return mapped

    def _build_tool_descriptions(self) -> str:
        """Construye descripciones de las tools disponibles CON sus parámetros exactos."""
        if not self.tools:
            return "No tools available."
        
        descriptions = []
        for tool in self.tools:
            tool_name = tool.name
            schema = self.tool_schemas.get(tool_name, {})
            params = schema.get('parameters', {})
            
            # Construir descripción con parámetros
            desc_parts = [f"- **{tool_name}**: {tool.description}"]
            
            if params:
                param_strs = []
                for param_name, param_info in params.items():
                    req = "(required)" if param_info.get('required', True) else "(optional)"
                    param_strs.append(f"    - `{param_name}` {req}")
                desc_parts.append("  Parameters:")
                desc_parts.extend(param_strs)
            
            descriptions.append("\n".join(desc_parts))
        
        return "\n\n".join(descriptions)

    def _build_system_prompt(self) -> str:
        """Construye el prompt del sistema para el orquestador."""
        
        tool_descriptions = self._build_tool_descriptions()
        
        prompt = f"""You are a Smart Orchestrator. Your job is to analyze user messages and create an ACTION PLAN.

## AVAILABLE TOOLS
{tool_descriptions}

## YOUR TASK
Analyze the user's message and the conversation context to determine:
1. What actions need to be executed (can be multiple)
2. The priority order of those actions
3. The parameters for each action

## CRITICAL RULES

### Rule 1: Contact Data Detection
If the user provides ANY contact information (name, email, phone):
- ALWAYS include a "save_contact" action with priority 1
- Extract the exact data provided
- This applies even if mixed with other requests

### Rule 2: Policy Acceptance
- Check the DYNAMIC INFO for "Policies accepted: Yes/No"
- If policies are NOT accepted AND the tool `accept_policies` exists:
  - On FIRST message: Set requires_policy_acceptance=true (bot should ask)
  - If user confirms (says yes/ok/acepto): Include "accept_policies" action with priority 0
- If policies ARE accepted: Never include policy actions

### Rule 3: Knowledge Search
If the user asks a question that might be answered by documents:
- Include "search_knowledge" action
- Use the user's question as the query
- This applies for questions about: requirements, policies, prices, locations, procedures

### Rule 4: Business Actions
For specific business requests (quotes, reservations, emails, etc.):
- Include the appropriate tool action
- Extract all parameters mentioned by the user
- If parameters are missing, the Synthesizer will ask for them

## OUTPUT FORMAT
You MUST return a valid JSON object with this exact structure:
{{
    "analysis": "Brief analysis of what the user wants",
    "detected_contact_data": {{"name": "...", "email": "...", "phone": "..."}} or null,
    "actions": [
        {{
            "type": "save_contact|search_knowledge|accept_policies|execute_tool",
            "priority": 0-10,
            "tool_name": "exact_tool_name",
            "params": {{}}
        }}
    ],
    "requires_policy_acceptance": true/false,
    "conversation_intent": "greeting|information_request|task_request|policy_confirmation|chitchat"
}}

## PRIORITY GUIDE
- 0: accept_policies (if needed)
- 1: save_contact (always first if data detected)
- 2: search_knowledge
- 3+: other business tools

## IMPORTANT
- Return ONLY the JSON, no markdown, no explanation
- Actions array can be empty if it's just chitchat
- Always analyze the FULL context before deciding"""

        return prompt

    def __call__(self, state: Dict[str, Any]) -> Dict[str, Any]:
        """Analiza el mensaje y genera el plan de acciones."""
        
        user_input = state.get("user_input", "")
        messages = state.get("messages", [])
        dynamic_info = state.get("dynamic_info", "")
        function_purpose = state.get("function_purpose", "")
        
        # Pre-análisis con regex (para datos de contacto)
        detected_contact = self._detect_contact_data(user_input)
        is_policy_confirmation = self._is_policy_confirmation(user_input, dynamic_info)
        policies_accepted = self._policies_already_accepted(dynamic_info)
        is_first = self._is_first_message(messages)
        
        # Convertir historial
        history_messages = self._convert_messages_to_langchain(messages)
        
        # Construir contexto para el LLM
        context_parts = [
            f"## DYNAMIC INFO\n{dynamic_info}",
            f"## BUSINESS INSTRUCTIONS\n{function_purpose}",
            f"## CONVERSATION STATE",
            f"- First message: {is_first}",
            f"- Policies accepted: {policies_accepted}",
            f"- Is policy confirmation: {is_policy_confirmation}",
            f"- Detected contact data (regex): {json.dumps(detected_contact) if detected_contact else 'None'}",
        ]
        
        # Agregar historial resumido
        if history_messages:
            history_summary = []
            for msg in history_messages[-6:]:  # Últimos 6 mensajes
                role = "User" if isinstance(msg, HumanMessage) else "Assistant"
                content = msg.content[:100] + "..." if len(msg.content) > 100 else msg.content
                history_summary.append(f"- {role}: {content}")
            context_parts.append(f"## RECENT CONVERSATION\n" + "\n".join(history_summary))
        
        context = "\n\n".join(context_parts)
        
        # Construir mensajes para el LLM
        llm_messages = [
            SystemMessage(content=self._build_system_prompt()),
            HumanMessage(content=f"{context}\n\n## USER MESSAGE\n{user_input}\n\nAnalyze and return the action plan JSON:")
        ]
        
        try:
            # Invocar modelo con JSON mode
            response = self.model.invoke(
                llm_messages,
                response_format={"type": "json_object"}
            )
            
            # Parsear respuesta
            plan_data = json.loads(response.content)
            
            # Enriquecer con datos detectados por regex si el LLM no los detectó
            if detected_contact and not plan_data.get("detected_contact_data"):
                plan_data["detected_contact_data"] = detected_contact
            
            # Asegurar que save_contact esté en las acciones si hay datos de contacto
            contact_tool = self._find_contact_tool()
            if detected_contact and contact_tool:
                has_save_contact = any(
                    a.get("type") == "save_contact" or a.get("tool_name") == contact_tool
                    for a in plan_data.get("actions", [])
                )
                if not has_save_contact:
                    # Mapear datos de contacto a parámetros reales de la tool
                    mapped_params = self._map_contact_data_to_tool_params(detected_contact, contact_tool)
                    plan_data["actions"].insert(0, {
                        "type": "save_contact",
                        "priority": 1,
                        "tool_name": contact_tool,
                        "params": mapped_params
                    })
                    # Re-ordenar por prioridad
                    plan_data["actions"].sort(key=lambda x: x.get("priority", 99))
            
            # Forzar accept_policies si es confirmación y no está en el plan
            policy_tool = self._find_policy_tool()
            if is_policy_confirmation and policy_tool and not policies_accepted:
                has_accept = any(
                    a.get("tool_name") == policy_tool 
                    for a in plan_data.get("actions", [])
                )
                if not has_accept:
                    # Obtener el nombre del parámetro correcto para la tool de políticas
                    policy_schema = self.tool_schemas.get(policy_tool, {})
                    policy_params = policy_schema.get('parameters', {})
                    policy_param_name = next(iter(policy_params.keys()), 'user_message')
                    
                    plan_data["actions"].insert(0, {
                        "type": "accept_policies",
                        "priority": 0,
                        "tool_name": policy_tool,
                        "params": {policy_param_name: user_input}
                    })
                    plan_data["actions"].sort(key=lambda x: x.get("priority", 99))
            
            # Marcar si necesita pedir aceptación de políticas
            if is_first and not policies_accepted and policy_tool:
                plan_data["requires_policy_acceptance"] = True
            
            log_msg = f"Plan generated: {len(plan_data.get('actions', []))} actions, intent: {plan_data.get('conversation_intent', 'unknown')}"
            
            return {
                "action_plan": plan_data,
                **self._add_log(log_msg)
            }
            
        except json.JSONDecodeError as e:
            self.logger.error(f"Failed to parse orchestrator response: {e}")
            
            # Buscar tools disponibles
            contact_tool = self._find_contact_tool()
            policy_tool = self._find_policy_tool()
            
            # Plan de fallback
            fallback_plan = {
                "analysis": "Error parsing response, defaulting to chitchat",
                "detected_contact_data": detected_contact if detected_contact else None,
                "actions": [],
                "requires_policy_acceptance": is_first and not policies_accepted and policy_tool is not None,
                "conversation_intent": "chitchat"
            }
            
            # Agregar save_contact si hay datos
            if detected_contact and contact_tool:
                mapped_params = self._map_contact_data_to_tool_params(detected_contact, contact_tool)
                fallback_plan["actions"].append({
                    "type": "save_contact",
                    "priority": 1,
                    "tool_name": contact_tool,
                    "params": mapped_params
                })
            
            return {
                "action_plan": fallback_plan,
                **self._add_log(f"Plan fallback due to parse error: {e}")
            }
            
        except Exception as e:
            self.logger.error(f"Orchestrator error: {e}")
            import traceback
            self.logger.error(traceback.format_exc())
            
            return {
                "action_plan": {
                    "analysis": f"Error: {str(e)}",
                    "detected_contact_data": None,
                    "actions": [],
                    "requires_policy_acceptance": False,
                    "conversation_intent": "error"
                },
                **self._add_log(f"Orchestrator error: {e}")
            }
