#!/usr/bin/env python3
"""
Pre-commit hook to prevent absolute module paths in the codebase.

Module path configuration values MUST use relative imports (starting with ".")
instead of absolute paths (starting with "local_deep_research.").
The module whitelist security boundary requires relative paths for search engines,
and absolute paths elsewhere are a packaging anti-pattern.

See: src/local_deep_research/security/module_whitelist.py
"""

import ast
import json
import sys
from pathlib import Path


# The absolute base that must NOT appear as a module path prefix
ABSOLUTE_BASE = "local_deep_research"

# Known legitimate uses of absolute package references that are NOT module_path
# config values. These are used for importlib.resources, package= kwargs, and
# normalization constants where the full package name is required by Python APIs.
LEGITIMATE_ABSOLUTE_REFS = frozenset(
    {
        # importlib.resources.files() requires the full package name
        "local_deep_research.defaults.settings",
        # importlib.import_module() to avoid circular imports
        "local_deep_research.utilities",
        # package= kwarg for relative import resolution
        "local_deep_research.llm.providers",
        # package= kwarg for relative import resolution in module_whitelist.py
        "local_deep_research.web_search_engines",
    }
)

# Directory patterns where absolute paths are allowed (matched as path components)
ALLOWED_DIR_PATTERNS = {
    "tests",  # Tests may verify rejection of absolute paths
    ".pre-commit-hooks",  # Hook files themselves
}

# Specific file suffixes where absolute paths are allowed
ALLOWED_FILE_SUFFIXES = {
    # Use os.sep-independent matching via PurePosixPath
    "security/module_whitelist.py",  # Defines the security boundary itself
}

# Basename patterns for test files
ALLOWED_BASENAME_PREFIXES = {"test_"}  # test_*.py
ALLOWED_BASENAME_SUFFIXES = {"_test.py"}  # *_test.py


class AbsoluteModulePathChecker(ast.NodeVisitor):
    """AST visitor to detect absolute module paths in string literals."""

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

    def visit_Constant(self, node):
        """Check string constants for absolute module paths."""
        if (
            isinstance(node.value, str)
            and node.value.startswith(ABSOLUTE_BASE + ".")
            and node.value not in LEGITIMATE_ABSOLUTE_REFS
        ):
            self.errors.append(
                (
                    node.lineno,
                    f'Absolute module path "{node.value}" found. '
                    f"Use a relative path (starting with '.') instead.",
                )
            )
        self.generic_visit(node)


def _is_file_allowed(filename: str) -> bool:
    """Check if this file is allowed to contain absolute module paths."""
    p = Path(filename)
    normalized = p.as_posix()
    parts = p.parts

    # Check directory patterns as path components
    for pattern in ALLOWED_DIR_PATTERNS:
        if pattern in parts:
            return True

    # Check specific file suffixes
    for suffix in ALLOWED_FILE_SUFFIXES:
        if normalized.endswith(suffix):
            return True

    # Check basename patterns
    basename = p.name
    for prefix in ALLOWED_BASENAME_PREFIXES:
        if basename.startswith(prefix):
            return True
    for suffix in ALLOWED_BASENAME_SUFFIXES:
        if basename.endswith(suffix):
            return True

    return False


def check_python_file(filename: str) -> bool:
    """Check a Python file for absolute module paths in string literals."""
    if _is_file_allowed(filename):
        return True

    try:
        with open(filename, "r", encoding="utf-8") as f:
            content = f.read()
    except Exception as e:
        print(f"Error reading {filename}: {e}")
        return False

    try:
        tree = ast.parse(content, filename=filename)
        checker = AbsoluteModulePathChecker(filename)
        checker.visit(tree)

        if checker.errors:
            print(f"\n{filename}:")
            for line_num, error in checker.errors:
                print(f"  Line {line_num}: {error}")
            return False

    except SyntaxError:
        # Skip files with syntax errors (let other tools handle that)
        pass
    except Exception as e:
        print(f"Error parsing {filename}: {e}")
        return False

    return True


def check_json_file(filename: str) -> bool:
    """Check a JSON config file for absolute module paths in module_path keys."""
    if _is_file_allowed(filename):
        return True

    try:
        with open(filename, "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception as e:
        print(f"Error reading {filename}: {e}")
        return False

    errors = []

    def _check_value(obj, path=""):
        if isinstance(obj, dict):
            for key, value in obj.items():
                current_path = f"{path}.{key}" if path else key
                # Only check values under module_path keys
                if key in ("module_path", "full_search_module") and isinstance(
                    value, str
                ):
                    if (
                        value.startswith(ABSOLUTE_BASE + ".")
                        and value not in LEGITIMATE_ABSOLUTE_REFS
                    ):
                        errors.append((current_path, value))
                elif isinstance(value, (dict, list)):
                    _check_value(value, current_path)
        elif isinstance(obj, list):
            for i, item in enumerate(obj):
                _check_value(item, f"{path}[{i}]")

    _check_value(data)

    if errors:
        print(f"\n{filename}:")
        for key_path, value in errors:
            print(
                f'  Key "{key_path}": Absolute module path "{value}" found. '
                f"Use a relative path (starting with '.') instead."
            )
        return False

    return True


def main():
    """Check all provided files for absolute module paths."""
    if len(sys.argv) < 2:
        print("Usage: check-absolute-module-paths.py <file1> <file2> ...")
        sys.exit(1)

    files_to_check = sys.argv[1:]
    has_errors = False

    for filename in files_to_check:
        if filename.endswith(".py"):
            if not check_python_file(filename):
                has_errors = True
        elif filename.endswith(".json"):
            if not check_json_file(filename):
                has_errors = True

    if has_errors:
        print("\n" + "=" * 70)
        print("Module Path Security: Absolute module paths detected!")
        print("=" * 70)
        print("\nModule paths must use relative imports (starting with '.')")
        print("instead of absolute paths starting with 'local_deep_research.'.")
        print("\nExamples:")
        print(
            '  BAD:  "local_deep_research.web_search_engines'
            '.engines.search_engine_local"'
        )
        print('  GOOD: ".engines.search_engine_local"')
        print("\nSee: src/local_deep_research/security/module_whitelist.py")
        sys.exit(1)
    else:
        sys.exit(0)


if __name__ == "__main__":
    main()
