"""
Samsung Security Maintenance Release (SMR) per-CVE detection.

For Samsung Android devices, security.samsungmobile.com's yearly page is a
MORE PRECISE source than the raw Google Android Security Bulletin (ASB):
Samsung explicitly excludes CVEs that don't apply to its own devices/chipsets
("Not applicable to Samsung devices") and adds Samsung Semiconductor-specific
fixes. Using raw ASB for a Samsung device produces false positives on
chipset-specific CVEs Samsung's own page says don't apply
(e.g. CVE-2025-59604 — Qualcomm-only, explicitly not-applicable to Samsung).

The page ignores its own year/month query params in the sense that it always
serves the FULL requested year's content server-side (all ~12 SMR sections
are present in the raw HTML — the accordion UI is pure client-side CSS/JS,
it doesn't gate what's delivered), so ?year=YYYY is fetched once and cached,
covering every month of that year.

Falls back to the raw-ASB scanner (android_cve_service) for months this page
doesn't cover (very old dates, or a fetch failure).
"""
from __future__ import annotations

import json
import logging
import re
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple

from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)

_SMR_URL = "https://security.samsungmobile.com/securityUpdate.smsb"
_CACHE_PREFIX = "smr_year_v1_"
_CACHE_TTL = timedelta(hours=24)  # the current year gains a new SMR monthly
_MONTHS = {"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4, "MAY": 5, "JUN": 6,
           "JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10, "NOV": 11, "DEC": 12}
_CVE_RE = re.compile(r"CVE-\d{4}-\d{4,7}")
_SEV_LIST_RE = {
    sev: re.compile(rf'<strong><font[^>]*>{sev}</font></strong><br\s*/?>([^<]*)', re.I)
    for sev in ("Critical", "High")
}
_NA_RE = re.compile(r"Not applicable to Samsung devices</font></strong><br\s*/?>([^<]*)", re.I)
_SEM_HEADER_RE = re.compile(r"Samsung Semiconductor patch is also included", re.I)


def _parse_smr_html(html: str) -> Dict[Tuple[int, int], List[Tuple[str, str]]]:
    """→ {(year, month): [(cve, severity)]}. severity is 'critical'|'high'.
    Google Critical/High minus the "Not applicable" list, plus the Samsung
    Semiconductor Critical/High list (Samsung's own chipset-fix scope)."""
    positions = [(m.start(), m.group(1), m.group(2))
                 for m in re.finditer(r"SMR-([A-Z]{3})-(\d{4})", html)]
    out: Dict[Tuple[int, int], List[Tuple[str, str]]] = {}
    for i, (pos, mon, yr) in enumerate(positions):
        month = _MONTHS.get(mon)
        if not month:
            continue
        end = positions[i + 1][0] if i + 1 < len(positions) else len(html)
        block = html[pos:end]

        def sev_cves(label: str, text: str) -> List[str]:
            m = _SEV_LIST_RE[label].search(text)
            return _CVE_RE.findall(m.group(1)) if m else []

        critical = sev_cves("Critical", block)
        high = sev_cves("High", block)
        na_m = _NA_RE.search(block)
        not_applicable = set(_CVE_RE.findall(na_m.group(1))) if na_m else set()

        sem_critical: List[str] = []
        sem_high: List[str] = []
        sem_m = _SEM_HEADER_RE.search(block)
        if sem_m:
            sem_block = block[sem_m.start():sem_m.start() + 2000]
            sem_critical = sev_cves("Critical", sem_block)
            sem_high = sev_cves("High", sem_block)

        pairs: List[Tuple[str, str]] = []
        seen: set = set()
        for cve in critical:
            if cve in not_applicable or cve in seen:
                continue
            seen.add(cve)
            pairs.append((cve, "critical"))
        for cve in high:
            if cve in not_applicable or cve in seen:
                continue
            seen.add(cve)
            pairs.append((cve, "high"))
        for cve in sem_critical:
            if cve in seen:
                continue
            seen.add(cve)
            pairs.append((cve, "critical"))
        for cve in sem_high:
            if cve in seen:
                continue
            seen.add(cve)
            pairs.append((cve, "high"))
        out[(int(yr), month)] = pairs
    return out


def fetch_smr_year(db: Session, year: int) -> Optional[Dict[Tuple[int, int], List[Tuple[str, str]]]]:
    """Cached fetch+parse of one year's SMR page (covers all its months).
    None on fetch failure (caller should fall back to ASB)."""
    from app.models.setting import Setting
    key = f"{_CACHE_PREFIX}{year}"
    row = db.query(Setting).filter(Setting.key == key).first()
    if row and row.value:
        try:
            blob = json.loads(row.value)
            ts = datetime.fromisoformat(blob["ts"])
            if datetime.now() - ts < _CACHE_TTL:
                return {tuple(map(int, k.split("-"))): [tuple(p) for p in v]
                        for k, v in blob["months"].items()}
        except Exception:
            pass
    import httpx
    try:
        with httpx.Client(timeout=30.0, follow_redirects=True,
                          headers={"User-Agent": "TrueVuln/1.0"}) as c:
            r = c.get(_SMR_URL, params={"year": year})
        if r.status_code != 200:
            return None
        parsed = _parse_smr_html(r.text)
    except Exception as e:
        logger.debug("SMR fetch failed for year %s: %s", year, e)
        return None
    if not parsed:
        return None
    payload = json.dumps({
        "ts": datetime.now().isoformat(),
        "months": {f"{y}-{m}": pairs for (y, m), pairs in parsed.items()},
    })
    if row:
        row.value = payload
    else:
        db.add(Setting(key=key, value=payload, description=f"Samsung SMR {year} (cve,severity) per month"))
    db.commit()
    return parsed


def get_smr_month(db: Session, year: int, month: int) -> Optional[List[Tuple[str, str]]]:
    """CVEs Samsung's own SMR page attributes as applicable for (year, month),
    or None if that year's page couldn't be fetched or doesn't cover the
    month (caller should fall back to raw ASB)."""
    parsed = fetch_smr_year(db, year)
    if parsed is None:
        return None
    return parsed.get((year, month))
