#!/usr/bin/env python3
"""
door.py - Echo Door v0 (WO#72)
===============================

The external controller/orchestrator. Echo Door is what sits between
the user and the Echo Shell, managing:

1. Multi-pass reasoning (think → plan → execute → reflect)
2. Tool invocation (Ginnie tools, Helix tools, etc.)
3. Session management (memory, receipts, corpus)
4. Wave scheduling (FibPi3D valley timing)

Architecture from ChatGPT's spec:
  User → [Door] → [Shell] → [LLM (genome)] → output
           ↓
    [Tool Orchestrator]
           ↓
    [Jazz/Wave Scheduler]

The Door doesn't think - it routes. The Shell thinks through the genome.
"""

import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from helix_cdc.echo.genome import EchoGenome, MISTRAL_GENOME
from helix_cdc.echo.shell import EchoShell, ShellOutput, create_shell


@dataclass
class DoorMessage:
    """A message in the Door conversation."""
    role: str  # "user", "assistant", "system", "tool"
    content: str
    tool_name: Optional[str] = None
    tool_result: Optional[Dict[str, Any]] = None
    timestamp: float = field(default_factory=time.time)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "role": self.role,
            "content": self.content,
            "tool_name": self.tool_name,
            "tool_result": self.tool_result,
            "timestamp": self.timestamp,
        }


@dataclass
class DoorConfig:
    """Echo Door configuration."""
    # Multi-pass reasoning
    enable_think_pass: bool = True
    enable_plan_pass: bool = True
    enable_reflect_pass: bool = True
    max_tool_calls: int = 5
    max_reasoning_cycles: int = 3

    # Wave scheduling
    enable_wave_timing: bool = True
    valley_threshold: float = 40.0

    # Memory
    max_conversation_length: int = 50
    persist_to_corpus: bool = True

    # Receipts
    receipts_dir: Path = field(default_factory=lambda: Path.home() / "helix-cdc/receipts")


@dataclass
class DoorResult:
    """Result from a Door interaction."""
    response: str
    passes: List[Dict[str, Any]]  # think, plan, execute, reflect passes
    tool_calls: List[Dict[str, Any]]
    shell_outputs: List[Dict[str, Any]]
    total_cycles: int
    duration_s: float
    xi_signature: str
    success: bool = True

    def to_dict(self) -> Dict[str, Any]:
        return {
            "response": self.response,
            "passes": self.passes,
            "tool_calls": self.tool_calls,
            "shell_outputs": self.shell_outputs,
            "total_cycles": self.total_cycles,
            "duration_s": self.duration_s,
            "xi_signature": self.xi_signature,
            "success": self.success,
        }


class EchoDoor:
    """
    Echo Door v0 - External controller/orchestrator.

    Routes between user, shell, and tools. Manages multi-pass reasoning.
    """

    def __init__(
        self,
        genome: Optional[EchoGenome] = None,
        config: Optional[DoorConfig] = None,
    ):
        """
        Initialize Echo Door.

        Args:
            genome: EchoGenome for the shell
            config: DoorConfig for behavior
        """
        self.genome = genome or MISTRAL_GENOME
        self.config = config or DoorConfig()

        # Create shell
        self.shell = create_shell(self.genome)

        # Tool registry
        self._tools: Dict[str, Callable] = {}

        # Conversation history
        self.messages: List[DoorMessage] = []

        # Load default tools
        self._load_default_tools()

    def _load_default_tools(self):
        """Load default Helix and symbolic tools."""
        try:
            from helix_cdc.ginnie.tools_helix import HELIX_TOOLS
            for name, func in HELIX_TOOLS.items():
                self._tools[f"helix_{name}"] = func
        except ImportError:
            pass

        try:
            from helix_cdc.ginnie.tools_symbolic import SYMBOLIC_TOOLS
            for name, func in SYMBOLIC_TOOLS.items():
                self._tools[f"symbolic_{name}"] = func
        except ImportError:
            pass

    def register_tool(self, name: str, func: Callable) -> None:
        """Register a custom tool."""
        self._tools[name] = func

    def list_tools(self) -> List[str]:
        """List available tools."""
        return list(self._tools.keys())

    def _wait_for_valley(self) -> bool:
        """Wait for CPU valley before heavy operations."""
        if not self.config.enable_wave_timing:
            return True

        try:
            from helix_cdc.jazz import fire_on_valley
            return fire_on_valley(self.config.valley_threshold)
        except ImportError:
            return True

    def _think_pass(self, prompt: str) -> Dict[str, Any]:
        """
        Think pass: Analyze what the user is asking.

        Returns analysis of intent, requirements, approach.
        """
        think_prompt = f"""[THINK PASS]
Analyze this user request. What are they asking for?
What approach should we take? What tools might we need?

User request: {prompt}

Provide brief analysis (2-3 sentences)."""

        output = self.shell.cycle(think_prompt)
        return {
            "pass": "think",
            "output": output.text,
            "cycle": output.cycle,
            "duration_s": output.duration_s,
        }

    def _plan_pass(self, prompt: str, think_output: str) -> Dict[str, Any]:
        """
        Plan pass: Create execution plan.

        Returns structured plan with steps.
        """
        plan_prompt = f"""[PLAN PASS]
Based on this analysis, create a brief action plan.

Analysis: {think_output}
Original request: {prompt}

List 1-3 concrete steps to accomplish this."""

        output = self.shell.cycle(plan_prompt)
        return {
            "pass": "plan",
            "output": output.text,
            "cycle": output.cycle,
            "duration_s": output.duration_s,
        }

    def _execute_pass(self, prompt: str, plan_output: str) -> tuple:
        """
        Execute pass: Run the plan, potentially calling tools.

        Returns (output, tool_calls).
        """
        tool_calls = []

        # Check if plan mentions any tools
        plan_lower = plan_output.lower()
        tools_to_try = []

        for tool_name in self._tools:
            # Check if tool is mentioned in plan
            short_name = tool_name.replace("helix_", "").replace("symbolic_", "")
            if short_name in plan_lower or tool_name in plan_lower:
                tools_to_try.append(tool_name)

        # Execute up to max_tool_calls tools
        for tool_name in tools_to_try[:self.config.max_tool_calls]:
            try:
                tool_func = self._tools[tool_name]
                result = tool_func()  # Most tools don't need args
                tool_calls.append({
                    "tool": tool_name,
                    "result": result,
                    "success": True,
                })
            except Exception as e:
                tool_calls.append({
                    "tool": tool_name,
                    "error": str(e),
                    "success": False,
                })

        # Generate response incorporating tool results
        tool_context = ""
        if tool_calls:
            tool_summaries = []
            for tc in tool_calls:
                if tc["success"]:
                    tool_summaries.append(f"- {tc['tool']}: completed")
                else:
                    tool_summaries.append(f"- {tc['tool']}: failed ({tc.get('error', 'unknown')})")
            tool_context = f"\n\nTool results:\n" + "\n".join(tool_summaries)

        execute_prompt = f"""[EXECUTE PASS]
Execute the plan and provide the response to the user.

Plan: {plan_output}
Original request: {prompt}{tool_context}

Provide the final response."""

        output = self.shell.cycle(execute_prompt)
        return (
            {
                "pass": "execute",
                "output": output.text,
                "cycle": output.cycle,
                "duration_s": output.duration_s,
            },
            tool_calls,
        )

    def _reflect_pass(self, prompt: str, execute_output: str) -> Dict[str, Any]:
        """
        Reflect pass: Evaluate if we answered well.

        Returns reflection and any corrections.
        """
        reflect_prompt = f"""[REFLECT PASS]
Did we answer the user's request well? Any corrections or additions?

Original request: {prompt}
Our response: {execute_output}

Brief evaluation (1-2 sentences). If good, just say "Response complete."
If needs correction, provide it."""

        output = self.shell.cycle(reflect_prompt)
        return {
            "pass": "reflect",
            "output": output.text,
            "cycle": output.cycle,
            "duration_s": output.duration_s,
        }

    def process(self, prompt: str) -> DoorResult:
        """
        Process a user prompt through the Door.

        Runs multi-pass reasoning: think → plan → execute → reflect

        Args:
            prompt: User's input

        Returns:
            DoorResult with response and metadata
        """
        start_time = time.perf_counter()
        passes = []
        tool_calls = []
        shell_outputs = []

        # Wait for valley before heavy processing
        self._wait_for_valley()

        # Add user message
        self.messages.append(DoorMessage(role="user", content=prompt))

        # Think pass
        if self.config.enable_think_pass:
            think_result = self._think_pass(prompt)
            passes.append(think_result)
            shell_outputs.append(think_result)
            think_output = think_result["output"]
        else:
            think_output = prompt

        # Plan pass
        if self.config.enable_plan_pass:
            plan_result = self._plan_pass(prompt, think_output)
            passes.append(plan_result)
            shell_outputs.append(plan_result)
            plan_output = plan_result["output"]
        else:
            plan_output = think_output

        # Execute pass
        execute_result, exec_tool_calls = self._execute_pass(prompt, plan_output)
        passes.append(execute_result)
        shell_outputs.append(execute_result)
        tool_calls.extend(exec_tool_calls)
        execute_output = execute_result["output"]

        # Reflect pass
        if self.config.enable_reflect_pass:
            reflect_result = self._reflect_pass(prompt, execute_output)
            passes.append(reflect_result)
            shell_outputs.append(reflect_result)

            # If reflection suggests correction, use that
            if "correction" in reflect_result["output"].lower():
                final_response = reflect_result["output"]
            else:
                final_response = execute_output
        else:
            final_response = execute_output

        # Add assistant message
        self.messages.append(DoorMessage(role="assistant", content=final_response))

        # Trim conversation if needed
        if len(self.messages) > self.config.max_conversation_length:
            self.messages = self.messages[-self.config.max_conversation_length:]

        duration = time.perf_counter() - start_time

        result = DoorResult(
            response=final_response,
            passes=passes,
            tool_calls=tool_calls,
            shell_outputs=shell_outputs,
            total_cycles=self.shell.state.cycle,
            duration_s=duration,
            xi_signature=self.genome.xi_signature(),
            success=True,
        )

        # Persist to corpus if enabled
        if self.config.persist_to_corpus:
            self._save_to_corpus(prompt, result)

        return result

    def _save_to_corpus(self, prompt: str, result: DoorResult):
        """Save interaction to corpus for training."""
        try:
            from helix_cdc.ginnie.corpus import CorpusRecord, append_to_corpus

            record = CorpusRecord(
                wo_id=f"door_{int(time.time())}",
                request=prompt,
                backend="local",
                request_type="door_interaction",
                plan={"passes": result.passes},
                steps=result.shell_outputs,
                final_summary=result.response,
                success=result.success,
                status="success" if result.success else "failed",
                duration_sec=result.duration_s,
                scars=self.shell.state.scars,
                reflex_edits=0,
                helix_metrics={},
                symbolic_metrics={"xi_signature": result.xi_signature},
            )
            append_to_corpus(record)
        except Exception:
            pass  # Don't fail if corpus capture fails

    def reset(self):
        """Reset Door state."""
        self.messages = []
        self.shell.reset()


def create_door(genome: Optional[EchoGenome] = None) -> EchoDoor:
    """Create an Echo Door with default or custom genome."""
    return EchoDoor(genome)
