#!/usr/bin/env python3
"""Render a triage result (JSON the agent produced) to a CLI table + HTML report.

The agent does the thinking: it reads the bug, classifies severity, traces the
root cause, and recommends tests, following SKILL.md and references/severity-rubric.md.
It writes that decision to triage.json. This script only presents it: a summary
table in the terminal and a self-contained HTML report to attach to the ticket.

Usage:
  python render.py triage.json                       # CLI table
  python render.py triage.json --html triage.html    # + HTML report

triage.json may be one object or a list of them (batch triage). Stdlib only.
"""
import argparse, json, sys, html, datetime

SEV = {  # code -> (label, css class, cli colour)
    "P0": ("Blocker", "p0", "\033[41m\033[97m"),
    "P1": ("Critical", "p1", "\033[31m"),
    "P2": ("Major", "p2", "\033[33m"),
    "P3": ("Minor", "p3", "\033[36m"),
    "P4": ("Trivial", "p4", "\033[32m"),
}
LAYERS = ["UI", "API", "Service", "DB", "Infra", "Third-party"]


def load(path):
    data = json.load(open(path))
    return data if isinstance(data, list) else [data]


def sev_code(t):
    return (t.get("triage", {}).get("severity") or "P3").upper().split()[0]


# ---------- cli ----------
def cli(bugs, color=True):
    C = {"b": "\033[1m", "d": "\033[2m", "x": "\033[0m"}
    if not color:
        C = {k: "" for k in C}
        for k in SEV:
            SEV[k] = (SEV[k][0], SEV[k][1], "")

    rows = sorted(bugs, key=lambda t: sev_code(t))
    idw = min(max([len(b.get("bug", {}).get("id", "")) for b in rows] + [6]), 12)
    titw = 34
    out = ["", f"{C['b']}Bug Triage{C['x']}  {C['d']}{len(rows)} bug(s) - {datetime.date.today()}{C['x']}"]
    hdr = f"{'BUG':<{idw}}  {'TITLE':<{titw}}  {'SEV':<4} {'CATEGORY':<12} {'LAYER':<11} {'PRIORITY':<12} DUP"
    out += ["-" * len(hdr), f"{C['b']}{hdr}{C['x']}", "-" * len(hdr)]
    for t in rows:
        code = sev_code(t)
        label, _, col = SEV.get(code, SEV["P3"])
        bug = t.get("bug", {})
        tri = t.get("triage", {})
        rc = t.get("root_cause", {})
        dup = t.get("duplicates") or []
        title = (bug.get("title", "") or "")[:titw]
        dupmark = f"{len(dup)}" if dup else "-"
        out.append(
            f"{bug.get('id',''):<{idw}}  {title:<{titw}}  "
            f"{col}{code:<4}{C['x']} {(tri.get('category','') or '')[:12]:<12} "
            f"{(rc.get('layer','') or '-')[:11]:<11} {(tri.get('sprint_priority','') or '')[:12]:<12} {dupmark}")
    out.append("-" * len(hdr))
    # legend + counts
    counts = {}
    for t in rows:
        counts[sev_code(t)] = counts.get(sev_code(t), 0) + 1
    summary = "  ".join(f"{SEV[k][2]}{k}{C['x']} {counts[k]}" for k in ("P0", "P1", "P2", "P3", "P4") if k in counts)
    out.append(f"{C['b']}Summary:{C['x']} {summary}")
    out.append(f"{C['d']}severity is advisory + justified - never inflated - dev/lead confirm{C['x']}")
    out.append("")
    return "\n".join(out)


# ---------- html ----------
def li(items):
    return "".join(f"<li>{html.escape(str(i))}</li>" for i in (items or []))


def card(t):
    e = lambda s: html.escape(str(s or ""), quote=True)
    bug = t.get("bug", {})
    tri = t.get("triage", {})
    rc = t.get("root_cause", {})
    ts = t.get("tests", {})
    dup = t.get("duplicates") or []
    code = sev_code(t)
    label, cls, _ = SEV.get(code, SEV["P3"])
    conf = tri.get("confidence", "")
    confbadge = f'<span class="conf">{e(conf)} confidence</span>' if conf else ""

    layer_dots = "".join(
        f'<span class="layer {"on" if l == rc.get("layer") else ""}">{l}</span>' for l in LAYERS)

    dup_html = ""
    if dup:
        rows = "".join(
            f'<tr><td class="mono">{e(d.get("id",""))}</td><td>{e(d.get("why",""))}</td>'
            f'<td class="mono">{e(d.get("similarity",""))}</td></tr>' for d in dup)
        dup_html = f'''<div class="sec dup"><h3>Possible duplicates ({len(dup)})</h3>
      <table class="mini"><tr><th>Ticket</th><th>Why it may be the same</th><th>Match</th></tr>{rows}</table>
      <p class="hint">Confirm before closing anything as duplicate. This is a lead, not a verdict.</p></div>'''

    return f'''<article class="bug {cls}">
  <div class="bug-head">
    <div><span class="sev {cls}">{code} {e(label)}</span> {confbadge}</div>
    <div class="bug-id mono">{e(bug.get("id",""))}</div>
  </div>
  <h2>{e(bug.get("title",""))}</h2>
  <div class="chips">
    <span class="chip">Category: <b>{e(tri.get("category",""))}</b></span>
    <span class="chip">Component: <b>{e(tri.get("component",""))}</b></span>
    <span class="chip">Priority: <b>{e(tri.get("sprint_priority",""))}</b></span>
    {f'<span class="chip">Reporter: {e(bug.get("reporter",""))}</span>' if bug.get("reporter") else ""}
  </div>

  <div class="sec"><h3>1. Triage</h3>
    <p><b>Severity {code} ({e(label)}).</b> {e(tri.get("severity_reason",""))}</p>
    <p><b>Business impact.</b> {e(tri.get("business_impact",""))}</p>
  </div>

  <div class="sec"><h3>2. Root cause</h3>
    <div class="layers">{layer_dots}</div>
    <p><b>Likely cause.</b> {e(rc.get("likely_cause",""))}</p>
    {f'<p><b>Related components.</b> {e(", ".join(rc.get("related_components",[])))}</p>' if rc.get("related_components") else ""}
    {f'<p><b>Investigate.</b></p><ol>{li(rc.get("investigation"))}</ol>' if rc.get("investigation") else ""}
    {f'<p><b>Check first:</b> {e(", ".join(rc.get("check_first",[])))}</p>' if rc.get("check_first") else ""}
  </div>

  <div class="sec"><h3>3. Test strategy</h3>
    {f'<p><b>Verification.</b> {e(ts.get("verification",""))}</p>' if ts.get("verification") else ""}
    {f'<p><b>Regression.</b></p><ul>{li(ts.get("regression"))}</ul>' if ts.get("regression") else ""}
    {f'<p><b>Edge cases.</b></p><ul>{li(ts.get("edge_cases"))}</ul>' if ts.get("edge_cases") else ""}
    {f'<p><b>Automation.</b> {e(ts.get("automation",""))}</p>' if ts.get("automation") else ""}
  </div>
  {dup_html}
</article>'''


def report(bugs):
    ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
    counts = {}
    for t in bugs:
        counts[sev_code(t)] = counts.get(sev_code(t), 0) + 1
    tiles = "".join(
        f'<div class="c {SEV[k][1]}"><div class="k">{k} {SEV[k][0]}</div><div class="v">{counts.get(k,0)}</div></div>'
        for k in ("P0", "P1", "P2", "P3", "P4"))
    cards = "\n".join(card(t) for t in sorted(bugs, key=sev_code))

    return f'''<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Bug Triage Report</title>
<style>
 :root{{--ink:#1a1c23;--sub:#5b6472;--line:#e4e7ec;--bg:#fbfaf7;--card:#fff;--accent:#7c3aed;
   --p0:#b91c1c;--p1:#dc2626;--p2:#ea580c;--p3:#ca8a04;--p4:#16a34a}}
 @media(prefers-color-scheme:dark){{:root{{--ink:#e9edf4;--sub:#9aa6b8;--line:#2a2f3a;--bg:#0d0f14;--card:#151922}}}}
 *{{box-sizing:border-box}}
 body{{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif;padding:32px 20px 60px}}
 .wrap{{max-width:940px;margin:0 auto}}
 h1{{font-size:26px;margin:0 0 4px;letter-spacing:-.02em}}
 .meta{{color:var(--sub);font-size:14px;margin-bottom:22px}}
 .cards{{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:26px}}
 .c{{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;border-top:3px solid var(--line)}}
 .c.p0{{border-top-color:var(--p0)}}.c.p1{{border-top-color:var(--p1)}}.c.p2{{border-top-color:var(--p2)}}
 .c.p3{{border-top-color:var(--p3)}}.c.p4{{border-top-color:var(--p4)}}
 .c .k{{color:var(--sub);font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600}}
 .c .v{{font-size:26px;font-weight:800;margin-top:4px}}
 article.bug{{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--line);border-radius:12px;padding:20px 22px;margin:16px 0}}
 article.p0{{border-left-color:var(--p0)}}article.p1{{border-left-color:var(--p1)}}article.p2{{border-left-color:var(--p2)}}
 article.p3{{border-left-color:var(--p3)}}article.p4{{border-left-color:var(--p4)}}
 .bug-head{{display:flex;justify-content:space-between;align-items:center;gap:12px}}
 .bug-id{{color:var(--sub);font-size:13px}}
 .mono{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}}
 .sev{{font-size:12px;font-weight:800;letter-spacing:.04em;padding:4px 10px;border-radius:7px;color:#fff}}
 .sev.p0{{background:var(--p0)}}.sev.p1{{background:var(--p1)}}.sev.p2{{background:var(--p2)}}
 .sev.p3{{background:var(--p3)}}.sev.p4{{background:var(--p4)}}
 .conf{{font-size:11px;color:var(--sub);margin-left:8px}}
 article h2{{font-size:19px;margin:12px 0 10px}}
 .chips{{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:6px}}
 .chip{{font-size:12.5px;color:var(--sub);background:var(--bg);border:1px solid var(--line);border-radius:7px;padding:4px 9px}}
 .chip b{{color:var(--ink)}}
 .sec{{border-top:1px solid var(--line);padding-top:12px;margin-top:14px}}
 .sec h3{{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);margin:0 0 8px}}
 .sec p{{margin:6px 0}} .sec ul,.sec ol{{margin:6px 0;padding-left:22px}} .sec li{{margin:3px 0}}
 .layers{{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}}
 .layer{{font-size:12px;font-weight:600;color:var(--sub);background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:3px 11px}}
 .layer.on{{background:var(--accent);color:#fff;border-color:var(--accent)}}
 table.mini{{width:100%;border-collapse:collapse;margin-top:6px;font-size:13px}}
 table.mini th{{text-align:left;color:var(--sub);font-size:11px;text-transform:uppercase;padding:6px 8px;border-bottom:1px solid var(--line)}}
 table.mini td{{padding:6px 8px;border-bottom:1px solid var(--line)}}
 .dup{{}} .hint{{color:var(--sub);font-size:12.5px;font-style:italic}}
 footer{{margin-top:26px;color:var(--sub);font-size:13px}}
</style></head><body><div class="wrap">
<h1>Bug Triage Report</h1>
<div class="meta">{len(bugs)} bug(s) - generated {ts} - severity is advisory and justified, a human confirms before assignment</div>
<div class="cards">{tiles}</div>
{cards}
<footer>Generated by the bug-triage-crew skill - The Testing Academy. Severity never inflated; every call is justified and a human owns the final decision.</footer>
</div></body></html>'''


def main():
    ap = argparse.ArgumentParser(description="Render agent triage JSON to CLI + HTML.")
    ap.add_argument("input", help="triage.json (object or list)")
    ap.add_argument("--html", metavar="FILE")
    ap.add_argument("--no-color", action="store_true")
    a = ap.parse_args()

    bugs = load(a.input)
    print(cli(bugs, color=not a.no_color and sys.stdout.isatty()))
    if a.html:
        open(a.html, "w").write(report(bugs))
        print(f"HTML report: {a.html}")


if __name__ == "__main__":
    main()
