"""CLI entry point for Tribunal.

Provides the ``tribunal`` command with subcommands for launching Claude Code,
managing context, worktrees, access, and the IDE statusline.
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import Sequence

from launcher import __version__
from launcher.banner import print_banner, print_compact_banner, print_version
from launcher.repair import _run_repair
from launcher.uninstall import _run_uninstall
from launcher.updater import check_for_updates
from launcher.wizard import is_first_run, run_wizard
from launcher.wrapper import launch_claude

_PLUGIN_DIR = Path.home() / ".claude" / "tribunal"



def _add_model_subparser(subparsers: argparse._SubParsersAction) -> None:  # type: ignore[type-arg]
    """Register the model subcommand and its sub-subcommands onto *subparsers*."""
    mp = subparsers.add_parser("model", help="Manage the default model for Claude Code.")
    model_sub = mp.add_subparsers(dest="model_command")

    model_sub.add_parser("list", help="List available models with descriptions.")
    model_sub.add_parser("get", help="Show the currently configured model.")
    model_sub.add_parser("clear", help="Reset to Claude Code default model.")

    set_p = model_sub.add_parser("set", help="Set the default model.")
    set_p.add_argument("model_id", help="Model ID (e.g. claude-sonnet-4-5 or minimax/MiniMax-Text-01).")
    set_p.add_argument("--force", action="store_true", default=False, help="Skip unrecognised-model warning.")

    add_p = model_sub.add_parser("add", help="Add a custom model to your model list.")
    add_p.add_argument("model_id", help="Model ID (e.g. minimax/MiniMax-Text-01).")
    add_p.add_argument("--name", default="", help="Human-friendly display name.")
    add_p.add_argument("--provider", default="", help="Provider name (e.g. MiniMax).")

    rm_p = model_sub.add_parser("remove", help="Remove a custom model from your model list.")
    rm_p.add_argument("model_id", help="Model ID to remove.")


def _add_skills_subparser(subparsers: argparse._SubParsersAction) -> None:  # type: ignore[type-arg]
    """Register the ``skills`` subcommand and its sub-subcommands onto *subparsers*."""
    skills_parser = subparsers.add_parser("skills", help="Manage project skills.")
    skills_sub = skills_parser.add_subparsers(dest="skills_command")

    skills_sub.add_parser("list", help="List all project skills.")

    show_p = skills_sub.add_parser("show", help="Show a skill's content.")
    show_p.add_argument("name", help="Skill name to display.")

    skills_sub.add_parser("init", help="Initialize .claude/ with project configuration.")

    analyze_p = skills_sub.add_parser("analyze", help="Analyze codebase and generate skills.")
    analyze_p.add_argument("--dry-run", action="store_true", default=False)
    analyze_p.add_argument("--model", default="sonnet")

    create_p = skills_sub.add_parser("create", help="Create a new skill from template.")
    create_p.add_argument("name", help="Skill name (kebab-case).")

    skills_sub.add_parser("doctor", help="Validate skill files and check for issues.")


def _add_ci_subparser(subparsers: argparse._SubParsersAction) -> None:  # type: ignore[type-arg]
    """Register the ``ci`` subcommand onto *subparsers*."""
    ci = subparsers.add_parser(
        "ci",
        help="Run all Tribunal quality checks standalone (no Claude Code required).",
    )
    ci.add_argument(
        "ci_files",
        nargs="*",
        metavar="FILE",
        help="Files to check. Defaults to git-changed files.",
    )
    ci.add_argument(
        "--json",
        action="store_true",
        default=False,
        help="Output machine-readable JSON report.",
    )
    ci.add_argument(
        "--coverage-threshold",
        type=int,
        default=None,
        metavar="PCT",
        help="Fail if estimated coverage is below this percentage (0-100).",
    )


def _add_worktree_subparser(subparsers: argparse._SubParsersAction) -> None:  # type: ignore[type-arg]
    """Register the ``worktree`` subcommand and its sub-subcommands onto *subparsers*."""
    wt = subparsers.add_parser("worktree", help="Manage git worktrees for /spec.")
    wt_sub = wt.add_subparsers(dest="wt_command")

    for name in ("detect", "create", "diff", "sync"):
        p = wt_sub.add_parser(name)
        p.add_argument("--json", action="store_true", default=False, help="JSON output.")
        p.add_argument("slug", help="Plan slug (filename without date and .md).")

    cleanup_p = wt_sub.add_parser("cleanup")
    cleanup_p.add_argument("--json", action="store_true", default=False, help="JSON output.")
    cleanup_p.add_argument(
        "--force", action="store_true", default=False, help="Force removal even with uncommitted changes."
    )
    cleanup_p.add_argument("slug", help="Plan slug (filename without date and .md).")

    st = wt_sub.add_parser("status")
    st.add_argument("--json", action="store_true", default=False, help="JSON output.")


def _build_parser() -> argparse.ArgumentParser:  # noqa: PLR0915
    """Build and return the top-level ``tribunal`` argument parser."""
    parser = argparse.ArgumentParser(
        prog="tribunal",
        description="Tribunal -- Claude Code with Tribunal configuration.",
    )
    parser.add_argument("--no-banner", action="store_true", help="Suppress startup banner.")
    parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output.")

    sub = parser.add_subparsers(dest="command")

    lp = sub.add_parser("launch", help="Launch Claude Code (default).")
    lp.add_argument("--model", default=None, help="Model for the session (e.g., sonnet, opus, haiku).")
    lp.add_argument("--save", action="store_true", default=False, help="Save --model as the persistent default.")
    lp.add_argument("claude_args", nargs=argparse.REMAINDER)

    rp = sub.add_parser("run", help="Launch Claude Code (alias for launch).")
    rp.add_argument("--model", default=None, help="Model for the session (e.g., sonnet, opus, haiku).")
    rp.add_argument("--save", action="store_true", default=False, help="Save --model as the persistent default.")
    rp.add_argument("claude_args", nargs=argparse.REMAINDER)

    sub.add_parser("statusline", help="Output IDE statusline.")

    sub.add_parser("version", help="Show version.")

    sub.add_parser("update", help="Check for updates.")

    bp = sub.add_parser("bundle", help="Create an air-gapped install bundle (tar.gz).")
    bp.add_argument("--output", "-o", default="tribunal-bundle.tar.gz", help="Output file path.")
    bp.add_argument("--vault-dir", default=None, help="Override vault directory to bundle.")

    cc = sub.add_parser("check-context", help="Get context usage percentage.")
    cc.add_argument("--json", action="store_true", default=False)

    sc = sub.add_parser("send-clear", help="Trigger Endless Mode continuation.")
    sc.add_argument("plan_path", nargs="?", default=None, help="Path to active plan file.")
    sc.add_argument("--general", action="store_true", default=False, help="No plan context.")

    # Aliases for send-clear
    for alias in ("endless", "handoff"):
        _sc = sub.add_parser(alias, help=f"Alias for send-clear ({alias} mode).")
        _sc.add_argument("plan_path", nargs="?", default=None, help="Path to active plan file.")
        _sc.add_argument("--general", action="store_true", default=False, help="No plan context.")

    rg = sub.add_parser("register-plan", help="Associate plan with session.")
    rg.add_argument("plan_path", help="Path to plan file.")
    rg.add_argument("status", help="Plan status (PENDING, COMPLETE, VERIFIED).")

    act = sub.add_parser("activate", help="Activate access.")
    act.add_argument("key", nargs="?", default=None, help="Activation key.")
    act.add_argument("--json", action="store_true", default=False)

    da = sub.add_parser("deactivate", help="Deactivate access.")
    da.add_argument("--json", action="store_true", default=False)

    st = sub.add_parser("status", help="Show access and session status.")
    st.add_argument("--json", action="store_true", default=False)

    vr = sub.add_parser("verify", help="Verify access authorization.")
    vr.add_argument("--json", action="store_true", default=False)

    tr = sub.add_parser("trial", help="Manage trial access.")
    tr.add_argument("--check", action="store_true", default=False)
    tr.add_argument("--start", action="store_true", default=False)
    tr.add_argument("--json", action="store_true", default=False)

    init_p = sub.add_parser("init", help="Interactive project setup wizard — configure Tribunal for this project.")
    init_p.add_argument(
        "--non-interactive",
        action="store_true",
        default=False,
        help="Skip prompts and apply CI-friendly defaults.",
    )
    init_p.add_argument(
        "--project-dir",
        default=None,
        help="Target project directory (defaults to cwd).",
    )

    sub.add_parser("doctor", help="Check Tribunal installation health (hooks, deps, worker service).")

    sub.add_parser("repair", help="Re-sync tribunal hooks and settings into ~/.claude/settings.json.")

    un = sub.add_parser(
        "uninstall",
        help="Remove Tribunal from Claude Code (hooks, plugin, shell aliases).",
    )
    un.add_argument(
        "--all",
        action="store_true",
        default=False,
        help="Also remove ~/.tribunal/ user data (sessions, observations, audit logs).",
    )
    un.add_argument(
        "--yes",
        "-y",
        action="store_true",
        default=False,
        help="Skip confirmation prompt.",
    )

    vp = sub.add_parser("vault", help="Browse installed Tribunal assets.")
    vault_sub = vp.add_subparsers(dest="vault_command")
    vault_sub.add_parser("list", help="List all installed assets.")
    vs = vault_sub.add_parser("show", help="Show an asset's content.")
    vs.add_argument("name", help="Asset name (e.g. spec-plan).")
    vsr = vault_sub.add_parser("search", help="Search assets by name.")
    vsr.add_argument("query", help="Search term.")

    # install subcommand
    ip = sub.add_parser("install", help="Install a Tribunal pack from registry, GitHub, or local path.")
    ip.add_argument(
        "pack_source",
        help="Registry name (e.g. python-strict), GitHub URL, or local path.",
    )

    # verify-pack subcommand
    vp = sub.add_parser("verify-pack", help="Verify integrity of an installed pack against its stored hash.")
    vp.add_argument(
        "pack_name",
        help="Name of the installed pack to verify.",
    )

    # list-packs subcommand
    sub.add_parser("list-packs", help="List available packs in the built-in registry.")

    _add_model_subparser(sub)
    _add_skills_subparser(sub)
    _add_worktree_subparser(sub)
    _add_ci_subparser(sub)

    return parser


def _list_vault_assets(plugin_dir: Path) -> list[tuple[str, str]]:
    """Walk plugin_dir and return (asset_type, name) for each installed asset.

    Prefers reading vault.json manifest when available for programmatic discovery.
    Falls back to filesystem scan.
    """
    import json

    vault_json = plugin_dir / "vault.json"
    if vault_json.exists():
        try:
            data = json.loads(vault_json.read_text())
            assets_map = data.get("assets", {})
            result: list[tuple[str, str]] = []
            for category, entries in assets_map.items():
                if isinstance(entries, list):
                    for entry in entries:
                        name = entry.get("name") or Path(entry.get("path", "")).stem
                        if name:
                            result.append((category, name))
            if result:
                return result
        except (json.JSONDecodeError, OSError, KeyError):
            pass  # Fall through to filesystem scan

    # Fallback: filesystem scan
    asset_dirs = ("agents", "commands", "rules", "hooks")
    assets: list[tuple[str, str]] = []
    for asset_type in asset_dirs:
        d = plugin_dir / asset_type
        if not d.is_dir():
            continue
        for f in sorted(d.iterdir()):
            if f.suffix in (".md", ".py", ".json"):
                assets.append((asset_type, f.stem))
    return assets


def _dispatch_vault(args: argparse.Namespace) -> None:
    """Route ``tribunal vault <subcommand>`` to the appropriate vault function."""
    from rich.console import Console
    from rich.table import Table

    vault_cmd: str | None = getattr(args, "vault_command", None)
    plugin_dir: Path = _PLUGIN_DIR
    console = Console()

    if vault_cmd is None or vault_cmd == "list":
        assets = _list_vault_assets(plugin_dir)
        if not assets:
            console.print("[yellow]No assets found in[/yellow]", str(plugin_dir))
            return
        table = Table(title="Installed Tribunal Assets", show_lines=False)
        table.add_column("Type", style="cyan")
        table.add_column("Name", style="green")
        for asset_type, name in assets:
            table.add_row(asset_type, name)
        console.print(table)

    elif vault_cmd == "show":
        name: str = args.name
        for asset_type, asset_name in _list_vault_assets(plugin_dir):
            if asset_name == name or asset_name == name.removesuffix(".md"):
                asset_dir = plugin_dir / asset_type
                for f in asset_dir.iterdir():
                    if f.stem == asset_name:
                        console.print(f.read_text())
                        return
        console.print(f"[red]Asset not found:[/red] {name}")
        sys.exit(1)

    elif vault_cmd == "search":
        query: str = args.query.lower()
        assets = _list_vault_assets(plugin_dir)
        matches = [(t, n) for t, n in assets if query in n.lower() or query in t.lower()]
        if not matches:
            console.print(f"No assets matching [bold]{args.query}[/bold]")
            return
        table = Table(title=f"Assets matching '{args.query}'", show_lines=False)
        table.add_column("Type", style="cyan")
        table.add_column("Name", style="green")
        for asset_type, name in matches:
            table.add_row(asset_type, name)
        console.print(table)

    else:
        print("Usage: tribunal vault {list|show <name>|search <query>}", file=sys.stderr)
        sys.exit(1)


def _dispatch_worktree(args: argparse.Namespace) -> None:
    """Route ``tribunal worktree <subcommand>`` to the appropriate worktree function."""
    from launcher.worktree import (
        cleanup,
        create,
        detect,
        diff,
        status,
        sync,
    )

    as_json: bool = getattr(args, "json", False)
    wt_cmd: str | None = args.wt_command

    if not wt_cmd:
        print("Usage: tribunal worktree {detect|create|diff|sync|cleanup|status}", file=sys.stderr)
        sys.exit(1)

    dispatch = {
        "detect": lambda: detect(args.slug, as_json=as_json),
        "create": lambda: create(args.slug, as_json=as_json),
        "diff": lambda: diff(args.slug, as_json=as_json),
        "sync": lambda: sync(args.slug, as_json=as_json),
        "cleanup": lambda: cleanup(args.slug, as_json=as_json, force=getattr(args, "force", False)),
        "status": lambda: status(as_json=as_json),
    }
    fn = dispatch.get(wt_cmd)
    if fn:
        fn()
    else:
        print(f"Unknown worktree command: {wt_cmd}", file=sys.stderr)
        sys.exit(1)


def _dispatch_access(command: str, args: argparse.Namespace) -> None:
    """Route access management commands (verify, status, activate, deactivate, trial)."""
    import json

    from launcher.access import get_access_details, verify_access

    as_json: bool = getattr(args, "json", False)

    if command == "verify":
        ok, msg = verify_access()
        if as_json:
            print(json.dumps({"authorized": ok, "message": msg}))
        else:
            print(msg)
        sys.exit(0 if ok else 1)

    elif command == "status":
        details = get_access_details()
        runtime = _get_runtime_status()
        cost = _get_cost_status()
        if as_json:
            print(json.dumps({**details, **runtime, "cost": cost}, indent=2))
        else:
            _print_access_status(details)
            _print_runtime_status(runtime)
            _print_cost_status(cost)

    elif command == "activate":
        activate_url = "https://tribunal.thebotclub.ai/activate"
        if as_json:
            print(json.dumps({"status": "web_required", "url": activate_url}))
        else:
            print("To activate Tribunal, visit:")
            print(f"  {activate_url}")
        sys.exit(0)

    elif command in ("deactivate", "trial"):
        if as_json:
            print(json.dumps({"status": "not_implemented"}))
        else:
            print(f"'{command}' is not yet implemented.")
        sys.exit(0)


def _dispatch_status(args: argparse.Namespace) -> None:
    """Route ``tribunal status`` — combines access + runtime + tribunal project status."""
    import json as _json

    from launcher.access import get_access_details

    as_json: bool = getattr(args, "json", False)
    details = get_access_details()
    runtime = _get_runtime_status()
    cost = _get_cost_status()
    tribunal_status = _get_tribunal_project_status()

    if as_json:
        print(_json.dumps({**details, **runtime, "cost": cost, "tribunal": tribunal_status}, indent=2))
    else:
        _print_access_status(details)
        _print_runtime_status(runtime)
        _print_cost_status(cost)
        _print_tribunal_project_status(tribunal_status)


def _get_tribunal_project_status(project_dir: Path | None = None) -> dict:
    """Collect Tribunal project-level status for the current directory."""
    from launcher import __version__

    base = project_dir or Path.cwd()
    claude_dir = base / ".claude"
    settings_path = claude_dir / "settings.json"
    tribunal_config_path = claude_dir / "tribunal.json"

    # Resolve the installed plugin dir
    plugin_dir = Path.home() / ".claude" / "tribunal"
    hooks_json = plugin_dir / "hooks" / "hooks.json"

    # Check .claude/settings.json link status
    settings_ok = False
    settings_note = "not found"
    if settings_path.exists():
        settings_ok = True
        settings_note = "present"

    # Load hook config from plugin
    hooks_enabled: dict[str, bool] = {}
    if hooks_json.exists():
        try:
            import json as _json
            raw = _json.loads(hooks_json.read_text(encoding="utf-8"))
            for event, hook_list in raw.get("hooks", {}).items():
                hooks_enabled[event] = True  # hook entry present = enabled
        except (OSError, Exception):
            pass

    # Load project tribunal config if present
    project_config: dict = {}
    if tribunal_config_path.exists():
        try:
            import json as _json
            project_config = _json.loads(tribunal_config_path.read_text(encoding="utf-8"))
        except (OSError, Exception):
            pass

    # Audit log summary
    audit_summary = _get_audit_summary()

    return {
        "version": __version__,
        "project_config": project_config,
        "hooks_enabled": hooks_enabled,
        "claude_settings_ok": settings_ok,
        "claude_settings_note": settings_note,
        "audit": audit_summary,
    }


def _get_audit_summary(log_file: Path | None = None) -> dict:
    """Parse the audit log and return a brief summary."""
    import json as _json

    if log_file is None:
        log_file = Path.home() / ".tribunal" / "audit.jsonl"

    if not log_file.exists():
        return {}

    fired = 0
    blocked = 0
    warned = 0
    last_ts = ""
    try:
        with log_file.open(encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    rec = _json.loads(line)
                    fired += 1
                    outcome = rec.get("outcome", "")
                    if outcome == "blocked":
                        blocked += 1
                    elif outcome == "warned":
                        warned += 1
                    ts = rec.get("timestamp", "")
                    if ts:
                        last_ts = ts
                except _json.JSONDecodeError:
                    continue
    except OSError:
        return {}

    return {
        "hooks_fired": fired,
        "blocked": blocked,
        "warned": warned,
        "last_session": last_ts[:10] if last_ts else "",
    }


def _print_tribunal_project_status(status: dict) -> None:
    """Pretty-print Tribunal project status."""
    if not status:
        return
    print()
    print(f"  Tribunal v{status.get('version', '?')}")

    # Claude settings
    settings_note = status.get("claude_settings_note", "not found")
    settings_icon = "✓" if status.get("claude_settings_ok") else "✗"
    print(f"  .claude/settings.json: {settings_icon} {settings_note}")

    # Project config
    project_config = status.get("project_config", {})
    if project_config:
        lang = project_config.get("language", "?")
        tdd = project_config.get("tdd", "?")
        print(f"  Project config: language={lang}, tdd={tdd}")
        secrets = "on" if project_config.get("secrets", True) else "off"
        spec = "on" if project_config.get("spec_workflow", True) else "off"
        ctx = "on" if project_config.get("context_monitor", True) else "off"
        print(f"    secrets={secrets}, spec={spec}, context_monitor={ctx}")
    else:
        print("  Project config: not initialised (run: tribunal init)")

    # Hooks
    hooks = status.get("hooks_enabled", {})
    if hooks:
        print(f"  Hooks: {', '.join(sorted(hooks.keys()))}")

    # Audit log
    audit = status.get("audit", {})
    if audit:
        fired = audit.get("hooks_fired", 0)
        blocked = audit.get("blocked", 0)
        warned = audit.get("warned", 0)
        last = audit.get("last_session", "")
        suffix = f"  (last: {last})" if last else ""
        print(f"  Audit: {fired} hooks fired, {blocked} blocked, {warned} warned{suffix}")
    else:
        print("  Audit: no log entries yet")


def _get_runtime_status() -> dict:
    """Return current runtime configuration — model, mode, and worktree state."""
    from launcher.config import get_config
    from launcher.worktree import _load_worktree_state

    cfg = get_config()
    return {
        "model": cfg.get("model"),
        "mode": cfg.get("mode", "quick"),
        "worktree": _load_worktree_state(),
    }


def _print_runtime_status(runtime: dict) -> None:
    """Print runtime configuration for the terminal."""
    model = runtime.get("model")
    print(f"  Model: {model if model else '(default - Claude picks)'}")
    print(f"  Mode: {runtime.get('mode', 'quick')}")
    wt = runtime.get("worktree")
    if wt:
        print(f"  Worktree: {wt.get('branch', '?')} → {wt.get('path', '?')}")
    else:
        print("  Worktree: none")


def _get_cost_status(sessions_dir: Path | None = None) -> dict:
    """Return session usage summary — count and last session date."""
    from datetime import datetime

    if sessions_dir is None:
        sessions_dir = Path.home() / ".tribunal" / "sessions"
    if not sessions_dir.exists():
        return {}
    try:
        entries = [e for e in sessions_dir.iterdir() if e.is_dir()]
        if not entries:
            return {}
        count = len(entries)
        latest = max(entries, key=lambda e: e.stat().st_mtime)
        last_date = datetime.fromtimestamp(latest.stat().st_mtime).strftime("%Y-%m-%d")
        return {"sessions": count, "last_session_date": last_date}
    except OSError:
        return {}


def _print_cost_status(cost: dict) -> None:
    """Pretty-print session usage summary."""
    if not cost:
        return
    count = cost.get("sessions", 0)
    last_date = cost.get("last_session_date", "")
    suffix = f" (last: {last_date})" if last_date else ""
    print(f"  Sessions: {count} run{suffix}")


def _print_access_status(details: dict) -> None:
    """Pretty-print access status for the terminal."""
    ok = details["authorized"]
    method = details.get("method")
    status = f"  Authorized: {'Yes' if ok else 'No'}"
    if method:
        status += f" (via {method})"
    print(status)

    cache = details.get("cache", {})
    if cache.get("valid"):
        print(f"  Cache: valid, expires in {cache['expires_in_hours']}h")
    else:
        print("  Cache: expired or missing")

    anthropic = details.get("sources", {}).get("anthropic")
    if anthropic:
        check = "pass" if anthropic["authorized"] else "fail"
        print(f"  Anthropic: {anthropic['email']} ({anthropic.get('display_name', '?')}) [{check}]")
        if anthropic.get("org_uuid"):
            org_ok = "pass" if anthropic["org_authorized"] else "fail"
            print(f"    Org UUID: {anthropic['org_uuid'][:12]}... [{org_ok}]")
        dom_ok = "pass" if anthropic["domain_authorized"] else "fail"
        print(f"    Domain: {dom_ok}")
    else:
        print("  Anthropic: not logged in")

    github = details.get("sources", {}).get("github")
    if github:
        for entry in github:
            check = "pass" if (entry["domain_authorized"] or entry["email_authorized"]) else "fail"
            print(f"  GitHub: {entry['email']} [{check}]")
    else:
        scope_err = details.get("sources", {}).get("github_scope_error")
        if scope_err:
            print("  GitHub: gh authenticated but missing 'user' scope — run: gh auth refresh -s user")
        else:
            print("  GitHub: gh CLI not available or not authenticated")



# Built-in known Claude models
BUILTIN_CLAUDE_MODELS: list[tuple[str, str, str]] = [
    ("claude-opus-4-5", "Claude Opus 4.5", "Most capable, best for complex tasks"),
    ("claude-sonnet-4-5", "Claude Sonnet 4.5", "Balanced performance and speed (recommended)"),
    ("claude-haiku-3-5", "Claude Haiku 3.5", "Fastest, best for simple tasks"),
    ("claude-opus-4", "Claude Opus 4", "Previous generation, highly capable"),
    ("claude-sonnet-4", "Claude Sonnet 4", "Previous generation balanced"),
    ("claude-3-7-sonnet-20250219", "Claude Sonnet 3.7", "Extended thinking support"),
]

# Popular third-party models shown as examples (passthrough — no validation)
EXAMPLE_THIRD_PARTY_MODELS: list[tuple[str, str, str]] = [
    ("minimax/MiniMax-Text-01", "MiniMax Text 01", "MiniMax — long-context model"),
    ("THUDM/glm-4-9b-chat", "GLM-4", "THUDM — open-source GLM model"),
    ("google/gemini-2.0-flash", "Gemini 2.0 Flash", "Google — fast multimodal model"),
    ("openai/gpt-4o", "GPT-4o", "OpenAI — multimodal flagship"),
    ("deepseek/deepseek-r1", "DeepSeek R1", "DeepSeek — reasoning model"),
]

_BUILTIN_IDS: frozenset[str] = frozenset(m[0] for m in BUILTIN_CLAUDE_MODELS)


def _dispatch_model(args: argparse.Namespace) -> None:  # noqa: PLR0915
    """Route ``tribunal model <subcommand>`` to the appropriate model function."""
    from rich.console import Console
    from rich.table import Table

    from launcher.config import (
        add_custom_model,
        clear_model,
        get_custom_models,
        get_model,
        remove_custom_model,
        set_model,
    )

    console = Console()
    model_cmd: str | None = getattr(args, "model_command", None)

    if model_cmd is None or model_cmd == "list":
        # Built-in Claude models
        table = Table(title="Built-in Claude Models", show_lines=False)
        table.add_column("Model ID", style="cyan")
        table.add_column("Display Name", style="green")
        table.add_column("Description", style="dim")
        current = get_model()
        for model_id, display, desc in BUILTIN_CLAUDE_MODELS:
            marker = " ◀ current" if model_id == current else ""
            table.add_row(model_id, display + marker, desc)
        console.print(table)

        # Popular third-party examples
        tp_table = Table(title="Popular Third-Party Models (passthrough examples)", show_lines=False)
        tp_table.add_column("Model ID", style="cyan")
        tp_table.add_column("Display Name", style="green")
        tp_table.add_column("Description", style="dim")
        for model_id, display, desc in EXAMPLE_THIRD_PARTY_MODELS:
            marker = " ◀ current" if model_id == current else ""
            tp_table.add_row(model_id, display + marker, desc)
        console.print(tp_table)

        # Custom models from config
        custom = get_custom_models()
        if custom:
            cm_table = Table(title="Your Custom Models", show_lines=False)
            cm_table.add_column("Model ID", style="cyan")
            cm_table.add_column("Name", style="green")
            cm_table.add_column("Provider", style="dim")
            for entry in custom:
                mid = entry.get("id", "")
                marker = " ◀ current" if mid == current else ""
                cm_table.add_row(mid, entry.get("name", "") + marker, entry.get("provider", ""))
            console.print(cm_table)

        console.print()
        console.print("[dim]Use [cyan]tribunal model set <model-id>[/cyan] to set a default.[/dim]")
        console.print("[dim]Use [cyan]tribunal model add <model-id>[/cyan] to save a custom model.[/dim]")

    elif model_cmd == "get":
        model = get_model()
        if model:
            console.print(f"Current model: [cyan]{model}[/cyan]")
        else:
            console.print("Current model: [dim]Claude Code default[/dim]")

    elif model_cmd == "set":
        model_id: str = args.model_id
        force: bool = getattr(args, "force", False)
        if not force and model_id not in _BUILTIN_IDS:
            custom_ids = {m.get("id") for m in get_custom_models()}
            tp_ids = {m[0] for m in EXAMPLE_THIRD_PARTY_MODELS}
            if model_id not in custom_ids and model_id not in tp_ids:
                console.print(
                    f"[yellow]⚠ '{model_id}' is not in the known model list.[/yellow]\n"
                    f"  Claude Code will handle the error if it's invalid.\n"
                    f"  Use [cyan]--force[/cyan] to suppress this warning."
                )
        set_model(model_id)
        console.print(f"[green]✓[/green] Default model set to [cyan]{model_id}[/cyan]")

    elif model_cmd == "clear":
        clear_model()
        console.print("[green]✓[/green] Model reset to Claude Code default")

    elif model_cmd == "add":
        model_id = args.model_id
        name = getattr(args, "name", "")
        provider = getattr(args, "provider", "")
        add_custom_model(model_id, name=name, provider=provider)
        label = name or model_id
        console.print(f"[green]✓[/green] Custom model [cyan]{label}[/cyan] added.")
        console.print(f"  [dim]Use [cyan]tribunal model set {model_id}[/cyan] to make it the default.[/dim]")

    elif model_cmd == "remove":
        model_id = args.model_id
        if model_id in _BUILTIN_IDS:
            console.print(f"[red]✗[/red] Cannot remove built-in model '{model_id}'.")
            sys.exit(1)
        removed = remove_custom_model(model_id)
        if removed:
            console.print(f"[green]✓[/green] Custom model [cyan]{model_id}[/cyan] removed.")
        else:
            console.print(f"[yellow]'{model_id}' not found in custom models.[/yellow]")
            sys.exit(1)

    else:
        print("Usage: tribunal model {list|get|set <model>|clear|add <model>|remove <model>}", file=sys.stderr)
        sys.exit(1)


def main(argv: Sequence[str] | None = None) -> None:  # noqa: PLR0915
    """Main entry point."""
    # Check for help flags to show compact banner
    if argv and ("-h" in argv or "--help" in argv):
        print_compact_banner()
        print()
    
    parser = _build_parser()
    args, remaining = parser.parse_known_args(argv)
    command: str | None = args.command

    if command is None or command in ("launch", "run"):
        # Run first-run setup wizard if this is the first launch
        headless = getattr(args, "headless", False)
        run_wizard(headless=headless)
        if not args.no_banner:
            print_banner()
        claude_args: list[str] = remaining
        if command in ("launch", "run") and hasattr(args, "claude_args"):
            ca = args.claude_args
            if ca and ca[0] == "--":
                ca = ca[1:]
            claude_args = ca + remaining
        model = getattr(args, "model", None)
        if model and getattr(args, "save", False):
            from launcher.config import set_model
            set_model(model)
        launch_claude(claude_args, model=model)

    elif command == "model":
        _dispatch_model(args)

    elif command == "statusline":
        from launcher.statusline import render_statusline

        sys.stdout.write(render_statusline() + "\n")

    elif command == "version":
        print_version()

    elif command == "update":
        check_for_updates()

    elif command == "bundle":
        from launcher.bundle import create_bundle
        create_bundle(
            output=getattr(args, "output", "tribunal-bundle.tar.gz"),
            vault_dir=getattr(args, "vault_dir", None),
        )

    elif command == "init":
        from launcher.init_wizard import run_init_wizard

        # Show animated banner for first-time setup
        non_interactive = getattr(args, "non_interactive", False)
        if not non_interactive:
            print_banner(animated=True)
        
        project_dir_str = getattr(args, "project_dir", None)
        run_init_wizard(
            project_dir=Path(project_dir_str) if project_dir_str else None,
            non_interactive=non_interactive,
        )

    elif command == "doctor":
        from launcher.doctor import run_doctor

        sys.exit(run_doctor())

    elif command == "repair":
        _run_repair()

    elif command == "uninstall":
        _run_uninstall(
            remove_all=getattr(args, "all", False),
            yes=getattr(args, "yes", False),
        )

    elif command == "check-context":
        from launcher.context import check_context

        check_context(as_json=getattr(args, "json", False))

    elif command in ("send-clear", "endless", "handoff"):
        from launcher.context import send_clear

        send_clear(plan_path=args.plan_path, general=args.general)

    elif command == "register-plan":
        from launcher.context import register_plan

        register_plan(args.plan_path, args.status)

    elif command == "vault":
        _dispatch_vault(args)

    elif command == "worktree":
        _dispatch_worktree(args)

    elif command == "ci":
        from launcher.ci_runner import run_ci

        sys.exit(
            run_ci(
                files=args.ci_files or None,
                as_json=getattr(args, "json", False),
                coverage_threshold=getattr(args, "coverage_threshold", None),
            )
        )

    elif command == "skills":
        from launcher.skills import handle_skills_command

        if not getattr(args, "skills_command", None):
            # No subcommand: print available skills to stdout and exit 0
            handle_skills_command(args)
            sys.exit(0)
        handle_skills_command(args)

    elif command in ("activate", "deactivate", "verify", "trial"):
        _dispatch_access(command, args)

    elif command == "status":
        _dispatch_status(args)

    elif command == "install":
        from launcher.install import install_pack

        install_pack(args.pack_source)

    elif command == "verify-pack":
        from launcher.install import _verify_installed_hash, _compute_pack_hash, _PACKS_DIR
        from pathlib import Path

        pack_dir = _PACKS_DIR / args.pack_name
        if not pack_dir.exists():
            print(f"Error: Pack '{args.pack_name}' is not installed.")
            print(f"Installed packs are in: {_PACKS_DIR}")
            sys.exit(1)

        is_valid, hash_val = _verify_installed_hash(pack_dir)
        stored_hash = pack_dir / ".installed-hash"
        if stored_hash.exists():
            stored = stored_hash.read_text().strip()
            print(f"Stored hash: {stored}")
        else:
            print("Warning: No stored hash found for this pack.")

        print(f"Computed hash: {hash_val}")

        if is_valid:
            print(f"✓ Pack '{args.pack_name}' integrity verified successfully!")
        else:
            print(f"✗ Warning: Hash mismatch! The pack may have been tampered with or corrupted.")
            sys.exit(1)

    elif command == "list-packs":
        from launcher.install import list_packs

        list_packs()

    else:
        parser.print_help()
        sys.exit(1)
