"""
Linux distro CVE remediation enrichment (step 2 of multi-source enrichment).

Unlike MSRC (monthly bulk doc), the Linux security trackers are queryable
PER CVE, cheaply and without auth — so we fetch on demand when a CVE detail
is opened for a Linux host, then cache into cve_remediations so repeat
views are instant. Same table + UI as the MSRC path.

Providers:
  ubuntu  → https://ubuntu.com/security/cves/<CVE>.json
            packages[].statuses[] (release_codename, status, description=
            fixed version), notices_ids (USN), mitigation.
  redhat  → https://access.redhat.com/hydra/rest/securitydata/cve/<CVE>.json
            (CentOS / AlmaLinux / Rocky / Oracle rebuild RHEL, so the RHSA
            + fixed package NVR is the actionable fix). affected_release[],
            mitigation/statement.
"""
import logging
import re
from datetime import datetime
from typing import List, Optional

import httpx
from sqlalchemy.orm import Session

from app.models.cve_remediation import CveRemediation

logger = logging.getLogger(__name__)

HTTP_TIMEOUT = 20.0
UA = {"User-Agent": "TrueVuln/1.0", "Accept": "application/json"}

UBUNTU_URL = "https://ubuntu.com/security/cves/{cve}.json"
REDHAT_URL = "https://access.redhat.com/hydra/rest/securitydata/cve/{cve}.json"


def provider_for_os(os_name: Optional[str]) -> Optional[str]:
    """Map an asset OS string to a Linux security provider, or None."""
    n = (os_name or "").lower()
    if "ubuntu" in n:
        return "ubuntu"
    if any(k in n for k in ("centos", "red hat", "redhat", "rhel", "rocky",
                            "alma", "oracle linux", "fedora")):
        return "redhat"
    return None


# ============================================================
# providers
# ============================================================

def fetch_ubuntu(cve_id: str) -> List[dict]:
    try:
        r = httpx.get(UBUNTU_URL.format(cve=cve_id.upper()), headers=UA,
                      timeout=HTTP_TIMEOUT, follow_redirects=True)
        if r.status_code == 404:
            return []
        r.raise_for_status()
        d = r.json()
    except (httpx.HTTPError, ValueError) as e:
        logger.debug("ubuntu fetch failed for %s: %s", cve_id, e)
        return []

    out: List[dict] = []
    for pkg in d.get("packages", []) or []:
        name = pkg.get("name")
        for s in pkg.get("statuses", []) or []:
            if s.get("status") != "released":
                continue
            codename = s.get("release_codename") or s.get("release") or ""
            fixed = (s.get("description") or "").strip()
            if not fixed:
                continue
            out.append({
                "kind": "fix",
                "title": f"{name} ({codename})"[:300],
                "detail": None,
                "kb": None,
                "fixed_build": fixed[:120],
                "url": None,
            })
    # USN advisories
    for usn in (d.get("notices_ids") or []):
        out.append({
            "kind": "advisory",
            "title": str(usn)[:300],
            "detail": None,
            "kb": None,
            "fixed_build": None,
            "url": f"https://ubuntu.com/security/notices/{usn}",
        })
    mit = (d.get("mitigation") or "").strip()
    if mit:
        out.append({"kind": "mitigation", "title": "Mitigation",
                    "detail": _clean(mit)[:4000], "kb": None,
                    "fixed_build": None, "url": None})
    return out


def fetch_redhat(cve_id: str) -> List[dict]:
    try:
        r = httpx.get(REDHAT_URL.format(cve=cve_id.upper()), headers=UA,
                      timeout=HTTP_TIMEOUT, follow_redirects=True)
        if r.status_code == 404:
            return []
        r.raise_for_status()
        d = r.json()
    except (httpx.HTTPError, ValueError) as e:
        logger.debug("redhat fetch failed for %s: %s", cve_id, e)
        return []

    out: List[dict] = []
    seen = set()
    for a in d.get("affected_release", []) or []:
        adv = a.get("advisory")
        pkg = a.get("package")
        if not adv:
            continue
        key = (adv, pkg)
        if key in seen:
            continue
        seen.add(key)
        out.append({
            "kind": "fix",
            "title": f"{adv}: {pkg}"[:300] if pkg else str(adv)[:300],
            "detail": (a.get("product_name") or None),
            "kb": adv,
            "fixed_build": (pkg or None),
            "url": f"https://access.redhat.com/errata/{adv}",
        })
    for field, label in (("mitigation", "Mitigation"), ("statement", "Statement")):
        val = (d.get(field) or "").strip()
        if val:
            out.append({"kind": "mitigation" if field == "mitigation" else "advisory",
                        "title": label, "detail": _clean(val)[:4000],
                        "kb": None, "fixed_build": None, "url": None})
    return out


_TAG_RE = re.compile(r"<[^>]+>")


def _clean(text: str) -> str:
    return re.sub(r"[ \t]{2,}", " ", _TAG_RE.sub(" ", text)).strip()


# ============================================================
# orchestration
# ============================================================

OSV_URL = "https://api.osv.dev/v1/vulns/{cve}"
# Reference URLs worth surfacing as advisories (distro errata / advisories
# across ecosystems). OSV aggregates many sources, so this is the gap-filler.
_ADVISORY_HINTS = ("errata", "/security/notices", "usn-", "rhsa", "dsa-",
                   "dla-", "ghsa", "/advisories/", "suse.com/security",
                   "alas", "rocky", "almalinux", "debian.org/security")
_HEX40 = re.compile(r"^[0-9a-f]{16,}$", re.I)


def fetch_osv(cve_id: str) -> List[dict]:
    """OSV.dev aggregator (Debian, Alpine, Rocky, Alma, SUSE, language
    ecosystems, ...). Augments the vendor providers + fills gaps for
    distros they don't cover. Stored under source='osv'."""
    try:
        r = httpx.get(OSV_URL.format(cve=cve_id.upper()), headers=UA,
                      timeout=HTTP_TIMEOUT, follow_redirects=True)
        if r.status_code == 404:
            return []
        r.raise_for_status()
        d = r.json()
    except (httpx.HTTPError, ValueError) as e:
        logger.debug("osv fetch failed for %s: %s", cve_id, e)
        return []

    out: List[dict] = []
    seen_fix = set()
    for a in d.get("affected", []) or []:
        pkg = a.get("package", {}) or {}
        eco = pkg.get("ecosystem")
        name = pkg.get("name")
        if not eco or not name:
            continue  # git-only / upstream entry → no actionable distro fix
        for rg in a.get("ranges", []) or []:
            for ev in rg.get("events", []) or []:
                fixed = ev.get("fixed")
                if not fixed or _HEX40.match(str(fixed)):
                    continue  # skip commit-hash "fixes"
                key = (eco, name, fixed)
                if key in seen_fix:
                    continue
                seen_fix.add(key)
                out.append({
                    "kind": "fix",
                    "title": f"{name} ({eco})"[:300],
                    "detail": None, "kb": None,
                    "fixed_build": str(fixed)[:120],
                    "url": None,
                })
    # advisory references (deduped, capped)
    adv_seen = set()
    for ref in d.get("references", []) or []:
        url = (ref.get("url") or "").strip()
        if not url:
            continue
        low = url.lower()
        if not any(h in low for h in _ADVISORY_HINTS):
            continue
        if url in adv_seen:
            continue
        adv_seen.add(url)
        out.append({"kind": "advisory", "title": url.split("/")[2][:300] if "//" in url else "advisory",
                    "detail": None, "kb": None, "fixed_build": None, "url": url})
        if len(adv_seen) >= 8:
            break
    return out


def enrich_cve_linux(db: Session, cve_id: str, os_name: Optional[str]) -> int:
    """Fetch + cache CVE remediations for a non-Windows host: the matching
    vendor provider (Ubuntu USN / RHEL-family errata) when the OS is known,
    PLUS OSV.dev as an additional source (own block, never overwrites
    vendor). Returns total rows stored. Caller commits.
    """
    cve = cve_id.upper()
    if not cve.startswith("CVE-"):
        return 0
    now = datetime.now()
    total = 0

    jobs = []  # (source, rows)
    provider = provider_for_os(os_name)
    if provider:
        jobs.append((provider, fetch_ubuntu(cve) if provider == "ubuntu" else fetch_redhat(cve)))
    jobs.append(("osv", fetch_osv(cve)))

    for source, rows in jobs:
        # Replace existing rows for this (cve, source) — keeps it current.
        db.query(CveRemediation).filter(
            CveRemediation.cve_id == cve, CveRemediation.source == source,
        ).delete(synchronize_session=False)
        for r in rows:
            db.add(CveRemediation(
                cve_id=cve, source=source, kind=r["kind"],
                title=r.get("title"), detail=r.get("detail"), kb=r.get("kb"),
                fixed_build=r.get("fixed_build"), url=r.get("url"), fetched_at=now,
            ))
        total += len(rows)
    return total


def has_cached(db: Session, cve_id: str, provider: str) -> bool:
    return db.query(
        db.query(CveRemediation).filter(
            CveRemediation.cve_id == cve_id.upper(),
            CveRemediation.source == provider,
        ).exists()
    ).scalar()
