"""
Additional integrity checks beyond baseline diff:
- link whitelist enforcement
- security-header audit
- suspicious URL filenames (from v4 heritage)
- external link reachability check
"""
import logging
import re
import time
from pathlib import Path
from urllib.parse import urlparse

import requests
import yaml

logger = logging.getLogger(__name__)

_BROWSER_UA = (
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)

# Webshell / backdoor filename markers. Word boundaries are essential:
# without them "mailer" matches the legitimate "PSEMailerAntispam.js" and
# "gate" matches "navigate", producing false positives on every scan.
_SUSPICIOUS_FILENAME_RE = re.compile(
    r"\b(shell|backdoor|webshell|c99|r57|wso|b374k|cmd|reverse_shell|bindshell)\b",
    re.I,
)


# ---------------------------------------------------------------------------
# Whitelist checks
# ---------------------------------------------------------------------------

def load_whitelist(path: str | Path) -> set[str]:
    """Load a YAML list file into a set. Returns empty set if missing."""
    p = Path(path)
    if not p.exists():
        return set()
    data = yaml.safe_load(p.read_text(encoding="utf-8")) or []
    if isinstance(data, list):
        return {str(s).strip() for s in data if s}
    return set()


def check_links_against_whitelist(
    page: dict,
    allowed_external_domains: set[str],
    base_netloc: str,
) -> list[dict]:
    """
    Return violations: external links to domains not in allowed_external_domains.
    Also flags noscript links and comment links to external domains.
    """
    violations: list[dict] = []

    for link_type, links in page.get("links", {}).items():
        if not isinstance(links, list):
            continue
        for link in links:
            if not isinstance(link, dict):
                continue
            if link.get("class") != "external":
                continue
            domain = urlparse(link.get("url", "")).netloc
            if domain and domain not in allowed_external_domains:
                violations.append({
                    "type": "unlisted_external_domain",
                    "link_type": link_type,
                    "url": link.get("url", ""),
                    "domain": domain,
                })

    # Comment links
    for href in page.get("comment_links", []):
        domain = urlparse(href).netloc
        if domain and domain != base_netloc and domain not in allowed_external_domains:
            violations.append({
                "type": "comment_link_to_unlisted_domain",
                "link_type": "comment",
                "url": href,
                "domain": domain,
            })

    # noscript links
    for hc in page.get("hidden_content", []):
        if hc.get("type") == "noscript_links":
            for href in hc.get("links", []):
                domain = urlparse(href).netloc
                if domain and domain != base_netloc and domain not in allowed_external_domains:
                    violations.append({
                        "type": "noscript_link_to_unlisted_domain",
                        "link_type": "noscript",
                        "url": href,
                        "domain": domain,
                    })

    return violations


# ---------------------------------------------------------------------------
# Security headers
# ---------------------------------------------------------------------------

def check_security_headers(response_headers: dict, required: list[str]) -> list[str]:
    """Return list of missing recommended security headers."""
    # Header lookup is case-insensitive
    present = {k.lower() for k in response_headers}
    return [h for h in required if h.lower() not in present]


# ---------------------------------------------------------------------------
# Suspicious filenames
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# External link reachability
# ---------------------------------------------------------------------------

def check_external_links(
    urls: list[str],
    timeout: int = 10,
    delay: float = 0.3,
    on_progress: object = None,
) -> dict[str, dict]:
    """
    Check a list of URLs for reachability via HEAD (fallback: GET).

    Returns {url: {"status": int|None, "final_url": str|None, "error": str|None}}.
    """
    results: dict[str, dict] = {}
    session = requests.Session()
    session.headers["User-Agent"] = "integrity-scanner/1.0 (external-link-check)"

    try:
        for i, url in enumerate(urls):
            if i > 0 and delay > 0:
                time.sleep(delay)
            logger.debug("Checking external link: %s", url)
            try:
                resp = session.head(url, timeout=timeout, allow_redirects=True)
                if resp.status_code == 405:
                    resp = session.get(url, timeout=timeout, allow_redirects=True, stream=True)
                    resp.close()
                # 403 often means bot-blocking, not a broken link — retry with browser UA
                if resp.status_code == 403:
                    try:
                        r2 = session.head(url, timeout=timeout, allow_redirects=True,
                                          headers={"User-Agent": _BROWSER_UA})
                        if r2.status_code == 405:
                            r2 = session.get(url, timeout=timeout, allow_redirects=True,
                                             stream=True, headers={"User-Agent": _BROWSER_UA})
                            r2.close()
                        resp = r2
                    except requests.exceptions.RequestException:
                        pass  # keep original 403
                results[url] = {"status": resp.status_code, "final_url": resp.url, "error": None}
            except requests.exceptions.RequestException as exc:
                results[url] = {"status": None, "final_url": None, "error": str(exc)}
            if on_progress:
                on_progress(i + 1, len(urls))
    finally:
        session.close()

    return results


# ---------------------------------------------------------------------------
# Asset hashing
# ---------------------------------------------------------------------------

def fetch_asset_hashes(
    urls: list[str],
    timeout: int = 15,
    delay: float = 0.2,
) -> dict[str, dict]:
    """
    Fetch binary assets and compute SHA-256 hashes via streaming GET.

    Returns {url: {"sha256": str|None, "size": int|None, "error": str|None}}.
    """
    import hashlib
    results: dict[str, dict] = {}
    session = requests.Session()
    session.headers["User-Agent"] = "integrity-scanner/1.0 (asset-check)"

    try:
        for i, url in enumerate(urls):
            if i > 0 and delay > 0:
                time.sleep(delay)
            logger.debug("Hashing asset: %s", url)
            try:
                resp = session.get(url, timeout=timeout, stream=True)
                if resp.status_code == 200:
                    h = hashlib.sha256()
                    size = 0
                    for chunk in resp.iter_content(chunk_size=65536):
                        h.update(chunk)
                        size += len(chunk)
                    results[url] = {"sha256": h.hexdigest(), "size": size, "error": None}
                else:
                    results[url] = {"sha256": None, "size": None, "error": f"HTTP {resp.status_code}"}
            except requests.exceptions.RequestException as exc:
                results[url] = {"sha256": None, "size": None, "error": str(exc)}
    finally:
        session.close()

    return results


def is_suspicious_url(url: str) -> bool:
    """Flag URLs with names typical for webshells / backdoors."""
    filename = urlparse(url).path.rsplit("/", 1)[-1]
    return bool(_SUSPICIOUS_FILENAME_RE.search(filename))
