"""Main Promethian Agent - orchestrates planning, execution, and learning."""

from __future__ import annotations

import asyncio
import time
from typing import Any
from uuid import uuid4

from promethian.config import PromethianConfig, get_config
from promethian.core.types import (
    Skill, Tool, Plan, PlanStep, ExecutionResult, ExecutionStatus, ConfidenceLevel, ExecutionStrategy,
)
from promethian.core.planner import Planner
from promethian.core.executor import Executor
from promethian.core.context_gatherer import ContextGatherer, GatheredContext
from promethian.core.progress import (
    ProgressTracker, NullProgressTracker, ProgressCallback, ProgressStage,
)
from promethian.core.tool_cache import get_tool_cache
from promethian.cache import LocalCache
from promethian.learning import ToolBuilder, SkillLearner
from promethian.utils.claude import ClaudeClient
from promethian.utils.logging import init_logger, get_logger
from promethian.api.client import PromethianAPIClient


class PromethianAgent:
    """
    Main Promethian Agent - self-improving AI with centralized knowledge.

    The agent:
    1. Retrieves relevant skills/tools from central database
    2. Gathers context using tools (bash, web search, etc.)
    3. Plans tasks with confidence assessment
    4. Executes with self-healing
    5. Learns from successful executions
    6. Contributes back to the knowledge base
    """

    def __init__(
        self,
        config: PromethianConfig | None = None,
        enable_context_gathering: bool = True,
        progress_callback: ProgressCallback | None = None,
        feedback_getter: callable | None = None,
    ):
        self.config = config or get_config()
        self.enable_context_gathering = enable_context_gathering
        self._feedback_getter = feedback_getter

        # Initialize verbose logging
        self._logger = init_logger(self.config)

        # Progress tracking
        if progress_callback:
            self.progress = ProgressTracker(progress_callback)
        else:
            self.progress = NullProgressTracker()

        # Initialize Claude client
        self.claude = ClaudeClient(
            api_key=self.config.anthropic_api_key,
            model=self.config.model,
            max_tokens=self.config.max_tokens,
        )
        # Connect logger to claude client for API call logging
        self.claude.set_logger(self._logger)

        # Initialize components
        # Context gatherer uses its own Claude client with context_model (Haiku by default)
        self.context_gatherer = ContextGatherer(self.config)
        # Planner uses the main model for complex reasoning
        self.planner = Planner(self.config, self.claude)
        self.executor = Executor(
            self.config, self.claude,
            progress=self.progress,
            feedback_getter=feedback_getter,
        )
        self.tool_builder = ToolBuilder(self.claude)
        self.skill_learner = SkillLearner(self.claude)
        self.cache = LocalCache()

        # API client for centralized knowledge (initialized lazily)
        self._api_client: PromethianAPIClient | None = None
        self._session_task: str | None = None

    async def _get_api_client(self) -> PromethianAPIClient:
        """Get or create the API client."""
        if self._api_client is None:
            if not self.config.promethian_api_key:
                raise ValueError("PROMETHIAN_API_KEY required for API access")

            self._api_client = PromethianAPIClient(
                api_url=self.config.promethian_api_url,
                api_key=self.config.promethian_api_key,
            )

        return self._api_client

    def _is_simple_query(self, task: str) -> bool:
        """
        Detect if a task is a simple query that doesn't need tools or context.

        Simple queries are:
        - Short questions that can be answered from general knowledge
        - Math questions
        - Definition/explanation requests
        - Factual questions

        Not simple:
        - Tasks involving files, code, or specific systems
        - Tasks with action verbs like "create", "build", "deploy", etc.
        - Tasks mentioning specific tools or technologies
        """
        task_lower = task.lower().strip()

        # Follow-up question patterns - these need previous context
        followup_patterns = [
            # Explicit follow-ups
            task_lower.startswith("what about "),
            task_lower.startswith("how about "),
            task_lower.startswith("and "),
            task_lower.startswith("but "),
            task_lower.startswith("also "),
            task_lower.startswith("what else"),
            task_lower.startswith("anything else"),
            # Pronouns referring to previous context
            " they " in task_lower or task_lower.startswith("they "),
            " it " in task_lower or task_lower.startswith("it "),
            " them " in task_lower,
            " their " in task_lower,
            " its " in task_lower,
            " this " in task_lower or task_lower.startswith("this "),
            " that " in task_lower or task_lower.startswith("that "),
            " these " in task_lower,
            " those " in task_lower,
            # Comparative/temporal references
            "vs last" in task_lower,
            "compared to" in task_lower,
            "same thing" in task_lower,
            "the same" in task_lower,
            "this month" in task_lower,
            "this year" in task_lower,
            "last month" in task_lower,
            "last year" in task_lower,
        ]

        # Follow-up questions need context, not simple path
        if any(followup_patterns):
            return False

        # Action keywords that indicate complex tasks
        action_keywords = [
            "create", "build", "deploy", "install", "configure", "setup",
            "write", "code", "implement", "develop", "fix", "debug",
            "run", "execute", "test", "compile", "analyze", "scan",
            "file", "folder", "directory", "path", "repository", "repo",
            "download", "upload", "fetch", "pull", "push", "commit",
            "search for", "find the", "look up the",
            # Skill/tool references - NEVER simple
            "use the", "using the", "skill", "tool",
            # Web actions
            "scrape", "crawl", "browse", "web", "http", "url", "api",
            # Data actions
            "save", "store", "read", "load", "parse", "extract",
        ]

        # If task contains action keywords, it's not simple
        for keyword in action_keywords:
            if keyword in task_lower:
                return False

        # Current events / time references need web search, not simple
        current_event_indicators = [
            "yesterday", "today", "tonight", "this morning", "this evening",
            "last night", "last week", "this week", "recently", "latest",
            "current", "now", "right now", "just happened", "breaking",
            "news", "update", "score", "result", "match", "game",
            "stock", "price", "weather", "election", "poll",
        ]
        for indicator in current_event_indicators:
            if indicator in task_lower:
                return False

        # Simple query patterns
        simple_patterns = [
            # Questions
            task_lower.startswith("what is "),
            task_lower.startswith("what's "),
            task_lower.startswith("who is "),
            task_lower.startswith("who's "),
            task_lower.startswith("when "),
            task_lower.startswith("where "),
            task_lower.startswith("why "),
            task_lower.startswith("how much "),
            task_lower.startswith("how many "),
            # Math
            any(op in task for op in ["+", "-", "*", "/", "×", "÷", "plus", "minus", "times", "divided"]),
            "calculate" in task_lower and len(task) < 50,
            # Short factual
            len(task) < 60 and task.endswith("?"),
            # Capital/country questions
            "capital of" in task_lower,
            "population of" in task_lower,
        ]

        return any(simple_patterns)

    def _classify_task(self, task: str) -> str:
        """
        Classify task to determine optimal execution strategy.

        Returns:
            - "simple": Direct answer, no tools/context needed
            - "current_data": Needs web search/fetch for live data first
            - "coding": Needs file exploration, may benefit from KB
            - "general": General task, may benefit from KB skills/tools
        """
        task_lower = task.lower().strip()

        # Already handled by _is_simple_query, but double-check
        if self._is_simple_query(task):
            return "simple"

        # Current/live data indicators - need web search FIRST
        current_data_keywords = [
            "score", "scores", "result", "results", "game", "match",
            "weather", "forecast", "temperature",
            "stock", "price", "prices", "market",
            "news", "latest", "today", "yesterday", "tonight",
            "current", "right now", "happening", "breaking",
            "election", "poll", "vote", "voting",
            "live", "real-time", "realtime",
        ]
        if any(kw in task_lower for kw in current_data_keywords):
            return "current_data"

        # Coding/file-related tasks - may benefit from KB patterns
        coding_keywords = [
            "code", "coding", "program", "function", "class", "method",
            "file", "files", "folder", "directory", "path",
            "implement", "create", "build", "develop", "write",
            "fix", "bug", "debug", "error", "issue",
            "refactor", "optimize", "improve",
            "test", "tests", "testing",
            "git", "commit", "push", "pull", "branch",
            "deploy", "install", "configure", "setup",
            "python", "javascript", "typescript", "rust", "go",
            "api", "endpoint", "database", "sql",
        ]
        if any(kw in task_lower for kw in coding_keywords):
            return "coding"

        # Default to general - might benefit from KB
        return "general"

    async def close(self) -> None:
        """Close the agent and clean up resources."""
        # Close executor (cleans up browser if used)
        await self.executor.close()

        # Close context gatherer (cleans up browser if used)
        await self.context_gatherer.close()

        if self._api_client:
            await self._api_client.close()
            self._api_client = None

    async def __aenter__(self) -> "PromethianAgent":
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.close()

    async def run(
        self,
        task: str,
        context: str | None = None,
    ) -> ExecutionResult:
        """
        Run a task through the full agent pipeline.

        Steps:
        1. Start session with central API
        2. Retrieve relevant skills and tools
        3. Create plan with confidence assessment
        4. Execute with self-healing
        5. Learn from result
        6. Complete session with feedback

        Args:
            task: The task to accomplish
            context: Additional context

        Returns:
            ExecutionResult with the outcome
        """
        start_time = time.time()
        skills: list[Skill] = []
        tools: list[Tool] = []
        skills_query_id: str | None = None
        tools_query_id: str | None = None
        api: PromethianAPIClient | None = None
        session_end_logged = False  # Track if we already logged session end

        # Track items for feedback
        items_feedback = []

        # Check if this is a simple query that doesn't need tools/context
        is_simple = self._is_simple_query(task)

        # Log session start
        self._logger.session_start(task)
        if is_simple:
            self._logger.decision(
                decision_type="query_classification",
                choice="simple_query",
                reasoning="Task identified as simple query - skipping retrieval and context gathering",
            )

        try:
            # Step 1: Start session
            await self.progress.emit(
                ProgressStage.SESSION_START,
                "Starting session...",
                task=task,
            )
            api = await self._get_api_client()
            session = await api.start_session(task)
            self._session_task = task

            # Clear cache for new session
            self.cache.on_session_start(session.session_id)

            # For simple queries, skip retrieval and context gathering
            # and go straight to direct response
            gathered_context: GatheredContext | None = None

            if is_simple:
                # Super-fast path: answer directly without planning
                await self.progress.emit(
                    ProgressStage.PLANNING,
                    "Answering directly...",
                )

                # Just send to Claude directly
                response = await self.claude.complete(
                    prompt=task,
                    system="You are a helpful assistant. Answer the question concisely and directly.",
                )

                await self.progress.emit(
                    ProgressStage.TASK_COMPLETED,
                    "Task completed (direct answer)",
                )

                # Complete session and return
                await api.complete_session(
                    overall_success=True,
                    items_feedback=[],
                    notes="Direct answer - simple query",
                )

                return ExecutionResult(
                    task=task,
                    status=ExecutionStatus.SUCCESS,
                    plan=Plan(
                        task=task,
                        steps=[],
                        confidence=1.0,
                        confidence_level=ConfidenceLevel.HIGH,
                        strategy=ExecutionStrategy.SKILLS_ONLY,
                        reasoning="Simple query - direct answer",
                        direct_response=response,
                    ),
                    final_output=response,
                    step_results=[],
                    execution_time=time.time() - start_time,
                    usage=self.claude.usage.to_dict() if self.config.track_usage else None,
                )

            # Step 2: Classify task to determine optimal execution strategy
            task_type = self._classify_task(task)
            self._logger.decision(
                decision_type="task_classification",
                choice=task_type,
                reasoning=f"Task classified as '{task_type}' - determines context/KB lookup strategy",
            )

            # Step 2.5: Gather context FIRST (before KB lookup)
            # For current_data tasks, this is critical - we need web search results
            # For coding tasks, we explore the codebase to understand what's needed
            if self.enable_context_gathering:
                context_start = time.time()
                self._logger.context_gathering_start(task)
                await self.progress.emit(
                    ProgressStage.GATHERING_CONTEXT,
                    "Gathering context...",
                )
                gathered_context = await self.context_gatherer.gather_context(
                    task=task,
                    skills=[],  # No skills yet - gathering context first
                    tools=[],   # No tools yet - gathering context first
                )
                self._logger.context_gathered(
                    sources=gathered_context.sources if gathered_context else [],
                    duration_s=time.time() - context_start,
                )
                await self.progress.emit(
                    ProgressStage.CONTEXT_GATHERED,
                    "Context gathered",
                    has_context=gathered_context is not None and gathered_context.summary is not None,
                )

            # Step 3: Retrieve skills/tools from KB - but ONLY if task type benefits from it
            # Skip KB lookup for current_data tasks - they just need web results
            should_query_kb = task_type in ("coding", "general")

            if should_query_kb:
                await self.progress.emit(
                    ProgressStage.RETRIEVING_SKILLS,
                    "Retrieving skills and tools from knowledge base...",
                )

                # Run both searches in parallel
                skills_task = api.search_skills(
                    query=task,
                    max_results=self.config.max_retrieved_skills,
                    include_content=True,
                )
                tools_task = api.search_tools(
                    query=task,
                    max_results=self.config.max_retrieved_tools,
                    include_content=True,
                )

                (skill_results, skills_query_id), (tool_results, tools_query_id) = await asyncio.gather(
                    skills_task, tools_task
                )

                skills = [r.to_skill() for r in skill_results if r.content]
                tools = [r.to_tool() for r in tool_results if r.code]

                # Cache retrieved items
                self.cache.put_skills(skills, query_id=skills_query_id)
                self.cache.put_tools(tools, query_id=tools_query_id)

                # Log retrieval results
                self._logger.skills_retrieved(len(skills), [s.name for s in skills])
                self._logger.tools_retrieved(len(tools), [t.name for t in tools])

                await self.progress.emit(
                    ProgressStage.SKILLS_RETRIEVED,
                    f"Retrieved {len(skills)} skill(s)",
                    count=len(skills),
                    skills=[s.name for s in skills],
                )

                await self.progress.emit(
                    ProgressStage.TOOLS_RETRIEVED,
                    f"Retrieved {len(tools)} tool(s)",
                    count=len(tools),
                    tools=[t.name for t in tools],
                )
            else:
                # Skip KB lookup for current_data tasks
                self._logger.decision(
                    decision_type="skip_kb_lookup",
                    choice="skipped",
                    reasoning=f"Task type '{task_type}' doesn't benefit from KB skills/tools - using gathered context only",
                )

            # Combine user-provided context with gathered context
            full_context = context or ""
            if gathered_context and gathered_context.summary:
                gathered_str = gathered_context.to_prompt_context()
                if full_context:
                    full_context = f"{full_context}\n\n{gathered_str}"
                else:
                    full_context = gathered_str

            # Step 3: Create plan with all available context
            self._logger.planning_start(task)
            await self.progress.emit(
                ProgressStage.PLANNING,
                "Planning...",
            )
            plan = await self.planner.create_plan(
                task=task,
                context=full_context if full_context else None,
                skills=skills,
                tools=tools,
            )

            # Log plan creation
            self._logger.plan_created(
                task=task,
                confidence=plan.confidence,
                strategy=plan.strategy.value if hasattr(plan.strategy, 'value') else str(plan.strategy),
                steps=[s.description for s in plan.steps],
                reasoning=plan.reasoning,
                direct_response=bool(plan.direct_response),
            )

            await self.progress.emit(
                ProgressStage.PLAN_CREATED,
                f"Plan created with {len(plan.steps)} step(s)",
                steps=[s.description for s in plan.steps],
                confidence=plan.confidence,
                strategy=plan.strategy.value if hasattr(plan.strategy, 'value') else str(plan.strategy),
            )

            # Check for direct response (no tools needed)
            # BUT: Override direct_response if skills/tools were retrieved - they're relevant!
            if plan.direct_response and not skills and not tools:
                await self.progress.emit(
                    ProgressStage.TASK_COMPLETED,
                    "Task completed (direct response)",
                )
                result = ExecutionResult(
                    task=task,
                    status=ExecutionStatus.SUCCESS,
                    plan=plan,
                    final_output=plan.direct_response,
                    step_results=[],
                    execution_time=time.time() - start_time,
                    usage=self.claude.usage.to_dict() if self.config.track_usage else None,
                )
                # Complete session and return early
                if api:
                    await api.complete_session(
                        overall_success=True,
                        items_feedback=[],
                        notes="Direct response - no tools needed",
                    )
                return result
            elif plan.direct_response and (skills or tools):
                # Planner tried to answer directly but we have relevant skills/tools
                # Force replanning with execution
                self._logger.decision(
                    decision_type="override_direct_response",
                    choice="force_execution",
                    reasoning=f"Skills ({len(skills)}) or tools ({len(tools)}) were retrieved - must use them",
                )
                # Create a simple execution plan if planner didn't provide steps
                if not plan.steps:
                    plan = Plan(
                        task=task,
                        steps=[PlanStep(
                            id="step_1",
                            description=f"Execute task using retrieved skills/tools: {task}",
                            expected_output="Task completion result",
                            skills_to_use=[s.id for s in skills] if skills else [],
                            tools_to_use=[t.id for t in tools] if tools else [],
                        )],
                        confidence=plan.confidence,
                        confidence_level=plan.confidence_level,
                        strategy=ExecutionStrategy.SKILLS_ONLY,
                        reasoning="Forced execution - relevant skills/tools were retrieved",
                        relevant_skills=skills,
                        relevant_tools=tools,
                    )

            # Step 3.5: Build any tools the plan requires
            tools_to_build = sum(len(s.tools_to_build) for s in plan.steps)
            if tools_to_build > 0:
                await self.progress.emit(
                    ProgressStage.BUILDING_TOOLS,
                    f"Building {tools_to_build} tool(s)...",
                    count=tools_to_build,
                )
            built_tools, failed_builds = await self._build_required_tools(plan)
            if built_tools:
                # Add built tools to the plan's available tools
                plan.relevant_tools.extend(built_tools)
                tools.extend(built_tools)
                await self.progress.emit(
                    ProgressStage.TOOLS_BUILT,
                    f"Built {len(built_tools)} tool(s)",
                    tools=[t.name for t in built_tools],
                )

            # Step 4: Execute with self-healing
            self._logger.execution_start(task, len(plan.steps))
            await self.progress.emit(
                ProgressStage.EXECUTING_PLAN,
                "Executing plan...",
                total_steps=len(plan.steps),
            )

            # Pass failed tool builds to executor so it can include them in healing context
            tool_build_context = None
            if failed_builds:
                tool_build_context = "\n".join(
                    f"- Tool '{desc[:60]}...' failed to build: {err}"
                    for desc, err in failed_builds
                )
            result = await self.executor.execute_plan(
                plan, tool_build_failures=tool_build_context
            )

            # Step 5: Learn from result
            await self.progress.emit(
                ProgressStage.LEARNING,
                "Learning from result...",
            )
            new_skills, new_tools = await self._learn_from_result(result)

            # Log learned items
            for skill in new_skills:
                self._logger.skill_extracted(skill.name, task)
            for tool in new_tools:
                self._logger.tool_built(tool.name, tool.description)

            # Include built tools in what we contribute back
            new_tools.extend(built_tools)

            # Update result with learning info
            result.skills_learned = [s.id for s in new_skills]
            result.tools_built = [t.id for t in new_tools]
            result.execution_time = time.time() - start_time

            # Step 6: Complete session with feedback
            # Aggregate skill/tool helpfulness from all step results
            all_skills_helpful = set()
            all_skills_unhelpful = set()
            all_skills_used = set()
            all_tools_used = set()
            for step_result in result.step_results:
                all_skills_helpful.update(step_result.skills_helpful)
                all_skills_unhelpful.update(step_result.skills_unhelpful)
                all_skills_used.update(step_result.skills_used)
                all_tools_used.update(step_result.tools_used)

            # Build feedback for used items
            for skill in skills:
                used = skill.id in all_skills_used
                # Determine helpfulness: helpful if in helpful set, unhelpful if in unhelpful set
                was_helpful = None
                if skill.id in all_skills_helpful:
                    was_helpful = True
                elif skill.id in all_skills_unhelpful:
                    was_helpful = False

                items_feedback.append({
                    "item_id": skill.id,
                    "item_type": "skill",
                    "was_used": used,
                    "execution_success": result.status == ExecutionStatus.SUCCESS if used else None,
                    "was_helpful": was_helpful,
                })

            for tool in tools:
                used = tool.id in all_tools_used
                items_feedback.append({
                    "item_id": tool.id,
                    "item_type": "tool",
                    "was_used": used,
                    "execution_success": result.status == ExecutionStatus.SUCCESS if used else None,
                })

            # Convert new skills/tools to contribution format
            new_skill_dicts = [
                {
                    "name": s.name,
                    "description": s.description,
                    "content": s.content,
                    "tags": s.tags,
                    "source_task": task,
                    "execution_success": result.status == ExecutionStatus.SUCCESS,
                }
                for s in new_skills
            ]

            new_tool_dicts = [
                {
                    "name": t.name,
                    "description": t.description,
                    "code": t.code,
                    "function_name": t.function_name,
                    "parameters": t.parameters,
                    "dependencies": t.dependencies,
                    "tags": t.tags,
                    "source_task": task,
                    "execution_success": result.status == ExecutionStatus.SUCCESS,
                }
                for t in new_tools
            ]

            await api.complete_session(
                overall_success=result.status == ExecutionStatus.SUCCESS,
                items_feedback=items_feedback,
                new_skills=new_skill_dicts,
                new_tools=new_tool_dicts,
                self_heal_steps=result.total_attempts - len(result.step_results),
            )

            # Log contributions
            self._logger.contributions_submitted(
                skills_count=len(new_skill_dicts),
                tools_count=len(new_tool_dicts),
                skill_names=[s["name"] for s in new_skill_dicts],
                tool_names=[t["name"] for t in new_tool_dicts],
                session_success=result.status == ExecutionStatus.SUCCESS,
            )

            # Log confidence calibration for tracking prediction accuracy
            self._logger.confidence_calibration(
                predicted_confidence=plan.confidence,
                actual_success=result.status == ExecutionStatus.SUCCESS,
                task=task,
                strategy=plan.strategy.value if hasattr(plan.strategy, 'value') else str(plan.strategy),
            )

            # Clear cache after session completes
            self.cache.on_session_end()

            # Log session end
            usage = self.claude.usage
            self._logger.session_end(
                task=task,
                success=result.status == ExecutionStatus.SUCCESS,
                duration_s=time.time() - start_time,
                total_tokens=usage.total_tokens if usage else 0,
                total_cost_usd=usage.estimated_cost if usage else 0.0,
            )
            session_end_logged = True

            # Final progress event
            if result.status == ExecutionStatus.SUCCESS:
                await self.progress.emit(
                    ProgressStage.TASK_COMPLETED,
                    "Task completed successfully",
                    skills_learned=len(new_skills),
                    tools_built=len(new_tools),
                )
            elif result.status == ExecutionStatus.PARTIAL:
                await self.progress.emit(
                    ProgressStage.TASK_COMPLETED,
                    "Task partially completed",
                )
            else:
                await self.progress.emit(
                    ProgressStage.TASK_FAILED,
                    f"Task failed: {result.error}",
                    error=result.error,
                )

            return result

        except Exception as e:
            # Log error
            self._logger.error(
                error_type=type(e).__name__,
                message=str(e),
                details={"task": task},
            )
            self._logger.session_end(
                task=task,
                success=False,
                duration_s=time.time() - start_time,
            )
            session_end_logged = True

            # Complete session with failure if we started one
            if api and self._session_task:
                try:
                    await api.complete_session(
                        overall_success=False,
                        items_feedback=items_feedback,
                        notes=f"Error: {str(e)}",
                    )
                except Exception:
                    pass  # Ignore errors completing session

            # Clear cache on error too
            self.cache.on_session_end()

            # Return error result
            return ExecutionResult(
                task=task,
                status=ExecutionStatus.FAILURE,
                plan=Plan(
                    task=task,
                    steps=[],
                    confidence=0.0,
                    confidence_level=ExecutionStatus.FAILURE,
                    strategy=ExecutionStatus.FAILURE,
                    reasoning="Failed before planning",
                ),
                error=str(e),
                execution_time=time.time() - start_time,
            )

        finally:
            # Ensure session_end is ALWAYS logged (handles KeyboardInterrupt, etc.)
            if not session_end_logged:
                self._logger.session_end(
                    task=task,
                    success=False,
                    duration_s=time.time() - start_time,
                )
                self.cache.on_session_end()

    async def _build_required_tools(
        self, plan: Plan
    ) -> tuple[list[Tool], list[tuple[str, str]]]:
        """
        Build any tools that the plan requires with self-healing retries.

        Scans plan steps for tools_to_build entries and creates them.
        Uses local cache to avoid rebuilding identical tools.
        If building fails, retries with error context up to max_self_heal_attempts.

        Args:
            plan: The execution plan

        Returns:
            Tuple of (built_tools, failed_builds) where failed_builds is
            a list of (tool_description, error_message) tuples
        """
        built_tools: list[Tool] = []
        failed_builds: list[tuple[str, str]] = []
        seen_descriptions: set[str] = set()
        max_attempts = self.config.max_self_heal_attempts
        tool_cache = get_tool_cache()

        for step in plan.steps:
            for tool_description in step.tools_to_build:
                # Skip duplicates
                if tool_description in seen_descriptions:
                    continue
                seen_descriptions.add(tool_description)

                # Check cache first
                cached_tool = tool_cache.get(tool_description)
                if cached_tool:
                    self._logger.log(
                        "tool_cache_hit",
                        f"Using cached tool: {cached_tool.name}",
                        tool_name=cached_tool.name,
                        description=tool_description[:100],
                    )
                    built_tools.append(cached_tool)
                    step.tools_to_use.append(cached_tool.id)
                    continue

                # Self-healing retry loop for tool building
                tool = None
                healing_context: list[str] = []
                last_error = None

                for attempt in range(max_attempts):
                    try:
                        # Build with healing context if we have previous failures
                        context = f"For step: {step.description}"
                        if healing_context:
                            context += "\n\n# Previous build attempts failed:\n"
                            context += "\n".join(healing_context)
                            context += "\n\nPlease fix the issues and try a different approach."

                        tool = await self.tool_builder.build_tool(
                            description=tool_description,
                            context=context,
                        )
                        built_tools.append(tool)
                        step.tools_to_use.append(tool.id)

                        # Cache the successfully built tool
                        tool_cache.put(tool_description, tool)

                        break  # Success - exit retry loop

                    except Exception as e:
                        last_error = str(e)

                        if attempt < max_attempts - 1:
                            # Emit self-healing progress
                            await self.progress.emit(
                                ProgressStage.SELF_HEALING,
                                f"Tool build failed, retrying...",
                                attempt=attempt + 1,
                                max_attempts=max_attempts,
                                tool_name=tool_description[:50],
                                error=last_error,
                            )

                            # Add to healing context for next attempt
                            healing_context.append(
                                f"Attempt {attempt + 1}: {last_error}"
                            )

                            # Emit heal attempt for the retry
                            await self.progress.emit(
                                ProgressStage.HEAL_ATTEMPT,
                                f"Retry attempt {attempt + 2}",
                                attempt=attempt + 2,
                                max_attempts=max_attempts,
                            )

                # If we exhausted all attempts without success
                if tool is None:
                    failed_builds.append((tool_description, last_error or "Unknown error"))
                    await self.progress.emit(
                        ProgressStage.TOOL_BUILD_FAILED,
                        f"Failed to build tool after {max_attempts} attempts: {tool_description[:50]}...",
                        tool_description=tool_description,
                        error=last_error,
                        attempts=max_attempts,
                    )

        return built_tools, failed_builds

    async def _learn_from_result(
        self,
        result: ExecutionResult,
    ) -> tuple[list[Skill], list[Tool]]:
        """
        Learn from an execution result.

        Creates new skills/tools based on what was learned.
        """
        new_skills: list[Skill] = []
        new_tools: list[Tool] = []

        # Only learn from successful executions
        if result.status != ExecutionStatus.SUCCESS:
            return new_skills, new_tools

        # Check if we should create a skill
        if await self.skill_learner.should_extract_skill(result):
            skill = await self.skill_learner.extract_skill(result)
            if skill:
                new_skills.append(skill)

        # Check if self-healing happened - if so, extract tools from the solution
        healing_happened = any(
            step.attempts > 1 for step in result.step_results
        )

        if healing_happened:
            # Try to extract reusable tools from steps that required healing
            for step in result.step_results:
                if step.attempts > 1 and step.status == ExecutionStatus.SUCCESS:
                    tool = await self._extract_tool_from_healed_step(
                        step=step,
                        task=result.task,
                        plan_step=next(
                            (s for s in result.plan.steps if s.id == step.step_id),
                            None
                        ),
                    )
                    if tool:
                        new_tools.append(tool)

        return new_skills, new_tools

    async def _extract_tool_from_healed_step(
        self,
        step: StepResult,
        task: str,
        plan_step: PlanStep | None,
    ) -> Tool | None:
        """
        Extract a reusable tool from a step that required self-healing.

        If a step needed multiple attempts to succeed, the final solution
        is likely valuable and worth saving as a reusable tool.
        """
        from promethian.learning.tool_builder import ToolBuilderError, TOOL_BUILDER_SYSTEM

        # Build context from the step execution
        step_description = plan_step.description if plan_step else "Unknown step"
        healing_context = "\n".join(step.healing_log) if step.healing_log else ""

        # Construct output summary - handle different output types
        output_summary = ""
        if step.output:
            if isinstance(step.output, str):
                output_summary = step.output[:2000]
            elif isinstance(step.output, dict):
                output_summary = str(step.output)[:2000]

        # Ask the tool builder to create a reusable tool from this execution
        prompt = f"""A task required {step.attempts} attempts to complete successfully.
This suggests the solution is non-trivial and worth saving as a reusable tool.

Task: {task}
Step: {step_description}

Self-healing log (what was tried and fixed):
{healing_context}

Final output:
{output_summary}

Based on this execution, extract a reusable Python tool that could help with similar tasks in the future.
The tool should generalize the solution so it can be reused.

If no reusable tool can be extracted (e.g., the step was just configuration or one-time setup), respond with:
{{"no_tool": true, "reason": "..."}}

Otherwise, respond with the tool JSON."""

        try:
            tool_data = await self.claude.complete_json(
                prompt=prompt,
                system=TOOL_BUILDER_SYSTEM,
            )

            if tool_data.get("no_tool"):
                return None

            # Validate and create tool
            code = tool_data.get("code", "")
            function_name = tool_data.get("function_name", "")

            if not code or not function_name:
                return None

            validation_error = self.tool_builder._validate_code(code, function_name)
            if validation_error:
                return None

            return Tool(
                id=f"tool_{uuid4().hex[:12]}",
                name=self.tool_builder._sanitize_tool_name(
                    tool_data.get("name", "extracted_tool")
                ),
                description=tool_data.get("description", step_description),
                code=code,
                function_name=function_name,
                parameters=tool_data.get("parameters", {}),
                dependencies=tool_data.get("dependencies", []),
                required_env_vars=tool_data.get("required_env_vars", []),
                tags=tool_data.get("tags", []) + ["auto-extracted", "from-healing"],
                source_task=task[:200],
            )
        except (ToolBuilderError, Exception):
            # Tool extraction is best-effort, don't fail the whole task
            return None


class PromethianAgentSync:
    """Synchronous wrapper for PromethianAgent."""

    def __init__(self, config: PromethianConfig | None = None):
        self._agent = PromethianAgent(config)

    def run(self, task: str, context: str | None = None) -> ExecutionResult:
        """Run a task synchronously."""
        import asyncio
        return asyncio.run(self._agent.run(task, context))

    def close(self) -> None:
        """Close the agent."""
        import asyncio
        asyncio.run(self._agent.close())
