# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Planner Skill - Metis, Titan of Wisdom and Cunning

Agentic planning and execution for complex multi-step tasks.

Metis was the Titan of wisdom, deep thought, and cunning -
she helped Zeus defeat Kronos through careful planning.

Features:
- Task decomposition - Break complex goals into steps
- Dependency analysis - Understand step ordering
- Execution planning - Create detailed action plans
- Self-correction - Detect failures and adapt
- Tool chaining - Automatically sequence tools
- Memory integration - Learn from past executions
- Parallel execution - Run independent steps concurrently
- Checkpointing - Resume interrupted plans

Architecture:
  Goal → Decompose → Plan → Execute → Verify → Adapt
           ↑                              |
           └──────── Self-correct ←───────┘

This transforms Familiar from a reactive assistant into a
proactive agent that can accomplish complex objectives.
"""

import hashlib
import json
import logging
import threading
import time
import traceback
from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# Import structured outputs for type-safe JSON parsing


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.structured import (
        parse_plan,
        parse_reflection,
    )

    HAS_STRUCTURED = True
except ImportError:
    HAS_STRUCTURED = False
    import re  # Fallback to regex

# Storage paths - use centralized paths
try:
    from familiar.core.paths import DATA_DIR, ensure_dir

    PLANNER_DIR = DATA_DIR / "planner"
except ImportError:
    DATA_DIR = _get_data_dir()
    PLANNER_DIR = DATA_DIR / "planner"

    def ensure_dir(path):
        path.mkdir(parents=True, exist_ok=True)
        return path


PLANS_DIR = PLANNER_DIR / "plans"
EXECUTIONS_DIR = PLANNER_DIR / "executions"
LEARNINGS_FILE = PLANNER_DIR / "learnings.json"

ensure_dir(PLANNER_DIR)
ensure_dir(PLANS_DIR)
ensure_dir(EXECUTIONS_DIR)


class StepStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    SKIPPED = "skipped"
    BLOCKED = "blocked"


class PlanStatus(str, Enum):
    DRAFT = "draft"
    READY = "ready"
    RUNNING = "running"
    PAUSED = "paused"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"


@dataclass
class Step:
    """A single step in a plan."""

    id: str
    description: str
    action_type: str  # "tool", "chat", "condition", "parallel", "human"
    action_config: Dict[str, Any]

    # Dependencies
    depends_on: List[str] = field(default_factory=list)  # Step IDs

    # Execution state
    status: StepStatus = StepStatus.PENDING
    result: Optional[Any] = None
    error: Optional[str] = None
    started_at: Optional[str] = None
    completed_at: Optional[str] = None
    retry_count: int = 0

    # Metadata
    estimated_duration: Optional[int] = None  # seconds
    is_critical: bool = True  # If false, plan can continue on failure
    max_retries: int = 2

    # For conditional steps
    condition: Optional[str] = None  # Expression to evaluate
    if_true: Optional[str] = None  # Step ID to execute if true
    if_false: Optional[str] = None  # Step ID to execute if false


@dataclass
class Plan:
    """A complete execution plan."""

    id: str
    goal: str
    description: str
    steps: List[Step]

    # State
    status: PlanStatus = PlanStatus.DRAFT
    current_step_index: int = 0

    # Context passed between steps
    context: Dict[str, Any] = field(default_factory=dict)

    # Metadata
    created_at: str = ""
    started_at: Optional[str] = None
    completed_at: Optional[str] = None
    created_by: str = "user"

    # Execution settings
    max_iterations: int = 50
    allow_parallel: bool = True
    auto_retry: bool = True
    require_approval: bool = False

    # Results
    final_result: Optional[Any] = None
    execution_log: List[Dict] = field(default_factory=list)


@dataclass
class Learning:
    """A learned pattern from past executions."""

    goal_pattern: str  # Regex pattern for goal matching
    successful_plan: List[Dict]  # Simplified plan structure
    success_rate: float
    times_used: int
    last_used: str
    notes: str = ""


# ============================================================
# PLAN STORAGE
# ============================================================


def _save_plan(plan: Plan):
    """Save plan to disk."""
    path = PLANS_DIR / f"{plan.id}.json"

    data = {
        "id": plan.id,
        "goal": plan.goal,
        "description": plan.description,
        "steps": [asdict(s) for s in plan.steps],
        "status": plan.status.value,
        "current_step_index": plan.current_step_index,
        "context": plan.context,
        "created_at": plan.created_at,
        "started_at": plan.started_at,
        "completed_at": plan.completed_at,
        "created_by": plan.created_by,
        "max_iterations": plan.max_iterations,
        "allow_parallel": plan.allow_parallel,
        "auto_retry": plan.auto_retry,
        "require_approval": plan.require_approval,
        "final_result": plan.final_result,
        "execution_log": plan.execution_log,
    }

    path.write_text(json.dumps(data, indent=2, default=str))


def _load_plan(plan_id: str) -> Optional[Plan]:
    """Load plan from disk."""
    path = PLANS_DIR / f"{plan_id}.json"

    if not path.exists():
        return None

    try:
        data = json.loads(path.read_text())

        steps = []
        for s in data.get("steps", []):
            steps.append(
                Step(
                    id=s["id"],
                    description=s["description"],
                    action_type=s["action_type"],
                    action_config=s["action_config"],
                    depends_on=s.get("depends_on", []),
                    status=StepStatus(s.get("status", "pending")),
                    result=s.get("result"),
                    error=s.get("error"),
                    started_at=s.get("started_at"),
                    completed_at=s.get("completed_at"),
                    retry_count=s.get("retry_count", 0),
                    estimated_duration=s.get("estimated_duration"),
                    is_critical=s.get("is_critical", True),
                    max_retries=s.get("max_retries", 2),
                    condition=s.get("condition"),
                    if_true=s.get("if_true"),
                    if_false=s.get("if_false"),
                )
            )

        return Plan(
            id=data["id"],
            goal=data["goal"],
            description=data.get("description", ""),
            steps=steps,
            status=PlanStatus(data.get("status", "draft")),
            current_step_index=data.get("current_step_index", 0),
            context=data.get("context", {}),
            created_at=data.get("created_at", ""),
            started_at=data.get("started_at"),
            completed_at=data.get("completed_at"),
            created_by=data.get("created_by", "user"),
            max_iterations=data.get("max_iterations", 50),
            allow_parallel=data.get("allow_parallel", True),
            auto_retry=data.get("auto_retry", True),
            require_approval=data.get("require_approval", False),
            final_result=data.get("final_result"),
            execution_log=data.get("execution_log", []),
        )

    except Exception as e:
        logger.error(f"Error loading plan {plan_id}: {e}")
        return None


def _list_plans() -> List[Plan]:
    """List all plans."""
    plans = []
    for path in PLANS_DIR.glob("*.json"):
        plan = _load_plan(path.stem)
        if plan:
            plans.append(plan)
    return sorted(plans, key=lambda p: p.created_at, reverse=True)


def _load_learnings() -> List[Learning]:
    """Load learned patterns."""
    if not LEARNINGS_FILE.exists():
        return []

    try:
        data = json.loads(LEARNINGS_FILE.read_text())
        return [Learning(**item) for item in data]
    except (json.JSONDecodeError, IOError, OSError, TypeError) as e:
        logger.warning(f"Failed to load planner learnings: {e}")
        return []


def _save_learnings(learnings: List[Learning]):
    """Save learned patterns."""
    data = [asdict(item) for item in learnings]
    LEARNINGS_FILE.write_text(json.dumps(data, indent=2))


# ============================================================
# PLAN GENERATION (LLM-based)
# ============================================================

PLANNING_SYSTEM_PROMPT = """You are a planning agent that decomposes complex goals into executable steps.

Given a goal, analyze it and create a detailed plan with the following structure:

1. Break the goal into discrete, actionable steps
2. Identify dependencies between steps (what must complete before what)
3. Determine the action type for each step:
   - "tool": Call a specific tool
   - "chat": Ask the LLM a question or generate content
   - "condition": Make a decision based on previous results
   - "parallel": Run multiple independent steps at once
   - "human": Require human input/approval

4. For each step, specify:
   - Clear description of what the step does
   - The action configuration (tool name + inputs, or prompt)
   - Dependencies (which steps must complete first)
   - Whether it's critical (plan fails if it fails)
   - Estimated duration

Output your plan as a JSON object with this structure:
{
  "description": "Brief description of the overall plan",
  "steps": [
    {
      "id": "step_1",
      "description": "What this step does",
      "action_type": "tool",
      "action_config": {
        "tool": "tool_name",
        "input": {"param": "value"}
      },
      "depends_on": [],
      "is_critical": true,
      "estimated_duration": 30
    },
    {
      "id": "step_2",
      "description": "Generate summary",
      "action_type": "chat",
      "action_config": {
        "prompt": "Summarize the following: {{step_1.result}}"
      },
      "depends_on": ["step_1"],
      "is_critical": true
    }
  ]
}

Use {{step_id.result}} to reference outputs from previous steps.
Use {{context.key}} to reference values from the plan context.

Think carefully about:
- What can be parallelized vs what must be sequential
- Error handling and recovery
- Minimal steps needed to achieve the goal
- Available tools and their capabilities
"""

AVAILABLE_TOOLS_TEMPLATE = """
Available tools you can use in the plan:

{tools_list}

Remember: Only use tools that exist. If a capability isn't available, use "chat" to have the LLM handle it.
"""

REFLECTION_PROMPT = """The previous step failed with this error:
{error}

Step that failed:
{step_description}

Previous successful results:
{context}

Analyze the failure and suggest one of these actions:
1. RETRY - Try the same step again (maybe transient error)
2. MODIFY - Modify the step's parameters and retry
3. SKIP - Skip this step and continue (if not critical)
4. REPLAN - Create new steps to work around the issue
5. ABORT - The goal cannot be achieved

Output your decision as JSON:
{{
  "action": "RETRY|MODIFY|SKIP|REPLAN|ABORT",
  "reason": "Why this action",
  "modifications": {{}},  // If MODIFY, what to change
  "new_steps": []  // If REPLAN, replacement steps
}}
"""


def _generate_plan_with_llm(goal: str, agent, context: Dict[str, Any] = None) -> Optional[Plan]:
    """Use LLM to generate a plan for a goal."""

    # Get available tools
    tools_list = ""
    if agent and hasattr(agent, "tools"):
        for tool in agent.tools.get_schemas():
            tools_list += f"- {tool['name']}: {tool['description']}\n"

    # Build prompt
    prompt = f"""Create a plan to achieve this goal:

GOAL: {goal}

{AVAILABLE_TOOLS_TEMPLATE.format(tools_list=tools_list)}

Additional context:
{json.dumps(context or {}, indent=2)}

Output only valid JSON for the plan."""

    try:
        # Call LLM
        messages = [{"role": "user", "content": prompt}]
        response = agent.provider.chat(
            messages=messages, tools=[], system=PLANNING_SYSTEM_PROMPT, max_tokens=4096
        )

        # Parse response using structured outputs
        response_text = response.text or ""

        if HAS_STRUCTURED:
            # Use Pydantic validation
            result = parse_plan(response_text)
            if not result.success:
                logger.error(f"Plan parsing failed: {result.error}")
                return None

            plan_data = result.raw_json
        else:
            # Fallback to regex (for backwards compatibility)
            import re

            json_match = re.search(r"\{[\s\S]*\}", response_text)
            if not json_match:
                logger.error("No JSON found in planning response")
                return None
            plan_data = json.loads(json_match.group())

        # Build Plan object
        plan_id = hashlib.md5(f"{goal}:{datetime.now().isoformat()}".encode()).hexdigest()[:12]

        steps = []
        for i, s in enumerate(plan_data.get("steps", [])):
            step_id = s.get("id", f"step_{i + 1}")
            steps.append(
                Step(
                    id=step_id,
                    description=s.get("description", ""),
                    action_type=s.get("action_type", "chat"),
                    action_config=s.get("action_config", {}),
                    depends_on=s.get("depends_on", []),
                    is_critical=s.get("is_critical", True),
                    estimated_duration=s.get("estimated_duration"),
                    condition=s.get("condition"),
                    if_true=s.get("if_true"),
                    if_false=s.get("if_false"),
                )
            )

        plan = Plan(
            id=plan_id,
            goal=goal,
            description=plan_data.get("description", ""),
            steps=steps,
            status=PlanStatus.READY,
            context=context or {},
            created_at=datetime.now().isoformat(),
            created_by="planner",
        )

        return plan

    except Exception as e:
        logger.error(f"Plan generation failed: {e}")
        return None


def _reflect_on_failure(step: Step, error: str, context: Dict[str, Any], agent) -> Dict[str, Any]:
    """Use LLM to analyze failure and decide next action."""

    prompt = REFLECTION_PROMPT.format(
        error=error,
        step_description=f"{step.description}\nConfig: {json.dumps(step.action_config)}",
        context=json.dumps(context, indent=2, default=str),
    )

    try:
        messages = [{"role": "user", "content": prompt}]
        response = agent.provider.chat(
            messages=messages,
            tools=[],
            system="You are an error analysis agent. Analyze failures and suggest recovery actions.",
            max_tokens=1024,
        )

        response_text = response.text or ""

        if HAS_STRUCTURED:
            # Use Pydantic validation
            result = parse_reflection(response_text)
            if result.success:
                # Convert Pydantic model to dict
                return {
                    "action": result.value.action,
                    "reason": result.value.reason,
                    "modifications": result.value.modifications,
                    "new_steps": result.value.new_steps,
                }
            else:
                logger.warning(f"Reflection parsing failed: {result.error}")
                return {"action": "ABORT", "reason": f"Could not parse reflection: {result.error}"}
        else:
            # Fallback to regex
            import re

            json_match = re.search(r"\{[\s\S]*\}", response_text)
            if json_match:
                return json.loads(json_match.group())
            return {"action": "ABORT", "reason": "Could not parse reflection"}

    except Exception as e:
        return {"action": "ABORT", "reason": str(e)}


# ============================================================
# PLAN EXECUTION
# ============================================================


class PlanExecutor:
    """
    Executes plans with monitoring, retry logic, and self-correction.

    Usage:
        executor = PlanExecutor(agent)
        result = executor.execute(plan)
    """

    def __init__(self, agent):
        self.agent = agent
        self._stop_requested = False

    def execute(self, plan: Plan, auto_approve: bool = True) -> Dict[str, Any]:
        """Execute a plan and return results."""

        if plan.status == PlanStatus.COMPLETED:
            return {
                "success": True,
                "result": plan.final_result,
                "message": "Plan already completed",
            }

        plan.status = PlanStatus.RUNNING
        plan.started_at = datetime.now().isoformat()
        _save_plan(plan)

        self._stop_requested = False
        iterations = 0

        try:
            while iterations < plan.max_iterations and not self._stop_requested:
                iterations += 1

                # Find next executable steps
                ready_steps = self._get_ready_steps(plan)

                if not ready_steps:
                    # Check if we're done or blocked
                    if self._all_steps_complete(plan):
                        break
                    elif self._has_failed_critical(plan):
                        plan.status = PlanStatus.FAILED
                        break
                    else:
                        # Blocked - shouldn't happen with proper dependency tracking
                        logger.warning("Plan appears blocked")
                        break

                # Execute ready steps (potentially in parallel)
                if plan.allow_parallel and len(ready_steps) > 1:
                    self._execute_parallel(plan, ready_steps)
                else:
                    for step in ready_steps:
                        if self._stop_requested:
                            break
                        self._execute_step(plan, step)

                _save_plan(plan)

            # Determine final status
            if self._all_steps_complete(plan):
                plan.status = PlanStatus.COMPLETED
                plan.final_result = self._compile_results(plan)
            elif self._stop_requested:
                plan.status = PlanStatus.PAUSED
            else:
                plan.status = PlanStatus.FAILED

            plan.completed_at = datetime.now().isoformat()
            _save_plan(plan)

            # Record learning if successful
            if plan.status == PlanStatus.COMPLETED:
                self._record_learning(plan)

            return {
                "success": plan.status == PlanStatus.COMPLETED,
                "status": plan.status.value,
                "result": plan.final_result,
                "iterations": iterations,
                "steps_completed": sum(1 for s in plan.steps if s.status == StepStatus.COMPLETED),
                "steps_failed": sum(1 for s in plan.steps if s.status == StepStatus.FAILED),
            }

        except Exception as e:
            logger.error(f"Plan execution error: {e}\n{traceback.format_exc()}")
            plan.status = PlanStatus.FAILED
            plan.completed_at = datetime.now().isoformat()
            _save_plan(plan)
            return {"success": False, "error": str(e)}

    def stop(self):
        """Request execution to stop."""
        self._stop_requested = True

    def _get_ready_steps(self, plan: Plan) -> List[Step]:
        """Get steps that are ready to execute (dependencies satisfied)."""
        ready = []

        for step in plan.steps:
            if step.status != StepStatus.PENDING:
                continue

            # Check dependencies
            deps_satisfied = True
            for dep_id in step.depends_on:
                dep_step = next((s for s in plan.steps if s.id == dep_id), None)
                if dep_step and dep_step.status != StepStatus.COMPLETED:
                    deps_satisfied = False
                    break

            if deps_satisfied:
                ready.append(step)

        return ready

    def _all_steps_complete(self, plan: Plan) -> bool:
        """Check if all critical steps are complete."""
        for step in plan.steps:
            if step.is_critical and step.status not in [StepStatus.COMPLETED, StepStatus.SKIPPED]:
                return False
        return True

    def _has_failed_critical(self, plan: Plan) -> bool:
        """Check if any critical step has failed."""
        for step in plan.steps:
            if step.is_critical and step.status == StepStatus.FAILED:
                return True
        return False

    def _execute_step(self, plan: Plan, step: Step):
        """Execute a single step."""
        step.status = StepStatus.RUNNING
        step.started_at = datetime.now().isoformat()

        plan.execution_log.append(
            {
                "timestamp": step.started_at,
                "event": "step_started",
                "step_id": step.id,
                "description": step.description,
            }
        )

        try:
            # Render templates in action config
            config = self._render_config(step.action_config, plan.context)

            result = None

            if step.action_type == "tool":
                result = self._execute_tool(config)

            elif step.action_type == "chat":
                result = self._execute_chat(config)

            elif step.action_type == "condition":
                result = self._execute_condition(step, plan)

            elif step.action_type == "human":
                result = self._execute_human_input(step, config)

            elif step.action_type == "parallel":
                # Parallel steps are handled separately
                pass

            else:
                raise ValueError(f"Unknown action type: {step.action_type}")

            # Success
            step.status = StepStatus.COMPLETED
            step.result = result
            step.completed_at = datetime.now().isoformat()

            # Store result in context
            plan.context[step.id] = {"result": result, "status": "completed"}

            plan.execution_log.append(
                {
                    "timestamp": step.completed_at,
                    "event": "step_completed",
                    "step_id": step.id,
                    "result_preview": str(result)[:200] if result else None,
                }
            )

        except Exception as e:
            error_msg = str(e)
            logger.error(f"Step {step.id} failed: {error_msg}")

            step.error = error_msg
            step.retry_count += 1

            plan.execution_log.append(
                {
                    "timestamp": datetime.now().isoformat(),
                    "event": "step_failed",
                    "step_id": step.id,
                    "error": error_msg,
                    "retry_count": step.retry_count,
                }
            )

            # Try self-correction
            if plan.auto_retry and step.retry_count <= step.max_retries:
                recovery = _reflect_on_failure(step, error_msg, plan.context, self.agent)

                plan.execution_log.append(
                    {
                        "timestamp": datetime.now().isoformat(),
                        "event": "reflection",
                        "step_id": step.id,
                        "recovery_action": recovery.get("action"),
                        "reason": recovery.get("reason"),
                    }
                )

                if recovery.get("action") == "RETRY":
                    step.status = StepStatus.PENDING  # Will retry
                    time.sleep(1)  # Brief pause before retry

                elif recovery.get("action") == "MODIFY":
                    # Apply modifications and retry
                    step.action_config.update(recovery.get("modifications", {}))
                    step.status = StepStatus.PENDING

                elif recovery.get("action") == "SKIP" and not step.is_critical:
                    step.status = StepStatus.SKIPPED
                    plan.context[step.id] = {"result": None, "status": "skipped"}

                elif recovery.get("action") == "REPLAN":
                    new_step_defs = recovery.get("new_steps", [])
                    if new_step_defs:
                        # Build Step objects from the LLM-provided dicts
                        new_steps = []
                        for i, s in enumerate(new_step_defs):
                            step_id = s.get("id", f"recovery_{step.id}_{i + 1}")
                            new_steps.append(
                                Step(
                                    id=step_id,
                                    description=s.get("description", ""),
                                    action_type=s.get("action_type", "chat"),
                                    action_config=s.get("action_config", {}),
                                    depends_on=s.get("depends_on", []),
                                    is_critical=s.get("is_critical", False),
                                    estimated_duration=s.get("estimated_duration"),
                                )
                            )

                        # Insert new steps after the failed step
                        failed_idx = plan.steps.index(step)
                        plan.steps[failed_idx + 1 : failed_idx + 1] = new_steps

                        plan.execution_log.append(
                            {
                                "timestamp": datetime.now().isoformat(),
                                "event": "replan_inserted",
                                "failed_step_id": step.id,
                                "new_step_ids": [s.id for s in new_steps],
                            }
                        )
                        logger.info(
                            f"Replan: inserted {len(new_steps)} recovery steps after {step.id}"
                        )

                    step.status = StepStatus.FAILED

                else:
                    step.status = StepStatus.FAILED
            else:
                step.status = StepStatus.FAILED

    def _execute_parallel(self, plan: Plan, steps: List[Step]):
        """Execute multiple steps in parallel."""
        threads = []

        for step in steps:
            t = threading.Thread(target=self._execute_step, args=(plan, step))
            t.start()
            threads.append(t)

        # Wait for all to complete
        for t in threads:
            t.join(timeout=300)  # 5 minute timeout per step

    def _execute_tool(self, config: Dict) -> Any:
        """Execute a tool action."""
        tool_name = config.get("tool", "")
        tool_input = config.get("input", {})

        if not tool_name:
            raise ValueError("No tool specified")

        if self.agent and hasattr(self.agent, "tools"):
            return self.agent.tools.execute(tool_name, tool_input)

        raise RuntimeError("No agent/tools available")

    def _execute_chat(self, config: Dict) -> str:
        """Execute a chat/LLM action."""
        prompt = config.get("prompt", "")
        system = config.get("system", "")  # system prompt override for this step

        if not prompt:
            raise ValueError("No prompt specified")

        if self.agent:
            # Pass system override if provided, otherwise use agent default
            if system and hasattr(self.agent, "chat"):
                return self.agent.chat(prompt, system_override=system, include_history=False)
            return self.agent.chat(prompt, include_history=False)

        raise RuntimeError("No agent available")

    def _execute_condition(self, step: Step, plan: Plan) -> bool:
        """Execute a conditional branch.

        Uses a safe recursive-descent expression parser (familiar.core.expr_parser)
        instead of eval().  The parser supports: ==, !=, <, <=, >, >=, contains,
        startswith, endswith, in, and, or, not, parentheses, string/number/bool
        literals, and bare-word strings.  It never calls eval() or exec().

        Falls back to LLM evaluation only when the expression is syntactically
        valid but the parser cannot resolve it (e.g. a natural-language condition
        that was not reduced to a simple comparison after template substitution).
        """
        condition = step.condition or step.action_config.get("condition", "")

        # Substitute {{step.result}} and {{context.key}} placeholders first
        condition = self._render_config({"c": condition}, plan.context)["c"]

        # --- Safe expression evaluation ---
        try:
            from familiar.core.expr_parser import ExpressionError, evaluate

            return evaluate(condition)
        except ExpressionError as e:
            # Syntactically invalid or unsupported operator — fall back to LLM
            logger.debug(
                f"Condition parser could not evaluate {condition!r}: {e}. Falling back to LLM."
            )
        except ImportError:
            logger.warning(
                "expr_parser not available; falling back to LLM for condition evaluation"
            )

        # --- LLM fallback (natural-language conditions) ---
        prompt = (
            f"Evaluate this condition and respond with only 'true' or 'false': "
            f"{condition}\n\nContext: {json.dumps(plan.context, default=str)}"
        )
        response = self.agent.chat(prompt, include_history=False)
        return "true" in response.lower()

    def _execute_human_input(self, step: Step, config: Dict, plan=None) -> str:
        """Wait for human input."""
        # In a real implementation, this would notify the user and wait
        # For now, we'll mark it as needing approval
        prompt = config.get("prompt", "Please provide input")

        if plan is not None:
            plan.execution_log.append(
                {
                    "timestamp": datetime.now().isoformat(),
                    "event": "human_input_required",
                    "step_id": step.id,
                    "prompt": prompt,
                }
            )

        # This would block waiting for input in a real implementation
        raise NotImplementedError("Human input steps require interactive mode")

    def _render_config(self, config: Dict, context: Dict) -> Dict:
        """Render template variables in config."""

        def render_value(value):
            if isinstance(value, str):
                # Find all {{...}} patterns
                pattern = r"\{\{([^}]+)\}\}"

                def replace(match):
                    path = match.group(1).strip()
                    parts = path.split(".")

                    # Navigate context
                    current = context
                    for part in parts:
                        if isinstance(current, dict):
                            current = current.get(part, "")
                        else:
                            return ""

                    return str(current) if current else ""

                return re.sub(pattern, replace, value)

            elif isinstance(value, dict):
                return {k: render_value(v) for k, v in value.items()}

            elif isinstance(value, list):
                return [render_value(v) for v in value]

            return value

        return render_value(config)

    def _compile_results(self, plan: Plan) -> Dict[str, Any]:
        """Compile final results from all steps."""
        results = {}

        for step in plan.steps:
            if step.status == StepStatus.COMPLETED and step.result is not None:
                results[step.id] = step.result

        # Get the last step's result as the primary result
        completed_steps = [s for s in plan.steps if s.status == StepStatus.COMPLETED]
        if completed_steps:
            results["final"] = completed_steps[-1].result

        return results

    def _record_learning(self, plan: Plan):
        """Record successful plan as a learning for future use."""
        learnings = _load_learnings()

        # Create simplified plan structure
        simplified_steps = []
        for step in plan.steps:
            simplified_steps.append(
                {
                    "description": step.description,
                    "action_type": step.action_type,
                    "action_config": step.action_config,
                    "depends_on": step.depends_on,
                }
            )

        # Check if similar goal pattern exists
        goal_lower = plan.goal.lower()

        # Create a simple pattern (first few significant words)
        words = re.findall(r"\b\w+\b", goal_lower)[:5]
        pattern = ".*" + ".*".join(words) + ".*"

        # Find existing or create new
        existing = next((entry for entry in learnings if entry.goal_pattern == pattern), None)

        if existing:
            existing.success_rate = (existing.success_rate * existing.times_used + 1) / (
                existing.times_used + 1
            )
            existing.times_used += 1
            existing.last_used = datetime.now().isoformat()
        else:
            learnings.append(
                Learning(
                    goal_pattern=pattern,
                    successful_plan=simplified_steps,
                    success_rate=1.0,
                    times_used=1,
                    last_used=datetime.now().isoformat(),
                )
            )

        _save_learnings(learnings)


# ============================================================
# TOOL HANDLERS
# ============================================================


def create_plan(data: dict) -> str:
    """Create a plan for achieving a goal."""
    goal = data.get("goal", "").strip()
    context = data.get("context", {})
    auto_execute = data.get("execute", False)

    if not goal:
        return "Please provide a goal"

    agent = data.get("_agent")

    if not agent:
        return "❌ No agent available for planning"

    # Check for similar past plans
    learnings = _load_learnings()
    for learning in learnings:
        if re.search(learning.goal_pattern, goal.lower(), re.IGNORECASE):
            logger.info(f"Found similar past plan with {learning.success_rate:.0%} success rate")

    # Generate plan
    plan = _generate_plan_with_llm(goal, agent, context)

    if not plan:
        return "❌ Failed to generate plan"

    _save_plan(plan)

    # Format output
    lines = [f"📋 **Plan Created: {plan.id}**\n"]
    lines.append(f"Goal: {plan.goal}")
    lines.append(f"Description: {plan.description}\n")
    lines.append(f"Steps ({len(plan.steps)}):")

    for i, step in enumerate(plan.steps, 1):
        deps = f" (after: {', '.join(step.depends_on)})" if step.depends_on else ""
        critical = " ⚠️" if step.is_critical else ""
        lines.append(f"  {i}. [{step.action_type}] {step.description}{deps}{critical}")

    if auto_execute:
        lines.append("\n🚀 Executing plan...")
        executor = PlanExecutor(agent)
        result = executor.execute(plan)

        lines.append(f"\nResult: {'✓ Success' if result['success'] else '✗ Failed'}")
        lines.append(f"Steps completed: {result.get('steps_completed', 0)}/{len(plan.steps)}")

        if result.get("result"):
            final = result["result"].get("final", "")
            if final:
                lines.append(f"\nFinal output:\n{str(final)[:500]}")
    else:
        lines.append(f"\nRun with: execute_plan id={plan.id}")

    return "\n".join(lines)


def execute_plan(data: dict) -> str:
    """Execute an existing plan."""
    plan_id = data.get("id", data.get("plan_id", ""))

    if not plan_id:
        return "Please provide a plan ID"

    plan = _load_plan(plan_id)
    if not plan:
        return f"Plan not found: {plan_id}"

    agent = data.get("_agent")
    if not agent:
        return "❌ No agent available"

    executor = PlanExecutor(agent)
    result = executor.execute(plan)

    lines = [f"📋 **Plan Execution: {plan.goal}**\n"]
    lines.append(f"Status: {result['status']}")
    lines.append(f"Steps completed: {result.get('steps_completed', 0)}/{len(plan.steps)}")
    lines.append(f"Iterations: {result.get('iterations', 0)}")

    if result.get("error"):
        lines.append(f"\n❌ Error: {result['error']}")

    if result.get("result"):
        final = result["result"].get("final", "")
        if final:
            lines.append(f"\n**Result:**\n{str(final)[:1000]}")

    return "\n".join(lines)


def list_plans(data: dict) -> str:
    """List all plans."""
    plans = _list_plans()

    if not plans:
        return "No plans found. Create one with: create_plan"

    status_filter = data.get("status")
    if status_filter:
        plans = [p for p in plans if p.status.value == status_filter]

    lines = ["📋 **Plans:**\n"]

    status_emoji = {
        "draft": "📝",
        "ready": "✅",
        "running": "🔄",
        "paused": "⏸️",
        "completed": "✓",
        "failed": "✗",
        "cancelled": "🚫",
    }

    for plan in plans[:20]:  # Limit to 20
        emoji = status_emoji.get(plan.status.value, "•")
        lines.append(f"{emoji} **{plan.goal[:50]}**")
        lines.append(f"    ID: {plan.id} | Status: {plan.status.value}")
        lines.append(f"    Steps: {len(plan.steps)} | Created: {plan.created_at[:10]}")
        lines.append("")

    return "\n".join(lines)


def get_plan(data: dict) -> str:
    """Get detailed information about a plan."""
    plan_id = data.get("id", data.get("plan_id", ""))

    if not plan_id:
        return "Please provide a plan ID"

    plan = _load_plan(plan_id)
    if not plan:
        return f"Plan not found: {plan_id}"

    lines = [f"📋 **Plan: {plan.goal}**\n"]
    lines.append(f"ID: {plan.id}")
    lines.append(f"Status: {plan.status.value}")
    lines.append(f"Description: {plan.description}")
    lines.append(f"Created: {plan.created_at}")

    if plan.started_at:
        lines.append(f"Started: {plan.started_at}")
    if plan.completed_at:
        lines.append(f"Completed: {plan.completed_at}")

    lines.append(f"\n**Steps ({len(plan.steps)}):**")

    status_emoji = {
        "pending": "○",
        "running": "►",
        "completed": "✓",
        "failed": "✗",
        "skipped": "⊘",
        "blocked": "⊗",
    }

    for step in plan.steps:
        emoji = status_emoji.get(step.status.value, "•")
        lines.append(f"\n{emoji} **{step.id}**: {step.description}")
        lines.append(f"    Type: {step.action_type}")

        if step.depends_on:
            lines.append(f"    Depends on: {', '.join(step.depends_on)}")

        if step.status == StepStatus.COMPLETED and step.result:
            result_preview = str(step.result)[:100]
            lines.append(f"    Result: {result_preview}...")

        if step.status == StepStatus.FAILED and step.error:
            lines.append(f"    Error: {step.error}")

    if plan.context:
        lines.append(f"\n**Context:** {len(plan.context)} entries")

    return "\n".join(lines)


def cancel_plan(data: dict) -> str:
    """Cancel a running plan."""
    plan_id = data.get("id", data.get("plan_id", ""))

    if not plan_id:
        return "Please provide a plan ID"

    plan = _load_plan(plan_id)
    if not plan:
        return f"Plan not found: {plan_id}"

    if not data.get("_confirmed"):
        from familiar.core.confirmations import needs_confirmation

        return needs_confirmation(
            tool_name="cancel_plan",
            tool_input=data,
            preview=f"Cancel plan '{plan_id}'. Running steps will be stopped.",
            risk="medium",
        )

    plan.status = PlanStatus.CANCELLED
    plan.completed_at = datetime.now().isoformat()
    _save_plan(plan)

    return f"✓ Cancelled plan: {plan.goal}"


def resume_plan(data: dict) -> str:
    """Resume a paused or failed plan."""
    plan_id = data.get("id", data.get("plan_id", ""))
    from_step = data.get("from_step")

    if not plan_id:
        return "Please provide a plan ID"

    plan = _load_plan(plan_id)
    if not plan:
        return f"Plan not found: {plan_id}"

    if plan.status not in [PlanStatus.PAUSED, PlanStatus.FAILED]:
        return f"Plan is not paused or failed (status: {plan.status.value})"

    # Reset failed steps to pending
    for step in plan.steps:
        if step.status == StepStatus.FAILED:
            step.status = StepStatus.PENDING
            step.error = None

    if from_step:
        # Reset everything from this step onwards
        found = False
        for step in plan.steps:
            if step.id == from_step:
                found = True
            if found:
                step.status = StepStatus.PENDING
                step.result = None
                step.error = None

    plan.status = PlanStatus.READY
    _save_plan(plan)

    # Execute
    return execute_plan({"id": plan_id, "_agent": data.get("_agent")})


def accomplish_goal(data: dict) -> str:
    """High-level command: Create and execute a plan for a goal."""
    goal = data.get("goal", "").strip()

    if not goal:
        return "Please provide a goal"

    # Create and execute in one step
    data["execute"] = True
    return create_plan(data)


def show_learnings(data: dict) -> str:
    """Show learned patterns from past executions."""
    learnings = _load_learnings()

    if not learnings:
        return "No learnings recorded yet. Successfully complete some plans first."

    lines = ["🧠 **Learned Patterns:**\n"]

    for learning in sorted(learnings, key=lambda entry: entry.times_used, reverse=True):
        lines.append(f"• Pattern: `{learning.goal_pattern[:50]}`")
        lines.append(f"  Success rate: {learning.success_rate:.0%} ({learning.times_used} uses)")
        lines.append(f"  Steps: {len(learning.successful_plan)}")
        lines.append("")

    return "\n".join(lines)


# ============================================================
# TOOL DEFINITIONS
# ============================================================

TOOLS = [
    {
        "name": "accomplish",
        "description": (
            "Accomplish a complex goal by automatically creating and executing a "
            "multi-step plan. Use this for tasks that require decomposition into "
            "sequential or parallel steps with tool chaining. The planner uses LLM "
            "reasoning to generate, execute, and self-correct the plan."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "goal": {"type": "string", "description": "The goal to accomplish (be specific)"},
                "context": {"type": "object", "description": "Additional context or parameters"},
            },
            "required": ["goal"],
        },
        "handler": accomplish_goal,
        "category": "planner",
    },
    {
        "name": "create_plan",
        "description": (
            "Create a detailed execution plan for achieving a goal without running it. "
            "Use this to preview and inspect a plan before committing to execution. "
            "The plan includes step dependencies, action types, and estimated durations. "
            "Set execute=true to run immediately after creation."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "goal": {"type": "string", "description": "The goal to plan for"},
                "context": {"type": "object", "description": "Additional context"},
                "execute": {
                    "type": "boolean",
                    "description": "Whether to execute immediately",
                    "default": False,
                },
            },
            "required": ["goal"],
        },
        "handler": create_plan,
        "category": "planner",
    },
    {
        "name": "execute_plan",
        "description": (
            "Execute an existing plan by running its steps in dependency order with "
            "automatic retry and self-correction on failure. Use this to run a plan "
            "that was previously created with create_plan. Independent steps may be "
            "executed in parallel if the plan allows it."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"id": {"type": "string", "description": "Plan ID to execute"}},
            "required": ["id"],
        },
        "handler": execute_plan,
        "category": "planner",
    },
    {
        "name": "list_plans",
        "description": (
            "List all plans with their current status, step counts, and creation dates. "
            "Use this to see an overview of draft, running, completed, and failed plans. "
            "Results can be filtered by status to find plans in a specific state."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "enum": [
                        "draft",
                        "ready",
                        "running",
                        "paused",
                        "completed",
                        "failed",
                        "cancelled",
                    ],
                    "description": "Filter by status",
                }
            },
        },
        "handler": list_plans,
        "category": "planner",
    },
    {
        "name": "get_plan",
        "description": (
            "Get detailed information about a specific plan including all steps, their "
            "statuses, results, and errors. Use this to inspect plan progress, debug "
            "failures, or review completed step outputs. Also shows the plan context "
            "and execution timeline."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"id": {"type": "string", "description": "Plan ID"}},
            "required": ["id"],
        },
        "handler": get_plan,
        "category": "planner",
    },
    {
        "name": "cancel_plan",
        "description": (
            "Cancel a running or pending plan and mark it as cancelled. Use this to "
            "stop a plan that is no longer needed or is producing undesired results. "
            "Running steps will be stopped and the plan cannot be resumed after "
            "cancellation."
        ),
        "requires_confirmation": True,
        "input_schema": {
            "type": "object",
            "properties": {"id": {"type": "string", "description": "Plan ID to cancel"}},
            "required": ["id"],
        },
        "handler": cancel_plan,
        "category": "planner",
    },
    {
        "name": "resume_plan",
        "description": (
            "Resume a paused or failed plan from where it left off or from a specific "
            "step. Use this to retry a plan after fixing the cause of a failure or to "
            "continue a plan that was paused. Failed steps are reset to pending before "
            "re-execution."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "id": {"type": "string", "description": "Plan ID to resume"},
                "from_step": {"type": "string", "description": "Step ID to resume from (optional)"},
            },
            "required": ["id"],
        },
        "handler": resume_plan,
        "category": "planner",
    },
    {
        "name": "learnings",
        "description": (
            "Show learned patterns extracted from successful plan executions. Use this "
            "to review what the planner has learned from past runs, including success "
            "rates and reusable step sequences. Learnings improve future plan generation "
            "for similar goals."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": show_learnings,
        "category": "planner",
    },
]
