from __future__ import annotations
import asyncio
import json
import re
import time
from dataclasses import dataclass
from pathlib import Path

import httpx

CACHE_DIR = Path.cwd() / "databases" / "hsts_preload_cache"
CACHE_TTL = 24 * 60 * 60

CHROME_URL = "https://raw.githubusercontent.com/chromium/chromium/main/net/http/transport_security_state_static.json"
FIREFOX_URL = "https://hg.mozilla.org/mozilla-central/raw-file/tip/security/manager/ssl/nsSTSPreloadList.inc"

PRELOAD_BROWSERS = ["chrome", "firefox", "edge", "ie"]


@dataclass
class PreloadResult:
    browser: str
    listed: bool
    include_subdomains: bool = False
    source_error: str | None = None


def _cache_path(name: str) -> Path:
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    return CACHE_DIR / f"{name}.json"


def _cache_fresh(p: Path) -> bool:
    try:
        return p.is_file() and (time.time() - p.stat().st_mtime) < CACHE_TTL
    except Exception:
        return False


async def _fetch_chrome_list(timeout: float = 60.0) -> dict[str, bool] | None:
    """Return mapping: host -> include_subdomains, or None on fetch/parse error."""
    cache = _cache_path("chrome")
    if _cache_fresh(cache):
        try:
            data = json.loads(cache.read_text())
            if data:
                return data
        except Exception:
            pass
    try:
        async with httpx.AsyncClient(timeout=timeout, headers={"User-Agent": "nercone-tls-test/1.0"},
                                     follow_redirects=True) as client:
            resp = await client.get(CHROME_URL)
        if resp.status_code != 200:
            return None
        raw = resp.text
    except Exception:
        return None
    # Strip // comments (json with comments) and parse a loose subset
    cleaned = re.sub(r"//[^\n]*", "", raw)
    cleaned = re.sub(r"/\*.*?\*/", "", cleaned, flags=re.DOTALL)
    result: dict[str, bool] = {}
    try:
        data = json.loads(cleaned)
        for entry in data.get("entries", []):
            name = entry.get("name")
            if name:
                result[name.lower()] = bool(entry.get("include_subdomains", False))
    except Exception:
        for m in re.finditer(r'"name"\s*:\s*"([^"]+)"\s*,\s*"policy"[^}]*?"include_subdomains"\s*:\s*(true|false)', cleaned):
            result[m.group(1).lower()] = m.group(2) == "true"
    if result:
        try:
            cache.write_text(json.dumps(result))
        except Exception:
            pass
    return result if result else None


async def _fetch_firefox_list(timeout: float = 60.0) -> dict[str, bool] | None:
    cache = _cache_path("firefox")
    if _cache_fresh(cache):
        try:
            data = json.loads(cache.read_text())
            if data:
                return data
        except Exception:
            pass
    try:
        async with httpx.AsyncClient(timeout=timeout, headers={"User-Agent": "nercone-tls-test/1.0"},
                                     follow_redirects=True) as client:
            resp = await client.get(FIREFOX_URL)
        if resp.status_code != 200:
            return None
        text = resp.text
    except Exception:
        return None
    result: dict[str, bool] = {}
    # Historically Firefox shipped the preload list as a C-array of
    #   { "host.example", true },
    # but the current nsSTSPreloadList.inc (2024+) is a plain CSV with
    #   host.example, 1
    # per line (1 = include_subdomains, 0 = not). Try both, brace-format
    # first; fall back to the CSV form.
    for m in re.finditer(r'\{\s*"([^"]+)"\s*,\s*(true|false)\s*\}', text):
        result[m.group(1).lower()] = m.group(2) == "true"
    if not result:
        # Plain CSV format. Accept lines that look like "host, 0|1" —
        # including single-label TLD entries (e.g. "dev, 1", "app, 1",
        # "google, 1") which Mozilla ships alongside FQDNs. The explicit
        # ", [01]" suffix is enough to reject the multiline "/* ... */"
        # license header whose continuation lines start with " *".
        line_re = re.compile(r'^\s*([a-z0-9][a-z0-9.\-]*[a-z0-9])\s*,\s*([01])\s*$', re.IGNORECASE)
        for line in text.splitlines():
            m = line_re.match(line)
            if not m:
                continue
            host = m.group(1).lower()
            # A bare "host" with no dot is only valid as a TLD-style entry.
            # Everything else must contain at least a dot or it's junk.
            if "." not in host and not host.isalnum():
                continue
            result[host] = m.group(2) == "1"
    if result:
        try:
            cache.write_text(json.dumps(result))
        except Exception:
            pass
    return result if result else None


def _lookup(host: str, table: dict[str, bool]) -> tuple[bool, bool]:
    h = host.lower().strip(".")
    if h in table:
        return True, table[h]
    # walk parent domains to catch include_subdomains matches
    parts = h.split(".")
    for i in range(1, len(parts)):
        parent = ".".join(parts[i:])
        if parent in table and table[parent]:
            return True, True
    return False, False


async def check_preload(host: str) -> list[PreloadResult]:
    chrome = await _fetch_chrome_list()
    firefox = await _fetch_firefox_list()
    results: list[PreloadResult] = []
    c_listed, c_sub = _lookup(host, chrome) if chrome is not None else (False, False)
    f_listed, f_sub = _lookup(host, firefox) if firefox is not None else (False, False)
    chrome_err = "source unavailable" if chrome is None else None
    firefox_err = "source unavailable" if firefox is None else None
    results.append(PreloadResult("chrome", c_listed, c_sub, chrome_err))
    results.append(PreloadResult("firefox", f_listed, f_sub, firefox_err))
    # Edge (Chromium) and Internet Explorer on Windows 10+ both rely on the
    # Chromium HSTS preload list via WinINet; no separate list is published
    # for either. Report both with the Chromium lookup so the UI doesn't
    # carry phantom "not supported" / "source unavailable" entries.
    results.append(PreloadResult("edge", c_listed, c_sub, chrome_err))
    results.append(PreloadResult("ie", c_listed, c_sub, chrome_err))
    return results
