"""Deterministic source-code secret scanner.

Detects hardcoded secrets in source files using two strategies loaded from
``secret_rules.json``:

1. High-confidence: known API key / token formats (regex on raw content).
2. Medium-confidence: variable assignments with secret-sounding names bound to
   non-placeholder string literals.

Rules are fully data-driven — add new secret formats or variable-name patterns
by editing ``secret_rules.json``, no Python changes required.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from maeris_mcp.scanners.rule_engine import build_finding, load_rules

# ---------------------------------------------------------------------------
# Rule loading
# ---------------------------------------------------------------------------

_RULES_PATH = Path(__file__).parent / "secret_rules.json"

# Extensions worth scanning for the assignment pattern
_SCANNABLE_EXTS = {
    ".py", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs",
    ".java", ".go", ".rb", ".php", ".cs", ".swift", ".kt",
    ".sh", ".bash", ".zsh", ".yaml", ".yml", ".toml", ".ini",
    ".cfg", ".conf", ".properties",
}

# Skip test / fixture directories
_TEST_PATH_RE = re.compile(
    r"[\\/](?:__tests__|tests?|spec|e2e|fixtures?|mocks?|stubs?)[\\/]"
    r"|\.(?:test|spec)\.[a-z]+$",
    re.IGNORECASE,
)

# Values that reference env vars — not hardcoded
_ENV_REF_RE = re.compile(
    r"process\.env\.|os\.environ|os\.getenv|\$\{|getenv\(|"
    r"System\.getenv|ENV\[|config\[|secrets\.|vault\.",
    re.IGNORECASE,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _line_number(content: str, pos: int) -> int:
    return content[:pos].count("\n") + 1


def _is_skippable(file_path: str) -> bool:
    norm = file_path.replace("\\", "/")
    if _TEST_PATH_RE.search(norm):
        return True
    ext = "." + norm.rsplit(".", 1)[-1].lower() if "." in norm.rsplit("/", 1)[-1] else ""
    return ext not in _SCANNABLE_EXTS and not norm.endswith(".env.example")


def _is_placeholder(value: str, placeholder_re: re.Pattern | None) -> bool:
    stripped = value.strip()
    if not stripped or len(stripped) < 4:
        return True
    if placeholder_re and placeholder_re.match(stripped):
        return True
    if len(set(stripped.lower())) <= 2:
        return True
    return False


# ---------------------------------------------------------------------------
# Per-file scanning
# ---------------------------------------------------------------------------

def _scan_file(file_path: str, content: str, rules: list[dict]) -> list[dict[str, Any]]:
    findings: list[dict[str, Any]] = []
    flagged_lines: set[int] = set()

    known_format_rules = [r for r in rules if r.get("strategy") == "known_format"]
    assignment_rules = [r for r in rules if r.get("strategy") == "variable_assignment"]

    # Strategy 1: known credential formats (high confidence)
    for rule in known_format_rules:
        pattern: re.Pattern = rule["_pattern"]
        for m in pattern.finditer(content):
            line = _line_number(content, m.start())
            if line in flagged_lines:
                continue
            line_text = content.splitlines()[line - 1] if line <= content.count("\n") + 1 else ""
            if _ENV_REF_RE.search(line_text):
                continue
            flagged_lines.add(line)
            snippet = line_text.strip()[:120]
            ctx = {
                "file_path": file_path,
                "line": line,
                "snippet": snippet,
                "label": rule.get("label", "Secret"),
            }
            findings.append(build_finding(rule, ctx))

    # Strategy 2: variable assignment with secret-looking name (medium confidence)
    for rule in assignment_rules:
        name_src = rule.get("name_pattern", "")
        if not name_src:
            continue
        try:
            name_re = re.compile(name_src, re.IGNORECASE | re.MULTILINE)
        except re.error:
            continue

        placeholder_src = rule.get("placeholder_pattern", "")
        placeholder_re = re.compile(placeholder_src, re.IGNORECASE) if placeholder_src else None

        env_ref_src = rule.get("env_ref_pattern", "")
        env_ref_re = re.compile(env_ref_src, re.IGNORECASE) if env_ref_src else _ENV_REF_RE

        for m in name_re.finditer(content):
            line = _line_number(content, m.start())
            if line in flagged_lines:
                continue
            value = m.group("val")
            if _is_placeholder(value, placeholder_re):
                continue
            line_text = (content.splitlines()[line - 1] if line <= content.count("\n") + 1 else "").strip()
            if env_ref_re.search(line_text):
                continue
            flagged_lines.add(line)
            var_name = m.group("name")
            ctx = {
                "file_path": file_path,
                "line": line,
                "snippet": line_text[:120],
                "name": var_name,
            }
            findings.append(build_finding(rule, ctx))

    return findings


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def scan_source_secrets(
    file_contents: dict[str, str],
) -> tuple[list[dict[str, Any]], str | None]:
    """Scan source files for hardcoded secrets and credentials.

    Args:
        file_contents: Dict mapping relative file paths to their string contents.

    Returns:
        (findings, error) — findings is the list of issue dicts;
        error is a human-readable string on failure, or None on success.
    """
    try:
        rules = load_rules(_RULES_PATH)
    except Exception as exc:
        return [], f"Secret scanner failed to load rules: {exc}"

    findings: list[dict[str, Any]] = []

    try:
        for file_path, content in file_contents.items():
            if not content:
                continue
            if _is_skippable(file_path):
                continue
            findings.extend(_scan_file(file_path, content, rules))
    except Exception as exc:
        return [], f"Secret scanner error: {exc}"

    return findings, None
