"""CLI for kv — command dispatch, argument parsing, formatted output."""

import argparse
import getpass
import os
import sys

from . import __version__
from .config import (
    find_project_root, init_project, load_config, save_config,
    list_environments, add_environment, secrets_dir, key_path,
)
from .crypto import is_key_wrapped
from .store import SecretStore, VaultLockedError


# ── ANSI formatting ────────────────────────────────────────

RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[38;2;100;200;150m"
YELLOW = "\033[38;2;255;200;100m"
CYAN = "\033[38;2;140;180;255m"
RED = "\033[38;2;255;100;100m"
VALUE = "\033[38;2;150;220;130m"
MUTED = "\033[38;2;120;120;140m"
WHITE = "\033[38;2;220;220;230m"
BLUE = "\033[38;2;100;150;255m"

# Status indicators
DOT_ON = f"{GREEN}\u25cf{RESET}"    # ● green filled
DOT_OFF = f"{MUTED}\u25cb{RESET}"   # ○ grey hollow


def _info(msg):
    print(f"  {msg}")


def _success(msg):
    print(f"  {GREEN}\u2713{RESET} {msg}")


def _error(msg):
    print(f"  {RED}\u2717 {msg}{RESET}", file=sys.stderr)


def _warn(msg):
    print(f"  {YELLOW}!{RESET} {msg}")


def _header(msg):
    print(f"\n  {BOLD}{CYAN}kv{RESET} {DIM}\u2014{RESET} {msg}")


def _box(lines, style="info"):
    """Draw a bordered box with the given lines."""
    width = max(len(line) for line in lines) + 2
    color = CYAN if style == "info" else YELLOW if style == "warn" else GREEN
    print(f"  {color}\u250c{'─' * width}\u2510{RESET}")
    for line in lines:
        padded = line.ljust(width - 2)
        print(f"  {color}\u2502{RESET} {padded} {color}\u2502{RESET}")
    print(f"  {color}\u2514{'─' * width}\u2518{RESET}")


def _cmd(name, desc):
    """Print a command with description, aligned."""
    print(f"    {YELLOW}{name:27s}{RESET}{DIM}{desc}{RESET}")


def _kv_line(key, value="", extra=""):
    k = f"{YELLOW}{key}{RESET}"
    v = f"{VALUE}{value}{RESET}" if value else ""
    e = f"{MUTED}{extra}{RESET}" if extra else ""
    parts = [f"  {k}"]
    if v:
        parts.append(v)
    if e:
        parts.append(e)
    print("  ".join(parts))


def _show_help():
    """Show branded help screen — the first thing a new user sees."""
    from .agent import is_agent_running
    has_vault = find_project_root() is not None

    print()
    print(f"  {BOLD}{CYAN}\u2591\u2592\u2593 kv {RESET}{DIM}\u2014 encrypted secrets for developers{RESET}")
    print()

    if not has_vault:
        _box([
            "No vault found in this directory.",
            "",
            f"Get started:  kv init",
        ], style="warn")
        print()
        return

    # Show quick status
    root = find_project_root()
    kp = key_path(root)
    wrapped = os.path.isfile(kp) and is_key_wrapped(kp)
    unlocked = is_agent_running()

    if unlocked:
        print(f"  {DOT_ON} vault unlocked \u2014 agent running")
    elif wrapped:
        print(f"  {DOT_OFF} vault locked {DIM}\u2014 run 'kv unlock' to open{RESET}")
    else:
        print(f"  {DOT_OFF} vault open {DIM}(no passphrase){RESET}")

    print()

    # Grouped commands
    print(f"  {WHITE}Getting started{RESET}")
    _cmd("kv init", "Set up a new vault")
    _cmd("kv unlock", "Open vault (start agent)")
    _cmd("kv lock", "Close vault (stop agent)")
    print()

    print(f"  {WHITE}Manage secrets{RESET}")
    _cmd("kv set KEY", "Store a secret (hidden input)")
    _cmd("kv ls", "List secret names")
    _cmd("kv rm KEY", "Delete a secret")
    print()

    print(f"  {WHITE}Use secrets{RESET}")
    _cmd("kv_api", "AI agent makes API call (key never leaves agent)")
    _cmd("kv run CMD", "Run command with secrets injected")
    _cmd("kv setup EDITOR", "Configure MCP (cursor, claude-code, vscode)")
    print()

    print(f"  {WHITE}Environments{RESET}")
    _cmd("kv env", "List environments")
    _cmd("kv env create NAME", "Create new environment")
    print()

    print(f"  {WHITE}Security & diagnostics{RESET}")
    _cmd("kv status", "Vault and agent status")
    _cmd("kv doctor", "Run health checks")
    _cmd("kv audit", "View audit log")
    _cmd("kv provider list", "Show API providers")
    print()

    print(f"  {DIM}v{__version__} \u2014 kvsecure.com{RESET}")
    print()


# ── Helpers ────────────────────────────────────────────────

def _require_project():
    """Find project root or exit with error."""
    root = find_project_root()
    if root is None:
        _error("not a kv project (no .secrets/ found)")
        _info(f"{DIM}run 'python -m kv init' to initialize{RESET}")
        sys.exit(1)
    return root


def _get_store():
    """Get a SecretStore for the current project.

    If the vault is passphrase-protected, prompts for passphrase (and TOTP
    if configured). Agents cannot respond to interactive prompts — this is
    the security boundary.
    """
    root = _require_project()
    kp = key_path(root)

    if os.path.isfile(kp) and is_key_wrapped(kp):
        from .agent import is_agent_running
        config = load_config(root)
        security = config.get("security", {})
        has_totp = security.get("totp", False)

        # Context-aware message
        lock_type = "passphrase + 2FA" if has_totp else "passphrase"
        if is_agent_running():
            print(f"\n  {BOLD}{CYAN}kv{RESET} {DIM}--{RESET} authenticate to access values {DIM}({lock_type}){RESET}\n")
        else:
            print(f"\n  {BOLD}{CYAN}kv{RESET} {DIM}--{RESET} vault locked {DIM}({lock_type}){RESET}\n")

        passphrase = getpass.getpass(f"  {YELLOW}Passphrase{RESET}: ")

        # Check TOTP if configured
        if has_totp:
            totp_enc = security.get("totp_secret_enc")
            if totp_enc:
                from .crypto import decrypt_totp_secret, verify_totp
                try:
                    totp_secret = decrypt_totp_secret(totp_enc, passphrase)
                except Exception:
                    _error("wrong passphrase")
                    sys.exit(1)
                code = input(f"  {YELLOW}TOTP code{RESET}:  ").strip()
                if not verify_totp(totp_secret, code):
                    _error("invalid TOTP code")
                    sys.exit(1)

        # Verify passphrase by trying to load key
        try:
            store = SecretStore(root, passphrase=passphrase)
            _ = store.master_key
        except Exception:
            _error("wrong passphrase")
            sys.exit(1)

        print(f"  {GREEN}unlocked{RESET}\n")
        return store

    return SecretStore(root)


def _get_env(args):
    """Get the environment name from args, falling back to default."""
    if args.env:
        return args.env
    root = _require_project()
    config = load_config(root)
    return config.get("default_env", "dev")


# ── Commands ───────────────────────────────────────────────

def cmd_init(args):
    """Initialize a new kv project."""
    # Check FIRST — don't prompt for passphrase if already initialized
    from .config import secrets_dir as _sdir, SECRETS_DIR
    check_path = os.path.join(os.getcwd(), SECRETS_DIR)
    if os.path.exists(check_path):
        _error("already initialized in this directory")
        _info(f"{DIM}vault exists at {check_path}{RESET}")
        sys.exit(1)

    print()
    print(f"  {BOLD}{CYAN}\u2591\u2592\u2593 kv {RESET}{DIM}\u2014 vault setup{RESET}")
    print()

    # Prompt for passphrase
    passphrase = None
    if not args.no_passphrase:
        _info(f"Create a passphrase to protect your vault.")
        _info(f"{DIM}This keeps secrets safe from AI agents and shell access.{RESET}")
        _info(f"{DIM}Press Enter to skip.{RESET}")
        print()
        p1 = getpass.getpass(f"  {YELLOW}Passphrase{RESET}: ")
        if p1:
            p2 = getpass.getpass(f"  {YELLOW}Confirm{RESET}:    ")
            if p1 != p2:
                _error("passphrases don't match")
                sys.exit(1)
            from .crypto import check_passphrase_strength
            ok, reason = check_passphrase_strength(p1)
            if not ok:
                _error(reason)
                sys.exit(1)
            passphrase = p1
        else:
            _warn(f"skipping passphrase {DIM}\u2014 master key stored in plaintext{RESET}")
            print()

    root = init_project(passphrase=passphrase)

    print()
    _success("vault created")

    # Auto-install hooks if Claude Code is detected
    claude_dir = os.path.join(root, ".claude")
    hooks_installed = False
    agent_guide = False
    if os.path.isdir(claude_dir):
        try:
            _setup_claude_hooks(root)
            hooks_installed = True
        except Exception:
            pass
        try:
            result = _setup_agent_guide(root)
            if result:
                agent_guide = True
        except Exception:
            pass
    # Also check home directory .claude
    home_claude = os.path.expanduser("~/.claude")
    if not hooks_installed and os.path.isdir(home_claude):
        try:
            _setup_claude_hooks(root)
            hooks_installed = True
        except Exception:
            pass
        try:
            result = _setup_agent_guide(root)
            if result:
                agent_guide = True
        except Exception:
            pass

    if hooks_installed:
        _success("AI agent security hooks installed")
    if agent_guide:
        _success("agent guide written")

    print()

    if passphrase:
        _box([
            "Add 2FA for extra security?",
            "Protects you if your passphrase is ever compromised.",
            "",
            "Run: kv setup-2fa",
        ], style="info")
    else:
        _box([
            "Your master key is not passphrase-protected.",
            "AI agents with shell access can read your secrets.",
            "Passphrase protection is required for agent safety.",
            "",
            "Add protection: kv upgrade-security",
        ], style="warn")

    print()
    _info(f"{WHITE}Quick start:{RESET}")
    _cmd("kv unlock", "Open your vault")
    _cmd("kv set API_KEY", "Store a secret")
    _cmd("kv run python app.py", "Run with secrets injected")
    print()
    _info(f"{WHITE}Environments:{RESET}")
    _info(f"  You have one environment: {GREEN}dev{RESET} (default)")
    _info(f"  {DIM}Create more: kv env create work{RESET}")
    print()


def cmd_set(args):
    """Set a secret."""
    store = _get_store()
    env = _get_env(args)

    # Parse KEY=VALUE or prompt for value
    if "=" in args.secret:
        key, value = args.secret.split("=", 1)
    else:
        key = args.secret
        print(f"\n  {DIM}\u250c {env} \u2510{RESET}")
        value = getpass.getpass(f"  {YELLOW}Value{RESET}: ")

    if not key:
        _error("key cannot be empty")
        sys.exit(1)

    store.set_secret(env, key, value)
    _success(f"{BOLD}{key}{RESET}{GREEN} saved {DIM}({env}) [encrypted]{RESET}")
    print()


def cmd_get(args):
    """Get a secret value."""
    store = _get_store()
    env = _get_env(args)

    value = store.get_secret(env, args.key)
    if value is None:
        _error(f"'{args.key}' not found in {env}")
        sys.exit(1)

    # Print raw value (no formatting) for piping
    print(value)


def cmd_ls(args):
    """List secrets."""
    from .agent import is_agent_running, agent_request

    env = args.env
    if not env:
        root = _require_project()
        config = load_config(root)
        env = config.get("default_env", "dev")

    # If agent is running and we don't need values, delegate (no passphrase)
    if not args.reveal and is_agent_running():
        try:
            result = agent_request("list", env=env)
            names = result.get("keys", [])
            if not names:
                _header(f"{env} {DIM}(empty){RESET}")
                print()
                _info(f"{DIM}no secrets set \u2014 add one: kv set API_KEY{RESET}")
                print()
                return

            _header(f"{env} ({len(names)} secret{'s' if len(names) != 1 else ''})")
            print()
            max_key = max(len(k) for k in names)
            for key in names:
                padded = key.ljust(max_key + 2)
                print(f"  {YELLOW}{padded}{RESET}{MUTED}\u25cf\u25cf\u25cf\u25cf\u25cf\u25cf\u25cf\u25cf{RESET}")
            print()
            return
        except Exception:
            pass  # Fall through to direct vault access

    # Agent not running or --reveal requested — need passphrase
    store = _get_store()

    secrets = store.list_secrets(env)
    if not secrets:
        _header(f"{env} {DIM}(empty){RESET}")
        print()
        _info(f"{DIM}no secrets set \u2014 add one: kv set API_KEY{RESET}")
        print()
        return

    _header(f"{env} ({len(secrets)} secret{'s' if len(secrets) != 1 else ''})")
    print()

    if args.reveal:
        for key, value in secrets:
            _kv_line(key, value)
    else:
        max_key = max(len(k) for k, _ in secrets)
        for key, _ in secrets:
            padded = key.ljust(max_key + 2)
            print(f"  {YELLOW}{padded}{RESET}{MUTED}\u25cf\u25cf\u25cf\u25cf\u25cf\u25cf\u25cf\u25cf{RESET}")
    print()


def cmd_rm(args):
    """Remove a secret."""
    store = _get_store()
    env = _get_env(args)

    if not args.force:
        confirm = input(f"  Remove '{args.key}' from {env}? [y/N] ").strip().lower()
        if confirm not in ("y", "yes"):
            _info("cancelled")
            return

    if store.remove_secret(env, args.key):
        _success(f"removed {BOLD}{args.key}{RESET}{GREEN} from {env}{RESET}")
    else:
        _error(f"'{args.key}' not found in {env}")
        sys.exit(1)


def cmd_run(args):
    """Run a command with secrets injected as env vars.

    If the agent daemon is running, delegates to it (no passphrase needed).
    Otherwise falls back to direct vault access (passphrase required).
    """
    from .agent import is_agent_running, agent_request

    # REMAINDER gives us a list of command parts
    cmd_parts = args.cmd
    if cmd_parts and cmd_parts[0] == "--":
        cmd_parts = cmd_parts[1:]
    if not cmd_parts:
        _error("no command specified")
        _info(f"{DIM}usage: kv run [-e ENV] COMMAND...{RESET}")
        sys.exit(1)

    env = args.env
    if not env:
        root = _require_project()
        config = load_config(root)
        env = config.get("default_env", "dev")

    # If daemon is running, delegate to it (no passphrase needed)
    if is_agent_running():
        if not args.quiet:
            _header(f"running via agent ({env})")
            print()
        try:
            result = agent_request("run", argv=cmd_parts, env=env)
            if "error" in result:
                _error(result["error"])
                sys.exit(result.get("exit_code", 1))
            if result.get("stdout", "").strip():
                print(result["stdout"].rstrip())
            if result.get("stderr", "").strip():
                print(result["stderr"].rstrip(), file=sys.stderr)
            if result.get("warning"):
                _warn(result["warning"])
            sys.exit(result.get("exit_code", 0))
        except Exception as e:
            _error(f"agent error: {e}")
            sys.exit(1)

    # No daemon — refuse to run (daemon provides all security layers)
    _error("kv agent is not running")
    _info(f"{DIM}the kv agent provides command validation, output redaction,")
    _info(f"network isolation, and file scanning. Start it first:{RESET}")
    print()
    _cmd("kv unlock", "Open your vault (foreground)")
    _cmd("kv unlock -b", "Open your vault (background)")
    print()
    sys.exit(1)


def cmd_export(args):
    """Export secrets as .env format."""
    from .env import export_dotenv

    store = _get_store()
    env = _get_env(args)

    secrets = store.get_all_secrets(env)
    if not secrets:
        _error(f"no secrets in {env}")
        sys.exit(1)

    output = export_dotenv(secrets, args.output)

    if args.output:
        _success(f"exported {len(secrets)} secrets to {args.output}")
    else:
        # Already printed to stdout via export_dotenv
        pass


def cmd_import(args):
    """Import secrets from a .env file."""
    from .env import import_dotenv

    store = _get_store()
    env = _get_env(args)

    if not os.path.isfile(args.file):
        _error(f"file not found: {args.file}")
        sys.exit(1)

    secrets = import_dotenv(args.file)
    if not secrets:
        _error("no secrets found in file")
        sys.exit(1)

    for key, value in secrets.items():
        store.set_secret(env, key, value)

    _success(f"imported {len(secrets)} secrets into {env}")


def cmd_envs(args):
    """List all environments."""
    from .agent import is_agent_running, agent_request

    root = _require_project()
    envs = list_environments(root)
    kp = key_path(root)
    unlocked = is_agent_running()

    _header("environments")
    print()

    if unlocked:
        try:
            for env in envs:
                result = agent_request("list", env=env)
                count = len(result.get("keys", []))
                _info(f"  {DOT_ON} {GREEN}{env}{RESET}  {DIM}{count} secret{'s' if count != 1 else ''}{RESET}")
        except Exception:
            for env in envs:
                _info(f"  {DOT_ON} {GREEN}{env}{RESET}")
    elif is_key_wrapped(kp):
        for env in envs:
            _info(f"  {DOT_OFF} {env}  {DIM}(locked){RESET}")
    else:
        store = SecretStore(root)
        for env in envs:
            count = store.env_count(env)
            _info(f"  {DOT_ON} {GREEN}{env}{RESET}  {DIM}{count} secret{'s' if count != 1 else ''}{RESET}")

    print()
    _info(f"{DIM}Commands:{RESET}")
    _cmd("kv env create <name>", "Create new environment")
    _cmd("kv set -e <env> KEY", "Store in specific environment")
    _cmd("kv run -e <env> CMD", "Run with specific environment")
    print()


def cmd_env(args):
    """Environment subcommands."""
    if args.env_action == "create":
        root = _require_project()
        if add_environment(root, args.name):
            _success(f"created environment: {args.name}")
        else:
            _error(f"environment '{args.name}' already exists")
            sys.exit(1)

    elif args.env_action == "copy":
        store = _get_store()
        count = store.copy_env(args.src, args.dst)
        _success(f"copied {count} secrets from {args.src} to {args.dst}")

    else:
        # No subcommand — show environments (same as kv envs)
        cmd_envs(args)


def cmd_status(args):
    """Show vault status."""
    from .agent import is_agent_running, agent_request

    root = _require_project()
    config = load_config(root)
    envs = config["environments"]
    security = config.get("security", {})
    kp = key_path(root)
    key_exists = os.path.isfile(kp)
    wrapped = key_exists and is_key_wrapped(kp)
    unlocked = is_agent_running()

    _header("status")
    print()

    # Vault state — the most important thing
    if unlocked:
        try:
            status = agent_request("status")
            sec_count = status.get("secrets", "?")
            pid = status.get("pid", "?")
            policy = status.get("policy", "?")
            agent_root = status.get("root", "")
            root_label = f", {agent_root}" if agent_root else ""
            print(f"  {DOT_ON} {BOLD}vault unlocked{RESET} \u2014 agent running {DIM}(PID {pid}{root_label}){RESET}")
        except Exception:
            sec_count = "?"
            policy = "?"
            print(f"  {DOT_ON} {BOLD}vault unlocked{RESET} \u2014 agent running")
    elif wrapped:
        print(f"  {DOT_OFF} {BOLD}vault locked{RESET}")
    elif key_exists:
        print(f"  {YELLOW}\u26a0{RESET} {BOLD}vault open{RESET} {DIM}(no passphrase){RESET}")
    else:
        print(f"  {RED}\u2717{RESET} {BOLD}no key found{RESET}")

    print()

    # Security
    _info(f"{WHITE}Security{RESET}")
    if wrapped:
        _info(f"  {DOT_ON} passphrase")
    elif key_exists:
        _info(f"  {DOT_OFF} passphrase {DIM}\u2014 kv upgrade-security{RESET}")
    if security.get("totp"):
        _info(f"  {DOT_ON} 2FA")
    else:
        _info(f"  {DOT_OFF} 2FA {DIM}\u2014 kv setup-2fa{RESET}")
    print()

    # Environments
    _info(f"{WHITE}Environments{RESET}")
    if unlocked:
        try:
            for env in envs:
                result = agent_request("list", env=env)
                count = len(result.get("keys", []))
                _info(f"  {DOT_ON} {GREEN}{env}{RESET}  {DIM}{count} secret{'s' if count != 1 else ''}{RESET}")
        except Exception:
            for env in envs:
                _info(f"  {DOT_ON} {GREEN}{env}{RESET}")
    elif wrapped:
        for env in envs:
            _info(f"  {DOT_OFF} {env}  {DIM}(locked){RESET}")
    else:
        store = SecretStore(root)
        for env in envs:
            count = store.env_count(env)
            _info(f"  {DOT_ON} {GREEN}{env}{RESET}  {DIM}{count} secret{'s' if count != 1 else ''}{RESET}")
    print()


def cmd_version(args):
    """Print version."""
    print(f"kv {__version__}")


def cmd_mcp(args):
    """Print MCP server config JSON for AI agents."""
    import json

    mcp_args = ["-m", "kv_mcp"]
    if args.allow_mutate:
        mcp_args.append("--allow-mutate")
    if args.allow_reveal:
        mcp_args.append("--allow-reveal")

    root = _require_project()

    config = {
        "mcpServers": {
            "kv": {
                "command": sys.executable,
                "args": mcp_args,
                "cwd": root,
            }
        }
    }

    _header("MCP server config")
    print()
    _info(f"Add this to your AI editor's MCP config file:")
    print()
    print(json.dumps(config, indent=2))
    print()
    if not args.allow_mutate and not args.allow_reveal:
        _info(f"{DIM}Default: safe tools only (status, envs, list, run){RESET}")
        _info(f"{DIM}Add --allow-mutate for set/rm, --allow-reveal for get{RESET}")
    print()


# ── Key Sharing ─────────────────────────────────────────


def cmd_export_key(args):
    """Export master key as a shareable kvkey_ string."""
    from .crypto import export_key
    store = _get_store()
    token = export_key(store.master_key)
    _header("export key")
    print()
    _info(f"Share this token with teammates via a secure channel:")
    print()
    print(f"  {token}")
    print()
    _info(f"{DIM}Teammate runs: kv import-key {token[:20]}...{RESET}")
    print()


def cmd_import_key(args):
    """Import a shared master key from a kvkey_ token."""
    from .crypto import import_key, save_key
    root = _require_project()
    kp = key_path(root)

    if os.path.isfile(kp):
        _error("master key already exists")
        _info(f"{DIM}Delete .secrets/key first if you want to replace it{RESET}")
        sys.exit(1)

    try:
        raw = import_key(args.token)
    except Exception as e:
        _error(f"invalid key token: {e}")
        sys.exit(1)

    if len(raw) != 32:
        _error(f"invalid key: expected 32 bytes, got {len(raw)}")
        sys.exit(1)

    save_key(raw, kp)
    _success("master key imported successfully")


# ── Doctor ──────────────────────────────────────────────


def cmd_doctor(args):
    """Run diagnostics on kv installation and project."""
    import importlib

    _header("doctor")
    print()

    errors = 0
    warnings = 0
    passed = 0

    def _pass(msg):
        nonlocal passed
        passed += 1
        print(f"  {GREEN}\u2713{RESET} {msg}")

    def _fail(msg):
        nonlocal errors
        errors += 1
        print(f"  {RED}\u2717{RESET} {msg}")

    def _warn(msg):
        nonlocal warnings
        warnings += 1
        print(f"  {YELLOW}!{RESET} {msg}")

    # 1. Python version
    v = sys.version_info
    if v >= (3, 10):
        _pass(f"Python {v.major}.{v.minor}.{v.micro}")
    else:
        _fail(f"Python {v.major}.{v.minor}.{v.micro} (3.10+ required)")

    # 2. Project initialized
    root = find_project_root()
    if root:
        _pass(f"Project found  {DIM}{root}{RESET}")
    else:
        _fail("No kv project found (run kv init)")
        print()
        _print_doctor_summary(passed, errors, warnings)
        sys.exit(1)

    # 3. Master key (file or KV_MASTER_KEY env var)
    kp = key_path(root)
    key = None
    key_is_wrapped = False
    if os.path.isfile(kp):
        if is_key_wrapped(kp):
            _pass(f"Master key present {DIM}(passphrase-protected){RESET}")
            key = True  # signal that key exists but we can't load it
            key_is_wrapped = True
        else:
            try:
                from .crypto import load_key
                key = load_key(kp)
                if len(key) == 32:
                    _warn(f"Master key not passphrase-protected  {DIM}(AI agents can read it){RESET}")
                else:
                    _fail(f"Master key wrong size ({len(key)} bytes, expected 32)")
                    key = None
            except Exception as e:
                _fail(f"Master key unreadable: {e}")
    elif os.environ.get("KV_MASTER_KEY", "").strip():
        try:
            import base64
            key = base64.urlsafe_b64decode(os.environ["KV_MASTER_KEY"].strip())
            if len(key) == 32:
                _pass(f"Master key from KV_MASTER_KEY env var (256-bit)")
            else:
                _fail(f"KV_MASTER_KEY wrong size ({len(key)} bytes, expected 32)")
                key = None
        except Exception as e:
            _fail(f"KV_MASTER_KEY env var invalid: {e}")
    else:
        _fail(f"Master key missing  {DIM}{kp}{RESET}")

    # 4. Config valid
    config = None
    try:
        config = load_config(root)
        env_count = len(config.get("environments", []))
        _pass(f"Config valid ({env_count} environment{'s' if env_count != 1 else ''})")
    except Exception as e:
        _fail(f"Config error: {e}")

    # 5. Environments exist
    envs = []
    if config:
        envs = config.get("environments", [])
        if envs:
            _pass(f"Environments: {', '.join(envs)}")
        else:
            _fail("No environments configured")

    # 6. Default env decryptable
    if key and envs:
        default_env = config.get("default_env", envs[0])
        if key_is_wrapped:
            _pass(f"Decrypt skipped  {DIM}{default_env} (vault locked — run kv agent){RESET}")
        else:
            try:
                store = SecretStore(root)
                data = store.load_env(default_env)
                secrets = data.get("secrets", {})
                count = len(secrets)
                _pass(f"Decrypt OK  {DIM}{default_env} ({count} secret{'s' if count != 1 else ''}){RESET}")
            except FileNotFoundError:
                _pass(f"Decrypt OK  {DIM}{default_env} (empty, no .enc file yet){RESET}")
            except Exception as e:
                _fail(f"Decrypt failed for {default_env}: {e}")

    # 7. Agent daemon
    from .agent import is_agent_running, agent_request
    if is_agent_running():
        try:
            status = agent_request("status")
            pid = status.get("pid", "?")
            sec_count = status.get("secrets", "?")
            _pass(f"Agent running  {DIM}(PID {pid}, {sec_count} secrets){RESET}")
        except Exception:
            _pass("Agent running")
    else:
        if key_is_wrapped:
            _warn(f"Agent not running  {DIM}(start with 'kv unlock'){RESET}")
        else:
            _pass(f"Agent not needed  {DIM}(vault not passphrase-protected){RESET}")

    # 8. MCP module
    try:
        importlib.import_module("kv_mcp")
        _pass("MCP server module available")
    except ImportError:
        _warn("MCP server module not found (kv_mcp)")

    # 8. Editor MCP configs — only check editors that have config files present
    found_any_editor = False
    for editor, cfg in _EDITOR_CONFIGS.items():
        config_path = os.path.join(root, cfg["file"])
        if os.path.isfile(config_path):
            found_any_editor = True
            try:
                import json as _json
                with open(config_path, "r", encoding="utf-8") as f:
                    data = _json.load(f)
                servers = data.get(cfg["key"], {})
                if isinstance(servers, dict) and "kv" in servers:
                    _pass(f"{editor} MCP configured  {DIM}{cfg['file']}{RESET}")
                else:
                    _warn(f"{editor} config exists but no kv entry  {DIM}(run 'kv setup {editor}'){RESET}")
            except Exception:
                _warn(f"{editor} config exists but invalid JSON")
    if not found_any_editor:
        _warn(f"No editor MCP configured  {DIM}(run 'kv setup cursor' or 'kv setup claude-code'){RESET}")

    print()
    _print_doctor_summary(passed, errors, warnings)
    if errors:
        sys.exit(1)


def _print_doctor_summary(passed, errors, warnings):
    total = passed + errors + warnings
    parts = []
    if passed:
        parts.append(f"{GREEN}{passed} passed{RESET}")
    if errors:
        parts.append(f"{RED}{errors} error{'s' if errors != 1 else ''}{RESET}")
    if warnings:
        parts.append(f"{YELLOW}{warnings} warning{'s' if warnings != 1 else ''}{RESET}")
    _info(", ".join(parts))
    print()


# ── MCP Editor Setup ─────────────────────────────────────

# Config file paths per editor (relative to project root unless absolute)
_EDITOR_CONFIGS = {
    "cursor": {
        "file": ".cursor/mcp.json",
        "scope": "project",
        "key": "mcpServers",
    },
    "claude-code": {
        "file": ".mcp.json",
        "scope": "project",
        "key": "mcpServers",
    },
    "vscode": {
        "file": ".vscode/mcp.json",
        "scope": "project",
        "key": "servers",
    },
}


def cmd_setup(args):
    """Auto-configure MCP for an AI editor."""
    import json

    editor = args.editor
    if editor not in _EDITOR_CONFIGS:
        _error(f"unknown editor: {editor}")
        _info(f"{DIM}supported: {', '.join(sorted(_EDITOR_CONFIGS))}{RESET}")
        sys.exit(1)

    root = _require_project()
    cfg = _EDITOR_CONFIGS[editor]

    # Build MCP server entry
    mcp_args = ["-m", "kv_mcp"]
    if args.allow_mutate:
        mcp_args.append("--allow-mutate")
    if args.allow_reveal:
        mcp_args.append("--allow-reveal")

    server_entry = {
        "command": sys.executable,
        "args": mcp_args,
        "cwd": root,
    }

    # Determine config file path
    config_path = os.path.join(root, cfg["file"])
    config_dir = os.path.dirname(config_path)

    # Load existing config or start fresh
    existing = {}
    if os.path.isfile(config_path):
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                existing = json.load(f)
                if not isinstance(existing, dict):
                    _error(f"{config_path} is not a JSON object")
                    _info(f"{DIM}expected {{...}}, got {type(existing).__name__}{RESET}")
                    sys.exit(1)
        except json.JSONDecodeError:
            _error(f"malformed JSON in {config_path}")
            _info(f"{DIM}fix the file manually or delete it, then re-run kv setup{RESET}")
            sys.exit(1)
        except OSError as exc:
            _error(f"cannot read {config_path}: {exc}")
            sys.exit(1)

    # Merge — add kv server without overwriting other servers
    servers_key = cfg["key"]
    if servers_key not in existing:
        existing[servers_key] = {}
    elif not isinstance(existing[servers_key], dict):
        _error(f"'{servers_key}' in {config_path} is not an object")
        _info(f"{DIM}expected a JSON object ({{}}), got {type(existing[servers_key]).__name__}{RESET}")
        sys.exit(1)
    existing[servers_key]["kv"] = server_entry

    # Write config
    os.makedirs(config_dir, exist_ok=True)
    tmp_path = config_path + ".tmp"
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(existing, f, indent=2)
        f.write("\n")
    os.replace(tmp_path, config_path)

    _header(f"MCP configured for {editor}")
    print()
    _success(f"wrote {config_path}")

    # For Claude Code: also configure security hooks + agent guide
    hooks_path = None
    agent_guide_path = None
    if editor == "claude-code":
        hooks_path = _setup_claude_hooks(root)
        _success(f"wrote {hooks_path}")
        agent_guide_path = _setup_agent_guide(root)
        if agent_guide_path:
            _success(f"wrote {agent_guide_path}")

    print()

    # Show what was configured
    profiles = ["safe"]
    if args.allow_mutate:
        profiles.append("mutate")
    if args.allow_reveal:
        profiles.append("reveal")
    _info(f"{YELLOW}Profiles{RESET}   {', '.join(profiles)}")
    _info(f"{YELLOW}Tools{RESET}      {4 + (2 if args.allow_mutate else 0) + (1 if args.allow_reveal else 0)}")
    if hooks_path:
        _info(f"{YELLOW}Hooks{RESET}      PreToolUse + PostToolUse (secret extraction blocked)")

    if not args.allow_mutate and not args.allow_reveal:
        print()
        _info(f"{DIM}Default: safe tools only (status, envs, list, run){RESET}")
        _info(f"{DIM}Add --allow-mutate for set/rm, --allow-reveal for get{RESET}")
    print()
    _info(f"{DIM}Restart {editor} to activate{RESET}")
    print()


def _setup_agent_guide(root):
    """Copy AGENT.md to the project root so AI agents read it automatically.

    Claude Code reads CLAUDE.md from the project root. We inject a reference
    to the kv agent guide so the agent knows how to use secrets.
    """
    # Find the AGENT.md that ships with kv-secrets package
    import importlib.resources
    try:
        # Python 3.11+
        agent_md = importlib.resources.files("kv").parent / "AGENT.md"
        if not agent_md.is_file():
            # Try relative to this file
            agent_md = os.path.join(os.path.dirname(os.path.dirname(__file__)), "AGENT.md")
            if not os.path.isfile(agent_md):
                return None
            content = open(agent_md, "r", encoding="utf-8").read()
        else:
            content = agent_md.read_text(encoding="utf-8")
    except Exception:
        # Fallback: find relative to package
        pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        agent_md = os.path.join(pkg_dir, "AGENT.md")
        if not os.path.isfile(agent_md):
            return None
        with open(agent_md, "r", encoding="utf-8") as f:
            content = f.read()

    # Write to project root
    dest = os.path.join(root, "KV_AGENT.md")
    with open(dest, "w", encoding="utf-8") as f:
        f.write(content)

    # Also append a reference to CLAUDE.md if it exists and doesn't already reference kv
    claude_md = os.path.join(root, "CLAUDE.md")
    kv_marker = "<!-- kv-secrets agent guide -->"
    if os.path.isfile(claude_md):
        with open(claude_md, "r", encoding="utf-8") as f:
            existing = f.read()
        if kv_marker not in existing:
            with open(claude_md, "a", encoding="utf-8") as f:
                f.write(f"\n\n{kv_marker}\n")
                f.write("## API Keys (kv-secrets)\n\n")
                f.write("This project uses kv-secrets for API key management. ")
                f.write("See `KV_AGENT.md` for how to use secrets safely.\n")
                f.write("Use `kv_api` for API calls, `kv_run` for running apps.\n")

    return dest


def _is_kv_hook_entry(entry):
    """Check if a hook entry belongs to kv-secrets."""
    for h in entry.get("hooks", []):
        cmd = h.get("command", "")
        if "kv.hook" in cmd or "kv_hook" in cmd:
            return True
    return False


def _setup_claude_hooks(root):
    """Configure PreToolUse/PostToolUse hooks for Claude Code.

    Merges with existing hooks — only adds/replaces the kv entries.
    Returns the path to the settings file.
    """
    import json as _json

    settings_path = os.path.join(root, ".claude", "settings.local.json")
    settings_dir = os.path.dirname(settings_path)

    existing = {}
    if os.path.isfile(settings_path):
        try:
            with open(settings_path, "r", encoding="utf-8") as f:
                existing = _json.load(f)
                if not isinstance(existing, dict):
                    existing = {}
        except (ValueError, OSError):
            existing = {}

    hooks = existing.setdefault("hooks", {})

    kv_pre = {
        "matcher": "Bash|Read|Write|Edit|Glob|Grep",
        "hooks": [
            {"type": "command", "command": "python3 -m kv.hook pre"}
        ]
    }
    kv_post = {
        "matcher": "Bash",
        "hooks": [
            {"type": "command", "command": "python3 -m kv.hook post"}
        ]
    }

    # Merge: remove existing kv entries, then append fresh ones
    pre_list = hooks.get("PreToolUse", [])
    pre_list = [h for h in pre_list if not _is_kv_hook_entry(h)]
    pre_list.append(kv_pre)
    hooks["PreToolUse"] = pre_list

    post_list = hooks.get("PostToolUse", [])
    post_list = [h for h in post_list if not _is_kv_hook_entry(h)]
    post_list.append(kv_post)
    hooks["PostToolUse"] = post_list

    # Write atomically
    os.makedirs(settings_dir, exist_ok=True)
    tmp_path = settings_path + ".tmp"
    with open(tmp_path, "w", encoding="utf-8") as f:
        _json.dump(existing, f, indent=2)
        f.write("\n")
    os.replace(tmp_path, settings_path)

    return settings_path


# ── Security commands ─────────────────────────────────────


def cmd_setup_2fa(args):
    """Set up TOTP two-factor authentication."""
    from .crypto import (
        generate_totp_secret, totp_uri, verify_totp,
        encrypt_totp_secret, is_key_wrapped,
    )

    root = _require_project()
    config = load_config(root)
    security = config.get("security", {})

    if not security.get("passphrase"):
        _error("2FA requires a passphrase-protected vault")
        _info(f"{DIM}run 'kv upgrade-security' first{RESET}")
        sys.exit(1)

    if security.get("totp"):
        _error("2FA is already configured")
        _info(f"{DIM}to reconfigure, remove 'totp' from .secrets/config.json{RESET}")
        sys.exit(1)

    _header("2FA setup")
    print()

    # Need passphrase to encrypt the TOTP secret
    passphrase = getpass.getpass(f"  {YELLOW}Passphrase{RESET}: ")

    # Verify passphrase works by trying to load the key
    kp = key_path(root)
    try:
        from .crypto import load_wrapped_key
        load_wrapped_key(kp, passphrase)
    except Exception:
        _error("wrong passphrase")
        sys.exit(1)

    print(f"  {GREEN}verified{RESET}\n")

    # Generate TOTP secret
    secret = generate_totp_secret()
    uri = totp_uri(secret)

    _info("Scan this QR code with your authenticator app:")
    _info(f"{DIM}(Google Authenticator, Authy, or any TOTP app){RESET}")
    print()

    # Try to render QR code in terminal
    try:
        import qrcode
        qr = qrcode.QRCode(border=1)
        qr.add_data(uri)
        qr.make(fit=True)
        qr.print_ascii(invert=True)
        print()
    except ImportError:
        _info(f"{DIM}install 'qrcode' for QR display: pip install kv-secrets[totp]{RESET}")
        print()

    _info(f"{DIM}Or enter manually:{RESET}")
    _kv_line("Secret", secret)
    print()

    _info(f"Verify setup {DIM}-- enter a code from your app:{RESET}")
    code = input(f"  {YELLOW}Code{RESET}:  ").strip()
    if not verify_totp(secret, code):
        _error("invalid code — 2FA not enabled")
        _info(f"{DIM}check your authenticator app time is synced{RESET}")
        sys.exit(1)

    # Encrypt and store TOTP secret in config
    encrypted = encrypt_totp_secret(secret, passphrase)
    security["totp"] = True
    security["totp_secret_enc"] = encrypted
    config["security"] = security
    save_config(root, config)

    print()
    _success(f"2FA enabled {DIM}-- passphrase + authenticator code required{RESET}")
    print()


def cmd_upgrade_security(args):
    """Upgrade an existing plaintext vault to passphrase-protected."""
    from .crypto import load_key, save_wrapped_key, is_key_wrapped

    root = _require_project()
    kp = key_path(root)

    if not os.path.isfile(kp):
        _error("no master key found")
        sys.exit(1)

    if is_key_wrapped(kp):
        _error("vault is already passphrase-protected")
        sys.exit(1)

    # Read the plaintext key
    master_key = load_key(kp)

    _header("upgrade security")
    print()
    _info("Encrypt your master key with a passphrase.")
    _info(f"{DIM}After this, every operation requires your passphrase.{RESET}")
    _info(f"{DIM}AI agents with shell access will not be able to read secrets.{RESET}")
    print()

    p1 = getpass.getpass(f"  {YELLOW}New passphrase{RESET}: ")
    from .crypto import check_passphrase_strength
    ok, reason = check_passphrase_strength(p1)
    if not ok:
        _error(reason)
        sys.exit(1)
    p2 = getpass.getpass(f"  {YELLOW}Confirm{RESET}:        ")
    if p1 != p2:
        _error("passphrases don't match")
        sys.exit(1)

    # Wrap and overwrite
    save_wrapped_key(master_key, p1, kp)

    # Update config
    config = load_config(root)
    if "security" not in config:
        config["security"] = {}
    config["security"]["passphrase"] = True
    save_config(root, config)

    print()
    _success(f"master key encrypted {DIM}-- passphrase required for all operations{RESET}")
    _info(f"{DIM}next: add 2FA with 'kv setup-2fa'{RESET}")
    print()


def cmd_allow(args):
    """Add a command to the kv_run allowlist."""
    root = _require_project()

    if not args.pattern:
        # List current allowlist and policy
        from .config import get_run_policy
        policy, allowed = get_run_policy(root)
        _header(f"kv_run policy: {policy}")
        print()
        if policy == "allowlist":
            if allowed:
                for cmd in allowed:
                    _info(f"{GREEN}\u2713{RESET} {cmd}")
            else:
                _info(f"{DIM}no commands allowed — kv_run will block everything{RESET}")
                _info(f"{DIM}add commands: kv allow \"python3 app.py\"{RESET}")
        else:
            _info(f"{DIM}using blocklist mode (shells, network tools blocked){RESET}")
            _info(f"{DIM}switch to allowlist: kv allow \"python3 app.py\"{RESET}")
        print()
        return

    from .config import add_allowed_command
    if add_allowed_command(root, args.pattern):
        _success(f"allowed: {BOLD}{args.pattern}{RESET}")
        _info(f"{DIM}kv_run policy set to: allowlist{RESET}")
    else:
        _info(f"already allowed: {args.pattern}")


def cmd_deny(args):
    """Remove a command from the kv_run allowlist."""
    root = _require_project()

    from .config import remove_allowed_command
    if remove_allowed_command(root, args.pattern):
        _success(f"removed: {BOLD}{args.pattern}{RESET}")
    else:
        _error(f"not in allowlist: {args.pattern}")


def cmd_network_allow(args):
    """Allow network access for a command during kv_run."""
    root = _require_project()

    if not args.pattern:
        # List current network-allowed commands
        config = load_config(root)
        security = config.get("security", {})
        net_cmds = security.get("network_commands", [])
        _header("network-allowed commands")
        print()
        if net_cmds:
            for cmd in net_cmds:
                _info(f"  {GREEN}\u2713{RESET} {cmd}")
        else:
            _info(f"{DIM}no commands have network access{RESET}")
            _info(f"{DIM}add one: kv network-allow \"twine upload\"{RESET}")
        print()
        _info(f"{DIM}all other kv_run commands are network-isolated{RESET}")
        print()
        return

    from .config import add_network_command
    if add_network_command(root, args.pattern):
        _success(f"network access allowed: {BOLD}{args.pattern}{RESET}")
        _info(f"{DIM}this command can now make outbound connections during kv_run{RESET}")
    else:
        _info(f"already allowed: {args.pattern}")


def cmd_network_deny(args):
    """Revoke network access for a command during kv_run."""
    root = _require_project()

    config = load_config(root)
    security = config.get("security", {})
    net_cmds = security.get("network_commands", [])
    if args.pattern not in net_cmds:
        _error(f"not in network-allowed list: {args.pattern}")
        return
    net_cmds.remove(args.pattern)
    security["network_commands"] = net_cmds
    save_config(root, config)
    _success(f"network access revoked: {BOLD}{args.pattern}{RESET}")


def cmd_audit(args):
    """View the audit log — what happened with your secrets."""
    from .audit import read_events, verify_chain, summary_stats

    if args.verify:
        ok, detail = verify_chain()
        if ok:
            _success(f"audit log integrity: {detail}")
        else:
            _error(f"audit log TAMPERED: {detail}")
            sys.exit(1)
        return

    if args.summary:
        stats = summary_stats()
        if stats["total"] == 0:
            _header("audit log (empty)")
            print()
            _info(f"{DIM}no events recorded yet — start the agent with 'kv unlock'{RESET}")
            print()
            return

        _header(f"audit summary ({stats['total']} events)")
        print()
        _info(f"  {YELLOW}Period{RESET}     {stats['first'][:19]} → {stats['last'][:19]}")

        if stats.get("by_event"):
            print()
            _info(f"  {WHITE}Events{RESET}")
            for evt, count in sorted(stats["by_event"].items(), key=lambda x: -x[1]):
                color = RED if evt in ("run_blocked", "leak_detected", "leak_file") else GREEN
                _info(f"    {color}{evt:20s}{RESET} {count}")

        if stats.get("by_provider"):
            print()
            _info(f"  {WHITE}Providers{RESET}")
            for prov, count in sorted(stats["by_provider"].items(), key=lambda x: -x[1]):
                _info(f"    {GREEN}{prov:20s}{RESET} {count} call{'s' if count != 1 else ''}")

        if stats.get("avg_api_ms"):
            _info(f"    {DIM}avg latency: {stats['avg_api_ms']}ms{RESET}")

        if stats.get("security_events", 0) > 0:
            print()
            _info(f"  {RED}Security events: {stats['security_events']}{RESET}")
            _info(f"  {DIM}run 'kv audit --security' for details{RESET}")

        print()
        return

    # Default: show recent events
    events = read_events(
        limit=args.limit,
        event_filter=args.event,
        provider_filter=args.provider,
        security_only=args.security,
    )

    if not events:
        label = "audit log"
        if args.security:
            label = "security events"
        elif args.provider:
            label = f"events for {args.provider}"
        _header(f"{label} (empty)")
        print()
        return

    title = "audit log"
    if args.security:
        title = "security events"
    elif args.provider:
        title = f"{args.provider} events"
    elif args.event:
        title = f"{args.event} events"

    _header(f"{title} (showing {len(events)})")
    print()

    for e in events:
        ts = e.get("ts", "?")[:19].replace("T", " ")
        evt = e.get("event", "?")

        # Color by event type
        if evt in ("run_blocked", "leak_detected", "leak_file"):
            evt_color = RED
        elif evt in ("agent_start", "agent_stop"):
            evt_color = CYAN
        elif evt == "api_call":
            evt_color = GREEN
        else:
            evt_color = YELLOW

        # Build detail string
        parts = []
        if e.get("provider"):
            parts.append(e["provider"])
        if e.get("path"):
            parts.append(e["path"])
        if e.get("program"):
            parts.append(e["program"])
        if e.get("status"):
            parts.append(f"→ {e['status']}")
        if e.get("exit_code") is not None and evt == "run_exec":
            parts.append(f"exit={e['exit_code']}")
        if e.get("duration_ms"):
            parts.append(f"{e['duration_ms']}ms")
        if e.get("reason"):
            parts.append(e["reason"])
        if e.get("count") and evt == "leak_detected":
            parts.append(f"{e['count']} secret{'s' if e['count'] != 1 else ''}")
        if e.get("env") and e["env"] != "dev":
            parts.append(f"[{e['env']}]")

        detail = "  ".join(parts)
        _info(f"  {DIM}{ts}{RESET}  {evt_color}{evt:16s}{RESET}  {detail}")

    print()


def cmd_provider(args):
    """Manage API providers for kv_api."""
    root = _require_project()

    if args.provider_action == "add":
        # Validate URL at registration time
        from .providers import _check_url_safety
        safe, reason = _check_url_safety(args.url + "/test")
        if not safe:
            _error(f"provider URL blocked: {reason}")
            _info(f"{DIM}custom providers must use HTTPS and public endpoints{RESET}")
            sys.exit(1)

        config = load_config(root)
        providers = config.setdefault("providers", {})
        name = args.name
        providers[name] = {
            "base_url": args.url,
            "secret_name": args.secret,
            "auth_type": args.auth,
        }
        if args.auth == "query":
            providers[name]["auth_query_param"] = args.query_param or "key"
        if args.auth == "header":
            providers[name]["auth_header_name"] = args.header_name or "Authorization"
        save_config(root, config)
        _success(f"provider added: {BOLD}{name}{RESET}")
        _info(f"{DIM}base_url: {args.url}{RESET}")
        _info(f"{DIM}secret:   {args.secret}{RESET}")
        _info(f"{DIM}auth:     {args.auth}{RESET}")
        print()
        _info(f"{DIM}Store the key: kv set {args.secret}{RESET}")
        _info(f"{DIM}Use it:        kv_api(provider=\"{name}\", path=\"/...\"){RESET}")
        print()

    elif args.provider_action == "list" or args.provider_action is None:
        from .providers import PROVIDERS, load_custom_providers
        load_custom_providers(root)
        config = load_config(root)
        custom_names = set(config.get("providers", {}).keys())

        _header(f"providers ({len(PROVIDERS)})")
        print()
        for name in sorted(PROVIDERS):
            p = PROVIDERS[name]
            tag = f" {DIM}(custom){RESET}" if name in custom_names else ""
            _info(f"  {GREEN}{name}{RESET}{tag}  {DIM}{p['auth_type']}  {p['secret_name']}{RESET}")
        print()

    elif args.provider_action == "rm":
        config = load_config(root)
        providers = config.get("providers", {})
        if args.name not in providers:
            _error(f"'{args.name}' is not a custom provider")
            _info(f"{DIM}built-in providers cannot be removed{RESET}")
            sys.exit(1)
        del providers[args.name]
        save_config(root, config)
        _success(f"removed provider: {args.name}")


def _agent_unlock():
    """Prompt for passphrase + TOTP, return authenticated SecretStore."""
    root = _require_project()
    kp = key_path(root)

    passphrase = None
    if os.path.isfile(kp) and is_key_wrapped(kp):
        config = load_config(root)
        security = config.get("security", {})
        has_totp = security.get("totp", False)

        lock_type = "passphrase + 2FA" if has_totp else "passphrase"
        _header(f"agent unlock ({lock_type})")
        print()

        passphrase = getpass.getpass(f"  {YELLOW}Passphrase{RESET}: ")

        if has_totp:
            totp_enc = security.get("totp_secret_enc")
            if totp_enc:
                from .crypto import decrypt_totp_secret, verify_totp
                try:
                    totp_secret = decrypt_totp_secret(totp_enc, passphrase)
                except Exception:
                    _error("wrong passphrase")
                    sys.exit(1)
                code = input(f"  {YELLOW}TOTP code{RESET}:  ").strip()
                if not verify_totp(totp_secret, code):
                    _error("invalid TOTP code")
                    sys.exit(1)

    store = SecretStore(root, passphrase=passphrase)
    try:
        _ = store.master_key
    except Exception:
        _error("wrong passphrase")
        sys.exit(1)

    return store, root


def cmd_agent(args):
    """Start the kv agent daemon — unlock once, use everywhere."""
    from .agent import is_agent_running, run_agent, agent_request, _sock_path

    mode = args.mode

    # No mode specified — interactive menu
    if mode is None:
        if is_agent_running():
            try:
                status = agent_request("status")
                pid = status.get("pid", "?")
                sec_count = status.get("secrets", "?")
                agent_root = status.get("root", "?")
                _header(f"vault unlocked \u2014 agent running {DIM}(PID {pid}, {sec_count} secrets, {agent_root}){RESET}")
            except Exception:
                _header("vault unlocked \u2014 agent running")
            print()
            _info(f"  {YELLOW}1{RESET}  Stop the agent")
            _info(f"  {YELLOW}2{RESET}  Show agent details")
            print()
            choice = input(f"  {DIM}Choose [1-2] or Enter to cancel:{RESET} ").strip()
            if choice == "1":
                mode = "stop"
            elif choice == "2":
                mode = "status"
            else:
                return
            # Fall through to mode handling below

        else:
            _header("unlock vault")
            print()
            _info("Unlock once. All sessions use it \u2014 no more passphrase prompts.")
            print()
            _info(f"  {YELLOW}1{RESET}  Run here, Ctrl+C to stop  {DIM}(recommended){RESET}")
            _info(f"  {YELLOW}2{RESET}  Run in background")
            _info(f"  {YELLOW}3{RESET}  Auto-start on login")
            print()
            choice = input(f"  {DIM}Choose [1-3]:{RESET} ").strip()
            if choice == "1":
                mode = "foreground"
            elif choice == "2":
                mode = "background"
            elif choice == "3":
                mode = "install"
            else:
                return
            # Fall through to mode handling below

    # Status
    if mode == "status":
        if is_agent_running():
            try:
                status = agent_request("status")
                _header("agent status")
                print()
                _info(f"{YELLOW}Project{RESET}        {status.get('root', '?')}")
                _info(f"{YELLOW}PID{RESET}            {status.get('pid', '?')}")
                _info(f"{YELLOW}Secrets{RESET}        {status.get('secrets', '?')}")
                _info(f"{YELLOW}Environments{RESET}   {status.get('environments', '?')}")
                _info(f"{YELLOW}Policy{RESET}         {status.get('policy', '?')}")
                _info(f"{YELLOW}Providers{RESET}      {', '.join(status.get('providers', []))}")
                print()
            except Exception as e:
                _error(f"agent running but status unavailable: {e}")
        else:
            _info("agent is not running")
            _info(f"{DIM}start with: kv unlock{RESET}")
        return

    # Stop
    if mode == "stop":
        if not is_agent_running():
            _info("agent is not running")
            return
        try:
            status = agent_request("status")
            pid = status.get("pid")
            if pid:
                import signal
                os.kill(pid, signal.SIGTERM)
                _success(f"agent stopped (PID {pid})")
            else:
                _error("could not determine agent PID")
        except Exception as e:
            _error(f"could not stop agent: {e}")
        return

    # Check if already running (for start modes)
    if mode in ("foreground", "background", "install") and is_agent_running():
        _error("agent is already running")
        _info(f"{DIM}use 'kv lock' first, or 'kv agent status' for details{RESET}")
        sys.exit(1)

    # Foreground
    if mode == "foreground":
        store, root = _agent_unlock()
        from .config import get_default_env
        run_agent(store, get_default_env(root))

    # Background
    elif mode == "background":
        store, root = _agent_unlock()
        from .config import get_default_env

        pid = os.fork()
        if pid > 0:
            # Parent — wait briefly, then check if daemon started
            import time
            time.sleep(1)
            if is_agent_running():
                _success(f"agent started in background (PID {pid})")
                _info(f"{DIM}stop with: kv agent stop{RESET}")
            else:
                _error("agent failed to start")
            return
        else:
            # Child — become daemon
            os.setsid()
            devnull = os.open(os.devnull, os.O_RDWR)
            os.dup2(devnull, 0)  # stdin
            os.dup2(devnull, 1)  # stdout
            os.dup2(devnull, 2)  # stderr — logs go to file only
            run_agent(store, get_default_env(root))
            sys.exit(0)

    # Install (systemd user service)
    elif mode == "install":
        _agent_install()


def _agent_install():
    """Install kv agent as a systemd user service for auto-start on login."""
    import shutil

    # Check for systemd
    systemctl = shutil.which("systemctl")
    if not systemctl:
        _error("systemd not found — auto-start requires systemd")
        _info(f"{DIM}use 'kv agent background' instead{RESET}")
        sys.exit(1)

    kv_path = shutil.which("kv")
    if not kv_path:
        _error("'kv' command not found in PATH")
        sys.exit(1)

    service_dir = os.path.expanduser("~/.config/systemd/user")
    service_path = os.path.join(service_dir, "kv-agent.service")

    _header("install auto-start")
    print()
    _info("This creates a systemd user service that starts the kv agent")
    _info("when you log in.")
    print()
    _info(f"{YELLOW}Important:{RESET} The service needs your passphrase at first login.")
    _info(f"After install, run {YELLOW}kv agent foreground{RESET} once to unlock.")
    _info(f"On subsequent logins, use {YELLOW}kv agent background{RESET} or start manually.")
    print()
    _info(f"{DIM}Note: Fully automated passphrase-free startup requires")
    _info(f"KV_PASSPHRASE env var — which is less secure.{RESET}")
    print()

    confirm = input(f"  Install to {service_path}? [y/N] ").strip().lower()
    if confirm not in ("y", "yes"):
        _info("cancelled")
        return

    # Write service file
    service_content = f"""\
[Unit]
Description=kv-secrets agent daemon
Documentation=https://github.com/SatishoBananamoto/kv-secrets

[Service]
Type=simple
ExecStart={kv_path} agent foreground
Restart=on-failure
RestartSec=5
Environment=KV_PASSPHRASE=%E{{KV_PASSPHRASE}}

[Install]
WantedBy=default.target
"""

    os.makedirs(service_dir, exist_ok=True)
    with open(service_path, "w", encoding="utf-8") as f:
        f.write(service_content)

    _success(f"wrote {service_path}")
    print()
    _info("Next steps:")
    _info(f"  {YELLOW}1.{RESET} Reload systemd:    {DIM}systemctl --user daemon-reload{RESET}")
    _info(f"  {YELLOW}2.{RESET} Enable auto-start: {DIM}systemctl --user enable kv-agent{RESET}")
    _info(f"  {YELLOW}3.{RESET} Start now:         {DIM}systemctl --user start kv-agent{RESET}")
    _info(f"  {YELLOW}4.{RESET} Check status:      {DIM}systemctl --user status kv-agent{RESET}")
    print()
    _info(f"{DIM}To set passphrase for auto-start (less secure):")
    _info(f"systemctl --user set-environment KV_PASSPHRASE='your-passphrase'")
    _info(f"Then: systemctl --user restart kv-agent{RESET}")
    print()


def cmd_unlock(args):
    """Open the vault — primary user-facing command."""
    from .agent import is_agent_running, run_agent, agent_request

    if is_agent_running():
        try:
            status = agent_request("status")
            sec_count = status.get("secrets", "?")
        except Exception:
            sec_count = "?"
        _header(f"vault is already unlocked {DIM}({sec_count} secrets){RESET}")
        print()
        return

    if args.install:
        _agent_install()
        return

    store, root = _agent_unlock()
    from .config import get_default_env

    if args.foreground:
        # Foreground — blocks terminal, shows logs (for debugging)
        run_agent(store, get_default_env(root))
    else:
        # Default: background — unlock and return to prompt
        pid = os.fork()
        if pid > 0:
            # Poll for up to 10 seconds — child may need time to
            # kill old agent (3s) + create socket
            import time
            started = False
            for _ in range(20):
                time.sleep(0.5)
                if is_agent_running():
                    started = True
                    break
            if started:
                print()
                _success(f"vault unlocked (PID {pid})")
                _info(f"{DIM}close with: kv lock{RESET}")
                print()
            else:
                _error("failed to unlock")
            return
        else:
            os.setsid()
            devnull = os.open(os.devnull, os.O_RDWR)
            os.dup2(devnull, 0)  # stdin
            os.dup2(devnull, 1)  # stdout
            os.dup2(devnull, 2)  # stderr — logs go to file only
            run_agent(store, get_default_env(root))
            sys.exit(0)


def cmd_lock(args):
    """Close the vault."""
    from .agent import is_agent_running, agent_request

    if not is_agent_running():
        _header(f"vault is already locked")
        print()
        return

    try:
        status = agent_request("status")
        pid = status.get("pid")
        if pid:
            import signal
            os.kill(pid, signal.SIGTERM)
            print()
            _success("vault locked")
            _info(f"{DIM}secrets cleared from memory{RESET}")
            print()
        else:
            _error("could not determine agent PID")
    except Exception as e:
        _error(f"could not lock: {e}")


# ── Argument parser ────────────────────────────────────────

def build_parser():
    parser = argparse.ArgumentParser(
        prog="kv",
        description="Encrypted secrets management for developers.",
    )
    parser.add_argument(
        "--version", action="version", version=f"kv {__version__}"
    )

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

    # init
    p = sub.add_parser("init", help="Initialize a new kv project")
    p.add_argument(
        "--no-passphrase", action="store_true",
        help="Skip passphrase setup (not recommended)",
    )

    # set
    p = sub.add_parser("set", help="Set a secret")
    p.add_argument("secret", help="KEY=VALUE or just KEY (prompts for value)")
    p.add_argument("-e", "--env", help="Environment (default: dev)")

    # get
    p = sub.add_parser("get", help="Get a secret value")
    p.add_argument("key", help="Secret key name")
    p.add_argument("-e", "--env", help="Environment (default: dev)")

    # ls
    p = sub.add_parser("ls", help="List secrets")
    p.add_argument("-e", "--env", help="Environment (default: dev)")
    p.add_argument("--reveal", action="store_true", help="Show decrypted values")

    # rm
    p = sub.add_parser("rm", help="Remove a secret")
    p.add_argument("key", help="Secret key to remove")
    p.add_argument("-e", "--env", help="Environment (default: dev)")
    p.add_argument("-f", "--force", action="store_true", help="Skip confirmation")

    # run — flags MUST come before positional (REMAINDER swallows everything)
    p = sub.add_parser("run", help="Run command with secrets injected")
    p.add_argument("-e", "--env", help="Environment (default: dev)")
    p.add_argument("-q", "--quiet", action="store_true", help="Suppress kv output")
    p.add_argument("cmd", nargs=argparse.REMAINDER, help="Command to run")

    # export
    p = sub.add_parser("export", help="Export secrets as .env format")
    p.add_argument("-e", "--env", help="Environment (default: dev)")
    p.add_argument("-o", "--output", help="Output file (default: stdout)")

    # import
    p = sub.add_parser("import", help="Import secrets from .env file")
    p.add_argument("file", help="Path to .env file")
    p.add_argument("-e", "--env", help="Environment (default: dev)")

    # envs
    sub.add_parser("envs", help="List all environments")

    # env (create/copy)
    p = sub.add_parser("env", help="Manage environments")
    env_sub = p.add_subparsers(dest="env_action")

    p_create = env_sub.add_parser("create", help="Create a new environment")
    p_create.add_argument("name", help="Environment name")

    p_copy = env_sub.add_parser("copy", help="Copy secrets between environments")
    p_copy.add_argument("src", help="Source environment")
    p_copy.add_argument("dst", help="Destination environment")

    # status
    sub.add_parser("status", help="Show project status")

    # doctor
    sub.add_parser("doctor", help="Check project health")

    # audit
    p = sub.add_parser("audit", help="View the audit log")
    p.add_argument("-n", "--limit", type=int, default=20, help="Number of events (default: 20)")
    p.add_argument("--event", help="Filter by event type (e.g., api_call, run_blocked)")
    p.add_argument("--provider", help="Filter by provider (e.g., openai)")
    p.add_argument("--security", action="store_true", help="Show security events only")
    p.add_argument("--summary", action="store_true", help="Show summary statistics")
    p.add_argument("--verify", action="store_true", help="Verify log integrity (tamper detection)")

    # export-key
    sub.add_parser("export-key", help="Export master key as shareable string")

    # import-key
    p = sub.add_parser("import-key", help="Import a shared master key")
    p.add_argument("token", help="Key token (kvkey_...)")

    # version
    sub.add_parser("version", help="Print version")

    # mcp
    p = sub.add_parser("mcp", help="Show MCP server config for AI agents")
    p.add_argument("--allow-mutate", action="store_true", help="Include mutate flag")
    p.add_argument("--allow-reveal", action="store_true", help="Include reveal flag")

    # setup
    p = sub.add_parser("setup", help="Auto-configure MCP for your AI editor")
    p.add_argument("editor", help="Editor name: cursor, claude-code, vscode")
    p.add_argument("--allow-mutate", action="store_true", help="Enable mutate tools")
    p.add_argument("--allow-reveal", action="store_true", help="Enable reveal tools")

    # security
    sub.add_parser("setup-2fa", help="Enable TOTP two-factor authentication")
    sub.add_parser("upgrade-security", help="Add passphrase protection to existing vault")
    p = sub.add_parser("agent", help=argparse.SUPPRESS)  # hidden — use unlock/lock instead
    p.add_argument("mode", nargs="?", choices=["foreground", "background", "install", "stop", "status"])

    # unlock / lock (primary interface — aliases for agent)
    p = sub.add_parser("unlock", help="Open your vault (passphrase + 2FA)")
    p.add_argument("--foreground", "-f", action="store_true", help="Run in foreground (show logs, block terminal)")
    p.add_argument("--install", action="store_true", help="Auto-start on login (systemd)")
    sub.add_parser("lock", help="Close your vault")

    # allowlist
    p = sub.add_parser("allow", help="Allow a command for kv_run (switches to allowlist policy)")
    p.add_argument("pattern", nargs="?", help="Command to allow (e.g., 'python3 app.py')")

    p = sub.add_parser("deny", help="Remove a command from the kv_run allowlist")
    p.add_argument("pattern", help="Command to remove")

    # network-allow / network-deny
    p = sub.add_parser("network-allow", help="Allow network access for a kv_run command")
    p.add_argument("pattern", nargs="?", help="Command to allow network (e.g., 'twine upload')")

    p = sub.add_parser("network-deny", help="Revoke network access for a kv_run command")
    p.add_argument("pattern", help="Command to revoke network from")

    # provider
    p = sub.add_parser("provider", help="Manage API providers for kv_api")
    prov_sub = p.add_subparsers(dest="provider_action")

    p_pa = prov_sub.add_parser("add", help="Add a custom provider")
    p_pa.add_argument("name", help="Provider name (e.g., stripe, twilio)")
    p_pa.add_argument("--url", required=True, help="Base URL (e.g., https://api.stripe.com)")
    p_pa.add_argument("--secret", required=True, help="Secret name in vault (e.g., STRIPE_SECRET_KEY)")
    p_pa.add_argument("--auth", default="bearer",
                       choices=["bearer", "x-api-key", "query", "basic", "header"],
                       help="Auth type (default: bearer)")
    p_pa.add_argument("--query-param", help="Query param name (for --auth query)")
    p_pa.add_argument("--header-name", help="Header name (for --auth header)")

    prov_sub.add_parser("list", help="List all providers")

    p_pr = prov_sub.add_parser("rm", help="Remove a custom provider")
    p_pr.add_argument("name", help="Provider name to remove")

    # ── Remote commands ───────────────────────────────────

    # signup / login / logout
    p = sub.add_parser("signup", help="Create a kv cloud account")
    p.add_argument("--api-url", help="Server URL (default: from session or env)")

    p = sub.add_parser("login", help="Log in to kv cloud")
    p.add_argument("--api-url", help="Server URL (default: from session or env)")

    sub.add_parser("logout", help="Log out (clear session)")

    # push / pull
    p = sub.add_parser("push", help="Push secrets to server")
    p.add_argument("-e", "--env", help="Environment (default: dev)")
    p.add_argument("--all", action="store_true", help="Push all environments")

    p = sub.add_parser("pull", help="Pull secrets from server")
    p.add_argument("-e", "--env", help="Environment (default: dev)")
    p.add_argument("--all", action="store_true", help="Pull all environments")

    # remote status
    sub.add_parser("remote", help="Show remote sync status")

    # team
    p = sub.add_parser("team", help="Team management")
    team_sub = p.add_subparsers(dest="team_action")

    p_tc = team_sub.add_parser("create", help="Create a team")
    p_tc.add_argument("name", help="Team name")

    p_ti = team_sub.add_parser("invite", help="Invite a member")
    p_ti.add_argument("email", help="Email address")

    team_sub.add_parser("members", help="List team members")

    p_tr = team_sub.add_parser("revoke", help="Revoke a member")
    p_tr.add_argument("email", help="Email address")

    team_sub.add_parser("key", help="Show shareable master key")

    p_tj = team_sub.add_parser("join", help="Join with a team key")
    p_tj.add_argument("key", help="Team key (kvkey_...)")

    # token
    p = sub.add_parser("token", help="API token management")
    tok_sub = p.add_subparsers(dest="token_action")

    p_tokc = tok_sub.add_parser("create", help="Create an API token")
    p_tokc.add_argument("name", help="Token name")
    p_tokc.add_argument("--scope", default="pull", help="Scope: pull, push, or admin")
    p_tokc.add_argument("--env", dest="token_env", help="Restrict to environment")
    p_tokc.add_argument("--expires", type=int, help="Expire in N days")

    tok_sub.add_parser("list", help="List API tokens")

    p_tokr = tok_sub.add_parser("revoke", help="Revoke an API token")
    p_tokr.add_argument("name", help="Token name")

    return parser


# ── Import remote commands ────────────────────────────────
from .cli_remote import (
    cmd_signup, cmd_login, cmd_logout,
    cmd_push, cmd_pull, cmd_remote_status,
    cmd_team, cmd_token,
)

COMMANDS = {
    "init": cmd_init,
    "set": cmd_set,
    "get": cmd_get,
    "ls": cmd_ls,
    "rm": cmd_rm,
    "run": cmd_run,
    "export": cmd_export,
    "import": cmd_import,
    "envs": cmd_envs,
    "env": cmd_env,
    "status": cmd_status,
    "doctor": cmd_doctor,
    "audit": cmd_audit,
    "export-key": cmd_export_key,
    "import-key": cmd_import_key,
    "version": cmd_version,
    "mcp": cmd_mcp,
    "setup": cmd_setup,
    "setup-2fa": cmd_setup_2fa,
    "upgrade-security": cmd_upgrade_security,
    "agent": cmd_agent,
    "unlock": cmd_unlock,
    "lock": cmd_lock,
    "allow": cmd_allow,
    "deny": cmd_deny,
    "network-allow": cmd_network_allow,
    "network-deny": cmd_network_deny,
    "provider": cmd_provider,
    # Remote
    "signup": cmd_signup,
    "login": cmd_login,
    "logout": cmd_logout,
    "push": cmd_push,
    "pull": cmd_pull,
    "remote": cmd_remote_status,
    "team": cmd_team,
    "token": cmd_token,
}


def main():
    parser = build_parser()
    args = parser.parse_args()

    if not args.command:
        _show_help()
        sys.exit(0)

    handler = COMMANDS.get(args.command)
    if handler:
        try:
            handler(args)
        except KeyboardInterrupt:
            print()
            sys.exit(130)
        except Exception as e:
            _error(str(e))
            sys.exit(1)
    else:
        _show_help()
        sys.exit(1)
