"""Executor with self-healing for task execution."""

from __future__ import annotations

import json
import subprocess
import sys
import time
import traceback
from typing import Any

from promethian.config import PromethianConfig
from promethian.core.types import (
    Plan, PlanStep, Skill, Tool,
    ExecutionResult, StepResult, ExecutionStatus,
)
from promethian.core.tool_runner import ToolRunner, ToolExecutor, ToolExecutionError
from promethian.core.error_analyzer import (
    analyze_error, is_permission_error, ErrorCategory,
    is_missing_dependency_error, extract_missing_module,
)
from promethian.core.permission_advisor import PermissionAdvisor
from promethian.core.plan_updater import PlanUpdater
from promethian.core.progress import ProgressTracker, NullProgressTracker, ProgressStage
from promethian.utils.claude import ClaudeClient
from promethian.utils.logging import get_logger


# Browser tool is optional - only available if playwright is installed
try:
    from promethian.tools.browser import get_browser, BrowserTool
    BROWSER_AVAILABLE = True
except ImportError:
    BROWSER_AVAILABLE = False
    get_browser = None
    BrowserTool = None

# Bash tool for shell command execution
from promethian.tools.bash import get_bash, BashTool
BASH_AVAILABLE = True

# Web search tool
from promethian.tools.web_search import get_web_search, APISearchTool, DDGSearchTool
WEB_SEARCH_AVAILABLE = True
WebSearchTool = APISearchTool | DDGSearchTool  # Type alias

# Web fetch tool
from promethian.tools.web_fetch import get_web_fetch, WebFetchTool
WEB_FETCH_AVAILABLE = True

# File tools (read, write, edit, glob, grep)
from promethian.tools.file_tools import get_file_tools, FileTools
FILE_TOOLS_AVAILABLE = True

# Todo tool for task tracking
from promethian.tools.todo import get_todo_tool, TodoWriteTool
TODO_TOOL_AVAILABLE = True


class ToolExecutionFailure(Exception):
    """Raised when tool execution fails and should trigger self-healing.

    This is raised when custom tools (not built-in tools like bash/browser)
    fail during execution, allowing the healing loop to retry with fixes.
    """
    def __init__(self, message: str, tool_name: str, error_details: str):
        super().__init__(message)
        self.tool_name = tool_name
        self.error_details = error_details


EXECUTION_SYSTEM_PROMPT = """You are an execution assistant for a coding AI agent.

Your job is to execute steps of a plan. For each step, you will:
1. Use the provided skills as guidance for your approach
2. Use the provided tools when appropriate
3. Generate the expected output

## Tool Usage Guidelines

When you call tools:
- You can call multiple tools in a single response. If tools are independent, call them in parallel for efficiency.
- Use specialized file tools (read, write, edit, glob, grep) instead of bash commands for file operations.
- Never use bash for: cat, head, tail, sed, awk, echo, find, grep - use the dedicated tools instead.

## File Operations

- ALWAYS read a file before editing it. Never modify code you haven't read.
- Use the edit tool for precise string replacements. The old_string must match exactly.
- When editing, include enough surrounding context to make the match unique.
- For large changes, use the write tool to replace the entire file.
- Use glob to find files by pattern (e.g., "**/*.py" for all Python files).
- Use grep to search file contents (regex supported).

## Code Quality

- Only make changes that are directly requested or clearly necessary.
- Don't over-engineer: avoid adding features, refactoring, or "improvements" beyond what's asked.
- Don't add comments, docstrings, or type annotations to code you didn't need to change.
- Don't add error handling for scenarios that can't happen.
- Keep solutions simple and focused on the task.

## When Executing

- Follow skill guidance closely - they contain proven approaches.
- Report progress clearly as you work.
- If something fails, explain what went wrong so we can fix it.
- Use the todo_write tool to track multi-step work and show progress."""


class Executor:
    """
    Executes plans with self-healing capability.

    When execution fails, the executor can:
    1. Analyze the failure
    2. Suggest fixes
    3. Retry with modifications

    Tool Execution Model (Claude Code style):
    - Direct subprocess execution (no VM)
    - Dependency isolation via cached venvs
    - Timeout protection (configurable, default 60s)
    - Working directory sandbox (temp dir)

    Built-in Tools:
    - Browser automation (playwright-based) for web tasks
    - Bash command execution for shell operations (git, npm, etc.)
    - Web search (DuckDuckGo) for finding information online

    Execution Modes:
    - use_sub_agents=False (default): Execute steps in main conversation
    - use_sub_agents=True: Each step gets its own sub-agent with isolated context

    Feedback Support:
    - Accepts a feedback_getter callable that returns pending user feedback
    - After each step, checks for feedback and incorporates into replanning
    """

    def __init__(
        self,
        config: PromethianConfig,
        claude: ClaudeClient,
        enable_browser: bool = True,
        enable_bash: bool = True,
        enable_web_search: bool = True,
        use_sub_agents: bool = True,  # Default to sub-agents for context preservation
        progress: ProgressTracker | None = None,
        feedback_getter: callable | None = None,  # Returns list[str] of pending feedback
    ):
        self.config = config
        self.claude = claude
        self.tool_runner = ToolRunner(config)
        self.use_sub_agents = use_sub_agents
        self._browser: BrowserTool | None = None
        self._bash: BashTool | None = None
        self._web_search: WebSearchTool | None = None
        self._web_fetch: WebFetchTool | None = None
        self._file_tools: FileTools | None = None
        self._todo_tool: TodoWriteTool | None = None
        self._enable_browser = enable_browser and BROWSER_AVAILABLE
        self._enable_bash = enable_bash and BASH_AVAILABLE
        self._enable_web_search = enable_web_search and WEB_SEARCH_AVAILABLE
        self._enable_web_fetch = WEB_FETCH_AVAILABLE
        self._enable_file_tools = FILE_TOOLS_AVAILABLE
        self._enable_todo_tool = TODO_TOOL_AVAILABLE

        # Progress tracking
        self.progress = progress or NullProgressTracker()

        # Permission advisor for credential errors
        self._permission_advisor = PermissionAdvisor(claude)

        # Plan updater for dynamic plan adaptation
        self._plan_updater = PlanUpdater(claude)

        # Max tool iterations per step (agentic loop limit) - configurable for cost control
        self._max_tool_iterations = config.max_tool_iterations

        # Sub-agent executor (lazy init)
        self._sub_agent_executor = None

        # Feedback collector for real-time user input
        self._feedback_getter = feedback_getter

        # Verbose logger
        self._logger = get_logger()

    async def _get_browser(self) -> BrowserTool:
        """Get or create browser tool instance."""
        if self._browser is None and self._enable_browser:
            self._browser = await get_browser()
        return self._browser

    def _get_bash(self) -> BashTool:
        """Get or create bash tool instance."""
        if self._bash is None and self._enable_bash:
            self._bash = get_bash(
                timeout=self.config.tool_timeout,
                working_dir=self.config.working_dir,
            )
        return self._bash

    def _get_web_search(self) -> WebSearchTool:
        """Get or create web search tool instance."""
        if self._web_search is None and self._enable_web_search:
            self._web_search = get_web_search()
        return self._web_search

    def _get_web_fetch(self) -> WebFetchTool:
        """Get or create web fetch tool instance."""
        if self._web_fetch is None and self._enable_web_fetch:
            self._web_fetch = get_web_fetch()
        return self._web_fetch

    def _get_file_tools(self) -> FileTools:
        """Get or create file tools instance."""
        if self._file_tools is None and self._enable_file_tools:
            self._file_tools = get_file_tools(working_dir=self.config.working_dir)
        return self._file_tools

    def _get_todo_tool(self) -> TodoWriteTool:
        """Get or create todo tool instance."""
        if self._todo_tool is None and self._enable_todo_tool:
            self._todo_tool = get_todo_tool()
        return self._todo_tool

    def _try_auto_install_dependency(self, error: str, stderr: str = "") -> bool:
        """
        Attempt to auto-install a missing Python dependency.

        Returns True if installation was attempted (success or failure).
        """
        if not is_missing_dependency_error(error, stderr):
            return False

        module, package = extract_missing_module(error, stderr)
        if not package:
            return False

        self._logger.log(
            "auto_install",
            f"Attempting to install missing package: {package}",
            module=module,
            package=package,
        )

        try:
            # Try pip install with --user flag to avoid permission issues
            result = subprocess.run(
                [sys.executable, "-m", "pip", "install", "--user", package],
                capture_output=True,
                text=True,
                timeout=120,
            )

            if result.returncode == 0:
                self._logger.log(
                    "auto_install_success",
                    f"Successfully installed {package}",
                    package=package,
                )
                return True
            else:
                self._logger.log(
                    "auto_install_failed",
                    f"Failed to install {package}: {result.stderr}",
                    package=package,
                    stderr=result.stderr[:500],
                )
                return True  # Still return True - we attempted

        except subprocess.TimeoutExpired:
            self._logger.log(
                "auto_install_timeout",
                f"Timeout installing {package}",
                package=package,
            )
            return True
        except Exception as e:
            self._logger.log(
                "auto_install_error",
                f"Error installing {package}: {e}",
                package=package,
                error=str(e),
            )
            return True

    def _get_sub_agent_executor(self):
        """Get or create the sub-agent executor."""
        if self._sub_agent_executor is None:
            from promethian.core.step_executor import SubAgentExecutor
            self._sub_agent_executor = SubAgentExecutor(
                config=self.config,
                tool_runner=self.tool_runner,
            )
        return self._sub_agent_executor

    async def close(self):
        """Clean up resources."""
        if self._browser is not None:
            await self._browser.close()
            self._browser = None
        # Bash and web search tools don't need explicit cleanup

    async def execute_plan(
        self,
        plan: Plan,
        adaptive: bool = True,
        tool_build_failures: str | None = None,
    ) -> ExecutionResult:
        """
        Execute all steps in a plan with adaptive planning.

        The plan can be dynamically updated after each step based on results:
        - New steps can be added if results reveal more work is needed
        - Remaining steps can be removed if they're no longer necessary
        - Existing steps can be modified based on new information

        Args:
            plan: The plan to execute
            adaptive: Whether to dynamically update the plan (default True)
            tool_build_failures: Optional string describing tools that failed to build,
                                 to be included in execution context for self-healing

        Returns:
            ExecutionResult with all step results
        """
        # Store tool build failures for use in step execution
        self._tool_build_failures = tool_build_failures
        start_time = time.time()
        step_results = []
        step_outputs: dict[str, Any] = {}
        total_attempts = 0
        tools_built: list[str] = []
        plan_updates_log: list[str] = []

        # Create tool executor from plan's relevant tools
        tool_executor = ToolExecutor(self.tool_runner, plan.relevant_tools)

        # Work with a mutable list of remaining steps
        remaining_steps = list(plan.steps)

        # Choose execution mode
        if self.use_sub_agents:
            sub_executor = self._get_sub_agent_executor()
            step_number = 0
            total_initial_steps = len(remaining_steps)

            while remaining_steps:
                step = remaining_steps.pop(0)
                step_number += 1

                # Check dependencies
                deps_satisfied = True
                for dep_id in step.dependencies:
                    if dep_id not in step_outputs:
                        step_results.append(StepResult(
                            step_id=step.id,
                            status=ExecutionStatus.FAILURE,
                            error=f"Dependency {dep_id} not satisfied",
                        ))
                        deps_satisfied = False
                        break

                if not deps_satisfied:
                    continue

                # Log and emit progress for step execution
                step_start_time = time.time()
                self._logger.step_start(
                    step_id=step.id,
                    description=step.description,
                    step_number=step_number,
                    total_steps=total_initial_steps + len(remaining_steps),
                )
                await self.progress.emit(
                    ProgressStage.EXECUTING_STEP,
                    f"Executing step {step_number}: {step.description[:50]}...",
                    current=step_number,
                    total=total_initial_steps + len(remaining_steps),
                    step_id=step.id,
                    step_description=step.description,
                )

                # Execute step with sub-agent
                result = await sub_executor.execute_step(
                    step=step,
                    plan=plan,
                    previous_outputs=step_outputs,
                    progress=self.progress,
                )

                tools_built.extend(result.tools_built)
                total_attempts += result.attempts
                step_results.append(result)

                # Log step completion
                step_duration = time.time() - step_start_time
                output_preview = str(result.output)[:200] if result.output else None
                self._logger.step_complete(
                    step_id=step.id,
                    success=result.status == ExecutionStatus.SUCCESS,
                    attempts=result.attempts,
                    duration_s=step_duration,
                    output_preview=output_preview,
                    error=result.error,
                )

                if result.status == ExecutionStatus.SUCCESS:
                    step_outputs[step.id] = result.output
                    await self.progress.emit(
                        ProgressStage.STEP_COMPLETED,
                        f"Step completed: {step.description[:50]}...",
                        step_id=step.id,
                        attempts=result.attempts,
                    )
                else:
                    await self.progress.emit(
                        ProgressStage.STEP_FAILED,
                        f"Step failed: {result.error}",
                        step_id=step.id,
                        error=result.error,
                        attempts=result.attempts,
                    )
                    # Stop execution on step failure - don't keep adding more steps
                    self._logger.log(
                        "execution_stopped",
                        f"Stopping execution: step {step.id} failed with unrecoverable error",
                        step_id=step.id,
                        error=result.error,
                    )
                    break

                # Check for user feedback before replanning
                user_feedback = None
                if self._feedback_getter:
                    pending = self._feedback_getter()
                    if pending:
                        user_feedback = "\n".join(pending)
                        await self.progress.emit(
                            ProgressStage.USER_FEEDBACK,
                            f"Received user feedback",
                            feedback=user_feedback,
                        )

                # Adaptive planning: update remaining steps based on result and feedback
                if adaptive and (remaining_steps or user_feedback):
                    remaining_steps, plan_updates_log = await self._update_plan_if_needed(
                        plan=plan,
                        completed_step=step,
                        step_result=result,
                        step_outputs=step_outputs,
                        remaining_steps=remaining_steps,
                        plan_updates_log=plan_updates_log,
                        user_feedback=user_feedback,
                    )

        else:
            # Original mode: execute in main conversation
            while remaining_steps:
                step = remaining_steps.pop(0)

                # Check dependencies
                deps_satisfied = True
                for dep_id in step.dependencies:
                    if dep_id not in step_outputs:
                        step_results.append(StepResult(
                            step_id=step.id,
                            status=ExecutionStatus.FAILURE,
                            error=f"Dependency {dep_id} not satisfied",
                        ))
                        deps_satisfied = False
                        break

                if not deps_satisfied:
                    continue

                # Execute step with self-healing and agentic tool loop
                result, attempts = await self._execute_step_with_healing(
                    step=step,
                    plan=plan,
                    step_outputs=step_outputs,
                    tool_executor=tool_executor,
                )

                tools_built.extend(result.tools_built)
                total_attempts += attempts
                step_results.append(result)

                if result.status == ExecutionStatus.SUCCESS:
                    step_outputs[step.id] = result.output
                else:
                    # Stop execution on step failure
                    self._logger.log(
                        "execution_stopped",
                        f"Stopping execution: step {step.id} failed",
                        step_id=step.id,
                        error=result.error,
                    )
                    break

                # Check for user feedback before replanning
                user_feedback = None
                if self._feedback_getter:
                    pending = self._feedback_getter()
                    if pending:
                        user_feedback = "\n".join(pending)
                        await self.progress.emit(
                            ProgressStage.USER_FEEDBACK,
                            f"Received user feedback",
                            feedback=user_feedback,
                        )

                # Adaptive planning: update remaining steps based on result and feedback
                if adaptive and (remaining_steps or user_feedback):
                    remaining_steps, plan_updates_log = await self._update_plan_if_needed(
                        plan=plan,
                        completed_step=step,
                        step_result=result,
                        step_outputs=step_outputs,
                        remaining_steps=remaining_steps,
                        plan_updates_log=plan_updates_log,
                        user_feedback=user_feedback,
                    )

        # Merge usage stats from sub-executor if used
        if self.use_sub_agents and self._sub_agent_executor is not None:
            self.claude.usage.merge(self._sub_agent_executor.usage_stats)

        # Determine overall status
        if all(r.status == ExecutionStatus.SUCCESS for r in step_results):
            overall_status = ExecutionStatus.SUCCESS
            error = None
        elif any(r.status == ExecutionStatus.SUCCESS for r in step_results):
            overall_status = ExecutionStatus.PARTIAL
            error = "Some steps failed"
        else:
            overall_status = ExecutionStatus.FAILURE
            error = step_results[-1].error if step_results else "No steps executed"

        # Combine outputs into final result
        final_output = await self._combine_outputs(
            task=plan.task,
            step_outputs=step_outputs,
        ) if step_outputs else None

        # Include plan updates and usage in the result
        result = ExecutionResult(
            task=plan.task,
            status=overall_status,
            plan=plan,
            step_results=step_results,
            final_output=final_output,
            error=error,
            tools_built=tools_built,
            total_attempts=total_attempts,
            execution_time=time.time() - start_time,
            usage=self.claude.usage.to_dict() if self.config.track_usage else None,
        )

        # Log plan updates if any occurred
        if plan_updates_log:
            result.plan_updates = plan_updates_log

        return result

    async def _update_plan_if_needed(
        self,
        plan: Plan,
        completed_step: PlanStep,
        step_result: StepResult,
        step_outputs: dict[str, Any],
        remaining_steps: list[PlanStep],
        plan_updates_log: list[str],
        user_feedback: str | None = None,
    ) -> tuple[list[PlanStep], list[str]]:
        """
        Check if the plan needs updating and apply changes if so.

        Returns the (possibly modified) remaining steps and updated log.
        """
        # If we have user feedback, always update the plan
        if user_feedback:
            should_update = True
        else:
            # Quick check if update is warranted
            should_update = await self._plan_updater.should_update_plan(
                plan=plan,
                completed_step=completed_step,
                step_result=step_result,
                remaining_steps=remaining_steps,
            )

        if not should_update:
            return remaining_steps, plan_updates_log

        # Emit progress for replanning
        replan_reason = "based on user feedback" if user_feedback else "based on step result"
        await self.progress.emit(
            ProgressStage.REPLANNING,
            f"Replanning {replan_reason}...",
            after_step=completed_step.id,
            has_feedback=user_feedback is not None,
        )

        # Perform full plan update
        updated_steps, reasoning = await self._plan_updater.update_plan(
            plan=plan,
            completed_step=completed_step,
            step_result=step_result,
            step_outputs=step_outputs,
            remaining_steps=remaining_steps,
            user_feedback=user_feedback,
        )

        # Log the update
        if updated_steps != remaining_steps:
            old_count = len(remaining_steps)
            new_count = len(updated_steps)
            old_ids = {s.id for s in remaining_steps}
            new_ids = {s.id for s in updated_steps}
            added = new_ids - old_ids
            removed = old_ids - new_ids

            log_entry = f"After step '{completed_step.id}': {reasoning}"
            if added:
                log_entry += f" Added: {added}"
            if removed:
                log_entry += f" Removed: {removed}"
            if old_count != new_count:
                log_entry += f" (steps: {old_count} -> {new_count})"

            plan_updates_log.append(log_entry)

            # Log plan update
            self._logger.plan_updated(
                after_step=completed_step.id,
                added_steps=list(added),
                removed_steps=list(removed),
                reasoning=reasoning,
            )

            # Emit progress for plan update
            await self.progress.emit(
                ProgressStage.PLAN_UPDATED,
                f"Plan updated: {reasoning}",
                added_steps=list(added),
                removed_steps=list(removed),
                new_step_count=new_count,
            )

        return updated_steps, plan_updates_log

    async def _execute_step_with_healing(
        self,
        step: PlanStep,
        plan: Plan,
        step_outputs: dict[str, Any],
        tool_executor: ToolExecutor,
    ) -> tuple[StepResult, int]:
        """
        Execute a step with self-healing retries.

        Returns (StepResult, number_of_attempts).
        """
        healing_log = []
        last_error = None

        for attempt in range(self.config.max_self_heal_attempts):
            try:
                # Execute the step
                output = await self._execute_step(
                    step=step,
                    plan=plan,
                    step_outputs=step_outputs,
                    healing_context=healing_log,
                    tool_executor=tool_executor,
                )

                return StepResult(
                    step_id=step.id,
                    status=ExecutionStatus.SUCCESS,
                    output=output,
                    attempts=attempt + 1,
                    skills_used=step.skills_to_use,
                    tools_used=step.tools_to_use,
                    healing_log=healing_log,
                    # Mark used skills as helpful on success
                    skills_helpful=step.skills_to_use.copy() if step.skills_to_use else [],
                ), attempt + 1

            except ToolExecutionFailure as e:
                # Custom tool failure - include tool-specific info for healing
                last_error = str(e)
                error_trace = e.error_details

                if attempt < self.config.max_self_heal_attempts - 1:
                    # Try auto-install if it's a missing dependency
                    self._try_auto_install_dependency(last_error, error_trace)

                    # Log self-healing start
                    self._logger.self_heal_start(
                        step_id=step.id,
                        attempt=attempt + 1,
                        max_attempts=self.config.max_self_heal_attempts,
                        error=last_error,
                    )

                    # Get healing suggestion with tool context
                    suggestion = await self._get_healing_suggestion(
                        step=step,
                        error=last_error,
                        trace=error_trace,
                        attempt=attempt,
                        failed_tool_name=e.tool_name,
                    )

                    # Log healing suggestion
                    self._logger.self_heal_suggestion(
                        step_id=step.id,
                        attempt=attempt + 1,
                        suggestion=suggestion,
                    )

                    healing_log.append(
                        f"Attempt {attempt + 1} - Tool '{e.tool_name}' failed: {e.error_details}\nFix: {suggestion}"
                    )

            except Exception as e:
                last_error = str(e)
                error_trace = traceback.format_exc()

                if attempt < self.config.max_self_heal_attempts - 1:
                    # Try auto-install if it's a missing dependency
                    self._try_auto_install_dependency(last_error, error_trace)

                    # Log self-healing start
                    self._logger.self_heal_start(
                        step_id=step.id,
                        attempt=attempt + 1,
                        max_attempts=self.config.max_self_heal_attempts,
                        error=last_error,
                    )

                    # Get healing suggestion
                    suggestion = await self._get_healing_suggestion(
                        step=step,
                        error=last_error,
                        trace=error_trace,
                        attempt=attempt,
                    )

                    # Log healing suggestion
                    self._logger.self_heal_suggestion(
                        step_id=step.id,
                        attempt=attempt + 1,
                        suggestion=suggestion,
                    )

                    healing_log.append(
                        f"Attempt {attempt + 1}: {last_error}\nFix: {suggestion}"
                    )

        # All attempts failed
        return StepResult(
            step_id=step.id,
            status=ExecutionStatus.FAILURE,
            error=last_error,
            attempts=self.config.max_self_heal_attempts,
            skills_used=step.skills_to_use,
            tools_used=step.tools_to_use,
            healing_log=healing_log,
            # Mark used skills as unhelpful on failure
            skills_unhelpful=step.skills_to_use.copy() if step.skills_to_use else [],
        ), self.config.max_self_heal_attempts

    async def _execute_single_tool(
        self,
        call: dict,
        plan: Plan,
        tool_executor: ToolExecutor,
    ) -> dict:
        """
        Execute a single tool call and return the result.

        Returns a dict with success/failure info.
        """
        tool_name = call["name"]
        tool_input = call["input"]

        # Log tool call start
        tool_start_time = self._logger.tool_call_start(tool_name, tool_input)

        # Browser tools
        if tool_name.startswith("browser_"):
            try:
                browser = await self._get_browser()
                result = await browser.execute_tool(tool_name, tool_input)
                if result.success:
                    self._logger.tool_call_complete(tool_name, True, tool_start_time, result=result.data)
                    return {"success": True, "result": result.data}
                else:
                    self._logger.tool_call_complete(tool_name, False, tool_start_time, error=result.error)
                    return {"success": False, "error": result.error}
            except Exception as e:
                self._logger.tool_call_complete(tool_name, False, tool_start_time, error=str(e))
                return {"success": False, "error": str(e)}

        # Bash tools
        elif tool_name in ("bash", "bash_script"):
            try:
                bash = self._get_bash()
                result = await bash.execute_tool(tool_name, tool_input)
                if result.success:
                    self._logger.tool_call_complete(tool_name, True, tool_start_time, result=result.stdout[:500] if result.stdout else None)
                    return {
                        "success": True,
                        "result": result.stdout,
                        "exit_code": result.exit_code,
                    }
                else:
                    error_msg = result.stderr or f"Command failed with exit code {result.exit_code}"
                    self._logger.tool_call_complete(tool_name, False, tool_start_time, error=error_msg)
                    return {
                        "success": False,
                        "error": error_msg,
                        "stdout": result.stdout,
                        "stderr": result.stderr,
                        "exit_code": result.exit_code,
                        "timed_out": result.timed_out,
                    }
            except Exception as e:
                self._logger.tool_call_complete(tool_name, False, tool_start_time, error=str(e))
                return {"success": False, "error": str(e)}

        # Web search tools
        elif tool_name in ("web_search", "web_news_search"):
            try:
                web_search = self._get_web_search()
                result = await web_search.execute_tool(tool_name, tool_input)
                if result.success:
                    formatted_results = []
                    for r in (result.results or []):
                        item = {
                            "title": r.title,
                            "url": r.url,
                            "snippet": r.snippet,
                        }
                        # Include content if available (Tavily)
                        if r.content:
                            item["content"] = r.content
                        if r.score is not None:
                            item["score"] = r.score
                        formatted_results.append(item)
                    return {
                        "success": True,
                        "query": result.query,
                        "source": result.source,  # "tavily" or "ddg"
                        "results": formatted_results,
                    }
                else:
                    return {
                        "success": False,
                        "error": result.error,
                        "query": result.query,
                    }
            except Exception as e:
                return {"success": False, "error": str(e)}

        # Web fetch tool
        elif tool_name == "web_fetch":
            try:
                web_fetch = self._get_web_fetch()
                result = await web_fetch.execute_tool(tool_name, tool_input)
                if result.success:
                    response = {
                        "success": True,
                        "url": result.url,
                        "content_type": result.content_type,
                        "is_binary": result.is_binary,
                    }
                    if result.content:
                        response["content"] = result.content
                    if result.file_path:
                        response["file_path"] = result.file_path
                    return response
                else:
                    return {
                        "success": False,
                        "url": result.url,
                        "error": result.error,
                    }
            except Exception as e:
                return {"success": False, "error": str(e)}

        # File tools (read, write, edit, glob, grep)
        elif tool_name in ("read", "write", "edit", "glob", "grep"):
            try:
                file_tools = self._get_file_tools()
                result = await file_tools.execute_tool(tool_name, tool_input)

                if tool_name == "read":
                    if result.success:
                        self._logger.tool_call_complete(tool_name, True, tool_start_time, result=f"Read {result.lines_read} lines from {result.file_path}")
                        return {
                            "success": True,
                            "content": result.content,
                            "file_path": result.file_path,
                            "total_lines": result.total_lines,
                            "lines_read": result.lines_read,
                        }
                    else:
                        self._logger.tool_call_complete(tool_name, False, tool_start_time, error=result.error)
                        return {"success": False, "error": result.error}

                elif tool_name == "write":
                    if result.success:
                        self._logger.tool_call_complete(tool_name, True, tool_start_time, result=f"Wrote {result.bytes_written} bytes to {result.file_path}")
                        return {
                            "success": True,
                            "file_path": result.file_path,
                            "bytes_written": result.bytes_written,
                            "created": result.created,
                        }
                    else:
                        self._logger.tool_call_complete(tool_name, False, tool_start_time, error=result.error)
                        return {"success": False, "error": result.error}

                elif tool_name == "edit":
                    if result.success:
                        self._logger.tool_call_complete(tool_name, True, tool_start_time, result=f"Made {result.replacements_made} replacement(s) in {result.file_path}")
                        return {
                            "success": True,
                            "file_path": result.file_path,
                            "replacements_made": result.replacements_made,
                        }
                    else:
                        self._logger.tool_call_complete(tool_name, False, tool_start_time, error=result.error)
                        return {"success": False, "error": result.error}

                elif tool_name == "glob":
                    if result.success:
                        return {
                            "success": True,
                            "files": result.files,
                            "pattern": result.pattern,
                            "count": len(result.files),
                        }
                    else:
                        return {"success": False, "error": result.error}

                elif tool_name == "grep":
                    if result.success:
                        matches = []
                        for m in result.matches:
                            match_data = {
                                "file": m.file,
                                "line": m.line_number,
                                "content": m.content,
                            }
                            if m.context_before:
                                match_data["context_before"] = m.context_before
                            if m.context_after:
                                match_data["context_after"] = m.context_after
                            matches.append(match_data)
                        return {
                            "success": True,
                            "matches": matches,
                            "pattern": result.pattern,
                            "files_searched": result.files_searched,
                        }
                    else:
                        return {"success": False, "error": result.error}

            except Exception as e:
                return {"success": False, "error": str(e)}

        # Todo tool
        elif tool_name == "todo_write":
            try:
                todo_tool = self._get_todo_tool()
                result = await todo_tool.execute_tool(tool_name, tool_input)
                if result.success:
                    return {
                        "success": True,
                        "todos": [t.to_dict() for t in result.todos],
                        "count": len(result.todos),
                    }
                else:
                    return {"success": False, "error": result.error}
            except Exception as e:
                return {"success": False, "error": str(e)}

        # Custom tools
        else:
            try:
                # Find the tool to check credentials
                tool_obj = None
                for t in plan.relevant_tools:
                    if t.name == tool_name:
                        tool_obj = t
                        break

                # Check for missing credentials before execution
                if tool_obj:
                    missing_creds = self.tool_runner.check_credentials(tool_obj)
                    if missing_creds:
                        cred_msg = self.tool_runner.get_missing_credentials_message(tool_obj)
                        return {
                            "success": False,
                            "error": "Missing required credentials",
                            "missing_credentials": [c.env_var for c in missing_creds],
                            "setup_instructions": cred_msg,
                        }

                result = await tool_executor.execute(
                    tool_name=tool_name,
                    tool_input=tool_input,
                )
                return {"success": True, "result": result}

            except ToolExecutionError as e:
                # Analyze the error for permission issues
                error_analysis = analyze_error(
                    error=str(e),
                    stderr=e.stderr,
                    tool_env_vars=tool_obj.required_env_vars if tool_obj else None,
                )

                error_result = {
                    "success": False,
                    "error": str(e),
                    "stdout": e.stdout,
                    "stderr": e.stderr,
                }

                # Add permission-specific advice if relevant
                if error_analysis.is_permission_error:
                    advice = self._permission_advisor.get_quick_advice(error_analysis)
                    error_result["error_category"] = error_analysis.category.value
                    error_result["api_name"] = error_analysis.api_name
                    error_result["permission_advice"] = advice

                return error_result

    async def _execute_step(
        self,
        step: PlanStep,
        plan: Plan,
        step_outputs: dict[str, Any],
        healing_context: list[str],
        tool_executor: ToolExecutor,
    ) -> Any:
        """
        Execute a single step with agentic tool loop.

        Unlike single-turn execution, this implements an agentic loop where:
        1. Claude receives the step and decides what tools to call
        2. Tools are executed and results are returned to Claude
        3. Claude can then call more tools based on the results
        4. Loop continues until Claude stops calling tools or max iterations reached
        """
        # Gather skill content
        skill_content = []
        for skill_id in step.skills_to_use:
            for skill in plan.relevant_skills:
                if skill.id == skill_id:
                    skill_content.append(f"## Skill: {skill.name}\n{skill.content}")

        # Gather tool schemas from custom tools and track their names
        tools = []
        custom_tool_names: set[str] = set()  # Track which tools are custom (can be improved)
        for tool_id in step.tools_to_use:
            for tool in plan.relevant_tools:
                if tool.id == tool_id:
                    schema = tool.get_schema()
                    tools.append(schema)
                    custom_tool_names.add(schema["name"])

        # Add file tools if enabled and relevant to the step (fundamental for coding)
        file_keywords = [
            "file", "read", "write", "edit", "create", "modify", "update",
            "code", "implement", "function", "class", "module", "script",
            "add", "remove", "delete", "change", "fix", "refactor",
            "import", "export", "config", "json", "yaml", "toml",
            ".py", ".js", ".ts", ".go", ".rs", ".java", ".cpp",
        ]
        step_needs_file_tools = any(kw in step.description.lower() for kw in file_keywords)

        if self._enable_file_tools and step_needs_file_tools:
            file_tools = self._get_file_tools()
            tools.extend(file_tools.get_tool_schemas())

        # Add browser tools if enabled and relevant to the step
        browser_keywords = ["browse", "web", "url", "page", "click", "screenshot", "navigate"]
        step_needs_browser = any(kw in step.description.lower() for kw in browser_keywords)

        if self._enable_browser and step_needs_browser:
            browser = await self._get_browser()
            tools.extend(browser.get_tool_schemas())

        # Add bash tools if enabled and relevant to the step
        bash_keywords = [
            "run", "execute", "command", "shell", "bash", "terminal",
            "git", "npm", "pip", "docker", "make", "curl", "wget",
            "install", "build", "test", "deploy",
        ]
        step_needs_bash = any(kw in step.description.lower() for kw in bash_keywords)

        if self._enable_bash and step_needs_bash:
            bash = self._get_bash()
            tools.extend(bash.get_tool_schemas())

        # Add web search tools if enabled and relevant to the step
        search_keywords = [
            "search", "find", "look up", "lookup", "research", "google",
            "discover", "learn about", "information on", "documentation",
            "how to", "what is", "examples of", "tutorial", "guide",
        ]
        step_needs_search = any(kw in step.description.lower() for kw in search_keywords)

        if self._enable_web_search and step_needs_search:
            web_search = self._get_web_search()
            tools.extend(web_search.get_tool_schemas())

        # Add web fetch tool for downloading URLs (always available when search is needed)
        if self._enable_web_fetch and step_needs_search:
            web_fetch = self._get_web_fetch()
            tools.extend(web_fetch.get_tool_schemas())

        # Add todo tool - always available for task tracking in multi-step work
        if self._enable_todo_tool:
            todo_tool = self._get_todo_tool()
            tools.extend(todo_tool.get_tool_schemas())

        # Build execution prompt
        prompt = self._build_execution_prompt(
            step=step,
            skill_content=skill_content,
            step_outputs=step_outputs,
            healing_context=healing_context,
        )

        # No tools available - simple completion
        if not tools:
            return await self.claude.complete(
                prompt=prompt,
                system=EXECUTION_SYSTEM_PROMPT,
            )

        # Agentic tool loop
        messages = [{"role": "user", "content": prompt}]
        all_tool_results = []
        all_text_parts = []
        custom_tool_failures: list[tuple[str, str]] = []  # (tool_name, error) for healing
        custom_tool_successes: set[str] = set()  # Track successful custom tools
        iteration = 0

        while iteration < self._max_tool_iterations:
            iteration += 1

            # Emit loop iteration progress (only after first iteration)
            if iteration > 1:
                await self.progress.emit(
                    ProgressStage.LOOP_ITERATION,
                    f"Agent loop iteration {iteration}",
                    iteration=iteration,
                    max_iterations=self._max_tool_iterations,
                )

            if iteration == 1:
                # First call - use complete_with_tools
                text, tool_calls = await self.claude.complete_with_tools(
                    prompt=prompt,
                    tools=tools,
                    system=EXECUTION_SYSTEM_PROMPT,
                )
            else:
                # Continuation - use continue_with_tool_result
                text, tool_calls = await self.claude.continue_with_tool_result(
                    messages=messages,
                    tool_results=tool_results_for_claude,
                    tools=tools,
                    system=EXECUTION_SYSTEM_PROMPT,
                )

            # Collect text output
            if text:
                all_text_parts.append(text)

            # Build assistant message content for conversation history
            assistant_content = []
            if text:
                assistant_content.append({"type": "text", "text": text})
            for call in tool_calls:
                assistant_content.append({
                    "type": "tool_use",
                    "id": call["id"],
                    "name": call["name"],
                    "input": call["input"],
                })

            messages.append({"role": "assistant", "content": assistant_content})

            # No more tools to call - we're done
            if not tool_calls:
                break

            # Execute all tool calls
            tool_results_for_claude = []
            for idx, call in enumerate(tool_calls):
                tool_name = call["name"]

                # Emit tool call progress
                await self.progress.emit(
                    ProgressStage.LOOP_TOOL_CALL,
                    f"Calling {tool_name}",
                    tool_name=tool_name,
                    tool_index=idx + 1,
                    total_tools=len(tool_calls),
                    iteration=iteration,
                )

                result = await self._execute_single_tool(call, plan, tool_executor)
                all_tool_results.append(result)

                # Track custom tool results for self-healing
                if tool_name in custom_tool_names:
                    if result.get("success", False):
                        custom_tool_successes.add(tool_name)
                    else:
                        error_msg = result.get("error", "Unknown error")
                        traceback_info = result.get("traceback", "")
                        custom_tool_failures.append((tool_name, f"{error_msg}\n{traceback_info}"))

                # Format for Claude's continue_with_tool_result
                tool_results_for_claude.append({
                    "tool_use_id": call["id"],
                    "content": json.dumps(result),
                })

        # Check for custom tool failures that should trigger self-healing
        # Only raise if: (1) there were custom tool failures, and (2) no successful
        # results from those same tools (meaning Claude didn't retry and succeed)
        if custom_tool_failures:
            failed_tool_names = {name for name, _ in custom_tool_failures}
            unrecovered_failures = failed_tool_names - custom_tool_successes

            # If any custom tool failed without subsequent success, raise for healing
            if unrecovered_failures:
                # Use the first unrecovered failure for the exception
                for tool_name, error_details in custom_tool_failures:
                    if tool_name in unrecovered_failures:
                        raise ToolExecutionFailure(
                            f"Custom tool '{tool_name}' failed and needs healing",
                            tool_name=tool_name,
                            error_details=error_details,
                        )

        # Return combined output
        combined_text = "\n\n".join(all_text_parts) if all_text_parts else ""

        if all_tool_results:
            return {"text": combined_text, "tool_results": all_tool_results}
        return combined_text

    def _build_execution_prompt(
        self,
        step: PlanStep,
        skill_content: list[str],
        step_outputs: dict[str, Any],
        healing_context: list[str],
    ) -> str:
        """Build the prompt for step execution."""
        parts = [
            f"# Step: {step.description}",
            f"\nExpected Output: {step.expected_output}",
        ]

        if skill_content:
            parts.append("\n# Guidance from Skills")
            parts.extend(skill_content)

        if step.dependencies and step_outputs:
            parts.append("\n# Previous Step Outputs")
            for dep_id in step.dependencies:
                if dep_id in step_outputs:
                    output = step_outputs[dep_id]
                    parts.append(f"\n## {dep_id}:\n{output}")

        # Include tool build failures so agent knows what tools are unavailable
        # (These failed after all self-healing retry attempts)
        if hasattr(self, '_tool_build_failures') and self._tool_build_failures:
            parts.append("\n# Warning: Some tools failed to build after all retry attempts")
            parts.append(self._tool_build_failures)

        if healing_context:
            parts.append("\n# Previous Attempts (learn from these)")
            for entry in healing_context:
                parts.append(f"\n{entry}")
            parts.append("\nPlease avoid the same mistakes.")

        parts.append("\nExecute this step and provide the expected output.")

        return "\n".join(parts)

    async def _get_healing_suggestion(
        self,
        step: PlanStep,
        error: str,
        trace: str,
        attempt: int,
        failed_tool_name: str | None = None,
    ) -> str:
        """Get a suggestion for how to fix a failed step."""
        if failed_tool_name:
            # Tool-specific healing prompt
            prompt = f"""A custom tool failed during execution and needs to be fixed.

Tool Name: {failed_tool_name}
Step: {step.description}
Expected Output: {step.expected_output}
Error: {error}

Error Trace:
{trace}

This was attempt {attempt + 1}. The tool code has a bug that caused this failure.
Analyze the error and suggest a SPECIFIC fix for the tool's code.
Focus on:
1. What line/logic in the tool caused the error
2. What the fix should be (be specific about the code change)
3. Any edge cases the tool should handle"""
        else:
            prompt = f"""A step in our execution plan failed.

Step: {step.description}
Expected Output: {step.expected_output}
Error: {error}

Error Trace:
{trace}

This was attempt {attempt + 1}. Analyze the error and suggest how to fix it.
Be specific about what went wrong and what should be changed."""

        return await self.claude.complete(prompt=prompt)

    async def _combine_outputs(
        self,
        task: str,
        step_outputs: dict[str, Any],
    ) -> Any:
        """Combine step outputs into a final result with human-readable formatting."""
        # Format each output for readability
        def format_output(output: Any) -> str:
            if output is None:
                return "(no output)"
            if isinstance(output, dict):
                # Check if it looks like raw tool results
                if "tool_results" in output or "success" in output:
                    return json.dumps(output, indent=2, default=str)[:2000]
                return json.dumps(output, indent=2, default=str)
            if isinstance(output, (list, tuple)):
                return json.dumps(output, indent=2, default=str)
            return str(output)

        # For a single step, still ask Claude to format it nicely
        # This prevents raw JSON dumps from being shown to users
        outputs_str = "\n\n".join(
            f"## Step: {step_id}\n```\n{format_output(output)}\n```"
            for step_id, output in step_outputs.items()
        )

        prompt = f"""Original Task: {task}

Raw Step Outputs:
{outputs_str}

Please provide a clear, human-readable summary of the results.
- Extract the key information and present it clearly
- If there are tool results with data, summarize what was found
- If there were any errors, explain them simply
- Keep the response concise but complete
- Format the output nicely with sections if appropriate"""

        return await self.claude.complete(prompt=prompt)
