"""Main orchestrator for ORBIT Assist.

Ties together context scanning, backend selection, and the interactive REPL.
This is the entry point called by the CLI.
"""

from __future__ import annotations

import logging
import sys

logger = logging.getLogger(__name__)


def run_assist(
    backend: str = "auto",
    no_scan: bool = False,
    task: str | None = None,
) -> None:
    """Launch the ORBIT Assist agent.

    Args:
        backend: LLM backend ("gemini", "claude", or "auto").
        no_scan: Skip initial directory scan.
        task: If set, run this single task non-interactively and exit.
    """
    from orbit.utils.display import console

    # 1. Resolve API key and backend
    agent = _create_backend(backend, console)
    if agent is None:
        return

    # 2. Scan workspace
    workspace_context_str = ""
    if not no_scan:
        workspace_context_str = _scan_workspace(console)

    # 3. Build system prompt
    from orbit.assist.system_prompt import build_system_prompt

    system_prompt = build_system_prompt(workspace_context_str)

    # 4. Non-interactive mode
    if task:
        _run_single_task(agent, task, system_prompt, console)
        return

    # 5. Interactive REPL
    _run_repl(agent, system_prompt, console)


def _create_backend(backend: str, console: object) -> object | None:
    """Create the appropriate LLM backend."""
    from orbit.llm_utils import load_api_key

    if backend == "claude":
        from orbit.assist.backends.claude import ClaudeBackend, is_available

        if not is_available():
            console.print(
                "\n[bold red]Error:[/bold red] Claude backend requires "
                "ANTHROPIC_API_KEY and claude-code-sdk.\n"
                "  Install: pip install claude-code-sdk\n"
                "  Set key: export ANTHROPIC_API_KEY=sk-...\n"
            )
            return None
        try:
            return ClaudeBackend(system_prompt="")
        except RuntimeError as e:
            console.print(f"\n[bold red]Error:[/bold red] {e}\n")
            return None

    # Default: Gemini (for both "gemini" and "auto")
    if backend == "auto":
        # Try Claude first if available, fall back to Gemini
        from orbit.assist.backends.claude import is_available as claude_available

        if claude_available():
            # Still prefer Gemini for cost efficiency unless explicitly asked
            pass

    api_key = load_api_key()
    if not api_key:
        console.print(
            "\n[bold red]Error:[/bold red] GOOGLE_API_KEY required for orbit assist.\n"
            "  Set it with: export GOOGLE_API_KEY=your-key\n"
            "  Get a key at: https://ai.google.dev/\n"
        )
        return None

    from orbit.assist.backends.gemini import GeminiBackend

    return GeminiBackend(api_key=api_key)


def _scan_workspace(console: object) -> str:
    """Scan the workspace and return formatted context string."""
    from orbit.assist.context import build_workspace_context, format_context_for_prompt

    with console.status("[dim]Scanning workspace...[/dim]"):
        context = build_workspace_context(".")

    # Print summary to user
    if context["datasets"]:
        console.print("[dim]Detected datasets:[/dim]")
        for ds in context["datasets"]:
            ver = f" ({ds.get('version', '')})" if ds.get("version") else ""
            console.print(f"  [dim]{ds['path']} [{ds['format']}{ver}][/dim]")
    if context["training_outputs"]:
        console.print("[dim]Training outputs:[/dim]")
        for to in context["training_outputs"]:
            console.print(f"  [dim]{to['path']} [{to['type']}][/dim]")

    env = context.get("environment", {})
    gpu_info = env.get("gpu", {})
    if gpu_info.get("available"):
        for g in gpu_info.get("gpus", []):
            vram = f" ({g['vram_total_mb']}MB)" if g.get("vram_total_mb") else ""
            console.print(f"[dim]GPU: {g['name']}{vram}[/dim]")
    else:
        console.print("[dim]GPU: None detected[/dim]")

    pkgs = env.get("installed_packages", {})
    if pkgs:
        pkg_names = ", ".join(sorted(pkgs.keys()))
        console.print(f"[dim]Packages: {pkg_names}[/dim]")

    console.print()
    return format_context_for_prompt(context)


def _run_single_task(agent: object, task: str, system_prompt: str, console: object) -> None:
    """Run a single task non-interactively.

    Exit codes:
      0 — task completed, no failures detected
      1 — tool execution failed, gate failed, or agent reported an error
    """
    tool_results: list[dict] = []

    def on_tool_call(name: str, args: dict, result: dict) -> None:
        console.print(f"  [dim]> {name}({_format_args(args)})[/dim]")
        if result:
            tool_results.append({"name": name, "result": result})

    with console.status("[dim]Working...[/dim]"):
        response = agent.run_turn(task, system_prompt, on_tool_call=on_tool_call)

    console.print()
    _print_response(response, console)

    # Determine exit code from tool results, not string matching on prose.
    # This makes `orbit assist --task` reliable for CI/CD.
    exit_code = _determine_exit_code(tool_results, response)
    sys.exit(exit_code)


def _determine_exit_code(tool_results: list[dict], response: str) -> int:
    """Determine exit code from tool execution results.

    Returns 0 for success, 1 for failure. Checks (in order):
    1. Any tool returned an "error" key -> 1
    2. orbit_gate returned passed=False -> 1
    3. orbit_debug found critical/high severity findings -> 1
    4. Agent response indicates failure (last resort string check) -> 1
    """
    for tr in tool_results:
        result = tr.get("result", {})
        name = tr.get("name", "")

        # Explicit error from any tool
        if result.get("error"):
            return 1

        # Gate: explicit pass/fail
        if name == "orbit_gate":
            if result.get("passed") is False:
                return 1
            if result.get("exit_code", 0) != 0:
                return 1

        # Debug: critical findings
        if name == "orbit_debug":
            findings = result.get("findings", [])
            if any(
                f.get("severity") in ("critical", "high")
                for f in findings
                if isinstance(f, dict)
            ):
                return 1

        # Any tool with non-zero exit code
        if result.get("exit_code", 0) != 0 and "passed" not in result:
            return 1

    # Fallback: check the agent's text response, but only for strong signals.
    # Avoid false positives from words like "error" appearing in explanations.
    response_lower = response.lower()
    failure_phrases = [
        "failed to ",
        "could not ",
        "unable to ",
        "command failed",
        "quality gate: fail",
        "grade: f",
    ]
    if any(phrase in response_lower for phrase in failure_phrases):
        return 1

    return 0


def _run_repl(agent: object, system_prompt: str, console: object) -> None:
    """Run the interactive REPL."""
    from rich.markdown import Markdown

    console.print("[bold]ORBIT Assist[/bold] [dim](type 'help' for commands, 'quit' to exit)[/dim]\n")

    try:
        while True:
            try:
                user_input = console.input("[bold cyan]you > [/bold cyan]")
            except EOFError:
                break

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

            cmd = user_input.lower()
            if cmd in ("quit", "exit", "q"):
                break

            if cmd == "help":
                _print_help(console)
                continue

            if cmd == "rescan":
                workspace_context_str = _scan_workspace(console)
                from orbit.assist.system_prompt import build_system_prompt

                system_prompt = build_system_prompt(workspace_context_str)
                console.print("[dim]Workspace rescanned.[/dim]\n")
                continue

            if cmd == "reset":
                agent.reset()
                console.print("[dim]Conversation reset.[/dim]\n")
                continue

            # Run agentic turn
            def on_tool_call(name: str, args: dict, result: dict) -> None:
                args_str = _format_args(args)
                status = "done" if result and "error" not in result else "error"
                icon = "+" if status == "done" else "x"
                console.print(f"  [{icon}] [dim]{name}({args_str})[/dim]")

            console.print()
            with console.status("[dim]Thinking...[/dim]"):
                response = agent.run_turn(
                    user_input, system_prompt, on_tool_call=on_tool_call
                )

            console.print()
            _print_response(response, console)
            console.print()

    except KeyboardInterrupt:
        pass

    console.print("\n[dim]Assistant closed.[/dim]\n")


def _print_response(response: str, console: object) -> None:
    """Print the agent's response, rendering markdown if detected."""
    if "```" in response or "**" in response or "###" in response:
        from rich.markdown import Markdown

        console.print(Markdown(response))
    else:
        console.print(response)


def _print_help(console: object) -> None:
    """Print help text."""
    console.print(
        "\n[bold]Commands:[/bold]\n"
        "  [cyan]rescan[/cyan]   — Re-scan the workspace for datasets and outputs\n"
        "  [cyan]reset[/cyan]    — Clear conversation history\n"
        "  [cyan]help[/cyan]     — Show this help\n"
        "  [cyan]quit[/cyan]     — Exit the assistant\n"
        "\n[bold]Example prompts:[/bold]\n"
        '  "analyze my dataset and check if it\'s ready for ACT"\n'
        '  "I want to train GR00T N1.6 on my data"\n'
        '  "my training loss is stuck at 0.3, what\'s wrong?"\n'
        '  "how do I train this on RunPod?"\n'
        '  "convert my ROS bags to LeRobot format"\n'
        '  "generate an OSMO workflow for my pipeline"\n'
    )


def _format_args(args: dict) -> str:
    """Format tool arguments for display."""
    if not args:
        return ""
    parts = []
    for k, v in args.items():
        if isinstance(v, str) and len(v) > 40:
            v = v[:37] + "..."
        parts.append(f"{k}={v!r}")
    return ", ".join(parts)
