import asyncio
import logging
import subprocess
import sys

import typer

from timmy.agent import create_timmy
from timmy.prompts import STATUS_PROMPT
from timmy.tool_safety import format_action_description, get_impact_level, is_allowlisted

logger = logging.getLogger(__name__)

app = typer.Typer(help="Timmy — sovereign AI agent")

# Stable session ID — Agno persists conversation history in SQLite keyed on this.
# Every `timmy chat` invocation reuses the same session so context carries over.
_CLI_SESSION_ID = "cli"

# Shared option definitions (reused across commands for consistency).
_BACKEND_OPTION = typer.Option(
    None,
    "--backend",
    "-b",
    help="Inference backend: 'ollama' (default) | 'grok' | 'claude'",
)
_MODEL_SIZE_OPTION = typer.Option(
    None,
    "--model-size",
    "-s",
    help="Model size (reserved for future use).",
)


def _is_interactive() -> bool:
    """Return True if stdin is a real terminal (human present)."""
    return hasattr(sys.stdin, "isatty") and sys.stdin.isatty()


def _read_message_input(message: list[str]) -> str:
    """Join CLI args into a message, reading from stdin when requested.

    Returns the final message string.  Raises ``typer.Exit(1)`` when
    stdin is explicitly requested (``-``) but empty.
    """
    message_str = " ".join(message)

    if message_str == "-" or not _is_interactive():
        try:
            stdin_content = sys.stdin.read().strip()
        except (KeyboardInterrupt, EOFError):
            stdin_content = ""
        if stdin_content:
            message_str = stdin_content
        elif message_str == "-":
            typer.echo("No input provided via stdin.", err=True)
            raise typer.Exit(1)

    return message_str


def _resolve_session_id(session_id: str | None, new_session: bool) -> str:
    """Return the effective session ID for a chat invocation."""
    import uuid

    if session_id is not None:
        return session_id
    if new_session:
        return str(uuid.uuid4())
    return _CLI_SESSION_ID


def _prompt_interactive(req, tool_name: str, tool_args: dict) -> None:
    """Display tool details and prompt the human for approval."""
    description = format_action_description(tool_name, tool_args)
    impact = get_impact_level(tool_name)

    typer.echo()
    typer.echo(typer.style("Tool confirmation required", bold=True))
    typer.echo(f"  Impact: {impact.upper()}")
    typer.echo(f"  {description}")
    typer.echo()

    if typer.confirm("Allow this action?", default=False):
        req.confirm()
        logger.info("CLI: approved %s", tool_name)
    else:
        req.reject(note="User rejected from CLI")
        logger.info("CLI: rejected %s", tool_name)


def _decide_autonomous(req, tool_name: str, tool_args: dict) -> None:
    """Auto-approve allowlisted tools; reject everything else."""
    if is_allowlisted(tool_name, tool_args):
        req.confirm()
        logger.info("AUTO-APPROVED (allowlist): %s", tool_name)
    else:
        req.reject(note="Auto-rejected: not in allowlist")
        logger.info("AUTO-REJECTED (not allowlisted): %s %s", tool_name, str(tool_args)[:100])


def _handle_tool_confirmation(agent, run_output, session_id: str, *, autonomous: bool = False):
    """Prompt user to approve/reject dangerous tool calls.

    When Agno pauses a run because a tool requires confirmation, this
    function displays the action, asks for approval via stdin, and
    resumes or rejects the run accordingly.

    When autonomous=True (or stdin is not a terminal), tool calls are
    checked against config/allowlist.yaml instead of prompting.
    Allowlisted calls are auto-approved; everything else is auto-rejected.

    Returns the final RunOutput after all confirmations are resolved.
    """
    interactive = _is_interactive() and not autonomous
    decide = _prompt_interactive if interactive else _decide_autonomous

    max_rounds = 10  # safety limit
    for _ in range(max_rounds):
        status = getattr(run_output, "status", None)
        is_paused = status == "PAUSED" or str(status) == "RunStatus.paused"
        if not is_paused:
            return run_output

        reqs = getattr(run_output, "active_requirements", None) or []
        if not reqs:
            return run_output

        for req in reqs:
            if not getattr(req, "needs_confirmation", False):
                continue
            te = req.tool_execution
            tool_name = getattr(te, "tool_name", "unknown")
            tool_args = getattr(te, "tool_args", {}) or {}
            decide(req, tool_name, tool_args)

        # Resume the run so the agent sees the confirmation result
        try:
            run_output = agent.continue_run(
                run_response=run_output, stream=False, session_id=session_id
            )
        except Exception as exc:
            logger.error("CLI: continue_run failed: %s", exc)
            typer.echo(f"Error resuming: {exc}", err=True)
            return run_output

    return run_output


@app.command()
def tick(
    prompt: str | None = typer.Argument(
        None, help="Optional journal prompt for Timmy to reflect on"
    ),
):
    """Run one thinking cycle. Pass a prompt to ask Timmy a specific question."""
    import asyncio

    from timmy.thinking import thinking_engine

    thought = asyncio.run(thinking_engine.think_once(prompt=prompt))
    if thought:
        typer.echo(f"[{thought.seed_type}] {thought.content}")
    else:
        typer.echo("No thought produced (thinking disabled or Ollama down).")


@app.command()
def think(
    topic: str = typer.Argument(..., help="Topic to reason about"),
    backend: str | None = _BACKEND_OPTION,
    model_size: str | None = _MODEL_SIZE_OPTION,
):
    """Ask Timmy to think carefully about a topic."""
    timmy = create_timmy(backend=backend, session_id=_CLI_SESSION_ID)
    timmy.print_response(f"Think carefully about: {topic}", stream=True, session_id=_CLI_SESSION_ID)


def _read_message_input(message: list[str]) -> str:
    """Join CLI arguments and read from stdin when appropriate."""
    message_str = " ".join(message)

    if message_str == "-" or not _is_interactive():
        try:
            stdin_content = sys.stdin.read().strip()
        except (KeyboardInterrupt, EOFError):
            stdin_content = ""
        if stdin_content:
            message_str = stdin_content
        elif message_str == "-":
            typer.echo("No input provided via stdin.", err=True)
            raise typer.Exit(1)

    return message_str


def _resolve_session_id(session_id: str | None, new_session: bool) -> str:
    """Return the effective session ID based on CLI flags."""
    import uuid

    if session_id is not None:
        return session_id
    if new_session:
        return str(uuid.uuid4())
    return _CLI_SESSION_ID


@app.command()
def chat(
    message: list[str] = typer.Argument(
        ..., help="Message to send (multiple words are joined automatically)"
    ),
    backend: str | None = _BACKEND_OPTION,
    model_size: str | None = _MODEL_SIZE_OPTION,
    new_session: bool = typer.Option(
        False,
        "--new",
        "-n",
        help="Start a fresh conversation (ignore prior context)",
    ),
    session_id: str | None = typer.Option(
        None,
        "--session-id",
        help="Use a specific session ID for this conversation",
    ),
    autonomous: bool = typer.Option(
        False,
        "--autonomous",
        "-a",
        help="Autonomous mode: auto-approve allowlisted tools, reject the rest (no stdin prompts)",
    ),
):
    """Send a message to Timmy.

    Conversation history persists across invocations. Use --new to start fresh,
    or --session-id to use a specific session.

    Use --autonomous for non-interactive contexts (scripts, dev loops). Tool
    calls are checked against config/allowlist.yaml — allowlisted operations
    execute automatically, everything else is safely rejected.

    Read from stdin by passing "-" as the message or piping input.
    """
    message_str = _read_message_input(message)
    session_id = _resolve_session_id(session_id, new_session)
    timmy = create_timmy(backend=backend, session_id=session_id)

    run_output = timmy.run(message_str, stream=False, session_id=session_id)
    run_output = _handle_tool_confirmation(timmy, run_output, session_id, autonomous=autonomous)

    content = run_output.content if hasattr(run_output, "content") else str(run_output)
    if content:
        from timmy.session import _clean_response

        typer.echo(_clean_response(content))


@app.command()
def repl(
    backend: str | None = _BACKEND_OPTION,
    model_size: str | None = _MODEL_SIZE_OPTION,
    session_id: str | None = typer.Option(
        None,
        "--session-id",
        help="Use a specific session ID for this conversation",
    ),
):
    """Start an interactive REPL session with Timmy.

    Keeps the agent warm between messages. Conversation history is persisted
    across invocations. Use Ctrl+C or Ctrl+D to exit gracefully.
    """
    from timmy.session import chat

    if session_id is None:
        session_id = _CLI_SESSION_ID

    typer.echo(typer.style("Timmy REPL", bold=True))
    typer.echo("Type your messages below. Use Ctrl+C or Ctrl+D to exit.")
    typer.echo()

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)

    try:
        while True:
            try:
                user_input = input("> ")
            except (KeyboardInterrupt, EOFError):
                typer.echo()
                typer.echo("Goodbye!")
                break

            user_input = user_input.strip()
            if not user_input:
                continue

            if user_input.lower() in ("exit", "quit", "q"):
                typer.echo("Goodbye!")
                break

            try:
                response = loop.run_until_complete(chat(user_input, session_id=session_id))
                if response:
                    typer.echo(response)
                    typer.echo()
            except Exception as exc:
                typer.echo(f"Error: {exc}", err=True)
    finally:
        loop.close()


@app.command()
def status(
    backend: str | None = _BACKEND_OPTION,
    model_size: str | None = _MODEL_SIZE_OPTION,
):
    """Print Timmy's operational status."""
    timmy = create_timmy(backend=backend, session_id=_CLI_SESSION_ID)
    timmy.print_response(STATUS_PROMPT, stream=False, session_id=_CLI_SESSION_ID)


@app.command()
def interview(
    backend: str | None = _BACKEND_OPTION,
    model_size: str | None = _MODEL_SIZE_OPTION,
):
    """Initialize Timmy and run a structured interview.

    Asks Timmy a series of questions about his identity, capabilities,
    values, and operation to verify he is working correctly.
    """
    import asyncio

    from timmy.interview import InterviewEntry, format_transcript, run_interview
    from timmy.session import chat

    typer.echo("Initializing Timmy for interview...\n")

    # Use a single persistent event loop for the entire interview.
    # Calling asyncio.run() per question kills the loop between calls,
    # orphaning MCP stdio transports and causing "Event loop is closed."
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)

    try:
        # Force agent creation by calling chat once with a warm-up prompt
        try:
            loop.run_until_complete(
                chat("Hello, Timmy. We're about to start your interview.", session_id="interview")
            )
        except Exception as exc:
            typer.echo(f"Warning: Initialization issue — {exc}", err=True)

        def _on_answer(entry: InterviewEntry) -> None:
            typer.echo(f"[{entry.category}]")
            typer.echo(f"  Q: {entry.question}")
            typer.echo(f"  A: {entry.answer}")
            typer.echo()

        typer.echo("Starting interview...\n")
        transcript = run_interview(
            chat_fn=lambda msg: loop.run_until_complete(chat(msg, session_id="interview")),
            on_answer=_on_answer,
        )

        # Print full transcript at the end
        typer.echo("\n" + format_transcript(transcript))
    finally:
        # Clean shutdown: close MCP sessions, then the loop
        try:
            from timmy.mcp_tools import close_mcp_sessions

            loop.run_until_complete(close_mcp_sessions())
        except Exception as exc:
            logger.warning("MCP session close failed: %s", exc)
            pass
        loop.close()


@app.command()
def up(
    dev: bool = typer.Option(False, "--dev", help="Enable hot-reload for development"),
    build: bool = typer.Option(True, "--build/--no-build", help="Rebuild images before starting"),
):
    """Start Timmy Time in Docker (dashboard + agents)."""
    cmd = ["docker", "compose"]
    if dev:
        cmd += ["-f", "docker-compose.yml", "-f", "docker-compose.dev.yml"]
    cmd += ["up", "-d"]
    if build:
        cmd.append("--build")

    mode = "dev mode (hot-reload active)" if dev else "production mode"
    typer.echo(f"Starting Timmy Time in {mode}...")
    result = subprocess.run(cmd)
    if result.returncode == 0:
        typer.echo(f"\n  Timmy Time running at http://localhost:8000  ({mode})\n")
    else:
        typer.echo("Failed to start. Is Docker running?", err=True)
        raise typer.Exit(1)


@app.command()
def down():
    """Stop all Timmy Time Docker containers."""
    subprocess.run(["docker", "compose", "down"], check=True)


@app.command()
def voice(
    whisper_model: str = typer.Option(
        "base.en", "--whisper", "-w", help="Whisper model: tiny.en, base.en, small.en, medium.en"
    ),
    use_say: bool = typer.Option(False, "--say", help="Use macOS `say` instead of Piper TTS"),
    threshold: float = typer.Option(
        0.015, "--threshold", "-t", help="Mic silence threshold (RMS). Lower = more sensitive."
    ),
    silence: float = typer.Option(1.5, "--silence", help="Seconds of silence to end recording"),
    backend: str | None = _BACKEND_OPTION,
    model_size: str | None = _MODEL_SIZE_OPTION,
):
    """Start the sovereign voice loop — listen, think, speak.

    Everything runs locally: Whisper for STT, Ollama for LLM, Piper for TTS.
    No cloud, no network calls, no microphone data leaves your machine.
    """
    from timmy.voice_loop import VoiceConfig, VoiceLoop

    config = VoiceConfig(
        whisper_model=whisper_model,
        use_say_fallback=use_say,
        silence_threshold=threshold,
        silence_duration=silence,
        backend=backend,
        model_size=model_size,
    )
    loop = VoiceLoop(config=config)
    loop.run()


@app.command()
def route(
    message: list[str] = typer.Argument(..., help="Message to route"),
):
    """Show which agent would handle a message (debug routing)."""
    full_message = " ".join(message)
    from timmy.agents.loader import route_request_with_match

    agent_id, matched_pattern = route_request_with_match(full_message)
    if agent_id:
        typer.echo(f"→ {agent_id} (matched: {matched_pattern})")
    else:
        typer.echo("→ orchestrator (no pattern match)")


@app.command()
def focus(
    topic: str | None = typer.Argument(
        None, help='Topic to focus on (e.g. "three-phase loop"). Omit to show current focus.'
    ),
    clear: bool = typer.Option(False, "--clear", "-c", help="Clear focus and return to broad mode"),
):
    """Set deep-focus mode on a single problem.

    When focused, Timmy prioritizes the active topic in all responses
    and deprioritizes unrelated context. Focus persists across sessions.

    Examples:
        timmy focus "three-phase loop"   # activate deep focus
        timmy focus                       # show current focus
        timmy focus --clear               # return to broad mode
    """
    from timmy.focus import focus_manager

    if clear:
        focus_manager.clear()
        typer.echo("Focus cleared — back to broad mode.")
        return

    if topic:
        focus_manager.set_topic(topic)
        typer.echo(f'Deep focus activated: "{topic}"')
    else:
        # Show current focus status
        if focus_manager.is_focused():
            typer.echo(f'Deep focus: "{focus_manager.get_topic()}"')
        else:
            typer.echo("No active focus (broad mode).")


@app.command(name="healthcheck")
def healthcheck(
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
    verbose: bool = typer.Option(
        False, "--verbose", "-v", help="Show verbose output including issue details"
    ),
    quiet: bool = typer.Option(False, "--quiet", "-q", help="Only show status line (no details)"),
):
    """Quick health snapshot before coding.

    Shows CI status, critical issues (P0/P1), test flakiness, and token economy.
    Fast execution (< 5 seconds) for pre-work checks.

    Refs: #710
    """
    import subprocess
    import sys
    from pathlib import Path

    script_path = (
        Path(__file__).resolve().parent.parent.parent
        / "timmy_automations"
        / "daily_run"
        / "health_snapshot.py"
    )

    cmd = [sys.executable, str(script_path)]
    if json_output:
        cmd.append("--json")
    if verbose:
        cmd.append("--verbose")
    if quiet:
        cmd.append("--quiet")

    result = subprocess.run(cmd)
    raise typer.Exit(result.returncode)


def main():
    app()
