# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""Source-validation HTML report.

A format distinct from the Auditor's: grouped by cell, and within each cell the
four source-validation tests in order, each with its findings. (In loaded-layer
mode every finding's cell is the validated object layer, so there is one cell
section; per-individual-.000-cell grouping would need the load to tag features
with their origin cell — see BACKLOG.)
"""
import html
from datetime import datetime, timezone

from . import checks

_ORDER = [
    ("1", checks.IN_SOURCE_NOT_CHART),
    ("2", checks.IN_CHART_NOT_SOURCE),
    ("3", checks.VALUE_MISMATCH),
    ("4", checks.IDENTITY_DISPLACED),
]

_SEV_COLOUR = {"C": "#c0392b", "E": "#d68910", "W": "#2471a3"}

_CSS = """
*{box-sizing:border-box} body{font:14px/1.5 -apple-system,Segoe UI,Roboto,
Helvetica,Arial,sans-serif;color:#222;margin:0;padding:24px;background:#fafafa}
h1{font-size:22px;margin:0 0 4px} h2{font-size:18px;margin:28px 0 6px;
border-bottom:2px solid #ddd;padding-bottom:4px}
.sub{color:#666;margin:0 0 16px}
.tiles{display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 4px}
.tile{background:#fff;border:1px solid #e2e2e2;border-radius:8px;padding:10px 16px;
min-width:90px} .tile .n{font-size:22px;font-weight:600} .tile .l{color:#777;
font-size:12px}
.test{margin:10px 0 4px} .test h3{font-size:15px;margin:14px 0 2px}
.badge{color:#fff;border-radius:4px;padding:1px 7px;font-size:12px;font-weight:600}
.zero{color:#1e8449;font-weight:600}
ul.find{margin:4px 0 12px;padding-left:22px} ul.find li{margin:1px 0}
.muted{color:#888} .det{color:#444}
.excluded{margin:8px 0 4px;padding:7px 10px;background:#fff8e1;
border:1px solid #f0e0a8;border-radius:5px;color:#6b5d2a;font-size:13px}
.exlist{margin-top:6px} .exlist ul{margin:4px 0 0 0;padding-left:20px}
.exlist li{margin:1px 0}
"""


def validator_html(report, enc_name="", source_name="", context=None):
    esc = html.escape
    context = context or {}
    enc_name = enc_name or "ENC object layer"
    gen = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")

    # Group findings by cell; source-side findings fold under the validated layer.
    by_cell = {}
    for f in report.findings:
        key = f.cell if (f.cell and f.cell != "(source)") else enc_name
        by_cell.setdefault(key, []).append(f)

    clip = context.get("clip_mode", getattr(report, "clip_mode", "none"))
    clip_label = {"coverage": "clipped to coverage polygon",
                  "bbox": "clipped to bounding box",
                  "none": "no area limit (whole source)"}.get(clip, clip)
    # Make the coverage decision explicit: which source it came from, and — when
    # it fell back to whole-source — why (so a flood of "not in chart" findings
    # is immediately explicable).
    cov_src = context.get("coverage_source")
    cov_polys = context.get("coverage_polys")
    cov_warn = context.get("coverage_warn")
    if clip == "coverage" and cov_src == "cells":
        clip_label = (f"clipped to chart coverage read from "
                      f"{cov_polys} cell polygon(s)")
    elif clip == "coverage":
        clip_label = "clipped to loaded ENC Coverage layer"
    elif cov_warn:
        clip_label = f"whole source — {cov_warn}"

    out = [f"<!doctype html><html><head><meta charset='utf-8'>"
           f"<title>Source validation — {esc(enc_name)}</title>"
           f"<style>{_CSS}</style></head><body>"]
    out.append("<h1>ENC Source-Validation Report</h1>")
    out.append(f"<p class='sub'>ENC: <b>{esc(enc_name)}</b> &nbsp;·&nbsp; "
               f"Source: <b>{esc(source_name or '—')}</b> &nbsp;·&nbsp; "
               f"{esc(clip_label)} &nbsp;·&nbsp; {gen}</p>")

    out.append("<div class='tiles'>")
    out.append(f"<div class='tile'><div class='n'>{context.get('n_chart', '—')}"
               f"</div><div class='l'>chart features</div></div>")
    out.append(f"<div class='tile'><div class='n'>{context.get('n_source', '—')}"
               f"</div><div class='l'>source features</div></div>")
    out.append(f"<div class='tile'><div class='n'>{len(report.findings)}"
               f"</div><div class='l'>findings</div></div>")
    ooa = getattr(report, "source_out_of_area", 0)
    if ooa:
        out.append(f"<div class='tile'><div class='n'>{ooa}</div>"
                   f"<div class='l'>source out of area</div></div>")
    out.append("</div>")

    # Passive disclosure of what exclusions withheld — a sign-off artifact must
    # name what was removed, since an exclusion you can't see is indistinguishable
    # from a dropped-feature bug. No interactivity; the flagging happens in the
    # GUI results list.
    ex_cls = getattr(report, "excluded_classes", []) or []
    n_cls = getattr(report, "enc_excluded_classes_count", 0)
    n_enc = getattr(report, "enc_excluded_features_count", 0)
    n_src = getattr(report, "source_excluded_count", 0)
    if ex_cls or n_cls or n_enc or n_src:
        bits = []
        if ex_cls:
            bits.append("ENC classes excluded by design: "
                        + esc(", ".join(ex_cls))
                        + (f" ({n_cls} feature{'s' if n_cls != 1 else ''})"
                           if n_cls else ""))
        if n_enc:
            bits.append(f"{n_enc} ENC feature{'s' if n_enc != 1 else ''} "
                        f"on the known-fault denylist")
        if n_src:
            bits.append(f"{n_src} source feature{'s' if n_src != 1 else ''} "
                        f"on the known-fault denylist")
        out.append("<div class='excluded'>Excluded from comparison: "
                   + "; ".join(bits) + ".")
        enc_list = getattr(report, "excluded_enc_list", []) or []
        src_list = getattr(report, "excluded_source_list", []) or []
        if enc_list or src_list:
            out.append("<div class='exlist'><b>Known faults flagged:</b><ul>")
            for e in enc_list:
                nm = esc(e.get("objnam") or "(no name)")
                ln = esc(e.get("lnam") or "")
                rsn = esc(e.get("reason") or "")
                out.append(f"<li>ENC: {nm} — LNAM <tt>{ln}</tt>"
                           + (f" — {rsn}" if rsn else "") + "</li>")
            for e in src_list:
                sid = esc(str(e.get("id") or ""))
                rsn = esc(e.get("reason") or "")
                out.append(f"<li>Source: {sid}"
                           + (f" — {rsn}" if rsn else "") + "</li>")
            out.append("</ul></div>")
        out.append("</div>")

    for cell in sorted(by_cell):
        fs = by_cell[cell]
        out.append(f"<h2>{esc(cell)} <span class='muted'>"
                   f"({len(fs)} finding{'s' if len(fs) != 1 else ''})</span></h2>")
        for num, meta in _ORDER:
            items = [f for f in fs if f.check.name == meta.name]
            colour = _SEV_COLOUR.get(meta.severity, "#555")
            out.append("<div class='test'>")
            if items:
                out.append(
                    f"<h3>Test {num} — {esc(meta.message)} "
                    f"<span class='badge' style='background:{colour}'>"
                    f"{len(items)}</span></h3>")
                out.append("<ul class='find'>")
                for f in items[:300]:
                    det = (f.detail or "").strip()
                    # the cell header already names the layer (klass); don't
                    # repeat "klass fid N" as a detail line
                    if f.fid is not None and det == f"{f.klass} fid {f.fid}":
                        det = f"fid {f.fid}"
                    if det:
                        line = esc(det)
                    elif f.fid is not None:
                        line = f"fid {esc(str(f.fid))}"
                    else:
                        line = esc(str(f.klass))
                    out.append(f"<li>{line}</li>")
                if len(items) > 300:
                    out.append(f"<li class='muted'>… and {len(items) - 300} "
                               f"more</li>")
                out.append("</ul>")
            else:
                out.append(f"<h3>Test {num} — {esc(meta.message)} "
                           f"<span class='zero'>✓ 0</span></h3>")
            out.append("</div>")

    try:
        from ..validation.engine import SIGNATURE_HTML
    except (ImportError, ValueError):
        from validation.engine import SIGNATURE_HTML
    out.append(SIGNATURE_HTML)
    out.append("</body></html>")
    return "".join(out)
