"""kv hooks — PreToolUse (block/warn) and PostToolUse (leak detection).

Hooks:
  PreToolUse:
    Layer 1 (static): Blocks direct access to key files and secret-revealing
                      CLI commands. Always active — no daemon required.
    Layer 2 (dynamic): Blocks access to files created during kv_run.
                       Requires kv agent daemon.

  PostToolUse:
    Scans Bash output for leaked secret values. Alerts user.

Usage in .claude/settings.local.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Read|Write|Edit|Glob|Grep",
        "hooks": [
          {"type": "command", "command": "python3 -m kv.hook pre"}
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": "python3 -m kv.hook post"}
        ]
      }
    ]
  }
}
"""

import json
import sys
import os


def _extract_all_paths(command_str):
    """Extract all tokens that look like file paths from a Bash command."""
    import shlex
    paths = []
    try:
        parts = shlex.split(command_str)
    except ValueError:
        parts = command_str.split()

    for part in parts:
        if part.startswith("-"):
            continue
        if part in ("|", ">", ">>", "<", "&&", "||", ";", "2>&1"):
            continue
        if "/" in part or part.startswith(".") or part.startswith("~"):
            expanded = os.path.expanduser(part)
            paths.append(expanded)
    return paths


def _allow():
    """Allow the tool call."""
    print(json.dumps({}))
    sys.exit(0)


def _deny(reason):
    """Deny the tool call."""
    output = {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": reason,
        }
    }
    print(json.dumps(output))
    sys.exit(0)


# ── Layer 1: Static protection (no daemon needed) ────────

def _find_secrets_dir():
    """Walk up from cwd to find .secrets/ directory."""
    path = os.path.abspath(os.getcwd())
    while True:
        sdir = os.path.join(path, ".secrets")
        if os.path.isdir(sdir):
            return os.path.realpath(sdir)
        parent = os.path.dirname(path)
        if parent == path:
            return None
        path = parent


def _is_protected_path(file_path, secrets_dir):
    """Check if a file path points to a protected kv-secrets file.

    Protected: .secrets/key, .secrets/*.enc
    Not protected: .secrets/config.json, .secrets/.gitignore (metadata only)
    """
    if not secrets_dir:
        return False
    try:
        real = os.path.realpath(os.path.expanduser(file_path))
    except (ValueError, OSError):
        return False
    # Key file
    if real == os.path.join(secrets_dir, "key"):
        return True
    # Encrypted environment files
    if real.startswith(secrets_dir + os.sep) and real.endswith(".enc"):
        return True
    return False


# kv CLI subcommands that reveal secret values
_REVEALING_SUBCOMMANDS = frozenset({"get", "export", "export-key"})

# kv CLI subcommands that manage the vault — human-only actions
_ADMIN_SUBCOMMANDS = frozenset({"init", "setup-2fa", "upgrade-security", "import-key"})


def _is_kv_reveal_command(command):
    """Check if a bash command runs kv with a secret-revealing subcommand.

    Catches:
      kv get SECRET
      kv ls --reveal
      kv export / kv export-key
      python3 -m kv get SECRET
    """
    import shlex
    try:
        tokens = shlex.split(command)
    except ValueError:
        tokens = command.split()

    i = 0
    while i < len(tokens):
        tok = tokens[i]
        basename = os.path.basename(tok)

        # Direct invocation: kv get, kv export, kv init, etc.
        if basename == "kv" and i + 1 < len(tokens):
            sub = tokens[i + 1]
            if sub in _REVEALING_SUBCOMMANDS:
                return True, f"kv {sub}"
            if sub in _ADMIN_SUBCOMMANDS:
                return True, f"kv {sub}"
            if sub == "ls" and "--reveal" in tokens[i + 2:]:
                return True, "kv ls --reveal"

        # Module invocation: python3 -m kv get
        if tok == "-m" and i > 0 and i + 2 < len(tokens):
            prev = os.path.basename(tokens[i - 1])
            if prev.startswith("python") and tokens[i + 1] == "kv":
                sub = tokens[i + 2]
                if sub in _REVEALING_SUBCOMMANDS:
                    return True, f"python -m kv {sub}"
                if sub in _ADMIN_SUBCOMMANDS:
                    return True, f"python -m kv {sub}"
                if sub == "ls" and "--reveal" in tokens[i + 3:]:
                    return True, "python -m kv ls --reveal"

        i += 1

    return False, ""


# Patterns in inline code that indicate key/secret access
_KEY_ACCESS_PATTERNS = (
    ".secrets/key",
    "kv.crypto",
    "kv.store",
    "kv-agent/kv.sock",
)


def _has_key_access_code(command):
    """Check if command contains code patterns that access keys or secrets."""
    for pattern in _KEY_ACCESS_PATTERNS:
        if pattern in command:
            return True
    # .secrets/*.enc references
    if ".secrets/" in command and ".enc" in command:
        return True
    return False


# ── Layer 2: Dynamic protection (daemon-dependent) ───────

# Patterns that indicate encoded data handling
_ENCODING_PATTERNS = (
    "base64", "b64decode", "b64encode",
    "bytes.fromhex", "fromhex", ".hex()",
    "binascii", "codecs.decode", "rot13",
    "chr(", "ord(", "bytearray",
    "decode(", "encode(",
)

_FILE_ACCESS_PATTERNS = (
    "open(", ".read(", "os.path", "pathlib",
    "glob.", "glob(", "shutil.", "with open",
    "readlines", "readline", "read_bytes",
    "scandir", "listdir", "walk(",
)


# ── Hook entry points ────────────────────────────────────

def _pre_hook(data):
    """PreToolUse: block secret extraction and tracked file access."""
    tool_name = data.get("tool_name", "")
    tool_input = data.get("tool_input", {})

    secrets_dir = _find_secrets_dir()

    # ── Layer 1: Static protection (always active, no daemon needed) ──

    if tool_name == "Bash":
        command = tool_input.get("command", "")
        if command:
            # Skip static analysis for commands that don't access files
            # Git commits contain message text that triggers false positives
            _is_safe_command = command.lstrip().startswith(("git commit", "git tag", "git log", "echo "))

            if not _is_safe_command:
                # Block kv CLI commands that reveal secrets or manage vault
                blocked, pattern = _is_kv_reveal_command(command)
                if blocked:
                    if any(a in pattern for a in ("init", "setup-2fa", "upgrade", "import-key")):
                        _deny(
                            f"[kv-secrets] blocked: '{pattern}' is a vault management command. "
                            f"Only the user can run this directly."
                        )
                    else:
                        _deny(
                            f"[kv-secrets] blocked: '{pattern}' would reveal secret values. "
                            f"Use kv_api or kv_run MCP tools instead."
                        )

                # Block file paths pointing to protected files
                if secrets_dir:
                    paths = _extract_all_paths(command)
                    for path in paths:
                        if _is_protected_path(path, secrets_dir):
                            _deny(
                                f"[kv-secrets] blocked: '{path}' is a protected kv-secrets file. "
                                f"Use kv_run or kv_api MCP tools to use secrets safely."
                            )

                # Block programmatic key access (inline code, scripts referencing internals)
                if _has_key_access_code(command):
                    _deny(
                        "[kv-secrets] blocked: command references kv-secrets internal files. "
                        "Use kv_run or kv_api MCP tools to use secrets safely."
                    )

    elif tool_name == "Read":
        file_path = tool_input.get("file_path", "")
        if file_path and secrets_dir and _is_protected_path(file_path, secrets_dir):
            _deny(
                f"[kv-secrets] blocked: '{file_path}' is a protected kv-secrets file. "
                f"Use kv_run or kv_api MCP tools to use secrets safely."
            )

    elif tool_name in ("Write", "Edit"):
        file_path = tool_input.get("file_path", "")
        if file_path and secrets_dir and _is_protected_path(file_path, secrets_dir):
            _deny(
                f"[kv-secrets] blocked: direct modification of '{file_path}' is not allowed. "
                f"Use kv_set MCP tool to manage secrets."
            )

    elif tool_name == "Glob":
        pattern = tool_input.get("pattern", "")
        glob_path = tool_input.get("path", "")
        if ".secrets" in pattern or ".secrets" in glob_path:
            _deny(
                "[kv-secrets] blocked: glob pattern accesses .secrets/ directory."
            )

    elif tool_name == "Grep":
        grep_path = tool_input.get("path", "")
        if grep_path and secrets_dir:
            try:
                real = os.path.realpath(os.path.expanduser(grep_path))
                if real.startswith(secrets_dir):
                    _deny(
                        "[kv-secrets] blocked: searching inside .secrets/ directory."
                    )
            except (ValueError, OSError):
                pass

    # ── Layer 2: Dynamic protection (requires running daemon) ──

    from kv.agent import is_agent_running, agent_request
    if not is_agent_running():
        _allow()

    try:
        tracked = agent_request("tracked_files")
        tracked_paths = tracked.get("files", [])
    except Exception:
        _allow()

    if not tracked_paths:
        _allow()

    # Build keyword fragments from tracked filenames
    keywords = set()
    for tracked_path in tracked_paths:
        filename = os.path.basename(tracked_path)
        stem = os.path.splitext(filename)[0]

        keywords.add(filename)
        if len(stem) >= 6:
            keywords.add(stem)

        for length in range(8, len(filename) + 1):
            for start in range(len(filename) - length + 1):
                keywords.add(filename[start:start + length])

    if tool_name == "Bash":
        command = tool_input.get("command", "")

        # Token-based path matching against tracked files
        paths_to_check = _extract_all_paths(command)
        for path in paths_to_check:
            try:
                result = agent_request("check_file", path=path)
                if result.get("tracked"):
                    _deny(
                        f"[kv-secrets] blocked: '{path}' was created during a "
                        f"secret-injected subprocess (kv_run). Reading it could "
                        f"expose encoded secret values."
                    )
            except Exception:
                continue

        # Keyword matching with file-access patterns
        has_file_access = any(p in command for p in _FILE_ACCESS_PATTERNS)
        if has_file_access:
            for keyword in keywords:
                if keyword in command:
                    _deny(
                        f"[kv-secrets] blocked: command contains file-access code "
                        f"referencing '{keyword}' which matches a tracked file."
                    )

        # Suspicious encoding + file access pattern (warn, don't block)
        has_encoding = any(p in command for p in _ENCODING_PATTERNS)
        if has_file_access and has_encoding:
            sys.stderr.write(
                "\n\033[38;2;255;100;100m"
                "[kv-secrets WARNING]\033[0m Suspicious command detected:\n"
                "  File-access code with encoding/decoding patterns.\n"
                "  This may be an attempt to read tracked files via encoded paths.\n"
                f"  Command: {command[:120]}...\n"
                "  \033[33mReview before allowing.\033[0m\n\n"
            )

    elif tool_name == "Read":
        file_path = tool_input.get("file_path", "")
        if file_path:
            try:
                result = agent_request("check_file", path=file_path)
                if result.get("tracked"):
                    _deny(
                        f"[kv-secrets] blocked: '{file_path}' was created during a "
                        f"secret-injected subprocess (kv_run)."
                    )
            except Exception:
                pass

    _allow()


def _post_hook(data):
    """PostToolUse: scan Bash output for leaked secret values.

    If a secret value appears in the tool output, alert the user via stderr.
    Can't undo the leak (agent already saw it) but can:
    1. Alert the user immediately
    2. Tell them which key to rotate
    3. Log the incident
    """
    tool_name = data.get("tool_name", "")
    if tool_name != "Bash":
        _allow()

    # Get the tool output/result
    tool_output = data.get("tool_result", "")
    if not tool_output:
        # Try alternate field names
        tool_output = data.get("tool_response", "")
    if not tool_output:
        _allow()

    # Convert to string if needed
    if isinstance(tool_output, dict):
        tool_output = json.dumps(tool_output)
    tool_output = str(tool_output)

    from kv.agent import is_agent_running, agent_request
    if not is_agent_running():
        _allow()

    # Get secret values from daemon for scanning
    try:
        result = agent_request("secret_names")
        secret_names = result.get("names", [])
    except Exception:
        _allow()

    if not secret_names:
        _allow()

    # Ask daemon to check output for leaked values
    # The daemon has the actual secret values — we don't want them in the hook
    # Response is boolean + count only (no names — avoids oracle side-channel)
    try:
        result = agent_request("check_leak", text=tool_output)
        has_leak = result.get("leaked", False)
        leak_count = result.get("count", 0)
    except Exception:
        _allow()

    if has_leak:
        # ALERT — secret values found in Bash output
        sys.stderr.write(
            "\n\033[1;38;2;255;50;50m"
            "=== KV-SECRETS LEAK DETECTED ===\033[0m\n\n"
            f"  {leak_count} secret value{'s' if leak_count != 1 else ''} "
            f"found in command output.\n\n"
            "  The AI agent has seen these values. They are compromised.\n"
            "  \033[1;33mRotate affected keys immediately:\033[0m\n\n"
            "    1. Run \033[1mkv ls\033[0m to see your secret names\n"
            "    2. Rotate the compromised keys at their provider\n"
            "    3. Update kv: \033[1mkv set KEY_NAME\033[0m\n\n"
            "  Details logged to ~/.kv/agent.log\n\n"
        )

    _allow()


def main():
    """Dispatch to pre or post hook based on argument."""
    mode = sys.argv[1] if len(sys.argv) > 1 else "pre"

    try:
        raw = sys.stdin.read()
        data = json.loads(raw)
    except (json.JSONDecodeError, ValueError, EOFError):
        _allow()

    if mode == "post":
        _post_hook(data)
    else:
        _pre_hook(data)


if __name__ == "__main__":
    main()
