"""CLI commands for brokr."""

import json
import os
import re
import subprocess
import sys
from importlib.metadata import version as pkg_version
from typing import Dict, Optional, Set

import typer
from rich.console import Console
from rich.table import Table

from brokr.project import (
    find_project_file,
    get_agent_secrets,
    get_project_secrets,
    read_project_name,
    resolve_project,
)
from brokr.store import BrokrError, get_store_dir, load_store, save_store

app = typer.Typer(help="Encrypted local secret store that replaces .env files.")
console = Console(stderr=True)

# Register sub-apps (imported after app creation to avoid circular imports)
def _register_sub_apps() -> None:
    from brokr.cli_agent import agent_app, grant_cmd, revoke_cmd
    from brokr.cli_project import project_app

    app.add_typer(project_app, name="project")
    app.add_typer(agent_app, name="agent")
    app.command("grant")(grant_cmd)
    app.command("revoke")(revoke_cmd)

_register_sub_apps()

_KEY_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
_MIN_PASSPHRASE_LEN = 8


def _get_version() -> str:
    try:
        return pkg_version("brokr")
    except Exception:
        return "dev"


def _version_callback(value: bool) -> None:
    if value:
        typer.echo(f"brokr {_get_version()}")
        raise typer.Exit()


@app.callback()
def main(
    version: Optional[bool] = typer.Option(
        None, "--version", callback=_version_callback, is_eager=True,
        help="Show version and exit.",
    ),
) -> None:
    """Encrypted local secret store that replaces .env files."""


def get_passphrase() -> str:
    env_val = os.environ.get("BROKR_PASSPHRASE")
    if env_val is not None:
        console.print(
            "[yellow]Using passphrase from BROKR_PASSPHRASE environment variable.[/yellow]"
        )
        return env_val
    if not sys.stdin.isatty():
        console.print("[red]Error: no TTY and BROKR_PASSPHRASE not set.[/red]")
        raise typer.Exit(code=1)
    return typer.prompt("Passphrase", hide_input=True)


def validate_key_name(name: str) -> str:
    if not _KEY_PATTERN.match(name):
        raise typer.BadParameter(
            "Key name must start with an uppercase letter and contain only "
            "uppercase letters, digits, and underscores."
        )
    return name


def mask_value(value: str) -> str:
    if len(value) <= 7:
        return "****"
    return value[:3] + "..." + value[-4:]


@app.command()
def init() -> None:
    """Initialize a new brokr secret store."""
    store_dir = get_store_dir()
    store_file = store_dir / "store.enc"

    if store_file.exists():
        typer.confirm(
            f"{store_file} already exists. Overwrite?", abort=True
        )

    passphrase = typer.prompt("Passphrase", hide_input=True)
    passphrase_confirm = typer.prompt("Confirm passphrase", hide_input=True)

    if passphrase != passphrase_confirm:
        console.print("[red]Passphrases do not match.[/red]")
        raise typer.Exit(code=1)

    if len(passphrase) < _MIN_PASSPHRASE_LEN:
        console.print(
            f"[yellow]Warning: passphrase is short (< {_MIN_PASSPHRASE_LEN} chars). "
            f"Consider using a stronger passphrase.[/yellow]"
        )

    store_dir.mkdir(parents=True, exist_ok=True)
    salt = os.urandom(16)
    salt_file = store_dir / "salt"
    salt_file.write_bytes(salt)
    os.chmod(salt_file, 0o600)

    save_store({"secrets": {}, "projects": {}}, passphrase, store_dir=store_dir)
    console.print("[green]brokr initialized at ~/.brokr[/green]")


@app.command("set")
def set_secret(
    key_name: str = typer.Argument(..., callback=validate_key_name),
    value: Optional[str] = typer.Option(None, "--value", help="Secret value"),
) -> None:
    """Store a secret under KEY_NAME."""
    store_dir = get_store_dir()
    passphrase = get_passphrase()
    try:
        data = load_store(passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    if value is None:
        value = typer.prompt("Secret value", hide_input=True)

    new_secrets = {**data.get("secrets", {}), key_name: value}
    new_data = {**data, "secrets": new_secrets}
    save_store(new_data, passphrase, store_dir=store_dir)
    console.print(f"[green]Secret {key_name} stored.[/green]")


@app.command("get")
def get_secret(
    key_name: str = typer.Argument(..., callback=validate_key_name),
) -> None:
    """Retrieve and print a secret value."""
    store_dir = get_store_dir()
    passphrase = get_passphrase()
    try:
        data = load_store(passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    secret = data["secrets"].get(key_name)
    if secret is None:
        console.print(f"[red]Key '{key_name}' not found.[/red]")
        raise typer.Exit(code=1)

    typer.echo(secret)


@app.command("list")
def list_secrets(
    show: bool = typer.Option(False, "--show", help="Reveal full secret values"),
) -> None:
    """List all stored secret keys."""
    store_dir = get_store_dir()
    passphrase = get_passphrase()
    try:
        data = load_store(passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    secrets = data.get("secrets", {})
    if not secrets:
        console.print("No secrets stored.")
        return

    table = Table()
    table.add_column("Key")
    table.add_column("Value")
    for key in sorted(secrets):
        val = secrets[key] if show else mask_value(secrets[key])
        table.add_row(key, val)
    console.print(table)


@app.command("rm")
def rm_secret(
    key_name: str = typer.Argument(..., callback=validate_key_name),
) -> None:
    """Remove a secret."""
    store_dir = get_store_dir()
    passphrase = get_passphrase()
    try:
        data = load_store(passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    if key_name not in data["secrets"]:
        console.print(f"[red]Key '{key_name}' not found.[/red]")
        raise typer.Exit(code=1)

    new_secrets = {k: v for k, v in data["secrets"].items() if k != key_name}

    # Cascade: scrub from all project secret lists and agent grants
    old_projects = data.get("projects", {})
    new_projects = {}
    affected = []
    for pname, pdata in old_projects.items():
        psecrets = pdata.get("secrets", [])
        pagents = pdata.get("agents", {})
        if key_name in psecrets:
            affected.append(pname)
            new_psecrets = [s for s in psecrets if s != key_name]
            new_pagents = {
                aname: [g for g in grants if g != key_name]
                for aname, grants in pagents.items()
            }
            new_projects[pname] = {**pdata, "secrets": new_psecrets, "agents": new_pagents}
        else:
            new_projects[pname] = pdata

    new_data = {**data, "secrets": new_secrets, "projects": new_projects}
    save_store(new_data, passphrase, store_dir=store_dir)
    console.print(f"[green]Secret {key_name} removed.[/green]")
    if affected:
        console.print(
            f"[yellow]Unlinked from projects: {', '.join(sorted(affected))}[/yellow]"
        )


@app.command()
def doctor() -> None:
    """Check the health of your brokr installation."""
    store_dir = get_store_dir()
    all_ok = True

    dir_exists = store_dir.is_dir()
    if dir_exists:
        console.print("[green]✓[/green] Store directory exists")
    else:
        console.print("[red]✗[/red] Store directory missing")
        console.print("  Run `brokr init` to create a store.")
        return

    store_file = store_dir / "store.enc"
    salt_file = store_dir / "salt"

    if store_file.exists():
        console.print("[green]✓[/green] store.enc exists")
    else:
        console.print("[red]✗[/red] store.enc missing")
        console.print("  Run `brokr init` to create a store.")
        all_ok = False

    if salt_file.exists():
        console.print("[green]✓[/green] salt exists")
    else:
        console.print("[red]✗[/red] salt missing")
        console.print("  Run `brokr init` to create a store.")
        all_ok = False

    # File permissions check
    for path in [store_file, salt_file]:
        if path.exists():
            mode = path.stat().st_mode & 0o777
            if mode != 0o600:
                console.print(
                    f"[yellow]⚠[/yellow] {path.name} has permissions {oct(mode)} "
                    f"(should be 0o600)"
                )
                all_ok = False

    if store_file.exists() and salt_file.exists():
        passphrase = get_passphrase()
        try:
            data = load_store(passphrase, store_dir=store_dir)
            console.print("[green]✓[/green] Store decrypts successfully")
        except BrokrError:
            console.print("[red]✗[/red] Store decryption failed")
            console.print("  Check your passphrase and try again.")
            all_ok = False
            data = None

        # Project health checks
        if data is not None:
            all_secrets = data.get("secrets", {})
            projects = data.get("projects", {})
            secret_count = len(all_secrets)
            project_count = len(projects)
            console.print(f"[green]✓[/green] {secret_count} secrets, {project_count} projects")

            for pname, pdata in projects.items():
                for sname in pdata.get("secrets", []):
                    if sname not in all_secrets:
                        console.print(
                            f"[yellow]⚠[/yellow] Project '{pname}' references "
                            f"deleted secret '{sname}'"
                        )
                        all_ok = False
                for aname, grants in pdata.get("agents", {}).items():
                    for gname in grants:
                        if gname not in all_secrets:
                            console.print(
                                f"[yellow]⚠[/yellow] Agent '{aname}' in '{pname}' "
                                f"references deleted secret '{gname}'"
                            )
                            all_ok = False

    # Check current directory project context
    project_file = find_project_file()
    if project_file is not None:
        try:
            pname = read_project_name(project_file)
            if data is not None and pname in data.get("projects", {}):
                console.print(f"[green]✓[/green] Project context: {pname}")
            elif data is not None:
                console.print(
                    f"[yellow]⚠[/yellow] .brokr references unknown project '{pname}'"
                )
                all_ok = False
        except ValueError as exc:
            console.print(f"[yellow]⚠[/yellow] Bad .brokr file: {exc}")
            all_ok = False

    if all_ok:
        console.print("\n[green]brokr is healthy.[/green]")


@app.command()
def passwd() -> None:
    """Change the store passphrase."""
    store_dir = get_store_dir()
    store_file = store_dir / "store.enc"
    salt_file = store_dir / "salt"

    if not store_file.exists():
        console.print("[red]No store found. Run `brokr init` first.[/red]")
        raise typer.Exit(code=1)

    old_passphrase = get_passphrase()
    try:
        data = load_store(old_passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    new_passphrase = typer.prompt("New passphrase", hide_input=True)
    new_passphrase_confirm = typer.prompt("Confirm new passphrase", hide_input=True)

    if new_passphrase != new_passphrase_confirm:
        console.print("[red]Passphrases do not match.[/red]")
        raise typer.Exit(code=1)

    if len(new_passphrase) < _MIN_PASSPHRASE_LEN:
        console.print(
            f"[yellow]Warning: passphrase is short (< {_MIN_PASSPHRASE_LEN} chars). "
            f"Consider using a stronger passphrase.[/yellow]"
        )

    # Delete old salt so save_store generates a fresh one
    salt_file.unlink(missing_ok=True)
    save_store(data, new_passphrase, store_dir=store_dir)

    console.print("[green]Passphrase changed successfully.[/green]")


@app.command("export")
def export_secrets(
    fmt: str = typer.Option("env", "--format", "-f", help="Output format: env or json"),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
) -> None:
    """Export secrets to stdout (env or json format)."""
    store_dir = get_store_dir()
    passphrase = get_passphrase()

    try:
        data = load_store(passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    secrets = data.get("secrets", {})

    if secrets and not yes:
        typer.confirm(
            f"This will output {len(secrets)} secret(s) in plaintext to stdout. Continue?",
            abort=True,
        )

    if fmt == "env":
        for key in sorted(secrets):
            # Shell-safe quoting
            val = secrets[key].replace("\\", "\\\\").replace('"', '\\"')
            typer.echo(f'{key}="{val}"')
    elif fmt == "json":
        typer.echo(json.dumps(secrets, indent=2, sort_keys=True))
    else:
        console.print(f"[red]Unknown format: {fmt}. Use 'env' or 'json'.[/red]")
        raise typer.Exit(code=1)


def build_run_env(
    secrets: Dict[str, str],
    base_env: Optional[Dict[str, str]] = None,
    only: Optional[Set[str]] = None,
) -> Dict[str, str]:
    if base_env is None:
        base_env = dict(os.environ)
    if only is not None:
        missing = only - set(secrets)
        for key in sorted(missing):
            console.print(f"[yellow]Warning: key '{key}' not found in store.[/yellow]")
        filtered = {k: v for k, v in secrets.items() if k in only}
    else:
        filtered = secrets
    return {**base_env, **filtered}


@app.command(
    "run",
    context_settings={"allow_extra_args": True, "allow_interspersed_args": False},
)
def run_cmd(
    ctx: typer.Context,
    only: Optional[str] = typer.Option(
        None, help="Comma-separated list of keys to inject"
    ),
    as_agent: Optional[str] = typer.Option(
        None, "--as", help="Run as agent (inject only granted secrets)"
    ),
) -> None:
    """Run a command with secrets injected as environment variables."""
    command = ctx.args
    if not command:
        console.print("[red]Error: no command provided.[/red]")
        raise typer.Exit(code=1)

    store_dir = get_store_dir()
    passphrase = get_passphrase()
    try:
        data = load_store(passphrase, store_dir=store_dir)
    except BrokrError as exc:
        console.print(f"[red]{exc}[/red]")
        raise typer.Exit(code=1)

    # Determine which secrets to inject
    if as_agent is not None:
        # Agent mode: requires project context
        try:
            project_name, _project_data = resolve_project(data)
            secrets = get_agent_secrets(data, project_name, as_agent)
        except BrokrError as exc:
            console.print(f"[red]{exc}[/red]")
            raise typer.Exit(code=1)
        if not secrets:
            console.print(
                f"[yellow]Warning: agent '{as_agent}' has no granted secrets.[/yellow]"
            )
        else:
            console.print(
                f"[dim]Injecting {len(secrets)} secret(s) as agent '{as_agent}' "
                f"in project '{project_name}'[/dim]"
            )
    else:
        # Check for project context (optional)
        project_file = find_project_file()
        if project_file is not None:
            try:
                project_name = read_project_name(project_file)
                secrets = get_project_secrets(data, project_name)
                all_count = len(data.get("secrets", {}))
                console.print(
                    f"[dim]Project '{project_name}': injecting "
                    f"{len(secrets)}/{all_count} secrets[/dim]"
                )
            except (ValueError, BrokrError) as exc:
                console.print(
                    f"[yellow]Warning: project context error ({exc}), "
                    f"falling back to all secrets.[/yellow]"
                )
                secrets = data.get("secrets", {})
        else:
            # No project context: V1 behavior — all secrets
            secrets = data.get("secrets", {})

        if not secrets:
            console.print(
                "[yellow]Warning: no secrets to inject.[/yellow]"
            )

    only_set: Optional[Set[str]] = None
    if only is not None:
        only_set = {k.strip() for k in only.split(",")}

    env = build_run_env(secrets, only=only_set)
    result = subprocess.run(command, env=env)
    raise SystemExit(result.returncode)
