"""
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()
            
            # Try to extract JSON from markdown code blocks (may have text before/after)
            if "```json" in response or "```" in response:
                # Find JSON code block
                import re
                # Match ```json ... ``` or ``` ... ```
                json_match = re.search(r'```(?:json)?\s*\n(.*?)\n```', response, re.DOTALL)
                if json_match:
                    response = json_match.group(1).strip()
                elif response.startswith("```"):
                    # Fallback: remove code fences from start/end
                    lines = response.split("\n")
                    if len(lines) > 2:
                        response = "\n".join(lines[1:-1])
            
            # Try direct JSON parse
            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.debug(f"JSON decode error: {e} (response preview: {str(response)[:200]})")
        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 validate_user_clarification(content: str) -> tuple[bool, str]:
    """
    Validate user-directed clarification requests and log warnings for vague ones.
    
    Note: We no longer block messages - just log warnings. The backend will
    handle surfacing requests with auto-generated defaults.
    
    Args:
        content: The message content being sent to USER
        
    Returns:
        Tuple of (is_valid, reason_or_fixed_content) - always returns True now
    """
    content_lower = content.lower()
    
    # Check if this looks like a question/request rather than a deliverable
    question_indicators = ["?", "need", "please provide", "can you", "what is", "which"]
    is_question = any(ind in content_lower for ind in question_indicators)
    
    # If it's a deliverable (has structured output markers), allow it
    deliverable_markers = ["#", "## ", "summary", "analysis", "findings", "recommendations", "complete"]
    is_deliverable = any(marker in content_lower for marker in deliverable_markers)
    
    if not is_question or is_deliverable:
        return (True, content)
    
    # Check if agent provided a default (ideal behavior)
    has_default = any(phrase in content_lower for phrase in [
        "if no response", "default to", "will assume", "will default", "unless"
    ])
    
    if not has_default:
        # Log warning but DON'T block - backend will add a default
        logger.debug(f"User request without default assumption (backend will add one)")
    
    # Always allow the message through - backend handles defaults
    return (True, content)


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"
    
    # Log USER-directed messages for debugging (no longer blocking)
    if parsed.get("send_to", "").upper() == "USER":
        validate_user_clarification(parsed["content"])  # Just for logging
    
    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
        
        # Check MCP tool availability
        mcp_available = False
        mcp_tools_list = []
        
        # First check if MCP is even attached
        if not hasattr(context, 'mcp'):
            logger.debug(f"Agent {agent_name}: No MCP access")
            mcp_available = False
        elif context.mcp is None:
            logger.warning(f"Agent {agent_name}: MCP middleware failed to inject client")
            mcp_available = False
        else:
            try:
                # MCP is present, try to list available tools
                tools_result = context.mcp.list_tools()
                
                if hasattr(tools_result, '__await__'):
                    mcp_tools_list = await tools_result
                else:
                    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.debug(f"Agent {agent_name}: MCP available with {len(mcp_tools_list)} tools")
            except Exception as e:
                logger.warning(f"Agent {agent_name}: MCP tools check failed: {e}")
                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 ACCESS TO {len(mcp_tools_list)} TOOLS - USE THEM!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Available tools:
{chr(10).join(f"  • {name}" for name in tool_names)}{"..." if len(mcp_tools_list) > 10 else ""}

HOW TO USE TOOLS:
When you need to use a tool (like creating a Zoom meeting), the system will
automatically call it for you. Just decide to use the tool and the system handles it.

For example, if you need to create a meeting:
- The system sees you want to use zoom.create_meeting
- It calls the tool automatically
- You get the result back
- Then respond with the outcome in JSON format

✅ TOOLS ARE AVAILABLE - Don't say "I don't have access"
✅ USE TOOLS IMMEDIATELY - Don't just discuss or coordinate
✅ RESPOND WITH JSON - Not code, not explanations
"""
        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):
You MUST respond with ONLY a valid JSON object. NO explanatory text before or after.
NO Python code. NO markdown outside the JSON block.

{{
  "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
}}

WRONG (don't do this):
  "I'll create the meeting... ```json {{"send_to": "USER"}}```"  ❌
  "result = await context.mcp.call_tool(...)" ❌

RIGHT (do this):
  ```json
  {{"send_to": "USER", "content": "Meeting created"}}
  ```  ✅

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
        tool_name_mapping = {}  # Map Anthropic names back to original MCP names
        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", {})
                
                # Convert dots to underscores for Anthropic (they don't like dots in tool names)
                anthropic_name = tool_name.replace(".", "_")
                tool_name_mapping[anthropic_name] = tool_name  # Save mapping for later
                
                tools_schema.append({
                    "name": anthropic_name,
                    "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":
                            # Convert Anthropic tool name back to original MCP name
                            anthropic_tool_name = block.name
                            tool_name_used = tool_name_mapping.get(anthropic_tool_name, anthropic_tool_name)
                            tool_input = block.input
                            
                            logger.debug(f"Agent {agent_name}: Calling MCP tool '{tool_name_used}'")
                            
                            # 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()
                
                # Check for empty response
                if not raw_output:
                    logger.warning(f"Agent {agent_name}: LLM returned empty response (stop_reason: {response.stop_reason})")
            
            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="broadcast",  # Prefer ALL over individual messages
    )
    
    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

