"""Wrapper around the Claude Code CLI.

Responsibilities:
1. Locate the ``claude`` binary on the system.
2. Inject Tribunal-specific environment variables (plugin path, config dir).
3. Exec into ``claude`` with all user-supplied arguments forwarded.
"""

from __future__ import annotations

import os
import re
import shutil
import subprocess
import sys
from collections.abc import Mapping
from pathlib import Path

from launcher.access import verify_access
from launcher.config import get_config, get_tribunal_home

_SAFE_ENV_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


SKILLFIELD_PLUGIN_DIR: Path = Path.home() / ".claude" / "tribunal"


def resolve_provider_env(model: str, cfg: Mapping[str, object]) -> dict[str, str]:
    """Return ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY overrides if model belongs to a configured provider."""
    providers = cfg.get("providers", {})
    if not isinstance(providers, dict):
        return {}
    for provider_cfg in providers.values():
        if not isinstance(provider_cfg, dict):
            continue
        models = provider_cfg.get("models", [])
        if not isinstance(models, list) or model not in models:
            continue
        overrides: dict[str, str] = {}
        base_url = provider_cfg.get("base_url")
        if base_url:
            overrides["ANTHROPIC_BASE_URL"] = str(base_url)
        api_key_env = provider_cfg.get("api_key_env")
        if api_key_env:
            config_env = cfg.get("env", {})
            if not isinstance(config_env, dict):
                config_env = {}
            key = config_env.get(api_key_env) or os.environ.get(str(api_key_env), "")
            if key:
                overrides["ANTHROPIC_API_KEY"] = str(key)
        return overrides
    return {}


def validate_provider_config(model: str, cfg: Mapping[str, object]) -> tuple[bool, str]:
    """Check that a provider model has its API key configured.

    Returns ``(valid, error_message)``.  ``valid=True`` means OK to proceed.
    Standard Anthropic models (not in any provider's model list) always return True.
    """
    providers = cfg.get("providers", {})
    if not isinstance(providers, dict):
        return True, ""

    for provider_name, provider_cfg in providers.items():
        if not isinstance(provider_cfg, dict):
            continue
        models = provider_cfg.get("models", [])
        if not isinstance(models, list) or model not in models:
            continue

        api_key_env = provider_cfg.get("api_key_env")
        if not api_key_env:
            return True, ""

        config_env = cfg.get("env", {})
        if not isinstance(config_env, dict):
            config_env = {}
        key = config_env.get(str(api_key_env)) or os.environ.get(str(api_key_env), "")

        if not key:
            return False, (
                f"Model '{model}' requires provider '{provider_name}', "
                f"but the API key variable '{api_key_env}' is not set.\n"
                f'  Fix: add it to ~/.tribunal/config.json under "env": {{"{api_key_env}": "your-key"}}\n'
                f"  Or remove the 'model' entry from config.json to use Claude's default model."
            )
        return True, ""

    return True, ""


def find_claude_binary() -> str:
    """Return the absolute path to the ``claude`` CLI binary.

    Search order:
    1. ``CLAUDE_BIN`` environment variable (explicit override).
    2. ``claude`` found on ``$PATH`` via :func:`shutil.which`.
    3. Common installation locations (npm global, Homebrew, etc.).

    Raises
    ------
    FileNotFoundError
        If the binary cannot be located anywhere.
    """
    env_bin = os.environ.get("CLAUDE_BIN")
    if env_bin and os.path.isfile(env_bin):
        return env_bin

    which_result = shutil.which("claude")
    if which_result:
        return which_result

    candidates: list[Path] = [
        Path.home() / ".npm-global" / "bin" / "claude",
        Path("/usr/local/bin/claude"),
        Path("/opt/homebrew/bin/claude"),
    ]
    for candidate in candidates:
        if candidate.is_file():
            return str(candidate)

    raise FileNotFoundError(
        "Could not locate the 'claude' CLI binary. "
        "Install it with: npm install -g @anthropic-ai/claude-code\n"
        "Or set the CLAUDE_BIN environment variable to its path."
    )


def _check_plugin_version() -> None:
    """Warn if the installed plugin requires a newer launcher version.

    Reads ``minLauncherVersion`` from ``sf/plugin.json`` (relative to the
    plugin directory) and compares it against the running launcher version.
    Prints a warning to stderr if the launcher is older; never raises.
    """
    import json as _json

    from launcher import __version__

    plugin_json = SKILLFIELD_PLUGIN_DIR / "plugin.json"
    if not plugin_json.is_file():
        return
    try:
        data = _json.loads(plugin_json.read_text(encoding="utf-8"))
        min_ver_str: str = data.get("minLauncherVersion", "")
        if not min_ver_str:
            return
        from packaging.version import Version  # type: ignore[import-untyped]

        if Version(__version__) < Version(min_ver_str):
            print(
                f"  ⚠️  Plugin requires Tribunal launcher >={min_ver_str} "
                f"(running {__version__}). Run: pip install --upgrade tribunal",
                file=sys.stderr,
            )
    except Exception:
        pass


def build_env() -> dict[str, str]:
    """Build the environment dict for the Claude subprocess.

    Merges the current environment with Tribunal-specific variables.
    Sets ``SF_SESSION_ID`` so hooks and context monitoring are
    scoped to this session.
    """
    env = os.environ.copy()

    env["CLAUDE_PLUGIN_DIR"] = str(SKILLFIELD_PLUGIN_DIR)

    env["SKILLFIELD_HOME"] = str(get_tribunal_home())

    if "SF_SESSION_ID" not in env:
        env["SF_SESSION_ID"] = str(os.getpid())

    cfg = get_config()

    # Pass multi-vault config to the plugin
    try:
        import json as _json

        from launcher.config import get_vaults_config
        env["SF_VAULT_CONFIG"] = _json.dumps(get_vaults_config())
    except Exception:
        pass

    extra_env: dict[str, str] = cfg.get("env", {})
    for key, value in extra_env.items():
        if _SAFE_ENV_KEY.match(key):
            env[key] = str(value)

    return env


def _start_hook_runner_daemon() -> None:
    """Start the persistent hook runner daemon if not already running."""
    try:
        hook_runner = SKILLFIELD_PLUGIN_DIR / "hooks" / "hook_runner.py"
        if not hook_runner.is_file():
            return
        import subprocess as _sp
        from pathlib import Path as _Path
        pid_file = _Path.home() / ".tribunal" / "hook-runner.pid"
        if pid_file.exists():
            try:
                pid = int(pid_file.read_text().strip())
                os.kill(pid, 0)
                return  # Already running
            except (ProcessLookupError, ValueError):
                pid_file.unlink(missing_ok=True)
        _sp.Popen(
            [sys.executable, str(hook_runner), "--start"],
            env=os.environ.copy(),
            stdout=_sp.DEVNULL,
            stderr=_sp.DEVNULL,
        )
    except Exception:
        pass  # Daemon is optional — never block launch


def launch_claude(extra_args: list[str] | None = None, model: str | None = None) -> None:
    """Launch the Claude Code CLI with Tribunal configuration.

    Parameters
    ----------
    extra_args:
        Additional CLI arguments to forward to ``claude``.  These are appended
        after any default arguments configured in the user's config.json.
    model:
        Optional model override. If not provided, uses the model from config.json.
        If neither is set, Claude Code uses its default model.

    Raises
    ------
    SystemExit
        If the user's email is not authorized.
    """
    os.umask(0o077)

    _check_plugin_version()

    authorized, message = verify_access()
    if not authorized:
        print(f"\n  {message}\n", file=sys.stderr)
        sys.exit(1)

    try:
        claude_bin = find_claude_binary()
    except FileNotFoundError as exc:
        print(str(exc), file=sys.stderr)
        sys.exit(1)
    env = build_env()

    cmd: list[str] = [claude_bin]

    cfg = get_config()
    # Respect user-supplied --model in extra_args (takes precedence over config)
    user_passed_model = extra_args is not None and "--model" in extra_args
    effective_model = model if model else (None if user_passed_model else cfg.get("model"))
    if effective_model:
        ok, errmsg = validate_provider_config(effective_model, cfg)
        if not ok:
            print(f"\n  {errmsg}\n", file=sys.stderr)
            sys.exit(1)
        cmd.extend(["--model", effective_model])
        provider_env = resolve_provider_env(effective_model, cfg)
        env.update(provider_env)

    default_args: list[str] = cfg.get("default_args", [])
    cmd.extend(default_args)

    if extra_args:
        cmd.extend(extra_args)

    SKILLFIELD_PLUGIN_DIR.mkdir(parents=True, exist_ok=True)

    # Start hook runner daemon (non-blocking; fails silently)
    _start_hook_runner_daemon()

    if sys.platform != "win32":
        os.execvpe(claude_bin, cmd, env)
    else:
        result = subprocess.run(cmd, env=env)
        sys.exit(result.returncode)
