"""Mail-V2 finding-bucket renderers.

Separates FAIL items into three buckets — the user's design constraint:

  hard_fail        public + evidence  → 🔴 Kritische Befunde
  manual_review    public, no evidence → 🔍 Manuelle Prüfung
  internal_reminder internal process   → 💼 Reminder (NEVER a fail)

The MC-DB stays as-is. If the LLM-Plausibility phase has already run
it stamps `c.llm_title` / `c.llm_recommendation` / `c.llm_severity`
onto each check; the renderer picks those up when present, otherwise
falls back to the original MC label verbatim. No question-form
rewriting here — that's the LLM-phase's job.
"""

from __future__ import annotations

from html import escape as h

from ._actions import action_for_finding
from ._label_norm import classify_check
from ._scope_filter import (
    filter_out_of_scope,
    get_last_drop_stats,
)
from ._style import (
    SZ_SMALL,
    TEXT,
    TEXT_MUTED,
    card,
    chip,
    section,
)


def _strip_qmark(s: str) -> str:
    """Normalise a string for dedup comparison."""
    return (s or "").strip().rstrip("?").strip().lower()


def _is_dup(a: str, b: str) -> bool:
    """True when a and b carry essentially the same content."""
    aa = _strip_qmark(a)
    bb = _strip_qmark(b)
    if not aa or not bb:
        return False
    if aa == bb:
        return True
    short, long = sorted((aa, bb), key=len)
    return short and short in long and len(short) > 30


def _collect_three_buckets(state: dict) -> tuple[list[dict], list[dict],
                                                  list[dict]]:
    """Split all FAIL items into the three buckets."""
    hard: list[dict] = []
    manual: list[dict] = []
    internal: list[dict] = []

    business_scope = state.get("business_scope") or set()
    for r in state.get("results") or []:
        # Drop sector-specific MCs that don't apply to this business
        scoped = filter_out_of_scope(
            getattr(r, "checks", []) or [], business_scope,
        )
        for c in scoped:
            sev = (getattr(c, "severity", "") or "").upper()
            # LLM-plausibility may downgrade — read llm_severity if set
            llm_sev = (getattr(c, "llm_severity", "") or "").upper()
            effective_sev = llm_sev or sev
            if effective_sev not in ("CRITICAL", "HIGH", "MEDIUM"):
                continue
            if getattr(c, "passed", True) or getattr(c, "skipped", False):
                continue
            # LLM may flag a finding as not plausible → drop
            if getattr(c, "llm_drop", False):
                continue
            bucket = classify_check(c)
            raw_label = getattr(c, "label", "")
            llm_title = getattr(c, "llm_title", "") or ""
            llm_recommendation = getattr(c, "llm_recommendation", "") or ""
            title = (llm_title or raw_label)[:200]
            hint = (getattr(c, "hint", "") or "")[:500]
            matched = (getattr(c, "matched_text", "") or "")[:400]
            action = action_for_finding(
                getattr(c, "id", ""), effective_sev, raw_label, hint,
            )
            entry = {
                "sev": effective_sev,
                "id": getattr(c, "id", ""),
                "title": title,
                "raw_label": raw_label,
                "hint": hint,
                "matched": matched,
                "llm_recommendation": llm_recommendation,
                "doc": getattr(r, "label", ""),
                "reg": getattr(c, "regulation", "") or "",
                "action": action.to_dict() if action else None,
            }
            if bucket == "hard_fail" and effective_sev in ("CRITICAL", "HIGH"):
                hard.append(entry)
            elif bucket == "internal_reminder":
                internal.append(entry)
            else:
                manual.append(entry)

    # B1 reachability (always hard if HIGH — directly observed)
    rb1 = state.get("reachability_finding") or {}
    if (rb1.get("severity") or "").upper() == "HIGH" and not rb1.get("passed"):
        notes = " · ".join(rb1.get("notes") or [])
        hard.append({
            "sev": "HIGH",
            "id": rb1.get("check_id", "COOKIE-CONSENT-UX-001"),
            "title": "Mobile Consent-Reachability — kein Reopen-Link im Footer",
            "raw_label": "Mobile Consent-Reachability",
            "hint": notes,
            "matched": "Footer-Scan: 0 Reopen-Anchor",
            "llm_recommendation": "",
            "doc": "Website-Footer",
            "reg": "DSGVO Art. 7 Abs. 3",
            "action": {"title": "Cookie-Einstellungen-Link im Footer ergänzen",
                       "target": "Website-Footer (alle Seiten)",
                       "detail": ("Footer-Link 'Cookie-Einstellungen' "
                                  "ergänzen, der den CMP direkt öffnet."),
                       "effort": "low"},
        })

    # B3 retention HIGH/MED fails (3-source evidence)
    for f in (state.get("retention_findings") or []):
        sev = (f.get("severity") or "").upper()
        if sev not in ("HIGH", "MEDIUM") or f.get("matches"):
            continue
        cookie = f.get("cookie_name") or "—"
        hard.append({
            "sev": sev,
            "id": "TH-RETENTION",
            "title": f"Speicherdauer-Konflikt für {cookie}",
            "raw_label": "Cookie-Speicherdauer-Konsistenz",
            "hint": (f"DSI {f.get('dsi_days')}d · Tabelle "
                     f"{f.get('table_days')}d · "
                     f"Realität {f.get('actual_days')}d"),
            "matched": (f.get("dsi_sentence") or "")[:200],
            "llm_recommendation": "",
            "doc": "Cookie-Richtlinie",
            "reg": "DSGVO Art. 13 Abs. 2 lit.a",
            "action": {"title": ("DSE / Cookie-Tabelle korrigieren "
                                 if "dsi" in (f.get("mismatch_type") or "")
                                 else "Cookie-Lifetime reduzieren"),
                       "target": "DSE + Cookie-Tabelle",
                       "detail": f"Mismatch-Typ: {f.get('mismatch_type')}",
                       "effort": "low"},
        })

    sev_rank = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}
    hard.sort(key=lambda x: (sev_rank.get(x["sev"], 9), x["title"]))
    return hard, manual, internal


def count_critical(state: dict) -> int:
    hard, _, _ = _collect_three_buckets(state)
    return len(hard)


def count_manual(state: dict) -> int:
    _, manual, _ = _collect_three_buckets(state)
    return len(manual)


def count_internal(state: dict) -> int:
    _, _, internal = _collect_three_buckets(state)
    return len(internal)


def _render_finding_card(it: dict, *, sev_key: str = "fail") -> str:
    head = (
        f'{chip(it["sev"], sev_key)}'
        f'<span style="margin-left:8px;font-weight:600;">{h(it["title"])}</span>'
    )
    meta = (
        f'<div style="font-size:{SZ_SMALL};color:{TEXT_MUTED};margin:4px 0;">'
        f'{h(it["id"])} · {h(it["doc"])} · {h(it["reg"])}</div>'
    )
    evidence = ""
    if it.get("matched"):
        evidence = (
            f'<div style="font-size:13px;color:{TEXT};margin-top:6px;'
            f'background:#fef3c7;padding:6px 8px;border-radius:4px;'
            f'border-left:2px solid #f59e0b;">'
            f'<strong>Beobachtet:</strong> <em>{h(it["matched"])}</em></div>'
        )
    hint = ""
    if it.get("hint") and not _is_dup(it.get("hint"), it.get("title")):
        hint = (
            f'<div style="font-size:13px;color:{TEXT_MUTED};margin-top:6px;">'
            f'{h(it["hint"])}</div>'
        )
    action_html = ""
    a = it.get("action")
    if a:
        # Skip action.target rendering when it duplicates the title
        target = "" if _is_dup(a.get("target", ""), it.get("title")) \
            else a.get("target", "")
        # Skip action.detail when it duplicates hint or title
        detail = a.get("detail", "")
        if _is_dup(detail, it.get("hint")) or _is_dup(detail, it.get("title")):
            detail = ""
        target_html = (
            f' <span style="color:{TEXT_MUTED};">({h(target)})</span>'
            if target else ""
        )
        detail_html = (
            f'<div style="margin-top:4px;color:{TEXT};">{h(detail)}</div>'
            if detail else ""
        )
        action_html = (
            f'<div style="font-size:13px;margin-top:8px;background:#dcfce7;'
            f'padding:8px 10px;border-radius:4px;border-left:2px solid #16a34a;">'
            f'<strong>→ {h(a["title"])}</strong>{target_html}'
            f'{detail_html}'
            f'</div>'
        )
    llm_html = ""
    if it.get("llm_recommendation"):
        llm_html = (
            f'<div style="font-size:13px;margin-top:8px;background:#dbeafe;'
            f'padding:8px 10px;border-radius:4px;border-left:2px solid #3b82f6;">'
            f'<strong>🤖 LLM-Plausibility:</strong> '
            f'{h(it["llm_recommendation"])}</div>'
        )
    return card(head + meta + evidence + hint + action_html + llm_html,
                sev=sev_key)


def render_critical(state: dict) -> str:
    hard, _, _ = _collect_three_buckets(state)
    if not hard:
        body = (
            '<p style="margin:0;color:#15803d;">'
            'Keine HIGH/CRITICAL-Befunde mit harter Evidenz im aktuellen Lauf.'
            '</p>'
        )
        return section("✅ 1. Kritische Befunde", body, sev="pass",
                       anchor="critical")
    cards = [_render_finding_card(it, sev_key="fail") for it in hard]
    intro = ('<p style="margin:0 0 8px;color:' + TEXT_MUTED + ';font-size:13px;">'
             'Findings mit direkt beobachtbarer Evidenz (öffentliche Daten). '
             'Pro Befund: Was wir geprüft haben · Beobachtung · Was zu tun ist.'
             '</p>')
    return section(f"🔴 1. Kritische Befunde ({len(hard)})",
                   intro + "".join(cards), sev="fail", anchor="critical")


def render_manual_review(state: dict) -> str:
    _, manual, _ = _collect_three_buckets(state)
    drop_stats = get_last_drop_stats()
    if not manual:
        if drop_stats.get("count"):
            note = ('<p style="margin:0;color:#64748b;font-size:13px;">'
                    f'Keine manuell zu prüfenden Punkte. '
                    f'Branchen-spezifische MCs ausgefiltert: '
                    f'{drop_stats["count"]} '
                    f'({", ".join(f"{k}:{v}" for k,v in drop_stats["by_prefix"].items())})'
                    '</p>')
            return section("✅ 2. Manuelle Prüfung", note, sev="pass",
                           anchor="manual")
        return ""
    cards = [_render_finding_card(it, sev_key="warn") for it in manual]
    intro = ('<p style="margin:0 0 8px;color:' + TEXT_MUTED + ';font-size:13px;">'
             'Diese Punkte sind öffentlich prüfbar, aber unser Audit konnte '
             'sie nicht eindeutig feststellen — Hinweis: Original-MC-Frage. '
             'Empfehlung: manuell beim Mandanten/DSB klären. '
             'Die LLM-Plausibilitätsprüfung hilft Frage→Aussage zu wandeln '
             '(siehe 🤖-Block pro Finding falls schon gelaufen).</p>')
    return section(f"🔍 2. Manuelle Prüfung erforderlich ({len(manual)})",
                   intro + "".join(cards), sev="warn", anchor="manual")


def render_internal_reminders(state: dict) -> str:
    _, _, internal = _collect_three_buckets(state)
    if not internal:
        return ""
    cards = [_render_finding_card(it, sev_key="info") for it in internal]
    intro = ('<p style="margin:0 0 8px;color:' + TEXT_MUTED + ';font-size:13px;">'
             'Interne Prozesse (TOM, DSFA, AVV, Löschkonzept, Schulungen, '
             'Incident-Response, VVT) sind von außen nicht prüfbar. '
             '<strong>Dies sind Reminder — kein Befund über die Website.</strong> '
             'Beim Mandanten die Existenz + Aktualität der Dokumente verifizieren.'
             '</p>')
    return section(f"💼 3. Interne Prozesse — Reminder ({len(internal)})",
                   intro + "".join(cards), sev="info", anchor="internal")
