# retoor <retoor@molodetz.nl>
from __future__ import annotations

import re

from devplacepy.services.jobs.isslop.analysis.signals import (
    AXIS_QUALITY,
    SEVERITY_MEDIUM,
    SEVERITY_STRONG,
    SEVERITY_WEAK,
    FileContext,
    Signal,
)

JS_LANGUAGES: frozenset[str] = frozenset({"javascript", "typescript"})

TOKEN_IN_STORAGE_PATTERN = re.compile(
    r"(?:local|session)Storage\.setItem\(\s*[\"'`][^\"'`]*(token|jwt|auth|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|password|session)",
    re.IGNORECASE,
)
INSECURE_COOKIE_PATTERN = re.compile(r"(document\.cookie\s*=|res\.cookie\(|response\.set_cookie\(|setCookie\()", re.IGNORECASE)
COOKIE_SECURITY_HINT = re.compile(r"httponly|secure|samesite", re.IGNORECASE)

USEEFFECT_BLOCK_PATTERN = re.compile(r"useEffect\s*\(\s*\(\s*\)\s*=>\s*\{", re.IGNORECASE)
EFFECT_RESOURCE_PATTERN = re.compile(r"\b(setInterval|setTimeout|addEventListener|\.subscribe\(|\.on\()", re.IGNORECASE)
EFFECT_CLEANUP_PATTERN = re.compile(r"return\s*(\(\s*\)\s*=>|function)")

CONDITIONAL_HOOK_PATTERN = re.compile(
    r"^\s*(if|for|while)\s*\([^)]*\)\s*\{[^}]{0,200}\buse(State|Effect|Ref|Memo|Callback|Context|Reducer)\s*\(",
    re.IGNORECASE | re.MULTILINE,
)
MIXED_CONTENT_PATTERN = re.compile(r"[\"'(]http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[\w.-]+", re.IGNORECASE)
JSON_PARSE_UNGUARDED_PATTERN = re.compile(r"JSON\.parse\(\s*(?:await\s+)?\w+(?:\.(?:text|body|data))?\s*\)")

ENV_ACCESS_PATTERN = re.compile(r"process\.env\.[A-Z0-9_]+")
ENV_FALLBACK_HINT = re.compile(r"process\.env\.[A-Z0-9_]+\s*(\|\||\?\?|,|\))")
PUBLIC_SECRET_PATTERN = re.compile(
    r"(NEXT_PUBLIC|VITE|REACT_APP|NUXT_PUBLIC)_[A-Z0-9_]*(SECRET|PRIVATE|SERVICE_ROLE|API_KEY|TOKEN|PASSWORD)",
    re.IGNORECASE,
)
SUPABASE_SERVICE_ROLE_PATTERN = re.compile(r"service_role|SUPABASE_SERVICE_ROLE_KEY", re.IGNORECASE)

MISSING_ROUTE_GUARD_PATTERN = re.compile(r"(router\.(get|post|put|delete|patch)|app\.(get|post|put|delete|patch))\s*\(", re.IGNORECASE)

ENV_DENSITY_THRESHOLD: int = 4
MAX_FINDINGS: int = 10


def _line_of(text: str, position: int) -> int:
    return text.count("\n", 0, position) + 1


def _effect_cleanup_findings(text: str) -> list[Signal]:
    findings: list[Signal] = []
    for match in USEEFFECT_BLOCK_PATTERN.finditer(text):
        start = match.end()
        depth = 1
        index = start
        while index < len(text) and depth > 0:
            char = text[index]
            if char == "{":
                depth += 1
            elif char == "}":
                depth -= 1
            index += 1
        body = text[start:index]
        if EFFECT_RESOURCE_PATTERN.search(body) and not EFFECT_CLEANUP_PATTERN.search(body):
            findings.append(
                Signal(
                    "MISSING_EFFECT_CLEANUP",
                    "useEffect registers a timer or listener without a cleanup return",
                    SEVERITY_MEDIUM,
                    AXIS_QUALITY,
                    2.0,
                    _line_of(text, match.start()),
                    match.group(0)[:80],
                )
            )
    return findings


def detect_vibe_errors(context: FileContext) -> list[Signal]:
    findings: list[Signal] = []
    text = context.text
    language = context.language

    public_secret = PUBLIC_SECRET_PATTERN.search(text)
    if public_secret:
        findings.append(
            Signal(
                "PUBLIC_ENV_SECRET",
                "Secret exposed through a client-side public env variable",
                SEVERITY_STRONG,
                AXIS_QUALITY,
                5.0,
                _line_of(text, public_secret.start()),
                public_secret.group(0)[:100],
            )
        )

    if language in JS_LANGUAGES or language == "html":
        token_storage = TOKEN_IN_STORAGE_PATTERN.search(text)
        if token_storage:
            findings.append(
                Signal(
                    "TOKEN_IN_WEB_STORAGE",
                    "Auth token stored in localStorage or sessionStorage, exposed to XSS",
                    SEVERITY_STRONG,
                    AXIS_QUALITY,
                    4.0,
                    _line_of(text, token_storage.start()),
                    token_storage.group(0)[:100],
                )
            )
        cookie_match = INSECURE_COOKIE_PATTERN.search(text)
        if cookie_match:
            window = text[cookie_match.start(): cookie_match.start() + 300]
            if not COOKIE_SECURITY_HINT.search(window):
                findings.append(
                    Signal(
                        "INSECURE_COOKIE",
                        "Cookie set without HttpOnly, Secure or SameSite flags",
                        SEVERITY_MEDIUM,
                        AXIS_QUALITY,
                        2.0,
                        _line_of(text, cookie_match.start()),
                        cookie_match.group(0)[:80],
                    )
                )
        mixed = MIXED_CONTENT_PATTERN.search(text)
        if mixed:
            findings.append(
                Signal(
                    "MIXED_CONTENT",
                    "Insecure http:// resource referenced",
                    SEVERITY_WEAK,
                    AXIS_QUALITY,
                    1.0,
                    _line_of(text, mixed.start()),
                    mixed.group(0)[:80],
                )
            )

    if language in JS_LANGUAGES:
        conditional_hook = CONDITIONAL_HOOK_PATTERN.search(text)
        if conditional_hook:
            findings.append(
                Signal(
                    "CONDITIONAL_HOOK",
                    "React hook called conditionally, violates rules of hooks",
                    SEVERITY_MEDIUM,
                    AXIS_QUALITY,
                    2.0,
                    _line_of(text, conditional_hook.start()),
                    conditional_hook.group(0).strip()[:80],
                )
            )
        findings.extend(_effect_cleanup_findings(text))
        unguarded = JSON_PARSE_UNGUARDED_PATTERN.search(text)
        if unguarded and "try" not in text[max(0, unguarded.start() - 120): unguarded.start()]:
            findings.append(
                Signal(
                    "UNGUARDED_JSON_PARSE",
                    "JSON.parse on a response without a guard for non-JSON payloads",
                    SEVERITY_MEDIUM,
                    AXIS_QUALITY,
                    1.0,
                    _line_of(text, unguarded.start()),
                    unguarded.group(0)[:80],
                )
            )
        if SUPABASE_SERVICE_ROLE_PATTERN.search(text) and PUBLIC_SECRET_PATTERN.search(text) is None:
            env_exposed = re.search(r"(NEXT_PUBLIC|VITE|REACT_APP)[A-Z0-9_]*[\s\S]{0,60}service_role", text, re.IGNORECASE)
            if env_exposed:
                findings.append(
                    Signal(
                        "SUPABASE_SERVICE_ROLE_EXPOSED",
                        "Supabase service_role key reachable from client code",
                        SEVERITY_STRONG,
                        AXIS_QUALITY,
                        5.0,
                        _line_of(text, env_exposed.start()),
                        env_exposed.group(0)[:100],
                    )
                )
        env_accesses = ENV_ACCESS_PATTERN.findall(text)
        env_fallbacks = ENV_FALLBACK_HINT.findall(text)
        if len(env_accesses) >= ENV_DENSITY_THRESHOLD and not env_fallbacks:
            findings.append(
                Signal(
                    "ENV_NO_VALIDATION",
                    f"{len(env_accesses)} process.env reads with no fallback or validation",
                    SEVERITY_WEAK,
                    AXIS_QUALITY,
                    1.0,
                    1,
                    f"{len(env_accesses)} unvalidated env reads",
                )
            )

    return findings[:MAX_FINDINGS]
