from __future__ import annotations

import re
from typing import Iterable, List, Sequence

import requests
from bs4 import BeautifulSoup

from ..model import Finding, ScanRequest

try:  # Playwright is optional; fall back to requests/BeautifulSoup if absent.
    from playwright.sync_api import sync_playwright
except ImportError:  # pragma: no cover - optional dependency
    sync_playwright = None

_INLINE_JS_PATTERNS = [
    re.compile(r"document\.write", re.IGNORECASE),
    re.compile(r"innerHTML\s*=", re.IGNORECASE),
    re.compile(r"createElement\s*\(\s*['\"]script['\"]", re.IGNORECASE),
]

_CSRF_TOKEN_HINTS = ("csrf", "xsrf", "token", "antiforgery", "authenticity")


def evaluate_dom_surface(request: ScanRequest) -> Iterable[Finding]:
    url = f"https://{request.domain}"
    findings: List[Finding] = []

    try:
        response = requests.get(
            url,
            headers={"User-Agent": request.user_agent},
            timeout=request.timeout,
            allow_redirects=True,
        )
    except requests.RequestException as exc:
        findings.append(
            Finding(
                check="static-analysis",
                title="DOM fetch failed",
                severity="high",
                description=f"Unable to retrieve landing page {url}: {exc}",
                remediation="Check DNS, firewall rules, and TLS configuration for the target host.",
                impact="Stateless analysis cannot validate exposure without a baseline document.",
                i18n_key="static.dom_fetch_failed",
                i18n_params={
                    "url": url,
                    "error": str(exc),
                },
            )
        )
        return findings

    soup = BeautifulSoup(response.text, "html.parser")

    findings.extend(_detect_forms_without_csrf(soup))
    findings.extend(_detect_inline_script_patterns(soup))

    if sync_playwright:
        findings.extend(_playwright_inline_checks([response.url], request.timeout))

        if not findings:
            findings.append(
                Finding(
                    check="static-analysis",
                    title="No static heuristics triggered",
                    severity="info",
                    description="DOM inspection did not reveal missing CSRF tokens or unsafe inline patterns.",
                    remediation="Maintain CSP nonces and server-side CSRF validation.",
                    impact="Heuristic coverage is limited and should complement dynamic testing.",
                    i18n_key="static.no_heuristics",
                )
            )

    return findings


def _detect_forms_without_csrf(soup: BeautifulSoup) -> List[Finding]:
    findings: List[Finding] = []

    for form in soup.find_all("form"):
        method = (form.get("method") or "get").lower()
        if method not in ("post", "put", "delete", "patch"):
            continue

        inputs = form.find_all("input", attrs={"type": "hidden"})
        has_token = False
        for hidden in inputs:
            name = hidden.get("name", "").lower()
            if any(hint in name for hint in _CSRF_TOKEN_HINTS):
                has_token = True
                break

        if not has_token:
            action = form.get("action") or "[inline]"
            findings.append(
                Finding(
                    check="static-analysis",
                    title="Form without CSRF token",
                    severity="medium",
                    description=f"Form POST target '{action}' lacks hidden CSRF token fields.",
                    remediation="Inject server-generated anti-CSRF tokens and validate them server side.",
                    evidence=form.decode()[:250],
                    impact="Forms without CSRF defenses expose authenticated actions to forgery.",
                    i18n_key="static.form_without_csrf",
                    i18n_params={"action": action},
                )
            )

    return findings


def _detect_inline_script_patterns(soup: BeautifulSoup) -> List[Finding]:
    findings: List[Finding] = []

    for script in soup.find_all("script"):
        if script.get("src"):
            continue

        content = script.string or ""
        snippet = content.strip()[:120]
        for pattern in _INLINE_JS_PATTERNS:
            if pattern.search(content):
                findings.append(
                    Finding(
                        check="static-analysis",
                        title="Inline script with dangerous pattern",
                        severity="medium",
                        description=f"Inline JavaScript matches pattern '{pattern.pattern}'.",
                        remediation="Move scripts to external files with CSP nonces or sanitize dynamic content.",
                        evidence=snippet,
                        impact="Inline scripts reduce CSP effectiveness and ease XSS exploitation.",
                        i18n_key="static.inline_script_pattern",
                        i18n_params={"pattern": pattern.pattern},
                    )
                )
                break

    return findings


def _playwright_inline_checks(urls: Sequence[str], timeout: float) -> List[Finding]:
    findings: List[Finding] = []
    timeout_ms = max(int(timeout * 1000), 1000)

    try:
        with sync_playwright() as playwright:
            browser = playwright.chromium.launch(headless=True)
            try:
                for url in urls:
                    page = browser.new_page()
                    page.route("**/*", lambda route: route.continue_())  # ensure read-only requests
                    page.goto(url, wait_until="domcontentloaded", timeout=timeout_ms)

                    inline_handlers: List[str] = page.eval_on_selector_all(
                        "*",
                        "elements => elements"
                        ".filter(el => Array.from(el.attributes).some(attr => attr.name.startsWith('on')))"
                        ".map(el => el.outerHTML.slice(0, 180))",
                    )
                    if inline_handlers:
                        findings.append(
                            Finding(
                                check="static-analysis",
                                title="Inline event handlers detected",
                                severity="low",
                                description="DOM contains on* handlers that bypass CSP nonces.",
                                remediation="Replace inline handlers with addEventListener and enforce CSP nonces.",
                                impact="Inline handlers are primary injection points for reflected XSS.",
                                evidence=" | ".join(inline_handlers[:3]),
                                i18n_key="static.inline_event_handlers",
                                i18n_params={"count": len(inline_handlers)},
                            )
                        )

                    dangerous_eval: List[str] = page.eval_on_selector_all(
                        "script",
                        "nodes => nodes"
                        ".map(node => node.textContent)"
                        ".filter(code => /eval\\s*\\(|new Function\\s*\\(/i.test(code))"
                        ".slice(0,3)",
                    )
                    if dangerous_eval:
                        findings.append(
                            Finding(
                                check="static-analysis",
                                title="Dynamic JavaScript execution",
                                severity="medium",
                                description="Inline scripts rely on eval()/Function constructors.",
                                remediation="Refactor code to avoid eval-style APIs and enforce CSP script-src hashes.",
                                impact="Dynamic evaluation eases execution of injected payloads.",
                                evidence=" | ".join(dangerous_eval),
                                i18n_key="static.dynamic_js_execution",
                                i18n_params={"count": len(dangerous_eval)},
                            )
                        )
            finally:
                browser.close()
    except Exception as exc:  # pragma: no cover - defensive
        findings.append(
            Finding(
                check="static-analysis",
                title="Playwright analysis failed",
                severity="low",
                description=f"Headless browser inspection failed: {exc}",
                remediation="Install Playwright browsers (`playwright install`) and ensure sandboxing permits headless runs.",
                impact="Inline handler heuristics limited to static parsing fallback.",
                i18n_key="static.playwright_failed",
                i18n_params={"error": str(exc)},
            )
        )

    return findings
