"""
Agent runtime core implementation.

Provides Agent base class with planning, execution, tool calling,
delegation, memory, caching, and auto-registration on message bus.
"""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
import hashlib
import json
import time
from typing import Any
import uuid
import logging

from .config import AgentConfig, BackendFactory, EvenAgeConfig
from .tooling import ToolRegistry, discover_tools
import os

# Module logger
logger = logging.getLogger(__name__)


@dataclass
class AgentMemory:
    """Memory interface for agents."""

    agent_name: str
    database: Any  # DatabaseService
    job_id: str | None = None

    def put(self, key: str, value: Any) -> None:
        """Store a memory entry."""
        self.database.memory_put(self.agent_name, key, value, self.job_id)

    def get(self, key: str) -> Any:
        """Retrieve a memory entry."""
        return self.database.memory_get(self.agent_name, key, self.job_id)

    def list(self) -> dict[str, Any]:
        """List all memory entries."""
        return self.database.memory_list(self.agent_name, self.job_id)


class Agent:
    """
    Base Agent class with full runtime capabilities.
    
    Provides:
    - Planning and execution
    - Tool calling with validation and caching
    - Delegation to other agents via message bus
    - Memory storage
    - Auto-registration on message bus
    - Trace logging to database
    
    Subclasses should override plan() method for custom planning logic.
    """

    # Class-level metadata (set by @actor decorator or subclass)
    ROLE: str = "agent"
    GOAL: str = "Complete assigned tasks"

    def __init__(
        self,
        config: AgentConfig,
        env_config: EvenAgeConfig | None = None,
        tools: ToolRegistry | None = None
    ):
        """
        Initialize agent.
        
        Args:
            config: Agent-specific configuration
            env_config: Environment configuration (loaded if None)
            tools: Tool registry (auto-discovered if None)
        """
        self.config = config
        self.env_config = env_config or EvenAgeConfig()

        # Create backends
        self.factory = BackendFactory(self.env_config)
        self.bus = self.factory.create_queue_backend()
        self.database = self.factory.create_database_backend()
        self.llm = self.factory.create_llm_backend()
        self.cache_backend = self.factory.create_cache_backend()
        self.storage = self.factory.create_storage_backend()

        # Tool registry - start with user tools, then add system tools
        self.tools = tools or discover_tools(config.name)

        # Add system tools for delegation and artifact storage
        self._register_system_tools()

        # Memory interface
        self.memory: AgentMemory | None = None

        # Current job context
        self.current_job_id: str | None = None

        # Heartbeat task
        self._heartbeat_task: asyncio.Task | None = None

        # Coordinator support
        self.is_coordinator = getattr(self.env_config, "has_coordinator", False) and (
            self.config.name == getattr(self.env_config, "coordinator_agent", "coordinator")
        )
        self.coordinator_agent = (
            getattr(self.env_config, "coordinator_agent", "coordinator")
            if getattr(self.env_config, "has_coordinator", False)
            else None
        )

    def _register_system_tools(self):
        """Register built-in system tools for delegation and artifacts."""
        from .system_tools import create_system_tools

        system_tools = create_system_tools(self.bus, self.storage)

        # Disallow any system tool that could bypass delegation safeguards
        reserved = {"delegate", "publish", "publish_task"}

        for name, func in system_tools.items():
            # Skip reserved/unsafe tool names
            if name in reserved:
                continue
            # Register each system tool
            if not self.tools.has(name):
                self.tools.register(func)

    async def connect_bus(self) -> None:
        """
        Connect to message bus and register agent.
        
        Starts heartbeat to maintain registration.
        """
        metadata = {
            "role": self.config.role or self.ROLE,
            "goal": self.config.goal or self.GOAL,
            "tools": self.tools.list_names(),
            "status": "active",
            "max_iterations": self.config.max_iterations,
            "allow_delegation": self.config.allow_delegation
        }

        await self.bus.register_agent(self.config.name, metadata)
        self.database.register_agent(self.config.name, metadata)

        # Start heartbeat
        if self._heartbeat_task is None:
            self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())

    async def _heartbeat_loop(self) -> None:
        """Periodic heartbeat to keep agent registered."""
        while True:
            try:
                await asyncio.sleep(30)  # Every 30 seconds
                metadata = {
                    "role": self.config.role,
                    "goal": self.config.goal,
                    "tools": self.tools.list_names(),
                    "status": "active"
                }
                await self.bus.register_agent(self.config.name, metadata)
            except asyncio.CancelledError:
                break
            except Exception:
                continue

    async def shutdown(self) -> None:
        """Shutdown agent and cleanup."""
        if self._heartbeat_task:
            self._heartbeat_task.cancel()
            try:
                await self._heartbeat_task
            except asyncio.CancelledError:
                pass

        # Update status to inactive
        metadata = {
            "role": self.config.role,
            "goal": self.config.goal,
            "tools": self.tools.list_names(),
            "status": "inactive"
        }
        await self.bus.register_agent(self.config.name, metadata)

    def plan(self, task: dict) -> list[dict]:
        """
        Create execution plan for a task.
        
        Default implementation: simple single-step plan.
        Subclasses should override for custom planning logic.
        
        Args:
            task: Task inputs
        
        Returns:
            List of steps, where each step is a dict with:
            - action: "tool" | "delegate" | "llm" | "custom"
            - Additional fields depending on action type
        """
        # Default: single LLM call step
        return [
            {
                "action": "llm",
                "prompt": f"Task: {json.dumps(task)}",
                "system": f"You are {self.config.role}. Your goal: {self.config.goal}"
            }
        ]

    async def handle(self, task: dict) -> dict:
        """
        Handle a task: plan, execute, return result.
        
        This is the main entry point for task execution.
        
        By default, uses autonomous_run() for intelligent, LLM-driven execution.
        Override plan() method if you need custom hardcoded plans instead.
        
        Args:
            task: Task inputs
        
        Returns:
            Structured result dict with status and output
        """
        # Set up job context (support both job_id and prompt_id for compatibility)
        execution_id = task.get("prompt_id") or task.get("job_id") or str(uuid.uuid4())
        self.current_job_id = execution_id  # Keep internal name for now

        self.memory = AgentMemory(
            agent_name=self.config.name,
            database=self.database,
            job_id=execution_id
        )

        # Trace start (log as prompt_id for new terminology)
        self.database.append_trace(
            execution_id,
            self.config.name,
            "task_start",
            {"inputs": task}
        )

        try:
            # Check if plan() was overridden (custom planning logic)
            # If plan() is default implementation, use autonomous_run instead
            plan_method = self.__class__.plan
            base_plan_method = Agent.plan
            
            if plan_method is base_plan_method:
                # Use autonomous agent loop (intelligent, LLM-driven)
                result = await self.autonomous_run(task)
            else:
                # Use custom plan (backward compatibility)
                plan = self.plan(task)
                
                self.database.append_trace(
                    execution_id,
                    self.config.name,
                    "plan_created",
                    {"plan": plan}
                )
                
                result = await self.execute_plan(plan, task)

            # Trace completion
            self.database.append_trace(
                execution_id,
                self.config.name,
                "task_complete",
                {"result": result}
            )

            # Auto-route result to coordinator if applicable
            if not self.is_coordinator and self.coordinator_agent:
                # Send result back to coordinator for synthesis
                try:
                    await self.bus.publish_task(
                        self.coordinator_agent,
                        {
                            "type": "specialist_result",
                            "source_agent": self.config.name,
                            "job_id": execution_id,
                            "result": result
                        }
                    )
                except Exception:
                    # Non-fatal: continue even if coordinator routing fails
                    pass

            return {
                "status": "success",
                "result": result.get("result") if isinstance(result, dict) else result,
                "job_id": execution_id,  # Keep for backward compatibility
                "prompt_id": execution_id  # Add new terminology
            }

        except Exception as e:
            # Trace error
            self.database.append_trace(
                execution_id,
                self.config.name,
                "task_error",
                {"error": str(e)}
            )

            return {
                "status": "error",
                "error": str(e),
                "job_id": execution_id,
                "prompt_id": execution_id
            }

    async def execute_plan(self, plan: list[dict], task: dict) -> Any:
        """
        Execute a plan.
        
        Iterates through steps and executes each based on action type.
        
        Args:
            plan: List of steps
            task: Original task inputs
        
        Returns:
            Final result (from last step)
        """
        context = {"task": task}
        result = None

        for i, step in enumerate(plan):
            action = step.get("action")

            if action == "tool":
                result = await self._execute_tool_step(step, context)
            elif action == "delegate":
                result = await self._execute_delegate_step(step, context)
            elif action == "llm":
                result = await self._execute_llm_step(step, context)
            elif action == "custom":
                result = await self._execute_custom_step(step, context)
            else:
                result = {"error": f"Unknown action: {action}"}

            context[f"step_{i}"] = result
            context["last"] = result

        return result

    async def _execute_tool_step(self, step: dict, context: dict) -> Any:
        """Execute a tool call step."""
        tool_name = step.get("name")
        params = step.get("params", {})
        use_cache = step.get("use_cache", True)

        return await self.call_tool(tool_name, params, use_cache=use_cache)

    async def _execute_delegate_step(self, step: dict, context: dict) -> Any:
        """Execute a delegation step."""
        agent_name = step.get("agent")
        payload = step.get("payload", {})

        task_id = await self.delegate(agent_name, payload)

        # Optionally wait for response (blocking)
        wait = step.get("wait", False)
        if wait:
            timeout = step.get("timeout", 30)
            response = await self.bus.wait_for_response(task_id, timeout)
            return response or {"status": "pending", "task_id": task_id}

        return {"status": "delegated", "task_id": task_id}

    async def _execute_llm_step(self, step: dict, context: dict) -> Any:
        """Execute an LLM call step."""
        prompt = step.get("prompt", "")
        system = step.get("system")

        # Simple string replacement for context variables
        for key, value in context.items():
            try:
                rep = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
            except Exception:
                rep = str(value)
            prompt = prompt.replace(f"{{{key}}}", rep)

        response = await self.llm.generate(prompt, system=system)
        return {"llm_response": response}

    async def _execute_custom_step(self, step: dict, context: dict) -> Any:
        """Execute a custom step (override in subclass)."""
        return {"error": "Custom step not implemented"}

    async def call_tool(
        self,
        name: str,
        params: dict[str, Any],
        use_cache: bool = True
    ) -> Any:
        """
        Call a tool with validation, guardrails, and caching.
        
        Args:
            name: Tool name
            params: Tool parameters
            use_cache: Whether to use cache
        
        Returns:
            Tool execution result
        """
        tool = self.tools.get(name)
        if not tool:
            raise ValueError(f"Tool not found: {name}")

        # Cache key
        cache_key = self._tool_cache_key(name, params)

        # Check cache
        if use_cache:
            cached = await self._get_from_cache(cache_key)
            if cached is not None:
                self.database.append_trace(
                    self.current_job_id or "unknown",
                    self.config.name,
                    "cache_hit",
                    {"tool": name, "params": params}
                )
                return cached

        # Validate inputs
        try:
            validated_params = tool.validate_inputs(params)
        except Exception as e:
            raise ValueError(f"Tool input validation failed: {e}")

        # Trace tool call
        self.database.append_trace(
            self.current_job_id or "unknown",
            self.config.name,
            "tool_call",
            {"tool": name, "params": validated_params}
        )

        # Execute
        start = time.time()
        try:
            result = tool.invoke(**validated_params)
            duration_ms = int((time.time() - start) * 1000)

            self.database.append_trace(
                self.current_job_id or "unknown",
                self.config.name,
                "tool_result",
                {
                    "tool": name,
                    "duration_ms": duration_ms,
                    "status": "success",
                    # Store both a lightweight preview and the full result when reasonable
                    "result_preview": str(result)[:500] if result is not None else None,
                    "result": result
                }
            )

            # Cache result
            if use_cache:
                await self._put_in_cache(cache_key, result, ttl=300)

            return result

        except Exception as e:
            self.database.append_trace(
                self.current_job_id or "unknown",
                self.config.name,
                "tool_error",
                {"tool": name, "error": str(e)}
            )
            raise

    async def delegate(self, agent_name: str, payload: dict) -> str:
        """
        Delegate a task to another agent.
        
        Args:
            agent_name: Target agent name
            payload: Task payload
        
        Returns:
            Task ID for tracking
        """
        if not self.config.allow_delegation:
            raise ValueError("Delegation not allowed for this agent")

        # Strict validation: only delegate to active, registered agents
        try:
            # message_bus exposes get_registered_agents() -> dict[name] = metadata
            registry = await self.bus.get_registered_agents()
            active = {name for name, meta in registry.items() if meta.get("status") == "active"}
        except Exception:
            active = set()

        if agent_name not in active:
            raise ValueError(
                f"Delegation target '{agent_name}' is not active or does not exist. "
                f"Available: {sorted(list(active))}"
            )

        # CRITICAL: Propagate job_id to maintain single job context across multi-agent execution
        # This ensures all traces from delegated agents belong to the same job
        if self.current_job_id and "job_id" not in payload:
            payload = {**payload, "job_id": self.current_job_id}

        task_id = await self.bus.publish_task(agent_name, payload)

        self.database.append_trace(
            self.current_job_id or "unknown",
            self.config.name,
            "delegation",
            {
                "target_agent": agent_name,
                "task_id": task_id,
                "payload": payload
            }
        )

        return task_id

    def _tool_cache_key(self, name: str, params: dict) -> str:
        """Generate cache key for tool call."""
        param_str = json.dumps(params, sort_keys=True)
        key_data = f"{name}:{param_str}"
        return f"tool:{hashlib.md5(key_data.encode()).hexdigest()}"

    async def _get_from_cache(self, key: str) -> Any:
        """Get value from cache backend."""
        try:
            if hasattr(self.cache_backend, 'get') and asyncio.iscoroutinefunction(self.cache_backend.get):
                return await self.cache_backend.get(key)
            return self.cache_backend.get(key)
        except Exception:
            return None

    async def _put_in_cache(self, key: str, value: Any, ttl: int | None = None) -> None:
        """Put value in cache backend."""
        try:
            if hasattr(self.cache_backend, 'set') and asyncio.iscoroutinefunction(self.cache_backend.set):
                await self.cache_backend.set(key, value, ttl)
            else:
                self.cache_backend.set(key, value, ttl)
        except Exception:
            pass

    async def autonomous_run(self, task: dict, max_iterations: int | None = None) -> dict:
        """
        Run agent autonomously using ReAct-style loop.
        
        The LLM decides what actions to take (use tools, delegate, or finish).
        This is the simple, intuitive way to create agents - no manual planning needed.
        
        Args:
            task: Task inputs
            max_iterations: Override max iterations from config
        
        Returns:
            Final result dict
        """
        max_iter = max_iterations or self.config.max_iterations or 5  # Reduced to 5 with forced finish at 4 tools
        
        # Build context
        task_description = (
            task.get('query')
            or task.get('message')
            or task.get('task')
            or task.get('description')
            or json.dumps(task)
        )
        
        # Available actions
        available_tools = self.tools.list_names()
        can_delegate = self.config.allow_delegation
        
        # Build available agents list for delegation
        available_agents = []
        if can_delegate:
            try:
                agents_info = await self.bus.list_agents()
                available_agents = [
                    a for a in agents_info 
                    if a.get("name") != self.config.name and a.get("status") == "active"
                ]
            except Exception:
                pass
        
        # Track conversation history
        history = []
        
        # Query deduplication: track normalized tool calls to prevent redundant searches
        seen_queries = set()

        for iteration in range(max_iter):
            # Cooperative cancellation: check before each iteration
            try:
                if self.current_job_id and hasattr(self.bus, "is_canceled"):
                    canceled = await self.bus.is_canceled(self.current_job_id)
                    if canceled:
                        self.database.append_trace(
                            self.current_job_id or "unknown",
                            self.config.name,
                            "task_cancelled",
                            {"iteration": iteration}
                        )
                        return {
                            "status": "canceled",
                            "result": "Execution canceled by user.",
                            "iterations": iteration,
                            "history": history
                        }
            except Exception:
                # If cancel check fails, continue execution
                pass
            # Build system prompt
            system_prompt = self._build_autonomous_system_prompt(
                available_tools, 
                available_agents,
                can_delegate
            )
            
            # Build user prompt with history
            user_prompt = self._build_autonomous_user_prompt(
                task_description,
                history,
                iteration
            )
            
            # Get LLM decision
            response = await self.llm.generate(user_prompt, system=system_prompt)
            logger.debug("[%s] LLM response: %s", self.config.name, str(response)[:500])
            
            # Trace LLM thinking
            self.database.append_trace(
                self.current_job_id or "unknown",
                self.config.name,
                "autonomous_think",
                {"iteration": iteration, "response": response}
            )
            
            # Parse action from response
            action = self._parse_autonomous_action(response)
            # Log parsed action for debugging
            logger.info(
                "[%s] iteration %d action=%s details=%s",
                self.config.name,
                iteration,
                action.get("type"),
                {k: v for k, v in action.items() if k != "answer"}
            )
            
            if action["type"] == "finish":
                # Agent is done - check if it's actually a successful completion or an error
                answer = action.get("answer", response)
                
                # Detect if the agent is reporting inability to complete the task
                error_indicators = [
                    "unable to complete",
                    "cannot complete",
                    "missing",
                    "not set",
                    "not functional",
                    "failed due to",
                    "error:",
                    "cannot proceed",
                    "not available"
                ]
                
                answer_lower = str(answer).lower()
                is_error = any(indicator in answer_lower for indicator in error_indicators)
                
                # Also check if all tool calls failed
                tool_calls = [h for h in history if h.get("action") == "tool"]
                all_tools_failed = len(tool_calls) > 0 and all("error" in h for h in tool_calls)
                
                if is_error or all_tools_failed:
                    return {
                        "status": "error",
                        "error": answer,
                        "iterations": iteration + 1,
                        "history": history
                    }
                
                return {
                    "status": "success",
                    "result": answer,
                    "iterations": iteration + 1
                }
            
            elif action["type"] == "tool":
                # Execute tool
                tool_name = action.get("tool")
                tool_params = action.get("params", {})
                
                # Query deduplication: create normalized key from tool+params
                query_key = f"{tool_name}:{json.dumps(tool_params, sort_keys=True)}"
                is_duplicate = False
                
                # For web_search, use aggressive deduplication to catch semantic duplicates
                if tool_name == "web_search" and "query" in tool_params:
                    normalized_query = tool_params["query"].lower().strip()
                    # Remove common filler words to catch semantic similarity
                    filler_words = ["the", "a", "an", "and", "or", "but", "full", "complete", "official", "list"]
                    query_words = set(word for word in normalized_query.split() if word not in filler_words)
                    
                    # Check if we've seen a very similar query before (>70% word overlap)
                    for seen in seen_queries:
                        if seen.startswith("web_search:"):
                            # Extract query from seen key: "web_search:{'query': '...'}"
                            try:
                                seen_params = json.loads(seen.split(":", 1)[1])
                                seen_query = seen_params.get("query", "").lower().strip()
                                seen_words = set(word for word in seen_query.split() if word not in filler_words)
                                
                                # Calculate word overlap
                                if query_words and seen_words:
                                    overlap = len(query_words & seen_words) / len(query_words | seen_words)
                                    if overlap > 0.7:  # 70% similarity threshold
                                        logger.info(
                                            "[%s] Skipping semantic duplicate search (%.0f%% similar): '%s' vs '%s'", 
                                            self.config.name,
                                            overlap * 100,
                                            tool_params["query"],
                                            seen_query
                                        )
                                        history.append({
                                            "action": "tool",
                                            "tool": tool_name,
                                            "params": tool_params,
                                            "result": f"Skipped: semantically similar to previous query '{seen_query[:50]}...'"
                                        })
                                        is_duplicate = True
                                        break
                            except:
                                pass
                
                if not is_duplicate:
                    seen_queries.add(query_key)
                    
                    try:
                        logger.info("[%s] calling tool=%s params=%s", self.config.name, tool_name, tool_params)
                        tool_result = await self.call_tool(tool_name, tool_params)
                        logger.info("[%s] tool=%s result=%s", self.config.name, tool_name, str(tool_result)[:200])
                        history.append({
                            "action": "tool",
                            "tool": tool_name,
                            "params": tool_params,
                            "result": tool_result
                        })
                    except Exception as e:
                        logger.exception("[%s] tool error for %s: %s", self.config.name, tool_name, e)
                        history.append({
                            "action": "tool",
                            "tool": tool_name,
                            "params": tool_params,
                            "error": str(e)
                        })
            
            elif action["type"] == "delegate":
                # Delegate to another agent
                agent_name = action.get("agent")
                delegate_task = action.get("task", task_description)
                logger.info("[%s] delegating to %s: %s...", self.config.name, agent_name, delegate_task[:120])
                # Validate target agent against available agents list
                allowed_agents = {a.get("name") for a in (available_agents or [])}
                if not agent_name or agent_name not in allowed_agents:
                    msg = (
                        f"Invalid delegation target '{agent_name}'. "
                        f"Choose one of: {sorted(list(allowed_agents)) or ['<no active agents>']}"
                    )
                    logger.warning("[%s] %s", self.config.name, msg)
                    history.append({
                        "action": "delegate",
                        "agent": agent_name,
                        "task": delegate_task,
                        "error": msg
                    })
                    # Continue loop to let the LLM choose a valid agent next
                    continue

                try:
                    task_id = await self.delegate(agent_name, {"query": delegate_task})
                    logger.info("[%s] delegation task_id=%s; waiting for response...", self.config.name, task_id)
                    
                    # Wait for result
                    timeout = action.get("timeout", 60)
                    result = await self.bus.wait_for_response(task_id, timeout)
                    logger.info("[%s] delegation result received: %s", self.config.name, str(result)[:200])
                    
                    # CRITICAL: Check if delegated task actually succeeded
                    # Don't blindly retry if the delegated agent couldn't complete it
                    result_status = None
                    result_data = None
                    
                    if isinstance(result, dict):
                        result_status = result.get("status")
                        result_data = result.get("result") or result.get("error")
                    
                    history.append({
                        "action": "delegate",
                        "agent": agent_name,
                        "task": delegate_task,
                        "result": result,
                        "status": result_status
                    })
                    
                    # If delegated task failed or was incomplete, finish with that information
                    # Don't keep retrying - it will just fail again
                    if result_status in ["error", "incomplete"]:
                        logger.warning(
                            "[%s] Delegated task to %s returned status=%s, stopping to avoid retry loop",
                            self.config.name,
                            agent_name,
                            result_status
                        )
                        return {
                            "status": "error",
                            "error": f"Delegated task to {agent_name} failed: {result_data}",
                            "iterations": iteration + 1,
                            "history": history
                        }
                    
                except Exception as e:
                    logger.exception("[%s] delegation error: %s", self.config.name, e)
                    history.append({
                        "action": "delegate",
                        "agent": agent_name,
                        "task": delegate_task,
                        "error": str(e)
                    })
                    # Delegation failed completely - don't retry
                    return {
                        "status": "error",
                        "error": f"Failed to delegate to {agent_name}: {str(e)}",
                        "iterations": iteration + 1,
                        "history": history
                    }
            
            else:
                # Unknown action - treat as thinking
                history.append({
                    "action": "think",
                    "thought": response
                })
            
            # Sufficiency heuristic: if we have enough tool results, suggest finishing
            # Count successful tool calls with actual results (excluding duplicates)
            successful_tool_calls = [
                h for h in history 
                if h.get("action") == "tool" 
                and "result" in h 
                and h["result"] not in [None, ""]
                and not str(h["result"]).startswith("Skipped:")
                and "error" not in h
            ]
            
            # CRITICAL: Force finish after 4 successful tool calls to prevent infinite loops
            if len(successful_tool_calls) >= 4:
                logger.warning(
                    "[%s] FORCING FINISH: %d successful tool calls completed. "
                    "Preventing excessive iterations.",
                    self.config.name,
                    len(successful_tool_calls)
                )
                return {
                    "status": "success",
                    "result": "Task completed with sufficient information gathered. Agent was stopped to prevent excessive iterations.",
                    "iterations": iteration + 1,
                    "history": history,
                    "forced_completion": True
                }
            
            # Soft hint after 2 successful calls
            if len(successful_tool_calls) >= 2:
                logger.info(
                    "[%s] Sufficiency check: %d successful tool calls completed. "
                    "Strongly suggesting to finish the task in next iteration.",
                    self.config.name,
                    len(successful_tool_calls)
                )
                # Add a STRONG hint in history for next LLM call
                history.append({
                    "action": "system_hint",
                    "message": (
                        f"IMPORTANT: You have gathered {len(successful_tool_calls)} pieces of information. "
                        "This is sufficient for most tasks. You MUST finish with a comprehensive answer in the next response. "
                        "Do NOT make additional tool calls unless absolutely critical information is missing."
                    )
                })
        
        # Max iterations reached
        return {
            "status": "incomplete",
            "result": "Maximum iterations reached. Unable to complete task.",
            "iterations": max_iter,
            "history": history
        }
    
    def _build_autonomous_system_prompt(
        self,
        available_tools: list[str],
        available_agents: list[dict],
        can_delegate: bool,
    ) -> str:
        """Build system prompt for autonomous agent."""
        # Optional: user-provided coordinator/agent prompt lives at agents/<name>/prompt.md
        custom_prompt = None
        try:
            prompt_path = os.path.join(os.getcwd(), "agents", self.config.name, "prompt.md")
            if os.path.isfile(prompt_path):
                with open(prompt_path, "r", encoding="utf-8") as f:
                    custom_prompt = f.read().strip()
        except Exception:
            custom_prompt = None

        tool_list = "\n".join([f"- {t}" for t in available_tools]) if available_tools else "None"

        agent_list = "None"
        if can_delegate and available_agents:
            agent_list = "\n".join(
                [
                    f"- {a['name']}: {a.get('role', 'N/A')} - {a.get('goal', 'N/A')}"
                    for a in available_agents
                ]
            )

        header = f"You are {self.config.role}.\nYour goal: {self.config.goal}"

        if custom_prompt:
            header += "\n\nAgent Policy (from prompt.md):\n" + custom_prompt

        system = f"""{header}

You act autonomously to complete the task. Always respond with a SINGLE JSON object only (no prose, no code fences).
Schema:
{{
  "type": "tool|delegate|finish|think",
  "tool": "<tool_name_if_type_tool>",
  "params": {{"param1": "value1"}},
  "agent": "<agent_name_if_type_delegate>",
  "task": "<task_text_if_type_delegate>",
  "timeout": 60,
  "answer": "<final_answer_if_type_finish>"
}}

## Available Tools:
{tool_list}

If you need to use a tool:
{{"type":"tool","tool":"<tool_name>","params":{{"param":"value"}}}}
"""

        if can_delegate:
            system += f"""
## Available Agents (for delegation):
{agent_list}

If you need to delegate:
{{"type":"delegate","agent":"<agent_name>","task":"<what should they do>","timeout":90}}

Rules for delegation:
- Only use agent names from the list above; do NOT invent new agent names.
- If none of the listed agents is appropriate, do not delegate; use tools or finish instead.
"""

        system += """
When you are done:
{"type":"finish","answer":"<final comprehensive answer>"}

## Important Guidelines:
- Avoid redundant tool calls - don't search for the same information multiple times
- After gathering 2-3 reliable sources, you likely have enough information to finish
- Quality over quantity - focus on synthesizing what you've found rather than endlessly searching
- If you see a system hint about having sufficient information, strongly consider finishing

Think step by step, but output JSON only. Do not include THOUGHT or any extra text; just the JSON object."""

        return system
    
    def _build_autonomous_user_prompt(
        self,
        task_description: str,
        history: list[dict],
        iteration: int
    ) -> str:
        """Build user prompt with task and history."""
        
        prompt = f"TASK: {task_description}\n\n"
        
        if history:
            prompt += "HISTORY:\n"
            for i, entry in enumerate(history):
                if entry["action"] == "tool":
                    prompt += f"{i+1}. Used tool '{entry['tool']}' with params {entry.get('params', {})}\n"
                    if "error" in entry:
                        prompt += f"   ERROR: {entry['error']}\n"
                    else:
                        result_str = str(entry.get('result', ''))[:500]
                        prompt += f"   RESULT: {result_str}\n"
                
                elif entry["action"] == "delegate":
                    prompt += f"{i+1}. Delegated to agent '{entry['agent']}': {entry.get('task', 'N/A')}\n"
                    if "error" in entry:
                        prompt += f"   ERROR: {entry['error']}\n"
                    else:
                        result_str = str(entry.get('result', ''))[:500]
                        prompt += f"   RESULT: {result_str}\n"
                
                elif entry["action"] == "think":
                    prompt += f"{i+1}. THOUGHT: {entry.get('thought', '')[:200]}\n"
                
                elif entry["action"] == "system_hint":
                    prompt += f"\n⚠️ SYSTEM: {entry.get('message', '')}\n\n"
            
            prompt += "\n"
        
        prompt += f"What should you do next? (Iteration {iteration + 1})\n"
        
        return prompt
    
    def _parse_autonomous_action(self, response: str) -> dict:
        """Parse LLM response to extract action, preferring JSON format with text fallback."""
        # 1) Try JSON extraction anywhere in the response
        try:
            start = response.find("{")
            end = response.rfind("}")
            if start != -1 and end != -1 and end > start:
                payload = response[start : end + 1]
                data = json.loads(payload)
                # Normalize keys
                norm = {str(k).lower(): v for k, v in data.items()}
                action_type = norm.get("type") or norm.get("action") or "think"
                action_type = str(action_type).lower()
                # Map common aliases
                if action_type == "tool":
                    tool = norm.get("tool") or norm.get("tool_name")
                    params = norm.get("params") or {}
                    return {"type": "tool", "tool": tool, "params": params}
                if action_type == "delegate":
                    agent = norm.get("agent") or norm.get("target")
                    task = norm.get("task") or norm.get("query") or norm.get("message")
                    timeout = norm.get("timeout") or 60
                    return {"type": "delegate", "agent": agent, "task": task, "timeout": timeout}
                if action_type == "finish":
                    answer = norm.get("answer") or norm.get("result") or response
                    return {"type": "finish", "answer": answer}
                # Default think
                return {"type": "think"}
        except Exception:
            pass

        # 2) Fallback to text parser for legacy format
        return self._parse_autonomous_action_text(response)

    def _parse_autonomous_action_text(self, response: str) -> dict:
        """Parse legacy text format with ACTION:/AGENT:/TOOL_NAME:/... lines."""
        lines = response.strip().split("\n")
        action_type = None
        data: dict[str, Any] = {}
        for line in lines:
            line = line.strip()
            if line.startswith("ACTION:"):
                action_str = line.replace("ACTION:", "").strip().upper()
                if "TOOL" in action_str:
                    action_type = "tool"
                elif "DELEGATE" in action_str:
                    action_type = "delegate"
                elif "FINISH" in action_str:
                    action_type = "finish"
            elif line.startswith("TOOL_NAME:"):
                data["tool"] = line.replace("TOOL_NAME:", "").strip()
            elif line.startswith("PARAMS:"):
                params_str = line.replace("PARAMS:", "").strip()
                try:
                    data["params"] = json.loads(params_str)
                except Exception:
                    data["params"] = {}
            elif line.startswith("AGENT:"):
                data["agent"] = line.replace("AGENT:", "").strip()
            elif line.startswith("TASK:"):
                data["task"] = line.replace("TASK:", "").strip()
            elif line.startswith("ANSWER:"):
                # Capture everything after ANSWER:
                answer_index = response.find("ANSWER:")
                if answer_index != -1:
                    data["answer"] = response[answer_index + 7 :].strip()
                else:
                    data["answer"] = line.replace("ANSWER:", "").strip()
        return {"type": action_type or "think", **data}
