"""
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.

includes robust json parsing, format repair, and depth validation.
"""

import json
import logging
from typing import Any, Optional
from synqed.agent import Agent, AgentLogicContext, get_interaction_protocol
from synqed.llm_utils import (
    extract_json_from_llm_output,
    validate_visible_reasoning,
    create_format_repair_prompt,
    create_depth_challenge_prompt,
    create_fallback_response,
)

logger = logging.getLogger(__name__)

# max retries for format repair and depth challenges
MAX_FORMAT_RETRIES = 1
MAX_DEPTH_RETRIES = 1


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
    for the Deep Reasoning Protocol.
    
    Required fields:
    - send_to: recipient of the message
    - content: the final chat message
    - visible_reasoning: exposed thinking process (NEW - required)
    - collaboration: dependencies and requests (NEW - optional but encouraged)
    
    Args:
        response: Raw agent output (can be string, dict, or other)
        
    Returns:
        Valid dict structure or error dict
    """
    try:
        # If already a dict, validate structure
        if isinstance(response, dict):
            # Check for required fields
            if "send_to" in response and "content" in response:
                # Ensure new reasoning fields exist (add empty if missing)
                if "visible_reasoning" not in response:
                    response["visible_reasoning"] = ""
                if "collaboration" not in response:
                    response["collaboration"] = ""
                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):
                # Ensure new reasoning fields exist
                if "visible_reasoning" not in parsed:
                    parsed["visible_reasoning"] = ""
                if "collaboration" not in parsed:
                    parsed["collaboration"] = ""
                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 format_content_with_reasoning(response: dict) -> dict:
    """
    format the response for frontend display.
    
    the frontend now reads visible_reasoning and collaboration as separate
    json fields, so we no longer embed html markers in the content.
    
    this function ensures the response has all required fields properly set.
    
    args:
        response: structured response dict
        
    returns:
        response with cleaned up fields
    """
    # ensure all fields exist and are strings
    response["visible_reasoning"] = (response.get("visible_reasoning") or "").strip()
    response["collaboration"] = (response.get("collaboration") or "").strip()
    response["content"] = (response.get("content") or "").strip()
    
    # remove any html comment markers that might have leaked through
    for field in ["content", "visible_reasoning", "collaboration"]:
        val = response.get(field, "")
        if val:
            val = val.replace("<!--REASONING_START-->", "")
            val = val.replace("<!--REASONING_END-->", "")
            val = val.replace("<!--COLLAB_START-->", "")
            val = val.replace("<!--COLLAB_END-->", "")
            response[field] = val.strip()
    
    return response


def validate_deep_reasoning(response: dict, agent_name: str) -> tuple[bool, list[str]]:
    """
    Validate that agent response contains deep, genuine reasoning.
    
    This is a core component of the Deep Reasoning Protocol v3.0.
    It detects shallow, fake, or placeholder reasoning and returns
    specific issues that need to be addressed.
    
    Args:
        response: The structured response dict
        agent_name: Name of the agent for logging
        
    Returns:
        Tuple of (is_valid, list_of_issues)
    """
    issues = []
    
    visible_reasoning = response.get("visible_reasoning", "")
    collaboration = response.get("collaboration", "")
    content = response.get("content", "")
    
    # ===== CHECK 0: Content length - must have actual deliverable =====
    if len(content.strip()) < 200:
        issues.append("INSUFFICIENT CONTENT: Response too short. Must contain actual deliverable (min 200 chars).")
    
    # ===== CHECK 1: Reasoning length =====
    if not visible_reasoning or len(visible_reasoning.strip()) < 100:
        issues.append("SHALLOW REASONING: Minimum 100 characters of structured thinking required.")
    
    # ===== CHECK 2: Acknowledgment-only responses (main problem) =====
    acknowledgment_phrases = [
        ("perfect, i'll", "Don't acknowledge - produce the actual deliverable"),
        ("great, i'll", "Don't acknowledge - produce the actual deliverable"),
        ("i'll start", "Don't plan - SHOW the actual work"),
        ("i'll create", "Don't plan - SHOW the actual creation"),
        ("starting work", "Don't announce - SHOW the actual work"),
        ("working on", "Don't describe working - SHOW the work"),
        ("i'll have this ready", "Don't promise - DELIVER now"),
        ("ready in 5", "Don't promise time - DELIVER content"),
        ("template ready", "Don't claim ready - SHOW the template"),
        ("content integrated", "Don't claim done - SHOW the content"),
        ("framework complete", "Don't claim complete - SHOW the framework"),
        ("design complete", "Don't claim complete - SHOW the design"),
    ]
    
    content_lower = content.lower()
    for phrase, reason in acknowledgment_phrases:
        if phrase in content_lower and len(content) < 500:
            issues.append(f"ACKNOWLEDGMENT-ONLY: '{phrase}' - {reason}")
            break
    
    # ===== CHECK 3: Placeholder phrases in reasoning =====
    shallow_phrases = [
        ("i'll look into", "show the actual analysis"),
        ("looking into this", "share your actual thinking"),
        ("let me check", "show what you found"),
        ("sounds good", "explain why with analysis"),
        ("great idea", "analyze critically"),
        ("perfect!", "explain what makes it good"),
        ("will do", "explain your approach"),
        ("on it", "share your actual plan"),
        ("got it", "explain your understanding"),
    ]
    
    reasoning_lower = visible_reasoning.lower()
    for phrase, reason in shallow_phrases:
        if phrase in reasoning_lower and len(visible_reasoning) < 150:
            issues.append(f"PLACEHOLDER: Contains '{phrase}' - {reason}")
            break
    
    # ===== CHECK 4: Fake work claims =====
    fake_work_phrases = [
        ("i completed", "Must SHOW the actual work"),
        ("i finished", "Must SHOW what was finished"),
        ("done!", "Must SHOW evidence of completion"),
        ("complete -", "Must SHOW the complete content"),
        ("ready for delivery", "Must SHOW the deliverable"),
    ]
    
    # Only flag if content is short (real completions have the content)
    depth_indicators = [
        "uncertainty", "uncertain", "unsure",
        "assumption", "assume", "assuming",
        "depend", "dependency", "relies on",
        "risk", "concern", "worry",
        "alternative", "alternatively", "option",
        "however", "but", "although",
        "question", "unclear", "unknown",
        "need to know", "missing", "required",
        "challenge", "disagree", "concern about",
    ]
    has_depth = any(indicator in reasoning_lower for indicator in depth_indicators)
    
    if not has_depth:
        for phrase, reason in fake_work_phrases:
            if phrase in content_lower:
                issues.append(f"FAKE WORK: Claims '{phrase}' without showing reasoning. {reason}")
                break
    
    # ===== CHECK 5: Instant agreement without analysis =====
    agreement_phrases = [
        "great!", "perfect!", "sounds good!", "excellent!", "love it!",
        "amazing!", "wonderful!", "fantastic!", "brilliant!", "awesome!"
    ]
    for phrase in agreement_phrases:
        if phrase in content_lower and len(visible_reasoning) < 100:
            issues.append(f"SHALLOW AGREEMENT: '{phrase}' without substantive analysis. Challenge or expand.")
            break
    
    # ===== CHECK 6: Missing depth indicators (for longer responses) =====
    if len(visible_reasoning) >= 50 and not has_depth:
        issues.append(
            "MISSING DEPTH: Reasoning lacks uncertainty, assumptions, dependencies, or alternatives. "
            "Add at least one of: UNCERTAINTY, ASSUMPTION, DEPENDENCY, RISK, or ALTERNATIVE."
        )
    
    # ===== CHECK 7: Empty collaboration when dependencies exist =====
    dependency_keywords = ["need", "require", "waiting", "depend", "blocked"]
    has_dependency_mention = any(kw in reasoning_lower for kw in dependency_keywords)
    if has_dependency_mention and (not collaboration or len(collaboration.strip()) < 20):
        issues.append(
            "MISSING COLLABORATION: Reasoning mentions dependencies but collaboration field is empty. "
            "Explicitly request what you need from teammates."
        )
    
    is_valid = len(issues) == 0
    
    if issues:
        logger.warning(f"Agent {agent_name} shallow reasoning detected: {issues}")
    
    return (is_valid, issues)


def enforce_structured_output(raw_output: Any, agent_name: str) -> dict:
    """
    enforce structured message format with robust json parsing.
    
    uses the new llm_utils for json extraction with fallback strategies.
    never returns [SYSTEM ERROR] chat messages - instead returns degraded
    but usable responses with error metadata.
    
    args:
        raw_output: raw output from agent logic (string or dict)
        agent_name: name of the agent for logging
        
    returns:
        structured dict with validation metadata
    """
    # if already a dict, use it directly
    if isinstance(raw_output, dict):
        parsed = raw_output
    else:
        # use robust json extraction
        try:
            parsed = extract_json_from_llm_output(str(raw_output))
        except ValueError as e:
            logger.warning(f"agent {agent_name} json extraction failed: {e}")
            # return fallback response instead of system error
            return create_fallback_response(
                agent_name, 
                str(raw_output)[:3000], 
                f"json parsing failed: {str(e)}"
            )
    
    # validate required fields - provide defaults instead of erroring
    if "send_to" not in parsed:
        parsed["send_to"] = "USER"
    if "content" not in parsed:
        # try to extract content from other fields or use raw
        parsed["content"] = parsed.get("message", parsed.get("response", str(raw_output)[:1000]))
    
    # ensure reasoning fields exist
    parsed["visible_reasoning"] = parsed.get("visible_reasoning", "")
    parsed["collaboration"] = parsed.get("collaboration", "")
    
    # validate reasoning depth
    reasoning_issues = validate_visible_reasoning(parsed.get("visible_reasoning", ""))
    parsed["_reasoning_valid"] = len(reasoning_issues) == 0
    parsed["_reasoning_issues"] = reasoning_issues
    
    if reasoning_issues:
        parsed["_needs_challenge"] = True
        logger.info(f"agent {agent_name} shallow reasoning: {reasoning_issues[:2]}")
    
    # infer action_intent if missing
    if "action_intent" not in parsed:
        content_lower = parsed["content"].lower()
        reasoning_lower = parsed.get("visible_reasoning", "").lower()
        combined = content_lower + " " + reasoning_lower
        
        if any(word in combined for word in ["call", "use tool", "schedule", "meeting"]):
            parsed["action_intent"] = "use_tool"
        elif any(word in combined for word in ["need", "please", "request", "depend", "waiting", "blocked"]):
            parsed["action_intent"] = "request_action"
        elif any(word in combined for word in ["done", "complete", "finished", "summary", "final", "deliverable"]):
            parsed["action_intent"] = "complete"
        elif any(word in combined for word in ["challenge", "disagree", "question", "concern"]):
            parsed["action_intent"] = "challenge"
        else:
            parsed["action_intent"] = "provide_info"
    
    if "requires_tool" not in parsed:
        parsed["requires_tool"] = parsed["action_intent"] == "use_tool"
    
    # log user-directed messages for debugging
    if parsed.get("send_to", "").upper() == "USER":
        validate_user_clarification(parsed["content"])
    
    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 Deep Reasoning Protocol v3.0 agent logic function.
    
    This factory function generates an async logic function that uses
    an LLM to produce deep, visible reasoning with explicit uncertainties,
    dependencies, and collaboration requests.
    
    Deep Reasoning Protocol Features:
    - Visible reasoning: All thinking exposed to user
    - No fake work: Claims must be supported by evidence
    - Dependency-first: Identify blockers before proceeding
    - Agent challenges: Question shallow reasoning from teammates
    - Structured output: visible_reasoning, collaboration, content
    
    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 produces structured responses with reasoning
        
    Response Format:
        {
            "send_to": "recipient",
            "visible_reasoning": "complete thinking with uncertainties",
            "collaboration": "requests and challenges to teammates",
            "content": "final chat message"
        }
    """
    # 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:
        """Deep Reasoning Protocol v3.0 agent logic with visible thinking."""
        latest = context.latest_message
        if not latest or not latest.content:
            return None
        
        # Check MCP tool availability
        mcp_available = False
        mcp_tools_list = []
        
        if hasattr(context, 'mcp') and context.mcp is not None:
            try:
                tools_result = context.mcp.list_tools()
                if hasattr(tools_result, '__await__'):
                    mcp_tools_list = await tools_result
                else:
                    mcp_tools_list = tools_result
                
                if isinstance(mcp_tools_list, dict):
                    mcp_tools_list = mcp_tools_list.get('tools', [])
                
                mcp_available = len(mcp_tools_list) > 0
            except Exception as e:
                logger.warning(f"Agent {agent_name}: MCP tools check failed: {e}")
        
        # Get interaction protocol with team roster
        protocol = get_interaction_protocol(exclude_agent=agent_name)
        
        capabilities_str = ", ".join(agent_capabilities)
        
        # MCP status section
        mcp_status = ""
        if mcp_available:
            tool_names = [t.get("name", "unknown") for t in mcp_tools_list[:10]]
            mcp_status = f"""
⚡ AVAILABLE TOOLS ({len(mcp_tools_list)} total):
{chr(10).join(f"  • {name}" for name in tool_names)}{"..." if len(mcp_tools_list) > 10 else ""}

When you decide to use a tool, the system calls it automatically.
Include tool usage reasoning and report real results only.
"""
        else:
            mcp_status = "❌ NO EXTERNAL TOOLS AVAILABLE"
        
        # Value-Maximizing Multi-Agent Protocol system prompt
        system_prompt = f"""{protocol}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
YOUR IDENTITY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ROLE: {agent_name}
DESCRIPTION: {agent_description}
CAPABILITIES: {capabilities_str}

{mcp_status}

{custom_instructions}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ CRITICAL: PRODUCE ACTUAL DELIVERABLES, NOT ACKNOWLEDGMENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

You MUST produce ACTUAL WORK PRODUCT in every response. Not plans. Not acknowledgments. REAL CONTENT.

ABSOLUTELY FORBIDDEN RESPONSES (will be rejected):
❌ "Perfect, I'll start working on..."
❌ "Great, I'll create the..."
❌ "I'll have this ready in 5 minutes"
❌ "Template framework ready" (without showing the actual template)
❌ "Content integrated" (without showing the actual content)
❌ "Starting work now" / "Beginning task" / "On it"
❌ Any response under 200 words that claims to be "working on" something

REQUIRED IN EVERY RESPONSE:
✓ If creating content → Include THE ACTUAL CONTENT in your message
✓ If designing → Include THE ACTUAL DESIGN SPECIFICATIONS  
✓ If analyzing → Include THE ACTUAL ANALYSIS with specific findings
✓ If drafting → Include THE ACTUAL DRAFT text

Example of WRONG response:
"I'll create the pitch deck content structure now."

Example of CORRECT response:
"Here is the complete pitch deck content structure:

SLIDE 1 - TITLE
• Headline: 'Unlock Your Experience Potential with Qualtrics'
• Subhead: 'Transform customer insights into measurable business growth'
• Visual: Hero image with Qualtrics dashboard preview

SLIDE 2 - AGENDA  
• Discovery: Understanding your current state
• Challenge: The experience gap costing you revenue
• Solution: How Qualtrics bridges that gap
• ROI: Projected 40% improvement in satisfaction scores
• Next Steps: 90-day pilot program proposal

SLIDE 3 - VALUE PROPOSITION
[...continues with FULL content for every slide...]"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REQUIRED JSON RESPONSE FORMAT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

RESPOND WITH EXACTLY ONE JSON OBJECT. NO TEXT BEFORE OR AFTER. NO CODE FENCES.

{{
  "send_to": "<recipient: agent_name | USER | ALL>",
  "visible_reasoning": "<2-3 paragraphs of structured thinking about value, uncertainties, next steps>",
  "collaboration": "<specific requests to teammates OR empty string if none needed>",
  "content": "<THE ACTUAL DELIVERABLE - full content, not a summary or plan>"
}}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIELD REQUIREMENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

visible_reasoning (REQUIRED - 2-3 paragraphs):
Structure your thinking clearly:
• Paragraph 1: What do I understand about this task? What's the high-value action?
• Paragraph 2: What uncertainties exist? What assumptions am I making?
• Paragraph 3: What's my approach and why is it the right one?

collaboration (when needed):
• Specific requests: "@[teammate]: I need [exact thing]"
• Challenges: "Your assumption about X doesn't account for Y"
• Dependencies: "Blocked on [specific thing] from [specific person]"

content (REQUIRED - this is the MAIN OUTPUT):
⚠️ THIS MUST CONTAIN ACTUAL WORK PRODUCT ⚠️
• If writing content → THE FULL CONTENT TEXT
• If creating structure → THE COMPLETE STRUCTURE with all sections
• If designing → THE FULL DESIGN SPECIFICATIONS
• If analyzing → THE COMPLETE ANALYSIS with findings
• MINIMUM 300+ words of actual deliverable content
• NOT a summary, NOT a plan, NOT an acknowledgment

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EXAMPLE RESPONSE (note: no code fences, just pure JSON)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

{{"send_to": "USER", "visible_reasoning": "I'm tasked with creating a pitch deck for Qualtrics. The high-value action is to produce complete slide content that can be immediately used.\\n\\nUNCERTAINTY: I don't have specific customer data, so I'll use industry-standard metrics. ASSUMPTION: Target audience is enterprise decision-makers.\\n\\nMy approach is to create a complete 6-slide deck with full content for each slide.", "collaboration": "", "content": "# COMPLETE PITCH DECK CONTENT\\n\\n## SLIDE 1: TITLE\\nHeadline: Unlock Your Experience Potential\\nSubheadline: Transform customer insights into measurable business growth\\n\\n## SLIDE 2: THE CHALLENGE\\nHeadline: The Experience Gap Is Costing You Revenue\\nKey statistics: 86% of buyers will pay more for better CX; Companies lose $75B annually due to poor service\\n\\n## SLIDE 3: THE SOLUTION\\nHeadline: Qualtrics Experience Management Platform\\nValue pillars: Listen, Understand, Act\\n\\n## SLIDE 4: PROVEN RESULTS\\nCase study: Similar company achieved 40% increase in satisfaction, 25% reduction in churn\\n\\n## SLIDE 5: IMPLEMENTATION\\nYour 90-Day Success Path with weekly milestones\\n\\n## SLIDE 6: NEXT STEPS\\nSchedule your personalized demo"}}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
        
        # Get conversation history
        history = context.get_conversation_history(workspace_wide=True, max_messages=15)
        
        # Add shared plan context if available
        plan_context = ""
        if context.shared_plan:
            plan_context = f"\n\n📋 SHARED WORKSPACE PLAN:\n{context.shared_plan}"
        
        # Build user message
        user_message = f"""CONVERSATION HISTORY:
{history}{plan_context}

YOUR TURN - RESPOND WITH A SINGLE JSON OBJECT

CRITICAL FORMATTING RULES:
- Output ONLY a JSON object, nothing else
- Do NOT wrap in code fences or markdown
- Do NOT include any text before or after the JSON
- The JSON must have: send_to, visible_reasoning, collaboration, content

Your "content" field MUST contain actual work product (300+ words), not acknowledgments.
"""
        
        # Prepare tools for function calling if MCP is available
        tools_schema = None
        tool_name_mapping = {}
        if mcp_available and mcp_tools_list:
            tools_schema = []
            for tool in mcp_tools_list[:20]:
                tool_name = tool.get("name", "")
                tool_desc = tool.get("description", f"Tool: {tool_name}")
                tool_params = tool.get("inputSchema", {})
                
                anthropic_name = tool_name.replace(".", "_")
                tool_name_mapping[anthropic_name] = tool_name
                
                tools_schema.append({
                    "name": anthropic_name,
                    "description": tool_desc,
                    "input_schema": tool_params if tool_params else {
                        "type": "object",
                        "properties": {},
                        "required": []
                    }
                })
        
        # Call LLM
        try:
            if provider == "anthropic":
                request_params = {
                    "model": model,
                    "max_tokens": 4000,  # high limit for full json + deliverables
                    "system": system_prompt,
                    "messages": [{"role": "user", "content": user_message}],
                }
                
                if tools_schema:
                    request_params["tools"] = tools_schema
                
                response = await client.messages.create(**request_params)
                
                # Handle tool use
                if response.stop_reason == "tool_use":
                    for block in response.content:
                        if block.type == "tool_use":
                            anthropic_tool_name = block.name
                            tool_name_used = tool_name_mapping.get(anthropic_tool_name, anthropic_tool_name)
                            tool_input = block.input
                            
                            try:
                                tool_result = await context.mcp.call_tool(tool_name_used, tool_input)
                                
                                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",
                                    "visible_reasoning": f"Used tool {tool_name_used} with parameters {json.dumps(tool_input)}. This was chosen because it directly accomplishes the requested action.",
                                    "collaboration": "",
                                    "content": f"✅ Tool {tool_name_used} executed: {str(result_summary)[:300]}",
                                    "action_intent": "use_tool",
                                    "tool_result": tool_result
                                }
                            except Exception as e:
                                logger.error(f"Tool call failed: {e}")
                                return {
                                    "send_to": latest.from_agent or "USER",
                                    "visible_reasoning": f"Attempted to use tool {tool_name_used} but it failed. Error: {str(e)}. Need to consider alternative approaches.",
                                    "collaboration": "",
                                    "content": f"❌ Tool {tool_name_used} failed: {str(e)}",
                                    "action_intent": "error"
                                }
                
                # Extract text response
                raw_output = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        raw_output += block.text
                raw_output = raw_output.strip()
                
                if not raw_output:
                    logger.warning(f"Agent {agent_name}: Empty LLM response")
                    return None
            
            elif provider == "openai":
                response = await client.chat.completions.create(
                    model=model,
                    max_tokens=4000,
                    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")
            
            # enforce structured output with robust json parsing
            structured_output = enforce_structured_output(raw_output, agent_name)
            
            # check for format errors and attempt repair if needed
            format_retries = 0
            while structured_output.get("_format_error") and format_retries < MAX_FORMAT_RETRIES:
                format_retries += 1
                logger.info(f"agent {agent_name}: attempting format repair (attempt {format_retries})")
                
                repair_prompt = create_format_repair_prompt(raw_output)
                try:
                    if provider == "anthropic":
                        repair_response = await client.messages.create(
                            model=model,
                            max_tokens=4000,
                            system=repair_prompt,
                            messages=[{"role": "user", "content": "reformat your previous response as valid json now."}],
                        )
                        repair_output = ""
                        for block in repair_response.content:
                            if hasattr(block, "text"):
                                repair_output += block.text
                        raw_output = repair_output.strip()
                    else:
                        repair_response = await client.chat.completions.create(
                            model=model,
                            max_tokens=4000,
                            messages=[
                                {"role": "system", "content": repair_prompt},
                                {"role": "user", "content": "reformat your previous response as valid json now."}
                            ],
                            response_format={"type": "json_object"},
                        )
                        raw_output = repair_response.choices[0].message.content.strip()
                    
                    structured_output = enforce_structured_output(raw_output, agent_name)
                except Exception as e:
                    logger.warning(f"format repair attempt failed: {e}")
                    break
            
            # check for shallow reasoning and attempt depth challenge if needed
            depth_retries = 0
            while structured_output.get("_needs_challenge") and depth_retries < MAX_DEPTH_RETRIES:
                depth_retries += 1
                issues = structured_output.get("_reasoning_issues", [])
                logger.info(f"agent {agent_name}: depth challenge (attempt {depth_retries}), issues: {issues[:2]}")
                
                challenge_prompt = create_depth_challenge_prompt(issues)
                try:
                    if provider == "anthropic":
                        challenge_response = await client.messages.create(
                            model=model,
                            max_tokens=4000,
                            system=system_prompt,
                            messages=[
                                {"role": "user", "content": user_message},
                                {"role": "assistant", "content": raw_output},
                                {"role": "user", "content": challenge_prompt}
                            ],
                        )
                        challenge_output = ""
                        for block in challenge_response.content:
                            if hasattr(block, "text"):
                                challenge_output += block.text
                        raw_output = challenge_output.strip()
                    else:
                        challenge_response = await client.chat.completions.create(
                            model=model,
                            max_tokens=4000,
                            messages=[
                                {"role": "system", "content": system_prompt},
                                {"role": "user", "content": user_message},
                                {"role": "assistant", "content": raw_output},
                                {"role": "user", "content": challenge_prompt}
                            ],
                            response_format={"type": "json_object"},
                        )
                        raw_output = challenge_response.choices[0].message.content.strip()
                    
                    structured_output = enforce_structured_output(raw_output, agent_name)
                except Exception as e:
                    logger.warning(f"depth challenge attempt failed: {e}")
                    break
            
            # clean up and format for frontend
            structured_output = format_content_with_reasoning(structured_output)
            
            return structured_output
            
        except Exception as e:
            logger.error(f"Error in agent {agent_name} logic: {e}")
            return {
                "send_to": latest.from_agent or "USER",
                "visible_reasoning": f"Error occurred during processing: {str(e)}",
                "collaboration": "",
                "content": 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

