#!/usr/bin/env python3
"""
Pre-commit hook to validate hardcoded settings key strings against the
allow/block lists defined in settings_routes.py.

Catches new settings keys that would be rejected by the runtime namespace
gate before they reach the release server.
"""

import ast
import re
import sys
from pathlib import Path

SETTINGS_ROUTES = (
    Path(__file__).resolve().parent.parent
    / "src"
    / "local_deep_research"
    / "web"
    / "routes"
    / "settings_routes.py"
)

WRITE_FUNC_NAMES = {"set_setting"}

# Regex patterns for JS files
JS_SAVE_SETTING_RE = re.compile(r"saveSetting\s*\(\s*['\"]([^'\"]+)['\"]")
JS_SETTINGS_API_URL_RE = re.compile(
    r"['\"]\/settings\/api\/([a-z][a-z0-9_]*\.[a-z0-9_.]+)['\"]"
)
JS_INLINE_COMMENT_RE = re.compile(r"/\*.*?\*/")

# Excluded paths/directories
SKIP_PATH_SEGMENTS = {
    "tests",
    "test",
    ".pre-commit-hooks",
    "migrations",
    "node_modules",
    "dist",
    "build",
    "vendor",
    "defaults",  # JSON schema files define keys — not write call sites
}

SKIP_NAME_PREFIXES = ("test_", "conftest")
SKIP_NAME_SUFFIXES = ("_test.py", "_test.js", ".min.js")
SKIP_EXACT_NAMES = {"settings_routes.py"}


def load_prefixes():
    """Parse ALLOWED_SETTING_PREFIXES and BLOCKED_SETTING_PREFIXES
    from settings_routes.py using AST."""
    try:
        source = SETTINGS_ROUTES.read_text(encoding="utf-8")
    except FileNotFoundError:
        print(
            f"FATAL: Cannot find settings_routes.py at {SETTINGS_ROUTES}\n"
            "The hook cannot validate settings keys without it."
        )
        sys.exit(1)

    tree = ast.parse(source, filename=str(SETTINGS_ROUTES))
    allowed = None
    blocked = None

    for node in ast.walk(tree):
        if not isinstance(node, ast.Assign):
            continue
        for target in node.targets:
            if not isinstance(target, ast.Name):
                continue
            if target.id == "ALLOWED_SETTING_PREFIXES":
                allowed = _extract_frozenset_strings(node.value)
            elif target.id == "BLOCKED_SETTING_PREFIXES":
                blocked = _extract_frozenset_strings(node.value)

    if allowed is None or blocked is None:
        print(
            "FATAL: Could not parse ALLOWED/BLOCKED_SETTING_PREFIXES "
            f"from {SETTINGS_ROUTES}"
        )
        sys.exit(1)

    return allowed, blocked


def _extract_frozenset_strings(node):
    """Extract string values from frozenset({...}) or frozenset((...,))."""
    if not isinstance(node, ast.Call):
        return None
    if not (isinstance(node.func, ast.Name) and node.func.id == "frozenset"):
        return None
    if not node.args:
        return None

    collection = node.args[0]
    strings = set()
    if isinstance(collection, (ast.Set, ast.Tuple, ast.List)):
        for elt in collection.elts:
            if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
                strings.add(elt.value)
    return strings if strings else None


def is_key_allowed(key, allowed, blocked):
    """Mirrors _is_allowed_new_setting_key from settings_routes.py."""
    if not isinstance(key, str) or not key or ".." in key:
        return False
    key_lower = key.lower()
    for prefix in blocked:
        if key_lower.startswith(prefix):
            return False
    for prefix in allowed:
        if key_lower.startswith(prefix):
            return True
    return False


def should_skip(filepath):
    """Check if a file should be skipped."""
    p = Path(filepath)
    name = p.name

    if name in SKIP_EXACT_NAMES:
        return True
    if name.startswith(SKIP_NAME_PREFIXES):
        return True
    if name.endswith(SKIP_NAME_SUFFIXES):
        return True
    if SKIP_PATH_SEGMENTS.intersection(p.parts):
        return True
    return False


class SettingsKeyChecker(ast.NodeVisitor):
    """AST visitor for Python files — detects set_setting("key", ...) calls."""

    def __init__(self, filename, allowed, blocked):
        self.filename = filename
        self.allowed = allowed
        self.blocked = blocked
        self.errors = []

    def visit_Call(self, node):
        key = self._extract_write_key(node)
        if key is not None and not is_key_allowed(
            key, self.allowed, self.blocked
        ):
            self.errors.append((node.lineno, key))
        self.generic_visit(node)

    def _extract_write_key(self, node):
        """Extract a hardcoded settings key from a set_setting call.

        Returns the key string if found, or None if the call is not
        applicable or uses a dynamic key.
        """
        # Pattern 1: set_setting("key", ...)
        if isinstance(node.func, ast.Name) and node.func.id in WRITE_FUNC_NAMES:
            return self._first_string_arg(node)

        # Pattern 2: obj.set_setting("key", ...)
        if (
            isinstance(node.func, ast.Attribute)
            and node.func.attr in WRITE_FUNC_NAMES
        ):
            return self._first_string_arg(node)

        return None

    @staticmethod
    def _first_string_arg(node):
        """Return the first positional arg if it's a settings-key-like string.

        A settings key contains at least one dot (e.g. "llm.provider") or
        underscore (e.g. "local_search_embedding_model"). Bare words like
        "value" or "config" are not settings keys and are skipped to avoid
        false positives on non-settings call sites.
        """
        if node.args and isinstance(node.args[0], ast.Constant):
            val = node.args[0].value
            if (
                isinstance(val, str)
                and len(val) > 2
                and ("." in val or "_" in val)
            ):
                return val
        return None


def check_python_file(filepath, allowed, blocked):
    """Check a Python file for settings key namespace violations."""
    try:
        with open(filepath, "r", encoding="utf-8") as f:
            content = f.read()
    except Exception:
        return []

    try:
        tree = ast.parse(content, filename=filepath)
    except SyntaxError:
        return []

    checker = SettingsKeyChecker(filepath, allowed, blocked)
    checker.visit(tree)
    return checker.errors


def check_js_file(filepath, allowed, blocked):
    """Check a JS file for settings key namespace violations."""
    errors = []
    try:
        with open(filepath, "r", encoding="utf-8") as f:
            lines = f.readlines()
    except Exception:
        return []

    for i, line in enumerate(lines, 1):
        stripped = line.strip()
        if stripped.startswith("//") or stripped.startswith("*"):
            continue

        # Strip inline /* ... */ comments to avoid false positives
        line = JS_INLINE_COMMENT_RE.sub("", line)

        # saveSetting('key', ...)
        for m in JS_SAVE_SETTING_RE.finditer(line):
            key = m.group(1)
            if not is_key_allowed(key, allowed, blocked):
                errors.append((i, key))

        # '/settings/api/key' literal URLs
        for m in JS_SETTINGS_API_URL_RE.finditer(line):
            key = m.group(1)
            if not is_key_allowed(key, allowed, blocked):
                errors.append((i, key))

    return errors


def main():
    if len(sys.argv) < 2:
        print("Usage: check-settings-key-namespace.py <file1> <file2> ...")
        sys.exit(1)

    allowed, blocked = load_prefixes()
    all_errors = []

    for filepath in sys.argv[1:]:
        if should_skip(filepath):
            continue

        if filepath.endswith(".py"):
            errors = check_python_file(filepath, allowed, blocked)
        elif filepath.endswith((".js", ".mjs")):
            errors = check_js_file(filepath, allowed, blocked)
        else:
            continue

        if errors:
            all_errors.append((filepath, errors))

    if all_errors:
        allowed_sorted = sorted(allowed)
        print("\nSettings key namespace violations found:\n")
        for filepath, errors in all_errors:
            print(f"{filepath}:")
            for line_num, key in errors:
                print(
                    f"  Line {line_num}: '{key}' does not match any allowed prefix"
                )
        print(
            f"\nAllowed prefixes: {', '.join(allowed_sorted)}\n"
            "Fix: Add the prefix to ALLOWED_SETTING_PREFIXES in "
            "settings_routes.py, or rename the key."
        )
        sys.exit(1)
    else:
        sys.exit(0)


if __name__ == "__main__":
    main()
