"""Server-side deterministic file summarizer for the test generation scan phase.

Replaces raw file content echoing with compact structured summaries.
Target: ~300–600 chars per file  (15–30x reduction vs raw source).

Output schema per file:
  {
    "t":   "page" | "component" | "util" | "api_route",
    "sel": [["data-testid", "submit-btn"], ["name", "email"], ...],  # ≤ 30
    "txt": ["Sign in", "Forgot password?", ...],                     # ≤ 20
    "rts": ["/login", "/dashboard", ...],                            # ≤ 15
    "nav": [{"m": "route", "to": "/notebook"},
            {"m": "modal", "fn": "toggleAppDetailsModal"},
            {"m": "state", "fn": "setShowUpgradeModal"},
            {"m": "tab", "dest": "window.open"}],                    # ≤ 10
    "imp": ["./Login/LoginForm", "../common/Button", ...],           # ≤ 20
  }

Keys are abbreviated to minimise serialised size.
The LLM prompt explains each key.
"""

import json
import re
from pathlib import Path
from typing import Literal

ComponentType = Literal["page", "component", "util", "api_route"]

# ---------------------------------------------------------------------------
# JS / TS extensions that support all extractors
# ---------------------------------------------------------------------------
_JS_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}

# ---------------------------------------------------------------------------
# Component-type classifier
# ---------------------------------------------------------------------------
_PAGE_PATH_RE = re.compile(
    r"(^|[/\\])(pages?|views?|screens?|routes?)[/\\]"
    r"|[/\\](app)[/\\].+\.(tsx?|jsx?)$"
    r"|(^|[/\\])(_app|_document|layout|index)\.(tsx?|jsx?)$",
    re.IGNORECASE,
)
_API_PATH_RE = re.compile(
    r"(^|[/\\])(api|routes?|controllers?|handlers?)[/\\]",
    re.IGNORECASE,
)
_API_CONTENT_RE = re.compile(
    r"export\s+default\s+function\s+handler\b"
    r"|router\.(get|post|put|patch|delete)\s*\("
    r"|app\.(get|post|put|patch|delete)\s*\(",
    re.IGNORECASE,
)
_COMPONENT_PATH_RE = re.compile(
    r"(^|[/\\])components?[/\\]",
    re.IGNORECASE,
)


def _classify(rel_path: str, content: str) -> ComponentType:
    p = rel_path.replace("\\", "/")
    # API routes first (can live inside pages/api)
    if _API_PATH_RE.search(p) or _API_CONTENT_RE.search(content):
        return "api_route"
    if _PAGE_PATH_RE.search(p):
        return "page"
    if _COMPONENT_PATH_RE.search(p):
        return "component"
    # Fallback: if file exports JSX / React component treat as component
    if re.search(r"return\s*\(\s*<|React\.createElement", content):
        return "component"
    return "util"


# ---------------------------------------------------------------------------
# Selector extractor — returns [(attr_type, value), ...]
# ---------------------------------------------------------------------------

# (attribute_name, regex_capturing_value, priority)
_SEL_PATTERNS: list[tuple[str, re.Pattern, int]] = [
    ("data-testid",       re.compile(r'data-testid=["\']([^"\']{1,80})["\']'),       1),
    ("data-cy",           re.compile(r'data-cy=["\']([^"\']{1,80})["\']'),           2),
    ("data-test",         re.compile(r'data-test=["\']([^"\']{1,80})["\']'),         3),
    ("data-automation-id",re.compile(r'data-automation-id=["\']([^"\']{1,80})["\']'),4),
    ("aria-label",        re.compile(r'aria-label=["\']([^"\']{1,80})["\']'),        5),
    ("id",                re.compile(r'\bid=["\']([^"\']{1,60})["\']'),              6),
    ("name",              re.compile(r'\bname=["\']([^"\']{1,60})["\']'),            7),
    ("placeholder",       re.compile(r'placeholder=["\']([^"\']{1,80})["\']'),       8),
]
# type= is intentionally omitted — too noisy ("text", "button" etc. add no signal)

# Values to skip even when found (generic / boolean / empty)
_SEL_SKIP = {
    "true", "false", "null", "undefined", "none", "on", "off",
    "text", "button", "submit", "reset", "hidden", "radio", "checkbox",
    "password", "email", "number", "tel", "search", "url", "date",
}


def _extract_selectors(content: str) -> list[list[str]]:
    seen: set[str] = set()
    results: list[list[str]] = []
    for attr, pat, _ in _SEL_PATTERNS:
        for m in pat.finditer(content):
            val = m.group(1).strip()
            if not val or val.lower() in _SEL_SKIP:
                continue
            key = f"{attr}:{val.lower()}"
            if key in seen:
                continue
            seen.add(key)
            results.append([attr, val])
            if len(results) >= 30:
                return results
    return results


# ---------------------------------------------------------------------------
# UI text extractor
# ---------------------------------------------------------------------------
_JSX_TEXT_RE = re.compile(r">([^<{]{2,60})<")
_ATTR_TEXT_RE = re.compile(
    r'(?:aria-label|title|placeholder)\s*=\s*["\']([^"\']{2,60})["\']',
    re.IGNORECASE,
)
_SKIP_TEXT_CHARS = re.compile(r"[{}<>\\=]|^\s*$|\$\{")
_SKIP_TEXT_PATTERNS = re.compile(
    r"^(true|false|null|undefined|import|export|const|let|var|return|function)$",
    re.IGNORECASE,
)


def _extract_ui_text(content: str) -> list[str]:
    seen: set[str] = set()
    results: list[str] = []

    def _add(val: str) -> None:
        v = val.strip()
        if not v or len(v) < 2 or len(v) > 60:
            return
        if _SKIP_TEXT_CHARS.search(v):
            return
        if _SKIP_TEXT_PATTERNS.match(v):
            return
        # Skip lines that look like code (contain parentheses, semicolons)
        if ";" in v or "()" in v:
            return
        key = v.lower()
        if key in seen:
            return
        seen.add(key)
        results.append(v)

    for m in _JSX_TEXT_RE.finditer(content):
        _add(m.group(1))
        if len(results) >= 20:
            return results

    for m in _ATTR_TEXT_RE.finditer(content):
        _add(m.group(1))
        if len(results) >= 20:
            return results

    return results


# ---------------------------------------------------------------------------
# Route extractor
# ---------------------------------------------------------------------------
_ROUTE_RE = re.compile(
    r'(?:router\.(?:push|replace)|href|to|navigate|redirect)\s*[=(]\s*["\']([^"\']{2,80})["\']'
    r'|<Link[^>]+href=["\']([^"\']+)["\']'
    r'|["\'](/[a-zA-Z0-9_\-/:.]{1,80})["\']',
)
_ROUTE_SKIP = re.compile(
    r"node_modules|\.css|\.scss|\.png|\.svg|\.jpg|\.ico|localhost",
    re.IGNORECASE,
)


def _extract_routes(content: str) -> list[str]:
    seen: set[str] = set()
    results: list[str] = []
    for m in _ROUTE_RE.finditer(content):
        val = (m.group(1) or m.group(2) or m.group(3) or "").strip()
        if not val:
            continue
        if not val.startswith("/"):
            continue
        if _ROUTE_SKIP.search(val):
            continue
        if val in seen:
            continue
        seen.add(val)
        results.append(val)
        if len(results) >= 15:
            break
    return results


# ---------------------------------------------------------------------------
# Navigation mechanism extractor
# ---------------------------------------------------------------------------
_NAV_ROUTER_RE = re.compile(r'router\.(push|replace)\s*\(\s*["\']([^"\']+)["\']')
_NAV_DISPATCH_RE = re.compile(
    r'dispatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:toggle|Toggle|Modal|modal|Open|open)[a-zA-Z0-9_$]*)\s*\('
)
_NAV_STATE_RE = re.compile(r'set(?:Show|Open|Visible|Is)([A-Z][a-zA-Z0-9]*)\s*\(\s*true\s*\)')
_NAV_WINDOW_RE = re.compile(r'window\.open\s*\(\s*["\']([^"\']{1,120})["\']')


def _extract_navigation(content: str) -> list[dict]:
    results: list[dict] = []
    seen: set[str] = set()

    for m in _NAV_ROUTER_RE.finditer(content):
        dest = m.group(2)
        key = f"route:{dest}"
        if key not in seen:
            seen.add(key)
            results.append({"m": "route", "to": dest})

    for m in _NAV_DISPATCH_RE.finditer(content):
        fn = m.group(1)
        key = f"modal:{fn}"
        if key not in seen:
            seen.add(key)
            results.append({"m": "modal", "fn": fn})

    for m in _NAV_STATE_RE.finditer(content):
        fn = "setShow" + m.group(1)
        key = f"state:{fn}"
        if key not in seen:
            seen.add(key)
            results.append({"m": "state", "fn": fn})

    for m in _NAV_WINDOW_RE.finditer(content):
        url = m.group(1)
        key = f"tab:{url}"
        if key not in seen:
            seen.add(key)
            results.append({"m": "tab", "dest": url})

    return results[:10]


# ---------------------------------------------------------------------------
# Import extractor (relative imports only)
# ---------------------------------------------------------------------------
_IMPORT_RE = re.compile(r'from\s+["\'](\./|\.\./)([^"\']+)["\']')
_REQUIRE_RE = re.compile(r'require\s*\(\s*["\'](\./|\.\./)([^"\']+)["\']')


def _extract_imports(content: str) -> list[str]:
    seen: set[str] = set()
    results: list[str] = []
    for pat in (_IMPORT_RE, _REQUIRE_RE):
        for m in pat.finditer(content):
            # Strip extension and trailing index
            raw = (m.group(1) + m.group(2)).rstrip("/")
            # Remove file extension
            raw = re.sub(r'\.(tsx?|jsx?|mjs|cjs|css|scss|module\..+)$', '', raw)
            if raw in seen:
                continue
            seen.add(raw)
            results.append(raw)
            if len(results) >= 20:
                return results
    return results


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

def summarize_file(rel_path: str, content: str) -> dict:
    """Extract compact structured summary from source file content.

    Args:
        rel_path: Relative file path (used for component type classification).
        content:  Raw source code string.

    Returns:
        Compact dict with abbreviated keys. Target ≤ 800 chars serialised.
    """
    if not content or not content.strip():
        return {"t": "util"}

    ext = Path(rel_path).suffix.lower()
    is_js = ext in _JS_EXTENSIONS

    component_type = _classify(rel_path, content)

    result: dict = {"t": component_type}

    if is_js:
        sel = _extract_selectors(content)
        txt = _extract_ui_text(content)
        rts = _extract_routes(content)
        nav = _extract_navigation(content)
        imp = _extract_imports(content)

        if sel:
            result["sel"] = sel
        if txt:
            result["txt"] = txt
        if rts:
            result["rts"] = rts
        if nav:
            result["nav"] = nav
        if imp:
            result["imp"] = imp
    else:
        # Non-JS files: routes and imports only
        rts = _extract_routes(content)
        imp = _extract_imports(content)
        if rts:
            result["rts"] = rts
        if imp:
            result["imp"] = imp

    # Size guardrail: progressively trim if over 1200 chars
    _trim_summary(result)
    return result


def _trim_summary(s: dict) -> None:
    """Trim summary fields progressively if total serialised size exceeds 1200 chars."""
    if len(json.dumps(s)) <= 1200:
        return
    for key, limit in [("imp", 10), ("txt", 10), ("sel", 20), ("nav", 5), ("rts", 8)]:
        if key in s and isinstance(s[key], list):
            s[key] = s[key][:limit]
        if len(json.dumps(s)) <= 1200:
            return
