"""Frontend / JSX sensitive data exposure scanner.

Rules are loaded from ``jsx_rules.json`` via the shared rule engine.
Each rule declares a ``check_type`` that determines the scanning strategy.

Add new frontend exposure patterns by editing the JSON file.
"""

from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Any

from maeris_mcp.scanners.rule_engine import build_finding, load_rules

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

_JSX_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte"}

# Skip test files
_TEST_PATH_RE = re.compile(
    r"[\\/](?:__tests__|tests?|spec|e2e)[\\/]|\.(?:test|spec)\.\w+$",
    re.IGNORECASE,
)

# JSX comment block — skip matches inside these
_JSX_COMMENT_RE = re.compile(r"\{/\*.*?\*/\}", re.DOTALL)

# Default type="password" detection pattern
_DEFAULT_TYPE_PASSWORD_RE = re.compile(r'type\s*=\s*["\']password["\']', re.IGNORECASE)


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

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


def _in_jsx_comment(content: str, match: re.Match) -> bool:
    pos = match.start()
    for cm in _JSX_COMMENT_RE.finditer(content):
        if cm.start() <= pos <= cm.end():
            return True
    return False


# ---------------------------------------------------------------------------
# Check-type handlers
# ---------------------------------------------------------------------------

def _handle_plaintext_render(rule: dict, file_path: str, content: str, flagged_lines: set[int]) -> list[dict[str, Any]]:
    """Detect sensitive fields rendered as visible text in JSX elements."""
    findings: list[dict[str, Any]] = []
    pattern: re.Pattern = rule["_pattern"]

    type_pw_src = rule.get("type_password_pattern")
    type_pw_re = re.compile(type_pw_src, re.IGNORECASE) if type_pw_src else _DEFAULT_TYPE_PASSWORD_RE

    for m in pattern.finditer(content):
        if _in_jsx_comment(content, m):
            continue
        if type_pw_re.search(m.group(0)):
            continue
        line = _line_number(content, m.start())
        flagged_lines.add(line)
        snippet = m.group(0).strip()[:120]
        ctx = {
            "file_path": file_path,
            "line": line,
            "snippet": snippet,
        }
        findings.append(build_finding(rule, ctx))

    return findings


def _handle_cleartext_input(rule: dict, file_path: str, content: str, flagged_lines: set[int]) -> list[dict[str, Any]]:
    """Detect sensitive fields bound to non-masked inputs."""
    findings: list[dict[str, Any]] = []
    pattern: re.Pattern = rule["_pattern"]
    context_window = rule.get("context_window", 400)

    type_pw_src = rule.get("type_password_pattern")
    type_pw_re = re.compile(type_pw_src, re.IGNORECASE) if type_pw_src else _DEFAULT_TYPE_PASSWORD_RE

    for m in pattern.finditer(content):
        if _in_jsx_comment(content, m):
            continue
        line = _line_number(content, m.start())
        if line in flagged_lines:
            continue

        # Check surrounding context for type="password"
        ctx_start = max(0, m.start() - context_window)
        ctx_end = min(len(content), m.end() + context_window)
        context = content[ctx_start:ctx_end]
        if type_pw_re.search(context):
            continue

        flagged_lines.add(line)
        snippet = m.group(0).strip()[:80]
        ctx = {
            "file_path": file_path,
            "line": line,
            "snippet": snippet,
        }
        findings.append(build_finding(rule, ctx))

    return findings


_CHECK_HANDLERS = {
    "plaintext_render": _handle_plaintext_render,
    "cleartext_input": _handle_cleartext_input,
}


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

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

    for rule in rules:
        check_type = rule.get("check_type", "plaintext_render")
        handler = _CHECK_HANDLERS.get(check_type)
        if handler:
            findings.extend(handler(rule, file_path, content, flagged_lines))

    return findings


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

def scan_sensitive_ui_exposure(
    file_contents: dict[str, str],
) -> tuple[list[dict[str, Any]], str | None]:
    """Scan JS/JSX/TSX/Vue/Svelte files for sensitive data rendered in the UI.

    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 message on failure, or None on success.
        findings may be empty (no issues found).
    """
    try:
        rules = load_rules(_RULES_PATH)
    except Exception as exc:
        return [], f"JSX/frontend scanner failed to load rules: {exc}"

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

    try:
        for file_path, content in file_contents.items():
            if not content:
                continue
            norm = file_path.replace("\\", "/")
            if _TEST_PATH_RE.search(norm):
                continue
            ext = os.path.splitext(norm)[1].lower()
            if ext not in _JSX_EXTENSIONS:
                continue
            findings.extend(_scan_jsx_file(file_path, content, rules))
    except Exception as exc:
        return [], f"JSX/frontend scanner error: {exc}"

    return findings, None
