"""CLI command for running agents."""

import json
import os
import select
import subprocess
import sys
import uuid
from datetime import datetime
from pathlib import Path

import click
from rich.console import Console
from rich.markdown import Markdown

from primordial.config import get_config
from primordial.github import GitHubResolver, GitHubResolverError, is_github_url, parse_github_url
from primordial.manifest import load_manifest
from primordial.security.key_vault import KeyVault
from primordial.security.permissions import format_permissions_for_display
from primordial.sandbox.manager import SandboxManager
from primordial.cli.helix import HelixSpinner

console = Console()


def _input_with_placeholder(prompt: str, placeholder: str) -> str:
    """Show a prompt with dim placeholder text that vanishes on first keystroke."""
    import readline  # noqa: F401 — ensures line editing works for input()
    import termios
    import tty

    # Print prompt + dim placeholder, cursor after prompt
    prompt_ansi = f"\033[1m{prompt}\033[0m"
    placeholder_ansi = f"\033[2m{placeholder}\033[0m"
    sys.stdout.write(f"{prompt_ansi}{placeholder_ansi}")
    # Move cursor back to right after the prompt
    sys.stdout.write(f"\r{prompt_ansi}")
    sys.stdout.flush()

    # Read one character in raw mode to detect first keystroke
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        first = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

    if first in ("\x03", "\x04"):  # Ctrl+C / Ctrl+D
        raise KeyboardInterrupt if first == "\x03" else EOFError

    # Clear the line and rewrite prompt + first char, then read the rest
    sys.stdout.write(f"\r\033[2K{prompt_ansi}{first}")
    sys.stdout.flush()

    # Use normal input() for the rest (with readline support)
    rest = input()
    return first + rest


def _detect_host_tz() -> str | None:
    """Detect the host machine's IANA timezone (e.g. 'America/New_York')."""
    if tz := os.environ.get("TZ"):
        return tz
    try:
        out = subprocess.run(
            ["readlink", "/etc/localtime"],
            capture_output=True, text=True, timeout=3,
        ).stdout.strip()
        if "/zoneinfo/" in out:
            return out.split("/zoneinfo/")[1]
    except Exception:
        pass
    return None


def _new_session_name() -> str:
    """Prompt for a custom session name or generate one."""
    auto_name = datetime.now().strftime("%Y%m%d-%H%M%S")
    name = click.prompt("Session name", default=auto_name).strip()
    return name or auto_name


def _pick_session(config, agent_name: str) -> Path:
    """Prompt user to create a new session or resume an existing one."""
    sessions = config.list_sessions(agent_name)

    if not sessions:
        return config.session_state_dir(agent_name, _new_session_name())

    console.print("\n[bold]Sessions[/bold]")
    console.print(f"  [dim]0)[/dim] New session")
    for i, name in enumerate(sessions, 1):
        console.print(f"  [dim]{i})[/dim] {name}")
    console.print()

    choice = click.prompt("Select session", type=int, default=0)

    if choice < 1 or choice > len(sessions):
        return config.session_state_dir(agent_name, _new_session_name())

    return config.session_state_dir(agent_name, sessions[choice - 1])


@click.command()
@click.argument("agent_path")
@click.option("--agent-read", is_flag=True, help="Primordial Protocol pipe mode (NDJSON stdin/stdout)")
@click.option("--ref", default=None, help="Git ref (branch, tag, commit) for GitHub agents")
@click.option("--refresh", is_flag=True, help="Force re-fetch of GitHub agent (ignore cache)")
@click.option("--yes", "-y", is_flag=True, help="Skip approval prompt")
@click.option("--session", "session_name", default=None, help="Session name to resume (skips prompt)")
def run(
    agent_path: str,
    agent_read: bool,
    ref: str | None,
    refresh: bool,
    yes: bool,
    session_name: str | None,
):
    """Run an agent in a sandbox.

    AGENT_PATH can be:
      - A local directory (./my-agent)
      - A GitHub URL (https://github.com/user/repo)
    """
    config = get_config()

    # Resolve agent path — GitHub URL, local path, or installed name
    if is_github_url(agent_path):
        try:
            github_ref = parse_github_url(agent_path, ref_override=ref)
            console.print(f"[dim]Resolving: {github_ref}[/dim]")
            resolver = GitHubResolver()
            agent_dir = resolver.resolve(github_ref, force_refresh=refresh)
        except GitHubResolverError as e:
            console.print(f"[red]GitHub resolve failed:[/red] {e}")
            raise SystemExit(1)
    else:
        agent_dir = Path(agent_path)
        if not agent_dir.exists():
            installed = config.agents_dir / agent_path
            if installed.exists():
                agent_dir = installed
            else:
                console.print(f"[red]Agent not found:[/red] {agent_path}")
                raise SystemExit(1)

    # Load manifest
    try:
        manifest = load_manifest(agent_dir)
    except (FileNotFoundError, ValueError) as e:
        console.print(f"[red]Invalid agent:[/red] {e}")
        raise SystemExit(1)

    # Session selection
    if session_name:
        state_dir = config.session_state_dir(manifest.name, session_name)
    elif agent_read:
        # Pipe mode — auto-create session
        state_dir = config.session_state_dir(
            manifest.name, datetime.now().strftime("%Y%m%d-%H%M%S")
        )
    else:
        state_dir = _pick_session(config, manifest.name)

    # Validate required API keys from manifest and prompt for missing ones
    vault = KeyVault(config.keys_file)

    # E2B key is always required for sandbox runtime
    if not vault.get_key("e2b"):
        console.print(
            "\n[bold yellow]E2B API key required[/bold yellow]\n"
            "[dim]Primordial runs agents in E2B sandboxes. "
            "Get a key at https://e2b.dev/dashboard[/dim]\n"
        )
        e2b_key = click.prompt("  Paste E2B_API_KEY", hide_input=True)
        if e2b_key.strip():
            vault.add_key("e2b", e2b_key.strip())
            console.print("  [green]Stored e2b.[/green]\n")
        else:
            console.print("[red]E2B key is required to run agents.[/red]")
            raise SystemExit(1)

    # Show permissions and ask for approval
    stored_providers = {e["provider"] for e in vault.list_keys()}
    console.print(f"\n[bold]{manifest.display_name}[/bold] [dim]v{manifest.version}[/dim]")
    console.print(f"[dim]{manifest.description}[/dim]")
    console.print(f"[dim]Session: {state_dir.name}[/dim]\n")
    console.print("[bold]Permissions[/bold]\n")
    for line in format_permissions_for_display(manifest, stored_providers=stored_providers):
        console.print(f"  {line}")
    console.print()
    if not yes and not click.confirm("Approve and run?"):
        console.print("[dim]Aborted.[/dim]")
        raise SystemExit(0)

    if manifest.keys:
        missing_required = []
        missing_optional = []
        for key_req in manifest.keys:
            if not vault.get_key(key_req.provider):
                if key_req.required:
                    missing_required.append(key_req)
                else:
                    missing_optional.append(key_req)

        if missing_required:
            console.print("[bold]Missing API keys[/bold]")
            for kr in missing_required:
                console.print(f"  [red]✗[/red] {kr.provider} [dim]({kr.resolved_env_var()})[/dim]")
            for kr in missing_optional:
                console.print(f"  [dim]○ {kr.provider} ({kr.resolved_env_var()}) — optional[/dim]")
            console.print()

            for kr in missing_required:
                key = click.prompt(
                    f"  Enter {kr.provider.upper()} API key ({kr.resolved_env_var()})",
                    hide_input=True,
                )
                if key.strip():
                    vault.add_key(kr.provider, key.strip())
                    console.print(f"  [dim]Stored {kr.provider}.[/dim]")
                else:
                    console.print(f"[red]Cannot proceed without {kr.provider} key.[/red]")
                    raise SystemExit(1)
            console.print()

        if missing_optional:
            for kr in missing_optional:
                console.print(f"  [dim]Optional key missing: {kr.provider} ({kr.resolved_env_var()})[/dim]")
    else:
        # Fallback: check the model provider key exists
        provider = manifest.runtime.default_model.provider

        api_key = vault.get_key(provider)
        if not api_key:
            console.print(f"[red]No API key found for provider '{provider}'.[/red]")
            console.print(f"[dim]Add one with:[/dim] primordial keys add {provider} <your-key>")
            console.print(f"[dim]Or run:[/dim] primordial setup")
            raise SystemExit(1)

    # Only inject keys the manifest declares — never leak unrelated keys
    if manifest.keys:
        allowed_providers = [kr.provider for kr in manifest.keys]
    else:
        allowed_providers = [manifest.runtime.default_model.provider]
    # E2B key is always needed for sandbox creation
    allowed_providers.append("e2b")
    env_vars = vault.get_env_vars(providers=allowed_providers)

    # Inject host timezone so agents see the user's local time
    if "TZ" not in env_vars:
        try:
            tz = _detect_host_tz()
            if tz:
                env_vars["TZ"] = tz
        except Exception:
            pass
    workspace = "."
    manager = SandboxManager()

    if agent_read:
        _run_json(manager, agent_dir, manifest, workspace, env_vars, state_dir)
    else:
        _run_chat(manager, agent_dir, manifest, workspace, env_vars, state_dir)


def _run_chat(
    manager: SandboxManager,
    agent_dir: Path,
    manifest,
    workspace: str,
    env_vars: dict,
    state_dir: Path | None = None,
) -> None:
    """Run an agent with a human-friendly chat loop."""
    agent_subtitle = f"Starting {manifest.display_name} v{manifest.version}"

    with HelixSpinner(console, subtitle=agent_subtitle) as spinner:
        try:
            session = manager.run_agent(
                agent_dir=agent_dir,
                manifest=manifest,
                workspace=Path(workspace).resolve(),
                env_vars=env_vars,
                state_dir=state_dir,
                on_status=spinner.set_phase,
            )
        except Exception as e:
            console.print(f"\n[red]Failed to start agent:[/red] {e}")
            raise SystemExit(1)

        spinner.set_phase("Waiting for agent to initialize...")
        if not session.wait_ready(timeout=120):
            console.print("\n[red]Agent failed to start (no ready signal)[/red]")
            stderr = session.stderr.strip()
            if stderr:
                console.print(f"[red]Agent stderr:[/red]\n{stderr}")
            session.shutdown()
            raise SystemExit(1)

    try:
        console.print(f"[dim]{manifest.display_name} ready[/dim]\n")
        msg_counter = 0
        first_prompt = True

        while True:
            try:
                if first_prompt:
                    user_input = _input_with_placeholder("> ", "Say something...")
                    first_prompt = False
                else:
                    user_input = console.input("[bold]> [/bold]")
            except (EOFError, KeyboardInterrupt):
                console.print("\n[dim]Ending session...[/dim]")
                break

            # Buffer pasted multi-line input: if more lines arrive
            # within a short window they're part of the same paste.
            paste_lines = [user_input]
            while True:
                ready, _, _ = select.select([sys.stdin], [], [], 0.05)
                if not ready:
                    break
                extra = sys.stdin.readline()
                if not extra:
                    break
                paste_lines.append(extra.rstrip("\n"))
            user_input = "\n".join(paste_lines)

            if user_input.strip().lower() in ("exit", "quit", "/exit", "/quit"):
                console.print("[dim]Ending session...[/dim]")
                break

            if not user_input.strip():
                continue

            if not session.is_alive:
                console.print("[red]Agent process exited unexpectedly.[/red]")
                stderr = session.stderr.strip()
                if stderr:
                    console.print(f"[dim]{stderr}[/dim]")
                break

            msg_counter += 1
            message_id = f"msg_{msg_counter:04d}"
            try:
                session.send_message(user_input, message_id)
            except Exception as e:
                console.print(f"[red]Send failed:[/red] {e}")
                stderr = session.stderr.strip()
                if stderr:
                    console.print(f"[dim]{stderr}[/dim]")
                break

            # Read responses until we get a done=true response
            accumulated_content = ""
            thinking = console.status("[dim]Thinking...[/dim]", spinner="dots")
            thinking.start()

            while True:
                msg = session.receive(timeout=300)
                if msg is None:
                    thinking.stop()
                    console.print("[yellow]Response timed out.[/yellow]")
                    break

                msg_type = msg.get("type")
                if msg_type == "response":
                    thinking.stop()
                    content = msg.get("content", "")
                    if content:
                        accumulated_content += content
                    if msg.get("done", False):
                        if accumulated_content:
                            console.print()
                            console.print(
                                Markdown(accumulated_content),
                                style="dim",
                            )
                            console.print()
                        break
                elif msg_type == "activity":
                    thinking.stop()
                    tool = msg.get("tool", "?")
                    desc = msg.get("description", "")
                    console.print(f"  [dim]› {tool}: {desc}[/dim]")
                    thinking = console.status("[dim]Thinking...[/dim]", spinner="dots")
                    thinking.start()
                elif msg_type == "error":
                    thinking.stop()
                    console.print(f"  [red]Error: {msg.get('error', 'Unknown')}[/red]")
                    break

            if not session.is_alive:
                console.print("[red]Agent process exited unexpectedly.[/red]")
                stderr = session.stderr.strip()
                if stderr:
                    console.print(f"[dim]{stderr}[/dim]")
                break

    finally:
        session.shutdown()
        console.print("[dim]Session ended.[/dim]")


def _run_json(
    manager: SandboxManager,
    agent_dir: Path,
    manifest,
    workspace: str,
    env_vars: dict,
    state_dir: Path | None = None,
) -> None:
    """Run an agent with JSON pipe I/O (for agent-to-agent)."""
    session = manager.run_agent(
        agent_dir=agent_dir,
        manifest=manifest,
        workspace=Path(workspace).resolve(),
        env_vars=env_vars,
        state_dir=state_dir,
    )

    try:
        if not session.wait_ready(timeout=120):
            _json_line({"type": "error", "error": "Agent failed to start"})
            raise SystemExit(1)

        _json_line({"type": "ready"})

        for line in sys.stdin:
            line = line.strip()
            if not line:
                continue
            try:
                incoming = json.loads(line)
            except json.JSONDecodeError:
                continue

            msg_type = incoming.get("type")
            if msg_type == "shutdown":
                break

            if msg_type == "message":
                content = incoming.get("content", "")
                message_id = incoming.get("message_id", f"auto_{uuid.uuid4().hex[:8]}")
                session.send_message(content, message_id)

                while True:
                    msg = session.receive(timeout=300)
                    if msg is None:
                        _json_line({"type": "error", "error": "timeout", "message_id": message_id})
                        break
                    _json_line(msg)
                    if msg.get("type") == "response" and msg.get("done", False):
                        break
                    if msg.get("type") == "error":
                        break

            if not session.is_alive:
                _json_line({"type": "error", "error": "Agent process exited"})
                break

    finally:
        session.shutdown()


def _json_line(data: dict) -> None:
    """Write a single NDJSON line to stdout."""
    sys.stdout.write(json.dumps(data) + "\n")
    sys.stdout.flush()
