"""
AgentFactory - Create Agent instances dynamically from specifications.

This module provides utilities for creating agents with generic LLM-powered logic
based on agent specifications generated by PlannerLLM.
"""

import json
import logging
from typing import Any, Optional
from synqed.agent import Agent, AgentLogicContext, get_interaction_protocol

logger = logging.getLogger(__name__)


def enforce_json_structure(response: Any) -> dict:
    """
    🔧 Universal JSON structure enforcement for all agent outputs.
    
    This function ensures ALL agent outputs are valid JSON dicts with required fields.
    If the output is invalid, returns an error structure that will be caught by validation.
    
    Args:
        response: Raw agent output (can be string, dict, or other)
        
    Returns:
        Valid dict structure or error dict
    """
    try:
        # If already a dict, validate it
        if isinstance(response, dict):
            # Check for required fields
            if "send_to" in response and "content" in response:
                return response
            # Missing required fields - mark as invalid
            return {
                "intent": "invalid_format",
                "payload": {"error": "Missing send_to or content fields"},
                "next_action": "retry"
            }
        
        # If string, try to parse as JSON
        if isinstance(response, str):
            response = response.strip()
            
            # Remove markdown code fences if present
            if response.startswith("```"):
                lines = response.split("\n")
                if len(lines) > 2:
                    # Remove first and last lines (code fences)
                    response = "\n".join(lines[1:-1])
            
            parsed = json.loads(response)
            if isinstance(parsed, dict):
                return parsed
            
        # Not valid JSON or not a dict
        return {
            "intent": "invalid_format",
            "payload": {"error": f"Invalid response type: {type(response)}"},
            "next_action": "retry"
        }
        
    except json.JSONDecodeError as e:
        logger.error(f"JSON decode error: {e}")
        logger.error(f"  Raw response content (first 500 chars): {str(response)[:500]}")
        logger.error(f"  Response type: {type(response)}")
        logger.error(f"  Response length: {len(str(response)) if response else 0}")
        return {
            "intent": "invalid_format",
            "payload": {"error": str(e)},
            "next_action": "retry"
        }
    except Exception as e:
        logger.error(f"Unexpected error in JSON enforcement: {e}")
        return {
            "intent": "invalid_format",
            "payload": {"error": str(e)},
            "next_action": "retry"
        }


def enforce_structured_output(raw_output: Any, agent_name: str) -> dict:
    """
    🔧 FIX: Enforce structured message format for all agent outputs.
    
    All inter-agent messages must follow this structure:
    {
        "send_to": "agent_name" | "USER",
        "content": "message content",
        "action_intent": "request_action" | "provide_info" | "use_tool" | "complete",
        "requires_tool": true | false
    }
    
    If output is invalid or unstructured, returns a rejection message.
    
    Args:
        raw_output: Raw output from agent logic (string or dict)
        agent_name: Name of the agent for logging
        
    Returns:
        Structured dict or retry request
    """
    # First, enforce basic JSON structure
    parsed = enforce_json_structure(raw_output)
    
    # Check if enforcement failed
    if parsed.get("intent") == "invalid_format":
        logger.warning(f"Agent {agent_name} produced invalid JSON structure")
        return {
            "send_to": "USER",
            "content": f"[SYSTEM ERROR: Agent {agent_name} produced invalid output]",
            "action_intent": "error",
            "requires_tool": False
        }
    
    # Validate required fields
    if "send_to" not in parsed or "content" not in parsed:
        logger.warning(f"Agent {agent_name} missing required fields (send_to, content)")
        return {
            "send_to": "USER",
            "content": f"[SYSTEM ERROR: Agent {agent_name} missing required fields]",
            "action_intent": "error",
            "requires_tool": False
        }
    
    # Add default fields if missing
    if "action_intent" not in parsed:
        # Infer intent from content
        content_lower = parsed["content"].lower()
        if any(word in content_lower for word in ["call", "use", "create", "schedule", "meeting"]):
            parsed["action_intent"] = "use_tool"
        elif any(word in content_lower for word in ["need", "please", "can you", "request"]):
            parsed["action_intent"] = "request_action"
        elif any(word in content_lower for word in ["done", "complete", "finished", "summary"]):
            parsed["action_intent"] = "complete"
        else:
            parsed["action_intent"] = "provide_info"
    
    if "requires_tool" not in parsed:
        # Infer if tool is needed
        parsed["requires_tool"] = parsed["action_intent"] == "use_tool"
    
    return parsed


def create_generic_agent_logic(
    agent_name: str,
    agent_description: str,
    agent_capabilities: list[str],
    provider: str = "anthropic",
    api_key: Optional[str] = None,
    model: Optional[str] = None,
    custom_instructions: str = "",
):
    """
    Create a generic LLM-powered agent logic function.
    
    This factory function generates an async logic function that uses
    an LLM to process messages and generate responses. The agent follows
    the interaction protocol and coordinates with other agents.
    
    Args:
        agent_name: Name of the agent
        agent_description: Description of the agent's purpose
        agent_capabilities: List of agent capabilities
        provider: LLM provider ("anthropic" or "openai")
        api_key: API key for the LLM
        model: Model name (e.g., "claude-sonnet-4-20250514", "gpt-4o")
        custom_instructions: Optional custom instructions for the agent
        
    Returns:
        Async function that can be used as agent logic
        
    Example:
        ```python
        logic_fn = create_generic_agent_logic(
            agent_name="researcher",
            agent_description="Research specialist",
            agent_capabilities=["research", "analysis", "fact-checking"],
            provider="anthropic",
            api_key=os.getenv("ANTHROPIC_API_KEY"),
            model="claude-sonnet-4-20250514"
        )
        
        agent = Agent(
            name="researcher",
            description="Research specialist",
            logic=logic_fn,
            capabilities=["research", "analysis", "fact-checking"]
        )
        ```
    """
    # Validate required parameters
    if not api_key:
        raise ValueError(f"API key is required for agent '{agent_name}'")
    
    # Initialize LLM client based on provider
    if provider == "anthropic":
        try:
            from anthropic import AsyncAnthropic
            client = AsyncAnthropic(api_key=api_key)
        except ImportError:
            raise ImportError(
                "anthropic package is required. Install with: pip install anthropic"
            )
    elif provider == "openai":
        try:
            from openai import AsyncOpenAI
            client = AsyncOpenAI(api_key=api_key)
        except ImportError:
            raise ImportError(
                "openai package is required. Install with: pip install openai"
            )
    else:
        raise ValueError(f"Unsupported provider: {provider}")
    
    # Use default model if not provided
    if not model:
        model = "claude-sonnet-4-20250514" if provider == "anthropic" else "gpt-4o"
    
    async def agent_logic(context: AgentLogicContext) -> dict:
        """Generic LLM-powered agent logic with structured output enforcement."""
        latest = context.latest_message
        if not latest or not latest.content:
            return None
        
        # 🔧 FIX: Validate MCP tool availability before execution
        mcp_available = False
        mcp_tools_list = []
        
        logger.info(f"🔍 Agent {agent_name}: Checking MCP availability...")
        logger.info(f"   Context type: {type(context).__name__}")
        logger.info(f"   Has 'mcp' attribute: {hasattr(context, 'mcp')}")
        
        # First check if MCP is even attached
        if not hasattr(context, 'mcp'):
            logger.error(f"❌ Agent {agent_name}: context.mcp NOT FOUND")
            logger.error(f"   This means MCP middleware was not attached to this agent instance")
            logger.error(f"   Agent instance ID: {id(context.memory._agent if hasattr(context, 'memory') and hasattr(context.memory, '_agent') else 'unknown')}")
            mcp_available = False
        elif context.mcp is None:
            logger.error(f"❌ Agent {agent_name}: context.mcp is None - MCP middleware failed to inject client!")
            mcp_available = False
        else:
            try:
                # MCP is present, try to list available tools
                # list_tools() may be sync or async depending on the client
                logger.debug(f"Agent {agent_name}: Attempting to list MCP tools...")
                tools_result = context.mcp.list_tools()
                
                if hasattr(tools_result, '__await__'):
                    # It's a coroutine, await it
                    mcp_tools_list = await tools_result
                else:
                    # It's already the result
                    mcp_tools_list = tools_result
                
                # Handle different return formats (list or dict)
                if isinstance(mcp_tools_list, dict):
                    mcp_tools_list = mcp_tools_list.get('tools', [])
                
                mcp_available = len(mcp_tools_list) > 0
                logger.info(f"Agent {agent_name}: ✅ MCP AVAILABLE with {len(mcp_tools_list)} tools!")
            except Exception as e:
                logger.error(f"Agent {agent_name}: ❌ MCP tools check failed: {e}", exc_info=True)
                mcp_available = False
        
        # Get interaction protocol with team roster
        protocol = get_interaction_protocol(exclude_agent=agent_name)
        
        # Build system prompt with emphasis on brevity and action
        capabilities_str = ", ".join(agent_capabilities)
        
        # Add MCP availability status to prompt with explicit usage instructions
        mcp_status = ""
        if mcp_available:
            tool_names = [t.get("name", "unknown") for t in mcp_tools_list[:10]]  # Show first 10
            mcp_status = f"""
⚡ YOU HAVE MCP TOOLS - USE THEM NOW!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Tools available ({len(mcp_tools_list)} total):
{chr(10).join(f"  • {name}" for name in tool_names)}{"..." if len(mcp_tools_list) > 10 else ""}

HOW TO USE TOOLS (Python code you can execute):
```python
# Example: Create a Zoom meeting
result = await context.mcp.call_tool(
    "zoom.create_meeting",
    {{
        "topic": "Product Launch Meeting",
        "duration": 60,
        "start_time": "2024-01-20T14:00:00Z"
    }}
)
```

✅ YOU CAN CALL THESE TOOLS DIRECTLY
❌ DO NOT say "I don't have access" - YOU DO HAVE ACCESS
⚡ USE TOOLS IMMEDIATELY instead of discussing or coordinating
"""
        else:
            mcp_status = "❌ MCP TOOLS NOT AVAILABLE: You cannot use external tools in this context"
        
        system_prompt = f"""{protocol}

YOUR ROLE: {agent_name}
YOUR DESCRIPTION: {agent_description}
YOUR CAPABILITIES: {capabilities_str}
DEFAULT COORDINATION STYLE: respond_to_sender

{mcp_status}

🔧 CRITICAL RULES:
1. BE BRIEF: Maximum 2 sentences per message
2. TAKE ACTION: If you have MCP tools, USE THEM NOW - don't just discuss
3. NO ESSAYS: Avoid long explanations, plans, or philosophical discussions
4. STRUCTURED OUTPUT ONLY: Always respond with valid JSON

{custom_instructions}

RESPONSE FORMAT (REQUIRED):
{{
  "send_to": "agent_name_or_USER",
  "content": "Brief message (max 2 sentences)",
  "action_intent": "use_tool|request_action|provide_info|complete",
  "requires_tool": true|false
}}

If you have MCP tools and can take action (e.g., create a Zoom meeting), DO IT IMMEDIATELY.
Don't coordinate, plan, or discuss - just ACT.
"""
        
        # Get conversation history (limited to prevent context bloat)
        history = context.get_conversation_history(workspace_wide=True, max_messages=10)
        
        # Add shared plan context if available
        plan_context = ""
        if context.shared_plan:
            plan_context = f"\n\nShared Plan:\n{context.shared_plan}"
        
        # Build user message with action reminder
        user_message = f"Conversation:\n{history}{plan_context}\n\n⚡ Respond with ACTION (use tools if available) in JSON format:"
        
        # Prepare tools for function calling if MCP is available
        tools_schema = None
        if mcp_available and mcp_tools_list:
            # Convert MCP tools to Anthropic tool format
            tools_schema = []
            for tool in mcp_tools_list[:20]:  # Limit to 20 tools to avoid context overflow
                tool_name = tool.get("name", "")
                tool_desc = tool.get("description", f"Tool: {tool_name}")
                tool_params = tool.get("inputSchema", {})
                
                tools_schema.append({
                    "name": tool_name.replace(".", "_"),  # Anthropic doesn't like dots
                    "description": tool_desc,
                    "input_schema": tool_params if tool_params else {
                        "type": "object",
                        "properties": {},
                        "required": []
                    }
                })
        
        # Call LLM based on provider with tool support
        try:
            if provider == "anthropic":
                # Build request params
                request_params = {
                    "model": model,
                    "max_tokens": 300,
                    "system": system_prompt,
                    "messages": [{"role": "user", "content": user_message}],
                }
                
                # Add tools if available
                if tools_schema:
                    request_params["tools"] = tools_schema
                
                response = await client.messages.create(**request_params)
                
                # Check if LLM wants to use a tool
                if response.stop_reason == "tool_use":
                    # Extract tool use blocks
                    for block in response.content:
                        if block.type == "tool_use":
                            tool_name_used = block.name.replace("_", ".")  # Convert back
                            tool_input = block.input
                            
                            # Actually call the MCP tool
                            try:
                                tool_result = await context.mcp.call_tool(tool_name_used, tool_input)
                                
                                # Return a message about the tool result
                                result_summary = tool_result.get("result", tool_result)
                                if isinstance(result_summary, dict):
                                    result_summary = json.dumps(result_summary, indent=2)
                                
                                return {
                                    "send_to": latest.from_agent or "USER",
                                    "content": f"✅ Used tool {tool_name_used}: {str(result_summary)[:200]}",
                                    "action_intent": "use_tool",
                                    "requires_tool": False,
                                    "tool_result": tool_result
                                }
                            except Exception as e:
                                logger.error(f"Tool call failed: {e}")
                                return context.reply(f"❌ Tool call failed: {str(e)}")
                
                # No tool use, extract text response
                raw_output = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        raw_output += block.text
                raw_output = raw_output.strip()
                
                # Debug logging to see what LLM actually returned
                if not raw_output:
                    logger.error(f"Agent {agent_name}: LLM returned EMPTY response!")
                    logger.error(f"  Response stop_reason: {response.stop_reason}")
                    logger.error(f"  Response content blocks: {len(response.content)}")
                    for i, block in enumerate(response.content):
                        logger.error(f"    Block {i}: type={block.type}, has_text={hasattr(block, 'text')}")
                else:
                    logger.debug(f"Agent {agent_name}: LLM returned {len(raw_output)} chars")
            
            elif provider == "openai":
                response = await client.chat.completions.create(
                    model=model,
                    max_tokens=300,
                    messages=[
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": user_message}
                    ],
                    response_format={"type": "json_object"},
                )
                raw_output = response.choices[0].message.content.strip()
            else:
                return context.reply("Unsupported LLM provider")
            
            # 🔧 FIX: Enforce structured output with JSON validation
            structured_output = enforce_structured_output(raw_output, agent_name)
            
            # 🔧 Additional validation: ensure it's JSON-compatible
            validated_output = enforce_json_structure(structured_output)
            if validated_output.get("intent") == "invalid_format":
                logger.error(f"Agent {agent_name} failed final JSON validation")
                return context.reply("[ERROR: Failed to produce valid structured output]")
            
            return structured_output
            
        except Exception as e:
            logger.error(f"Error in agent {agent_name} logic: {e}")
            # Return error message to sender
            return context.reply(f"Error processing request: {str(e)}")
    
    return agent_logic


def create_agent_from_spec(
    agent_spec: dict[str, Any],
    custom_instructions: str = "",
) -> Agent:
    """
    Create an Agent instance from an agent specification.
    
    This function takes an agent specification (typically generated by
    PlannerLLM.create_agents_from_task) and creates a fully functional
    Agent instance with generic LLM-powered logic.
    
    Args:
        agent_spec: Agent specification dictionary containing:
            - name: Agent name
            - description: Agent description
            - capabilities: List of capabilities
            - role: Team/domain identifier
            - provider: LLM provider
            - api_key: API key
            - model: Model name
        custom_instructions: Optional custom instructions for the agent
        
    Returns:
        Configured Agent instance
        
    Raises:
        ValueError: If required fields are missing from agent_spec
        
    Example:
        ```python
        agent_spec = {
            "name": "researcher",
            "description": "Research specialist",
            "capabilities": ["research", "analysis"],
            "role": "research_team",
            "provider": "anthropic",
            "api_key": os.getenv("ANTHROPIC_API_KEY"),
            "model": "claude-sonnet-4-20250514"
        }
        
        agent = create_agent_from_spec(agent_spec)
        ```
    """
    # Validate required fields
    required_fields = ["name", "description", "capabilities", "role", "provider", "api_key"]
    missing_fields = [field for field in required_fields if field not in agent_spec]
    if missing_fields:
        raise ValueError(f"Agent spec missing required fields: {missing_fields}")
    
    # Extract fields
    name = agent_spec["name"]
    description = agent_spec["description"]
    capabilities = agent_spec["capabilities"]
    role = agent_spec["role"]
    provider = agent_spec["provider"]
    api_key = agent_spec["api_key"]
    model = agent_spec.get("model")
    
    # Create generic logic function
    logic_fn = create_generic_agent_logic(
        agent_name=name,
        agent_description=description,
        agent_capabilities=capabilities,
        provider=provider,
        api_key=api_key,
        model=model,
        custom_instructions=custom_instructions,
    )
    
    # Create Agent instance
    agent = Agent(
        name=name,
        description=description,
        logic=logic_fn,
        role=role,
        capabilities=capabilities,
        default_coordination="respond_to_sender",
    )
    
    logger.info(f"Created agent '{name}' from specification")
    return agent


def create_agents_from_specs(
    agent_specs: list[dict[str, Any]],
    custom_instructions: str = "",
) -> list[Agent]:
    """
    Create multiple Agent instances from agent specifications.
    
    Args:
        agent_specs: List of agent specifications
        custom_instructions: Optional custom instructions for all agents
        
    Returns:
        List of configured Agent instances
        
    Example:
        ```python
        planner = PlannerLLM(provider="anthropic", api_key="...")
        agent_specs = await planner.create_agents_from_task("Plan a conference")
        
        agents = create_agents_from_specs(agent_specs)
        
        # Register agents
        for agent in agents:
            AgentRuntimeRegistry.register(agent.name, agent)
        ```
    """
    agents = []
    for agent_spec in agent_specs:
        try:
            agent = create_agent_from_spec(agent_spec, custom_instructions)
            agents.append(agent)
        except Exception as e:
            logger.error(f"Failed to create agent from spec: {e}")
            continue
    
    logger.info(f"Created {len(agents)} agents from {len(agent_specs)} specifications")
    return agents

