"""Mail-V2 Cookie-Inventar — single table with per-cookie status + action.

Merges three sources:

  - declared in DSE / cookie-table (state["cmp_vendors"][i]["cookies"])
  - live in browser (state["banner_result"]["cookies_detailed"])
  - cookie_audit comparison (state["cookie_audit"]: declared/undocumented)

Status hierarchy per cookie:

  UNDOC     — in browser, NOT in declared list           HIGH
  MISMATCH  — declared with different category/duration  MED
  ORPH      — declared, NOT in browser                   LOW
  OK        — declared + in browser, values agree        PASS

Per-row fields (each `❌` when not ascertainable):
  name, vendor, category, duration, retention_grounds, country,
  third_country (bool), processing_company, sources, status, action
"""

from __future__ import annotations

from html import escape as h

from ._style import chip


# EU + EWR + CH — no third-country transfer.
EU_EEA_CH = {
    "DE", "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI",
    "FR", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
    "PL", "PT", "RO", "SK", "SI", "ES", "SE",
    "IS", "LI", "NO", "CH",
}
# Adequacy decisions (limited list — most relevant in cookie context).
ADEQUACY = {"US", "UK", "JP", "KR", "IL", "CA", "NZ", "AR", "UY", "AD"}


def _norm(s: str | None) -> str:
    return (s or "").strip().lower()


def _norm_for_match(s: str) -> str:
    """Normalised name for fuzzy matching.

    Common patterns in DSE-tables: wildcards (`_ga*`, `_ga.*`, `_pk_id.*`,
    `<random>`), trailing dots, brackets. Browser cookies often have
    a runtime suffix (`_ga_K8YL3M9T`, `__cf_bm_session_hash`). We strip
    trailing wildcards / suffix-noise so the prefix-match below works.

    IMPORTANT: leading `_`/`__` are MEANINGFUL (`__cf_bm`, `_ga`) and
    must NOT be stripped.
    """
    out = _norm(s)
    out = out.replace("*", "").replace("…", "")
    out = re.sub(r"\.\*$", "", out)
    out = re.sub(r"\.\$?$", "", out)
    out = re.sub(r"<[^>]+>", "", out)
    out = re.sub(r"\{[^}]+\}", "", out)
    return out.strip()


def _matches(decl_key: str, browser_key: str) -> bool:
    """Fuzzy match between a declared cookie name and a browser cookie.

    Rules (in priority order):
      1. exact match after normalisation
      2. declared is a PREFIX of browser (declared "_ga" matches
         browser "_ga_k8yl3m9t")
      3. browser is a PREFIX of declared (rare: declared has a
         specific variant, browser only generic — e.g. declared
         "__cf_bm_session" with browser "__cf_bm")
    """
    if not decl_key or not browser_key:
        return False
    if decl_key == browser_key:
        return True
    # Only allow prefix-match for prefixes ≥ 3 chars to avoid garbage
    # (e.g. declared "_" matching everything).
    if len(decl_key) >= 3 and browser_key.startswith(decl_key):
        return True
    if len(browser_key) >= 3 and decl_key.startswith(browser_key):
        return True
    return False


# Need re-import for the regex use above
import re  # noqa: E402


def _missing(value: str | None) -> bool:
    if value is None:
        return True
    v = str(value).strip()
    if not v:
        return True
    return v.lower() in ("—", "?", "unknown", "n/a", "tbd")


def _x_or(value: str | None) -> str:
    """Render `❌` when the value is missing, else escape + return."""
    if _missing(value):
        return '<span style="color:#dc2626;font-weight:700;" title="fehlend">❌</span>'
    return h(str(value))


def _country_third(country: str | None) -> tuple[str, bool, str | None]:
    """Return (display, is_third_country, adequacy_tag).

    is_third_country=True when outside EU/EEA/CH.
    adequacy_tag e.g. "DPF" or None.
    """
    if _missing(country):
        return ("", False, None)
    code = (country or "").strip().upper()
    # accept "Germany" → "DE" via crude mapping for the most common names
    name_map = {
        "DEUTSCHLAND": "DE", "GERMANY": "DE", "IRELAND": "IE", "IRLAND": "IE",
        "USA": "US", "UNITED STATES": "US",
    }
    code = name_map.get(code, code)
    if code in EU_EEA_CH:
        return (code, False, None)
    tag = "DPF" if code in ADEQUACY else "RISK"
    return (code, True, tag)


def _vendor_type_tag(cookie_name: str) -> str:
    """Lookup the cookie in the chatbot-KB and return a [Chat]/[Chat+AI] tag."""
    try:
        from ..chatbot_cookie_classification_check import _detect_provider, _KB
        match = _detect_provider(cookie_name)
        if not match:
            return ""
        prov_id, _pat = match
        prov = (_KB.get("providers") or {}).get(prov_id) or {}
        if prov.get("ai_capable"):
            return ' <span style="display:inline-block;background:#dbeafe;color:#1e40af;font-size:10px;padding:1px 6px;border-radius:999px;margin-left:4px;">Chat+AI</span>'
        return ' <span style="display:inline-block;background:#f1f5f9;color:#475569;font-size:10px;padding:1px 6px;border-radius:999px;margin-left:4px;">Chat</span>'
    except Exception:
        return ""


def _src_chip(in_dse: bool, in_table: bool, in_browser: bool,
              in_ocr: bool) -> str:
    parts: list[str] = []
    if in_dse:
        parts.append("DSE")
    if in_table:
        parts.append("Tabelle")
    if in_ocr:
        parts.append("OCR")
    if in_browser:
        parts.append("Browser")
    return " · ".join(parts) if parts else "—"


def _build_status(declared: bool, in_browser: bool,
                  cookie_audit_undeclared: set,
                  cookie_audit_compliant: set,
                  name_lc: str) -> tuple[str, str]:
    if name_lc in cookie_audit_undeclared or (in_browser and not declared):
        return "UNDOC", "fail"
    if declared and not in_browser:
        return "ORPH", "warn"
    if declared and in_browser:
        return "OK", "pass"
    return "—", "info"


def build_cookie_inventory(state: dict) -> tuple[list[dict], dict]:
    """Build the merged inventory + summary."""
    cmp_vendors = state.get("cmp_vendors") or []
    banner = state.get("banner_result") or {}
    cookies_detailed = banner.get("cookies_detailed") or []
    cookie_audit = state.get("cookie_audit") or {}

    # 1) Declared
    declared: dict[str, dict] = {}
    for v in cmp_vendors:
        vname = (v.get("name") or "").strip()
        vcountry = (v.get("country") or "").strip()
        vproc = (v.get("processing_company") or "").strip()
        vretention = (v.get("persistence") or "").strip()  # vendor-level
        src = (v.get("source") or "").lower()
        in_dse = "dse" in src or "table_crawled" in src
        in_table = ("table" in src or "pasted" in src
                    or "html_table" in src)
        in_ocr = "tesseract" in src or "ocr" in src
        for c in (v.get("cookies") or []):
            cname = (c.get("name") or "").strip()
            if not cname:
                continue
            key = _norm(cname)
            entry = declared.setdefault(key, {
                "name": cname,
                "vendor": vname,
                "category": "",
                "duration": "",
                "retention_grounds": "",
                "country": vcountry,
                "processing_company": vproc,
                "in_dse": False,
                "in_table": False,
                "in_ocr": False,
            })
            entry["category"] = (entry["category"]
                                 or (c.get("category") or "").strip())
            entry["duration"] = (entry["duration"]
                                 or (c.get("duration")
                                     or c.get("persistence") or "").strip())
            # cookie-level overrides if richer
            if not entry["country"] and vcountry:
                entry["country"] = vcountry
            if not entry["processing_company"] and vproc:
                entry["processing_company"] = vproc
            if not entry["retention_grounds"] and vretention:
                entry["retention_grounds"] = vretention
            entry["in_dse"] = entry["in_dse"] or in_dse
            entry["in_table"] = entry["in_table"] or in_table
            entry["in_ocr"] = entry["in_ocr"] or in_ocr

    # 2) Browser
    browser: dict[str, dict] = {}
    for c in cookies_detailed:
        cname = (c.get("name") or "").strip()
        if not cname:
            continue
        browser[_norm(cname)] = c

    # 3) cookie_audit hints
    undeclared_set: set = {
        _norm((c.get("name") if isinstance(c, dict) else c) or "")
        for c in (cookie_audit.get("undeclared_in_browser") or [])
    }
    compliant_set: set = {
        _norm((c.get("name") if isinstance(c, dict) else c) or "")
        for c in (cookie_audit.get("compliant") or [])
    }

    # Build fuzzy-match-Index: declared-key (normalised) → list of
    # browser-keys that match. Browser-key only matches ONE declared
    # entry (the longest prefix match wins) so we don't double-count.
    decl_match_index: dict[str, list[str]] = {k: [] for k in declared}
    browser_to_decl: dict[str, str] = {}
    for bkey in browser:
        bnorm = _norm_for_match(bkey)
        best = ""
        best_len = -1
        for dkey in declared:
            dnorm = _norm_for_match(dkey)
            if _matches(dnorm, bnorm) and len(dnorm) > best_len:
                best = dkey
                best_len = len(dnorm)
        if best:
            decl_match_index[best].append(bkey)
            browser_to_decl[bkey] = best

    # all_keys = declared + browser, but browser-keys that fuzzy-match
    # an existing declared entry are FOLDED into the declared row
    # (avoid double-counting them as both ORPH and UNDOC).
    matched_browser_keys = set(browser_to_decl.keys())
    all_keys = (set(declared.keys())
                | (set(browser.keys()) - matched_browser_keys))
    rows: list[dict] = []
    for key in sorted(all_keys):
        d = declared.get(key) or {}
        b = browser.get(key) or {}
        name = d.get("name") or b.get("name") or key
        vendor = (d.get("vendor")
                  or b.get("domain") or "").strip() or ""
        country = d.get("country", "")
        country_display, is_third, adq = _country_third(country)
        in_browser = (key in browser) or bool(decl_match_index.get(key))
        is_declared = key in declared
        status, sev = _build_status(
            is_declared, in_browser, undeclared_set, compliant_set, key,
        )
        sources = _src_chip(
            d.get("in_dse", False),
            d.get("in_table", False),
            in_browser,
            d.get("in_ocr", False),
        )
        rows.append({
            "name": name,
            "vendor": vendor,
            "category": d.get("category", ""),
            "duration": d.get("duration", ""),
            "retention_grounds": d.get("retention_grounds", ""),
            "country": country_display,
            "third_country": is_third,
            "third_country_tag": adq,
            "processing_company": d.get("processing_company", ""),
            "sources": sources,
            "status_code": status,
            "status_sev": sev,
            "declared": is_declared,
            "in_browser": in_browser,
        })

    order = {"UNDOC": 0, "MISMATCH": 1, "ORPH": 2, "OK": 3, "—": 4}
    rows.sort(key=lambda r: (order.get(r["status_code"], 9),
                              r["name"].lower()))

    summary = {
        "total": len(rows),
        "ok": sum(1 for r in rows if r["status_code"] == "OK"),
        "undoc": sum(1 for r in rows if r["status_code"] == "UNDOC"),
        "orph": sum(1 for r in rows if r["status_code"] == "ORPH"),
        "mismatch": sum(1 for r in rows if r["status_code"] == "MISMATCH"),
        "declared": sum(1 for r in rows if r["declared"]),
        "in_browser": sum(1 for r in rows if r["in_browser"]),
        "third_country": sum(1 for r in rows if r["third_country"]),
        "missing_country": sum(1 for r in rows if _missing(r["country"])),
        "missing_duration": sum(1 for r in rows if _missing(r["duration"])),
    }
    return rows, summary


def render_inventory_rows(rows: list[dict]) -> list[list[str]]:
    """Cell-rows for `_style.table`.

    Columns: Name | Vendor | Kat | Speicherdauer | Löschfrist |
             Sitzland | Verantwortlich | Quelle | Status
    """
    out: list[list[str]] = []
    for r in rows:
        country_html = _x_or(r["country"])
        if r["third_country"]:
            tag = r.get("third_country_tag") or "RISK"
            tag_color = "#92400e" if tag == "DPF" else "#dc2626"
            country_html += (
                f' <span style="font-size:10px;color:{tag_color};'
                f'font-weight:700;">[{tag}]</span>'
            )
        out.append([
            f'<code>{h(r["name"])}</code>{_vendor_type_tag(r["name"])}',
            h(r["vendor"]) if r["vendor"] else
              '<span style="color:#dc2626;">❌</span>',
            _x_or(r["category"]),
            _x_or(r["duration"]),
            _x_or(r["retention_grounds"]),
            country_html,
            _x_or(r["processing_company"]),
            h(r["sources"]),
            chip(r["status_code"], r["status_sev"]),
        ])
    return out


def inventory_headers() -> list[str]:
    return ["Name", "Vendor", "Kat.", "Speicherdauer", "Löschfrist",
            "Sitzland", "Verantwortlich", "Quelle", "Status"]
