from __future__ import annotations

import re
from typing import Iterable, List, Sequence

import requests
from bs4 import BeautifulSoup

from ..localization.helpers import create_translated_finding
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(
            create_translated_finding(
                check="static-analysis",
                i18n_key="static.dom_fetch_failed",
                severity="high",
                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(
                create_translated_finding(
                    check="static-analysis",
                    i18n_key="static.no_heuristics",
                    severity="info",
                )
            )

    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(
                create_translated_finding(
                    check="static-analysis",
                    i18n_key="static.form_without_csrf",
                    severity="medium",
                    i18n_params={"action": action},
                    evidence=form.decode()[:250]
                )
            )

    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(
                    create_translated_finding(
                        check="static-analysis",
                        i18n_key="static.inline_script_pattern",
                        severity="medium",
                        i18n_params={"pattern": pattern.pattern},
                        evidence=snippet
                    )
                )
                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(
                            create_translated_finding(
                                check="static-analysis",
                                i18n_key="static.inline_event_handlers",
                                severity="low",
                                i18n_params={"count": len(inline_handlers)},
                                evidence=" | ".join(inline_handlers[:3])
                            )
                        )

                    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(
                            create_translated_finding(
                                check="static-analysis",
                                i18n_key="static.dynamic_js_execution",
                                severity="medium",
                                i18n_params={"count": len(dangerous_eval)},
                                evidence=" | ".join(dangerous_eval)
                            )
                        )
            finally:
                browser.close()
    except Exception as exc:  # pragma: no cover - defensive
        findings.append(
            create_translated_finding(
                check="static-analysis",
                i18n_key="static.playwright_failed",
                severity="low",
                i18n_params={"error": str(exc)},
            )
        )

    return findings
