"""
Security audit checks (50-53).

Checks:
  50. api_keys_in_code — API keys/tokens in tracked .py, .html, .js files
  51. env_var_discipline — Scripts that hardcode API keys instead of os.environ.get()
  52. path_traversal_guards — serve.py file-serving routes missing path traversal protection
  53. no_secrets_in_configs — JSON config files containing secret-like values
"""

import json
import os
import re

from . import register_check, register_section


# Known API key patterns (prefix, regex, description)
API_KEY_PATTERNS = [
    ("ElevenLabs", re.compile(r'sk_[a-f0-9]{40,}'), "ElevenLabs API key (sk_...)"),
    ("Google AI", re.compile(r'AIza[A-Za-z0-9_-]{35}'), "Google API key (AIza...)"),
    ("OpenAI", re.compile(r'sk-[A-Za-z0-9]{20,}'), "OpenAI API key (sk-...)"),
    ("Anthropic", re.compile(r'sk-ant-[A-Za-z0-9_-]{20,}'), "Anthropic API key (sk-ant-...)"),
    ("fal.ai", re.compile(r'fal_[a-f0-9]{32,}'), "fal.ai API key"),
    ("AWS", re.compile(r'AKIA[A-Z0-9]{16}'), "AWS access key (AKIA...)"),
    ("Generic Bearer", re.compile(r'''['"]Bearer\s+[A-Za-z0-9_-]{20,}['"]'''), "Hardcoded Bearer token"),
]


def check_api_keys_in_code(base, discovered):
    """Scan tracked .py, .html, .js files for hardcoded API keys/tokens."""
    results = {"pass": [], "fail": [], "warn": []}

    scannable_exts = {".py", ".html", ".js", ".json"}
    findings = []

    for rel in sorted(discovered):
        ext = os.path.splitext(rel)[1]
        if ext not in scannable_exts:
            continue
        # Skip archive directories
        if "archive" in rel.lower() or "_archive" in rel:
            continue

        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        for provider, pattern, desc in API_KEY_PATTERNS:
            for match in pattern.finditer(content):
                # Find approximate line number
                pos = match.start()
                line_num = content[:pos].count('\n') + 1
                findings.append((rel, line_num, provider, desc))

    if findings:
        for rel, line_num, provider, desc in findings:
            results["fail"].append(
                f"SECURITY: {rel}:{line_num} — {desc}"
            )
    else:
        results["pass"].append("No hardcoded API keys found in tracked files")

    return results


def check_env_var_discipline(base, discovered):
    """Verify scripts use os.environ.get() for API keys instead of hardcoding."""
    results = {"pass": [], "fail": [], "warn": []}

    # Patterns that suggest an API key is being assigned directly
    # (not via os.environ or environment variable)
    HARDCODED_KEY_ASSIGN = re.compile(
        r'''(?:api_key|apikey|api_token|secret_key|auth_token)\s*=\s*['"][A-Za-z0-9_-]{10,}['"]''',
        re.IGNORECASE,
    )
    ENV_VAR_USAGE = re.compile(
        r'os\.environ|os\.getenv|environ\.get|\.env|dotenv',
        re.IGNORECASE,
    )

    for rel in sorted(discovered):
        if not rel.endswith(".py"):
            continue
        if "archive" in rel.lower():
            continue

        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        basename = os.path.basename(rel)

        hardcoded = HARDCODED_KEY_ASSIGN.findall(content)
        uses_env = ENV_VAR_USAGE.search(content)

        if hardcoded and not uses_env:
            results["fail"].append(
                f"{basename}: hardcodes API key values without os.environ — "
                f"use environment variables"
            )
        elif hardcoded and uses_env:
            results["warn"].append(
                f"{basename}: uses os.environ but also has hardcoded key-like assignments"
            )
        # Only report pass for files that actually deal with API keys
        elif uses_env:
            results["pass"].append(f"{basename}: uses environment variables for credentials")

    return results


def check_path_traversal_guards(base, discovered):
    """Verify serve.py file-serving routes have path traversal protection.

    Any route that reads a file based on user-supplied path segments must:
    1. Resolve the path (.resolve())
    2. Verify it starts with the allowed base directory (.startswith())
    """
    results = {"pass": [], "fail": [], "warn": []}

    # Find all serve.py files
    serve_files = [rel for rel in discovered
                   if os.path.basename(rel) == "serve.py" and rel.endswith(".py")]

    if not serve_files:
        results["warn"].append("No serve.py found")
        return results

    for rel in serve_files:
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        basename = rel

        # Find file-serving patterns: open(path), read_bytes(), _file_response()
        # that use user-supplied path components
        file_serve_patterns = re.findall(
            r'def\s+(\w+).*?(?=\ndef\s|\Z)',
            content,
            re.DOTALL,
        )

        # Check for .resolve() and .startswith() combination
        has_resolve = ".resolve()" in content
        has_startswith = ".startswith(" in content

        # Count how many file-serving routes exist
        file_routes = len(re.findall(r'_file_response|read_bytes|open\(.*path', content))

        if file_routes > 0:
            if has_resolve and has_startswith:
                # Check if ALL file-serving routes have protection
                # by looking at each route handler
                resolve_count = content.count(".resolve()")
                startswith_count = content.count(".startswith(")

                if resolve_count >= 2 and startswith_count >= 2:
                    results["pass"].append(
                        f"{basename}: path traversal guards present "
                        f"({resolve_count} .resolve(), {startswith_count} .startswith())"
                    )
                else:
                    results["warn"].append(
                        f"{basename}: has some path traversal guards but may not cover all "
                        f"{file_routes} file-serving routes"
                    )
            else:
                missing = []
                if not has_resolve:
                    missing.append(".resolve()")
                if not has_startswith:
                    missing.append(".startswith()")
                results["fail"].append(
                    f"{basename}: serves {file_routes} file routes but missing "
                    f"path traversal guards: {', '.join(missing)}"
                )
        else:
            results["pass"].append(f"{basename}: no file-serving routes detected")

    return results


def check_no_secrets_in_configs(base, discovered):
    """Scan JSON config files for secret-like values."""
    results = {"pass": [], "fail": [], "warn": []}

    SECRET_KEY_NAMES = re.compile(
        r'(?:api_key|apikey|secret|token|password|credential|auth_key)',
        re.IGNORECASE,
    )
    SECRET_VALUE_PATTERNS = [
        re.compile(r'^sk_[a-f0-9]{20,}$'),
        re.compile(r'^AIza[A-Za-z0-9_-]{30,}$'),
        re.compile(r'^sk-[A-Za-z0-9]{20,}$'),
        re.compile(r'^fal_[a-f0-9]{20,}$'),
        re.compile(r'^ghp_[A-Za-z0-9]{36}$'),
    ]

    for rel in sorted(discovered):
        if not rel.endswith(".json"):
            continue
        # Skip pricing and schema files
        if "schema" in rel or "pricing" in rel:
            continue
        if "archive" in rel.lower():
            continue

        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                data = json.load(f)
        except (json.JSONDecodeError, IOError, OSError):
            continue

        basename = os.path.basename(rel)
        issues = []

        def scan_dict(obj, path=""):
            if isinstance(obj, dict):
                for key, val in obj.items():
                    current_path = f"{path}.{key}" if path else key
                    if isinstance(val, str) and SECRET_KEY_NAMES.search(key):
                        if len(val) > 10:
                            issues.append(f"{current_path} has suspicious value (len={len(val)})")
                    if isinstance(val, str):
                        for sp in SECRET_VALUE_PATTERNS:
                            if sp.match(val):
                                issues.append(f"{current_path} matches API key pattern")
                                break
                    scan_dict(val, current_path)
            elif isinstance(obj, list):
                for i, item in enumerate(obj):
                    scan_dict(item, f"{path}[{i}]")

        scan_dict(data)

        if issues:
            for issue in issues:
                results["fail"].append(f"{basename}: {issue}")
        else:
            results["pass"].append(f"{basename}: no secrets detected")

    return results


# ═══════════════════════════════════════════════════════════════
# REGISTRATION
# ═══════════════════════════════════════════════════════════════

register_check("api_keys", "API Keys in Code", check_api_keys_in_code, "security", quick=True)
register_check("env_discipline", "Environment Variable Discipline", check_env_var_discipline, "security")
register_check("path_traversal", "Path Traversal Guards", check_path_traversal_guards, "security")
register_check("secrets_in_configs", "Secrets in Config Files", check_no_secrets_in_configs, "security", quick=True)

register_section("security", [
    "api_keys", "env_discipline", "path_traversal", "secrets_in_configs",
])
