"""Click CLI — thin dispatcher, handles exit codes."""

import os
import sys
from pathlib import Path

import click
from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax

import noisegate.config as config
from noisegate import __version__
from noisegate.config import (
    PROVIDERS,
    bootstrap,
    get_api_keys,
    get_default_program_name,
    get_provider_model,
    save_api_key,
    save_default_model,
    save_provider_model,
    set_default_program,
)
from noisegate.exceptions import NoDefaultProgramError, NoiseGateError
from noisegate.importer import import_program_from_file, import_program_from_url
from noisegate.llm import FALLBACK_MODEL, get_default_model
from noisegate.loader import load_reports
from noisegate.policy import load_policy
from noisegate.program import (
    list_programs,
    load_program,
    remove_program,
    resolve_program,
    save_program,
)
from noisegate.renderer import render_human, render_json
from noisegate.triage import run_triage

console = Console()
err_console = Console(stderr=True)


def _inject_stored_api_keys() -> None:
    """Set env vars from stored config for any keys not already in the environment."""
    provider_env = {p["id"]: p["env_var"] for p in PROVIDERS if p["env_var"]}
    for provider, key in get_api_keys().items():
        env_var = provider_env.get(provider)
        if env_var and env_var not in os.environ and key:
            os.environ[env_var] = key


@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.version_option(__version__, "--version", "-V")
def cli() -> None:
    """NoiseGate — AI-powered bug bounty report triage."""
    bootstrap()
    _inject_stored_api_keys()


# ---------------------------------------------------------------------------
# triage
# ---------------------------------------------------------------------------

@cli.command()
@click.argument("files", nargs=-1, required=True)
@click.option("--program", "-p", default=None, help="Program name to use (overrides default).")
@click.option("--json", "output_json", is_flag=True, default=False,
              help="Machine-readable JSON output.")
@click.option("--model", "-m", default=None,
              help=f"LLM model string (default: configured or {FALLBACK_MODEL}).")
def triage(
    files: tuple[str, ...], program: str | None, output_json: bool, model: str | None
) -> None:
    """Evaluate a bug report against a program.

    FILES is one or more files that together make up a single report
    (e.g. a writeup and a separate PoC file).
    """
    try:
        prog_name = program or get_default_program_name()
        if prog_name is None:
            raise NoDefaultProgramError()

        prog = load_program(resolve_program(prog_name))
        report = load_reports(files)
        policy = load_policy(config._POLICY_FILE)

        with console.status("[dim]Triaging...[/dim]"):
            result = run_triage(policy.checks, report, prog, model=model or get_default_model())

        if output_json:
            click.echo(render_json(result))
        else:
            render_human(result, console=console)

        # Exit codes: 0=PASS, 1=WARN, 2=FAIL
        exit_code = {"PASS": 0, "WARN": 1, "FAIL": 2}.get(result.verdict.value, 3)
        sys.exit(exit_code)

    except (NoiseGateError, FileNotFoundError) as e:
        err_console.print(f"[red]Error:[/red] {e}")
        sys.exit(3)
    except KeyboardInterrupt:
        sys.exit(3)


# ---------------------------------------------------------------------------
# program
# ---------------------------------------------------------------------------

@cli.group()
def program() -> None:
    """Manage bug bounty program definitions."""


@program.command("add")
@click.argument("name")
@click.option("--url", "-u", default=None, help="Seed URL to crawl for the program.")
@click.option("--file", "-f", default=None, help="Path to a local file (HTML, PDF, or plain text).")
@click.option("--model", "-m", default=None,
              help=f"LLM model string (default: configured or {FALLBACK_MODEL}).")
@click.option("--verbose", "-v", is_flag=True, default=False,
              help="Print the extracted content of each fetched page (crawl only).")
def program_add(
    name: str, url: str | None, file: str | None, model: str | None, verbose: bool
) -> None:
    """Save a bug bounty program from a URL (crawled) or local file."""
    if not url and not file:
        raise click.UsageError("Provide either --url or --file.")
    if url and file:
        raise click.UsageError("--url and --file are mutually exclusive.")

    try:
        resolved_model = model or get_default_model()
        usage_line: list[str] = []

        def _on_complete(used_model: str, prompt_tokens: int, completion_tokens: int) -> None:
            usage_line.append(
                f"{used_model}  ·  {prompt_tokens:,} in / {completion_tokens:,} out"
            )

        if file:
            prog = import_program_from_file(
                Path(file), model=resolved_model, on_complete=_on_complete,
            )
        else:
            assert url is not None  # guaranteed by the mutual-exclusion checks above

            def _on_fetch_start(fetched_url: str) -> None:
                console.print(f"Crawling {fetched_url} ", end="")

            def _on_fetch_end(fetched_url: str, status: int, content: str) -> None:
                color = "green" if status < 400 else "red"
                console.print(f"[{status}]", style=color, markup=False)
                if verbose:
                    console.print(Panel(Syntax(content, "markdown"), border_style="dim"))

            def _on_warning(msg: str) -> None:
                console.print(f"[yellow]⚠[/yellow]  {msg}")

            prog = import_program_from_url(
                url, model=resolved_model,
                on_fetch_start=_on_fetch_start, on_fetch=_on_fetch_end,
                on_warning=_on_warning, on_complete=_on_complete,
            )

        out_path = config._PROGRAMS_DIR / f"{name}.yaml"
        save_program(prog, out_path)
        set_default_program(name)

        console.print(f"\n[green]Program '[bold]{name}[/bold]' saved.[/green]")
        console.print(f"To view details: noisegate program show {name}")

        if usage_line:
            console.print()
            console.print(f"[dim]{usage_line[0]}[/dim]")

    except NoiseGateError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        sys.exit(3)


@program.command("list")
def program_list() -> None:
    """List all saved programs."""
    programs = list_programs()
    if not programs:
        console.print("[dim]No programs saved yet.[/dim]")
        console.print("Run: noisegate program add <name> --url URL")
        return

    default = get_default_program_name()
    for p in programs:
        marker = " [green](default)[/green]" if p == default else ""
        console.print(f"  • {p}{marker}")


@program.command("show")
@click.argument("name")
def program_show(name: str) -> None:
    """Print a saved program."""
    try:
        path = resolve_program(name)
        prog = load_program(path)
        console.print(Panel(
            Syntax(path.read_text(), "yaml"),
            title=f"[bold]{prog.name}[/bold]",
            subtitle=f"[dim]{path}[/dim]",
        ))
    except NoiseGateError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        sys.exit(3)


@program.command("use")
@click.argument("name")
def program_use(name: str) -> None:
    """Set the default program for triage."""
    try:
        set_default_program(name)
        console.print(f"[green]Default program set to '[bold]{name}[/bold]'.[/green]")
    except NoiseGateError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        sys.exit(3)


@program.command("remove")
@click.argument("name")
@click.confirmation_option(prompt="Are you sure you want to remove this program?")
def program_remove(name: str) -> None:
    """Delete a saved program."""
    try:
        remove_program(name)
        console.print(f"[green]Program '[bold]{name}[/bold]' removed.[/green]")
    except NoiseGateError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        sys.exit(3)


# ---------------------------------------------------------------------------
# llm
# ---------------------------------------------------------------------------

@cli.command()
def llm() -> None:
    """Configure an LLM provider, API key, and model."""
    stored_keys = get_api_keys()
    current_model = get_default_model()

    # --- Provider selection ---
    console.print()
    console.print("Choose a provider:\n")
    for i, p in enumerate(PROVIDERS, 1):
        has_key = p["env_var"] and (stored_keys.get(p["id"]) or os.environ.get(p["env_var"], ""))
        configured = has_key or get_provider_model(p["id"])
        key_marker = " [green]✓[/green]" if configured else ""
        is_default = current_model and current_model.startswith(f"{p['id']}:")
        default_marker = " [dim](default)[/dim]" if is_default else ""
        console.print(f"  {i}  {p['name']}{key_marker}{default_marker}")
    console.print()

    valid = {str(i) for i in range(1, len(PROVIDERS) + 1)}
    while True:
        raw = click.prompt("Provider", default="1")
        if raw in valid:
            break
        console.print(f"[yellow]Please enter a number between 1 and {len(PROVIDERS)}.[/yellow]")

    provider = PROVIDERS[int(raw) - 1]

    # --- API key ---
    if provider["env_var"]:
        existing = os.environ.get(provider["env_var"], "") or stored_keys.get(provider["id"], "")
        if existing:
            masked = f"{existing[:10]}{'*' * 6}"
            key_prompt = f"API key [{masked}]"
            console.print("[dim]Press Enter to keep existing key.[/dim]")
        else:
            key_prompt = "API key"
        key = click.prompt(key_prompt, default=existing or "", hide_input=True, show_default=False)
        if key:
            save_api_key(provider["id"], key)
            os.environ[provider["env_var"]] = key

    # --- Model ---
    configured_model = get_provider_model(provider["id"]) or provider["default_model"]
    model = click.prompt("Model", default=configured_model)
    save_provider_model(provider["id"], model)

    # --- Set as default provider? ---
    set_as_default = click.confirm(
        f"Set {provider['name']} as the default provider?",
        default=True,
    )
    if set_as_default:
        save_default_model(model)

    console.print("[green]Done.[/green]")
