#!/usr/bin/env python3
"""
Echo Terminal CLI - Interactive REPL for Echo Agent Dispatch

WO-ECHO-TERMINAL-01: Thin REPL wrapper that wires existing Echo agents
into a terminal command. Routes user queries to appropriate agents via
intent detection and displays results with receipt info.

Usage:
    echo                    # Start interactive REPL
    echo "run tests"        # Single command mode
    echo --help             # Show help

Built on existing infrastructure:
    - helix_cdc/echo/agents/ - DispatchableAgent pool
    - helix_cdc/echo/ops/helpers.py - Intent detection
    - helix_cdc/receipts.py - Receipt verification
"""

from __future__ import annotations

import argparse
import asyncio
import hashlib
import json
import os
import readline  # Enable line editing in input()
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

# Project root
PROJECT_ROOT = Path(__file__).resolve().parents[3]

# ANSI color codes for terminal output
class Colors:
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    BLUE = "\033[34m"
    MAGENTA = "\033[35m"
    CYAN = "\033[36m"
    RED = "\033[31m"

    @classmethod
    def disable(cls):
        """Disable colors for non-TTY output."""
        cls.RESET = ""
        cls.BOLD = ""
        cls.DIM = ""
        cls.GREEN = ""
        cls.YELLOW = ""
        cls.BLUE = ""
        cls.MAGENTA = ""
        cls.CYAN = ""
        cls.RED = ""


# Check if we're in a terminal
if not sys.stdout.isatty():
    Colors.disable()


@dataclass
class SessionState:
    """
    Session state for multi-turn context tracking.

    Tracks:
    - Session ID for coauthor trace
    - Query history
    - Last agent results
    - Session start time
    """
    session_id: str = field(default_factory=lambda: hashlib.sha256(
        f"echo:{datetime.now(timezone.utc).isoformat()}:{time.time_ns()}".encode()
    ).hexdigest()[:16])

    start_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    query_history: List[Dict[str, Any]] = field(default_factory=list)
    last_result: Optional[Dict[str, Any]] = None
    agent_calls: int = 0

    def record_query(self, query: str, agent_type: str, result: Any, elapsed_ms: float):
        """Record a query and its result."""
        entry = {
            "query": query,
            "agent_type": agent_type,
            "elapsed_ms": elapsed_ms,
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }
        self.query_history.append(entry)
        self.last_result = result
        self.agent_calls += 1


def route_query(query: str) -> Dict[str, Any]:
    """
    Route a query to the appropriate agent based on intent detection.

    Uses intent detectors from helix_cdc/echo/ops/helpers.py to determine
    which agent should handle the query.

    Returns:
        dict with:
            - agent_type: str - The agent type to dispatch to
            - confidence: float - Routing confidence
            - detection: dict - Full detection result
    """
    # Import intent detectors
    try:
        from helix_cdc.echo.ops.helpers import (
            detect_test_intent,
            detect_patch_intent,
            detect_uncertainty_intent,
        )
    except ImportError as e:
        return {
            "agent_type": "codebase",
            "confidence": 0.3,
            "detection": {"error": str(e)},
            "fallback": True,
        }

    # Run intent detectors
    test_intent = detect_test_intent(query)
    patch_intent = detect_patch_intent(query)
    uncertainty_intent = detect_uncertainty_intent(query)

    # Priority routing:
    # 1. Test intent (execution) - highest priority if parseable command
    # 2. Patch intent (modification) - if clear edit request
    # 3. Uncertainty intent (search) - for knowledge gaps
    # 4. Codebase agent - default fallback

    # Check test intent first (if we have a parseable command)
    if test_intent.get("is_test") and test_intent.get("parsed_command"):
        return {
            "agent_type": "test",
            "confidence": test_intent["confidence"],
            "detection": test_intent,
            "intent": "execution",
        }

    # Check patch intent
    if patch_intent.get("is_patch") and patch_intent.get("confidence", 0) > 0.5:
        return {
            "agent_type": "patch",
            "confidence": patch_intent["confidence"],
            "detection": patch_intent,
            "intent": "modification",
        }

    # Check uncertainty intent
    if uncertainty_intent.get("is_uncertain"):
        uncertainty_type = uncertainty_intent.get("uncertainty_type", "codebase")
        suggested_agents = uncertainty_intent.get("suggested_agents", ["codebase"])
        return {
            "agent_type": suggested_agents[0] if suggested_agents else "codebase",
            "confidence": uncertainty_intent["confidence"],
            "detection": uncertainty_intent,
            "intent": uncertainty_type,
        }

    # Check if chat agent is available for fallback
    try:
        from helix_cdc.echo.agents import get_agent
        chat_agent = get_agent("chat")
        chat_available = chat_agent is not None and chat_agent.available()
    except Exception:
        chat_available = False

    # Fallback: Use chat if available, else codebase
    if chat_available:
        return {
            "agent_type": "chat",
            "confidence": 0.5,
            "detection": {},
            "intent": "conversation",
            "fallback": True,
        }
    else:
        return {
            "agent_type": "codebase",
            "confidence": 0.5,
            "detection": {},
            "intent": "codebase_search",
            "fallback": True,
        }


async def execute_agent(agent_type: str, query: str, context: Dict[str, Any]) -> Dict[str, Any]:
    """
    Execute an agent with the given query.

    Args:
        agent_type: Type of agent to dispatch (codebase, test, patch, plan)
        query: User's query
        context: Additional context (routing info, session state)

    Returns:
        Agent result dict or error dict
    """
    try:
        from helix_cdc.echo.agents import AGENT_REGISTRY, get_agent
    except ImportError as e:
        return {
            "error": f"Failed to import agent registry: {e}",
            "agent_type": agent_type,
            "query": query,
        }

    # Ensure agents are registered by importing them
    try:
        from helix_cdc.echo.agents import codebase_agent
        from helix_cdc.echo.agents import test_agent
        from helix_cdc.echo.agents import patch_agent
        from helix_cdc.echo.agents import plan_agent
        from helix_cdc.echo.agents import search_agent
        from helix_cdc.echo.agents import chat_agent
    except ImportError:
        pass  # Some agents may not be available

    agent = get_agent(agent_type)

    if agent is None:
        # Try codebase as fallback
        agent = get_agent("codebase")
        if agent is None:
            return {
                "error": f"Agent '{agent_type}' not found and no fallback available",
                "available_agents": list(AGENT_REGISTRY.keys()),
            }
        agent_type = "codebase"

    if not agent.available():
        return {
            "error": f"Agent '{agent_type}' is not available (check dependencies)",
            "agent_type": agent_type,
        }

    # Execute the agent
    try:
        result = await agent.execute(query, context)
        return result.to_dict() if hasattr(result, 'to_dict') else result
    except Exception as e:
        return {
            "error": f"Agent execution failed: {e}",
            "agent_type": agent_type,
            "query": query,
        }


def format_result(result: Dict[str, Any], routing: Dict[str, Any]) -> str:
    """
    Format agent result for terminal display.

    Args:
        result: Agent result dict
        routing: Routing decision dict

    Returns:
        Formatted string for display
    """
    lines = []

    agent_type = result.get("agent_type", routing.get("agent_type", "unknown"))
    elapsed_ms = result.get("elapsed_ms", 0)
    confidence = result.get("confidence", 0)

    # Header with agent info
    lines.append(f"{Colors.CYAN}[{agent_type}]{Colors.RESET} "
                 f"{Colors.DIM}{elapsed_ms:.0f}ms | conf={confidence:.2f}{Colors.RESET}")

    # Check for errors
    if result.get("error"):
        lines.append(f"{Colors.RED}Error: {result['error']}{Colors.RESET}")
        return "\n".join(lines)

    # Display result based on agent type
    result_data = result.get("result", {})

    if agent_type == "codebase":
        # Codebase search results
        matches = result_data.get("matches", [])
        if matches:
            lines.append(f"{Colors.GREEN}Found {len(matches)} matches:{Colors.RESET}")
            for m in matches[:5]:  # Show top 5
                file_line = f"{m['file']}:{m['line']}"
                content = m.get("content", "")[:80]
                lines.append(f"  {Colors.YELLOW}{file_line}{Colors.RESET}")
                lines.append(f"    {Colors.DIM}{content}{Colors.RESET}")
        else:
            lines.append(f"{Colors.YELLOW}No matches found{Colors.RESET}")

        search_terms = result_data.get("search_terms", [])
        if search_terms:
            lines.append(f"{Colors.DIM}Search terms: {', '.join(search_terms)}{Colors.RESET}")

    elif agent_type == "test":
        # Test execution results
        status = result_data.get("status", "UNKNOWN")
        status_color = Colors.GREEN if status == "PASS" else Colors.RED
        lines.append(f"Status: {status_color}{status}{Colors.RESET}")

        exit_code = result_data.get("exit_code")
        if exit_code is not None:
            lines.append(f"Exit code: {exit_code}")

        receipt_path = result_data.get("receipt_path")
        if receipt_path:
            lines.append(f"{Colors.DIM}Receipt: {receipt_path}{Colors.RESET}")

        # Show stdout/stderr snippets
        stdout = result_data.get("stdout", "")
        if stdout:
            stdout_lines = stdout.strip().split("\n")
            lines.append(f"{Colors.DIM}--- stdout ({len(stdout_lines)} lines) ---{Colors.RESET}")
            for line in stdout_lines[-10:]:  # Last 10 lines
                lines.append(f"  {line[:120]}")

    elif agent_type == "patch":
        # Patch results
        status = result_data.get("status", "UNKNOWN")
        status_color = Colors.GREEN if status in ("APPLIED", "PASS") else Colors.RED
        lines.append(f"Status: {status_color}{status}{Colors.RESET}")

        file_path = result_data.get("file_path")
        if file_path:
            lines.append(f"File: {file_path}")

        pre_test = result_data.get("pre_test_status")
        post_test = result_data.get("post_test_status")
        if pre_test:
            lines.append(f"Pre-test: {pre_test}")
        if post_test:
            lines.append(f"Post-test: {post_test}")

    elif agent_type == "plan":
        # Plan results
        steps = result_data.get("steps", [])
        if steps:
            lines.append(f"{Colors.GREEN}Plan ({len(steps)} steps):{Colors.RESET}")
            for i, step in enumerate(steps, 1):
                kind = step.get("kind", "unknown")
                desc = step.get("description", "")[:80]
                agent = step.get("agent", "")
                lines.append(f"  {i}. [{kind}] {desc}")
                if agent:
                    lines.append(f"     {Colors.DIM}→ {agent}{Colors.RESET}")

    elif agent_type == "chat":
        # Chat/LLM response
        answer = result_data.get("answer", "")
        lines.append(f"{Colors.GREEN}{answer}{Colors.RESET}")
        latency = result_data.get("latency_sec", 0)
        backend = result_data.get("backend", "unknown")
        lines.append(f"{Colors.DIM}[{backend} | {latency:.1f}s]{Colors.RESET}")

    else:
        # Generic result display
        if isinstance(result_data, dict):
            lines.append(json.dumps(result_data, indent=2, default=str)[:500])
        else:
            lines.append(str(result_data)[:500])

    # Sources
    sources = result.get("sources", [])
    if sources:
        lines.append(f"{Colors.DIM}Sources: {', '.join(sources[:5])}{Colors.RESET}")

    # Agent ID for reference
    agent_id = result.get("agent_id")
    if agent_id:
        lines.append(f"{Colors.DIM}Agent ID: {agent_id}{Colors.RESET}")

    return "\n".join(lines)


def print_help():
    """Print help message."""
    help_text = f"""
{Colors.BOLD}Echo Terminal - Agent Dispatch REPL{Colors.RESET}

{Colors.CYAN}Commands:{Colors.RESET}
  <query>       Route query to appropriate agent
  /agents       List available agents
  /history      Show query history
  /session      Show session info
  /help, ?      Show this help
  /quit, exit   Exit REPL

{Colors.CYAN}Query Examples:{Colors.RESET}
  "where is spatial memory implemented?"  → codebase agent
  "run tests"                             → test agent
  "pytest tests/test_receipts.py"         → test agent (parsed command)
  "edit file.py to add logging"           → patch agent
  "plan how to add feature X"             → plan agent

{Colors.CYAN}Agent Types:{Colors.RESET}
  codebase  - Ripgrep search over helix-cdc code
  test      - Execute allowlisted test commands
  patch     - Apply code edits with pre/post test gates
  plan      - Work order planning
  search    - Search local documentation
  chat      - LLM-backed conversational responses

{Colors.DIM}All queries produce receipts for audit.{Colors.RESET}
"""
    print(help_text)


def print_agents():
    """List available agents."""
    try:
        from helix_cdc.echo.agents import list_agents, AGENT_REGISTRY

        # Import agents to ensure they're registered
        try:
            from helix_cdc.echo.agents import codebase_agent, test_agent, patch_agent, plan_agent, search_agent, chat_agent
        except ImportError:
            pass

        agents = list_agents()

        print(f"\n{Colors.BOLD}Available Agents:{Colors.RESET}")
        for agent in agents:
            status = f"{Colors.GREEN}available{Colors.RESET}" if agent["available"] else f"{Colors.RED}unavailable{Colors.RESET}"
            print(f"  {Colors.CYAN}{agent['agent_type']:<12}{Colors.RESET} ({agent['class_name']}) - {status}")
        print()

    except ImportError as e:
        print(f"{Colors.RED}Failed to list agents: {e}{Colors.RESET}")


async def repl_loop(session: SessionState):
    """Main REPL loop."""
    print(f"\n{Colors.BOLD}Echo Terminal{Colors.RESET} - Session {session.session_id[:8]}")
    print(f"{Colors.DIM}Type /help for commands, /quit to exit{Colors.RESET}")

    # Show LLM backend status
    try:
        from helix_cdc.echo.agents import get_agent
        # Import to ensure registration
        try:
            from helix_cdc.echo.agents import chat_agent
        except ImportError:
            pass
        chat = get_agent("chat")
        if chat and chat.available():
            backend = os.environ.get("ECHO_BACKEND", "llama_cpp")
            print(f"{Colors.DIM}LLM backend: {backend}{Colors.RESET}")
        else:
            print(f"{Colors.YELLOW}LLM backend: unavailable (dispatcher-only mode){Colors.RESET}")
    except Exception:
        pass

    print()  # Empty line before prompt

    while True:
        try:
            query = input(f"{Colors.MAGENTA}echo>{Colors.RESET} ").strip()
        except (EOFError, KeyboardInterrupt):
            print(f"\n{Colors.DIM}Session ended.{Colors.RESET}")
            break

        if not query:
            continue

        # Handle commands
        if query in ("/quit", "/exit", "exit", "quit"):
            print(f"{Colors.DIM}Session ended. {session.agent_calls} agent calls.{Colors.RESET}")
            break

        if query in ("/help", "?", "help"):
            print_help()
            continue

        if query == "/agents":
            print_agents()
            continue

        if query == "/history":
            if not session.query_history:
                print(f"{Colors.DIM}No queries in history.{Colors.RESET}")
            else:
                print(f"\n{Colors.BOLD}Query History:{Colors.RESET}")
                for i, entry in enumerate(session.query_history[-10:], 1):
                    ts = entry.get("timestamp", "")[:19]
                    agent = entry.get("agent_type", "?")
                    q = entry.get("query", "")[:50]
                    elapsed = entry.get("elapsed_ms", 0)
                    print(f"  {i}. [{agent}] {q}... ({elapsed:.0f}ms)")
                print()
            continue

        if query == "/session":
            elapsed = datetime.now(timezone.utc) - session.start_time
            print(f"\n{Colors.BOLD}Session Info:{Colors.RESET}")
            print(f"  ID: {session.session_id}")
            print(f"  Started: {session.start_time.isoformat()}")
            print(f"  Duration: {elapsed}")
            print(f"  Queries: {len(session.query_history)}")
            print(f"  Agent calls: {session.agent_calls}")
            print()
            continue

        # Route and execute query
        routing = route_query(query)
        agent_type = routing["agent_type"]

        print(f"{Colors.DIM}Routing to {agent_type}...{Colors.RESET}")

        context = {
            "session_id": session.session_id,
            "routing": routing,
            "query_count": len(session.query_history),
        }

        start = time.perf_counter()
        result = await execute_agent(agent_type, query, context)
        elapsed_ms = (time.perf_counter() - start) * 1000

        # Update result with timing if not present
        if "elapsed_ms" not in result:
            result["elapsed_ms"] = elapsed_ms

        # Record in session
        session.record_query(query, agent_type, result, elapsed_ms)

        # Display result
        print()
        print(format_result(result, routing))
        print()


def single_query(query: str) -> int:
    """Execute a single query and exit."""
    session = SessionState()

    routing = route_query(query)
    agent_type = routing["agent_type"]

    context = {
        "session_id": session.session_id,
        "routing": routing,
    }

    result = asyncio.run(execute_agent(agent_type, query, context))

    print(format_result(result, routing))

    # Return exit code based on result
    if result.get("error"):
        return 1
    return 0


def run_agent_mode(query: str, verbose: bool = False, json_output: bool = False) -> int:
    """
    Run in agent mode (LLM calls tools iteratively).

    WO-ECHO-AGENT-LOOP-01: Agent mode uses the AgentLoop to iterate
    on tasks, calling tools and observing results until completion.

    Args:
        query: The task/request to execute
        verbose: Enable verbose output
        json_output: Output as JSON

    Returns:
        Exit code (0 for success, 1 for error)
    """
    try:
        from helix_cdc.echo.agent_loop import AgentLoop, AgentLoopConfig
    except ImportError as e:
        print(f"{Colors.RED}Error: Agent loop not available: {e}{Colors.RESET}")
        return 1

    config = AgentLoopConfig(verbose=verbose)

    # Check backend availability
    if config.backend == "ollama":
        try:
            from echo_cog.backend_ollama import check_ollama_health
            is_healthy, message = check_ollama_health()
            if not is_healthy:
                print(f"{Colors.RED}Error: {message}{Colors.RESET}")
                print(f"{Colors.DIM}Start Ollama with: ollama serve{Colors.RESET}")
                print(f"{Colors.DIM}Pull model with: ollama pull {config.model}{Colors.RESET}")
                return 1
            if verbose:
                print(f"{Colors.DIM}{message}{Colors.RESET}")
        except ImportError:
            print(f"{Colors.RED}Error: echo_cog.backend_ollama not available{Colors.RESET}")
            return 1

    print(f"{Colors.CYAN}[agent mode]{Colors.RESET} Running: {query[:80]}...")

    loop = AgentLoop(config)
    result = asyncio.run(loop.run(query))

    if json_output:
        print(json.dumps(result.to_dict(), indent=2, default=str))
    else:
        # Format output
        print()
        print(f"{Colors.BOLD}{'='*60}{Colors.RESET}")
        print(f"{Colors.GREEN}Response:{Colors.RESET}")
        print(result.response)
        print(f"{Colors.BOLD}{'='*60}{Colors.RESET}")
        print(f"{Colors.DIM}Turns: {result.turns} | "
              f"Tool calls: {len(result.tool_calls)} | "
              f"Elapsed: {result.total_elapsed_ms:.0f}ms{Colors.RESET}")

        if result.tool_calls:
            print(f"\n{Colors.CYAN}Tool calls:{Colors.RESET}")
            for tc in result.tool_calls:
                status = f"{Colors.GREEN}OK{Colors.RESET}" if tc.success else f"{Colors.RED}FAIL{Colors.RESET}"
                print(f"  - {tc.name}: {status} ({tc.elapsed_ms:.0f}ms)")

        if result.receipt_path:
            print(f"\n{Colors.DIM}Receipt: {result.receipt_path}{Colors.RESET}")

        if result.error:
            print(f"\n{Colors.RED}Error: {result.error}{Colors.RESET}")

    return 0 if result.success else 1


def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(
        prog="helix-echo",
        description="Echo Terminal - Interactive agent dispatch REPL",
    )
    parser.add_argument(
        "query",
        nargs="*",
        help="Query to execute (starts REPL if not provided)",
    )
    parser.add_argument(
        "--no-color",
        action="store_true",
        help="Disable colored output",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON (single query mode only)",
    )
    parser.add_argument(
        "--agent", "-a",
        action="store_true",
        help="Run in agent mode (LLM calls tools iteratively until task complete)",
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Verbose output (agent mode)",
    )

    args = parser.parse_args()

    if args.no_color:
        Colors.disable()

    # Agent mode
    if args.agent:
        if not args.query:
            print(f"{Colors.RED}Error: Agent mode requires a query{Colors.RESET}")
            print(f"{Colors.DIM}Usage: helix-echo --agent 'run tests'{Colors.RESET}")
            sys.exit(1)
        query = " ".join(args.query)
        sys.exit(run_agent_mode(query, verbose=args.verbose, json_output=args.json))

    # Single query mode (dispatcher)
    if args.query:
        query = " ".join(args.query)

        if args.json:
            session = SessionState()
            routing = route_query(query)
            result = asyncio.run(execute_agent(routing["agent_type"], query, {"session_id": session.session_id}))
            print(json.dumps(result, indent=2, default=str))
            sys.exit(0 if not result.get("error") else 1)

        sys.exit(single_query(query))

    # Interactive REPL mode
    session = SessionState()
    try:
        asyncio.run(repl_loop(session))
    except KeyboardInterrupt:
        print(f"\n{Colors.DIM}Session ended.{Colors.RESET}")

    sys.exit(0)


if __name__ == "__main__":
    main()
