#!/usr/bin/env python3
"""Inject RPR coverage-analysis annotations into gencov HTML.

Reads the workflow journal (verify results joined with analyze results to recover
file names), then inserts a collapsible <details> box under each uncovered
(modified-uncovered) source line, stating reachability + test method / reason.

Re-runnable: each HTML file is restored from a <file>.orig backup first.
"""
import json, re, glob, collections, html as H, os, sys

JOURNAL = sys.argv[1] if len(sys.argv) > 1 else \
    "/home/assam/.claude/projects/-home-assam-postgres/0eb3e686-f6f0-4088-9c00-42eca9447c9d/subagents/workflows/wf_780efe52-7df/journal.jsonl"
HTML_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "html")

# ---- 1. load workflow data ------------------------------------------------
analyze = {}                              # (func, lines) -> file
func2file = collections.defaultdict(set)
verify = {}                              # (func, lines) -> verify record
for line in open(JOURNAL):
    try:
        o = json.loads(line)
    except Exception:
        continue
    if o.get("type") != "result":
        continue
    r = o.get("result")
    if not isinstance(r, dict):
        continue
    if "blocks" in r and r.get("file"):
        for b in r["blocks"]:
            analyze[(b["functionName"], b["lines"])] = r["file"]
            func2file[b["functionName"]].add(r["file"])
    elif "reachable" in r:
        verify[(r.get("functionName"), r.get("lines"))] = r


def expand(s):
    out = []
    for part in re.split(r"[ ,]+", (s or "").strip()):
        m = re.match(r"^(\d+)(?:-(\d+))?$", part)
        if m:
            a = int(m.group(1)); b = int(m.group(2)) if m.group(2) else a
            out += list(range(a, b + 1))
    return out


# Group verify records by (file, func, reachable), then split each group into
# "flows": consecutive uncovered blocks with NO executed (covered) line between
# them merge into one box (straight-line flow / same basic block); an executed
# line in between means a real branch, so they stay separate. Reachable and
# unreachable blocks never merge with each other.
raw = collections.defaultdict(list)      # (file, func, reach) -> [(lo, hi, lines, v)]
for (func, lines), v in verify.items():
    file = analyze.get((func, lines))
    if not file:
        ff = func2file.get(func)
        file = list(ff)[0] if ff and len(ff) == 1 else None
    if not file:
        continue
    lns = expand(lines)
    if not lns:
        continue
    raw[(file, func, v["reachable"])].append((min(lns), max(lns), lines, v))


def _read_src(file):
    h = os.path.join(HTML_ROOT, file + ".html")
    o = h + ".orig"
    p = o if os.path.exists(o) else h
    return open(p).read() if os.path.exists(p) else ""


covered_map = {}                         # file -> set of executed line numbers
for (file, _, _) in raw:
    if file not in covered_map:
        covered_map[file] = set(int(m) for m in re.findall(
            r'modified-covered">\s*<td class="line-num">(\d+)<', _read_src(file)))

ann = collections.defaultdict(dict)      # file -> {anchor_line: payload}
for (file, func, reach), items in raw.items():
    items.sort()
    cov = covered_map.get(file, set())
    seg, segs, prev_hi = [], [], None
    for lo, hi, lines, v in items:
        if prev_hi is not None and any(l in cov for l in range(prev_hi + 1, lo)):
            segs.append(seg); seg = []
        seg.append((lines, v)); prev_hi = hi
    if seg:
        segs.append(seg)
    for s in segs:
        alllines = sorted({ln for ls, _ in s for ln in expand(ls)})
        ann[file][alllines[0]] = {"func": func, "reachable": reach, "subs": s}

CSS = """
<style id="rpr-annot-css">
.rpr-annot td { padding: 0 !important; }
.rpr-annot-cell { padding: 2px 8px 2px 80px !important; background: #fbfbfd;
    border-bottom: 1px solid #e9ecef; }
.rpr-annot summary { cursor: pointer; font-size: 0.82em; list-style: none;
    font-family: -apple-system, sans-serif; padding: 2px 0; }
.rpr-annot summary::-webkit-details-marker { display: none; }
.rpr-annot summary:before { content: "\\25B6"; color: #adb5bd; margin-right: 6px;
    font-size: 0.8em; }
.rpr-annot[open] summary:before { content: "\\25BC"; }
.rpr-badge { padding: 1px 7px; border-radius: 3px; color: #fff; font-weight: 700;
    font-size: 0.8em; margin-right: 6px; }
.rpr-reach .rpr-badge { background: #2f9e44; }
.rpr-unreach .rpr-badge { background: #e8590c; }
.rpr-meta { color: #495057; font-size: 0.95em; }
.rpr-meta code { color: #7048e8; background: #f3f0ff; padding: 0 4px; border-radius: 3px; }
.rpr-annot-body { margin: 4px 0 6px 18px; font-size: 0.84em; color: #212529;
    font-family: -apple-system, sans-serif; }
.rpr-annot-body b { color: #1864ab; }
.rpr-reach.rpr-annot-cell, .rpr-annot.rpr-reach .rpr-annot-body { }
.rpr-annot-body pre { white-space: pre-wrap; word-break: break-word; background: #f1f3f5;
    padding: 6px 8px; border-radius: 4px; margin: 3px 0; font-size: 0.95em;
    border-left: 3px solid #ced4da; }
.rpr-idx { font-size: 0.9em; color: #495057; margin: 2px 0 8px; line-height: 1.7; }
.rpr-idx code { color: #862e9c; background: #f8f0fc; padding: 0 4px; border-radius: 3px; }
.rpr-rep { color: #868e96; font-weight: 400; font-size: 0.9em; }
</style>
"""

CLS_LABEL = {"testable": "Testable", "hard-to-reach": "Hard to reach",
             "defensive-unreachable": "Defensive (unreachable)", "dead-code": "Dead code"}


SQL_CLAUSE = (r'(?:SELECT|FROM|WHERE|WINDOW|PATTERN|DEFINE|SUBSET|MEASURES|'
              r'AFTER MATCH|PARTITION BY|ORDER BY|GROUP BY|ROWS BETWEEN|'
              r'RANGE BETWEEN|GROUPS BETWEEN|HAVING|UNION ALL|UNION|VALUES|'
              r'PREPARE|EXECUTE|DEALLOCATE|INSERT INTO|CREATE TABLE|'
              r'CREATE TEMP TABLE|CREATE TEMP|DROP TABLE|RESET|SET)\b')


def fmt(t):
    """Improve readability: put each SQL clause / statement and each prose
    sentence on its own line -- a single newline, no blank lines."""
    t = t or ""
    # some agent outputs embedded literal "\n" (backslash+n) instead of real
    # newlines; turn those into actual newlines first
    t = t.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\t", "    ")
    # break before each SQL clause, collapsing any surrounding whitespace
    # (existing newline + indentation) into a single newline
    t = re.sub(r'[ \t]*\n?[ \t]+(?=' + SQL_CLAUSE + r')', '\n', t)
    t = re.sub(r';[ \t]*\n?[ \t]*', ';\n', t)                # one newline after ';'
    # one prose sentence per line, but NOT after a list number like "1." / "2."
    t = re.sub(r'(?<![0-9]\.)(?<=[.:]) +(?=[A-Z(`"\'])', '\n', t)
    t = re.sub(r'[ \t]+\n', '\n', t)                         # strip trailing spaces
    t = re.sub(r'\n{3,}', '\n\n', t)                         # at most one blank line
    return t


def render_single(reach, v):
    parts = []
    if reach:
        body = v.get("refinedTest") or v.get("verdict", "")
        parts.append(f'<b>How to test</b><pre>{H.escape(fmt(body))}</pre>')
        if v.get("verdict") and v.get("refinedTest"):
            parts.append(f'<b>Verdict</b><pre>{H.escape(fmt(v["verdict"]))}</pre>')
    else:
        if v.get("verdict"):
            parts.append(f'<b>Reason</b><pre>{H.escape(fmt(v["verdict"]))}</pre>')
        if v.get("refinedFix"):
            parts.append(f'<b>Recommended fix</b><pre>{H.escape(fmt(v["refinedFix"]))}</pre>')
    return "".join(parts)


def box(p):
    subs = sorted(p["subs"], key=lambda s: (expand(s[0]) or [0])[0])
    reach = p["reachable"]
    rcls = "rpr-reach" if reach else "rpr-unreach"
    badge = "Reachable" if reach else "Unreachable"
    uniqcls = list(dict.fromkeys(v["finalClassification"] for _, v in subs))
    cls_label = (CLS_LABEL.get(uniqcls[0], uniqcls[0]) if len(uniqcls) == 1
                 else "mixed: " + " / ".join(CLS_LABEL.get(c, c) for c in uniqcls))
    alllines = sorted({ln for ls, _ in subs for ln in expand(ls)})
    lo, hi = alllines[0], alllines[-1]
    rangestr = f"{lo}" if lo == hi else f"{lo}-{hi}"
    nlines = len(alllines)
    uniqconf = list(dict.fromkeys(v["confidence"] for _, v in subs))
    confstr = uniqconf[0] if len(uniqconf) == 1 else "mixed"
    summary = (f'<span class="rpr-badge">{badge}</span>'
               f'<span class="rpr-meta">{cls_label} &middot; confidence {H.escape(confstr)} '
               f'&middot; <code>{H.escape(p["func"])} @{rangestr}</code>'
               + (f' &middot; {nlines} lines' if nlines > 1 else "") + '</span>')
    if len(subs) == 1:
        body = render_single(reach, subs[0][1])
    else:
        # straight-line flow spanning several uncovered lines: show a per-line
        # classification index + one representative explanation (same flow ->
        # same test for reachable, same root cause for unreachable).
        idx = " &middot; ".join(
            f'<code>@{H.escape(ls)}</code> {CLS_LABEL.get(v["finalClassification"], v["finalClassification"])}'
            for ls, v in subs)
        parts = [f'<div class="rpr-idx"><b>Covers</b> {idx}</div>']
        if reach:
            rep = max((v for _, v in subs),
                      key=lambda v: len(v.get("refinedTest", "") or v.get("verdict", "")))
            tbody = rep.get("refinedTest") or rep.get("verdict", "")
            parts.append(f'<b>How to test</b> <span class="rpr-rep">(one test covers '
                         f'this flow)</span><pre>{H.escape(fmt(tbody))}</pre>')
        else:
            rep_v = max((v for _, v in subs), key=lambda v: len(v.get("verdict", "")))
            rep_f = max((v for _, v in subs), key=lambda v: len(v.get("refinedFix", "")))
            if rep_v.get("verdict"):
                parts.append(f'<b>Reason</b> <span class="rpr-rep">(representative of '
                             f'{len(subs)} blocks &mdash; same root cause)</span>'
                             f'<pre>{H.escape(fmt(rep_v["verdict"]))}</pre>')
            if rep_f.get("refinedFix"):
                parts.append(f'<b>Recommended fix</b><pre>{H.escape(fmt(rep_f["refinedFix"]))}</pre>')
        body = "".join(parts)
    return (f'<tr class="rpr-annot {rcls}"><td></td>'
            f'<td colspan="3" class="rpr-annot-cell">'
            f'<details class="rpr-annot {rcls}"><summary>{summary}</summary>'
            f'<div class="rpr-annot-body">{body}</div></details></td></tr>\n')


ROW_RE = re.compile(
    r'(<tr class="modified-uncovered">\s*<td class="line-num">(\d+)</td>.*?</tr>)',
    re.DOTALL)

total = 0
for file, lines in sorted(ann.items()):
    h = os.path.join(HTML_ROOT, file + ".html")
    orig = h + ".orig"
    if not os.path.exists(h):
        print(f"  SKIP (no html): {file}")
        continue
    if not os.path.exists(orig):
        os.rename(h, orig)
        src = open(orig).read()
    else:
        src = open(orig).read()           # always rebuild from pristine backup
    # inject CSS once, before </style> closing of first style block's end -> just after <head>... use </title>
    if "rpr-annot-css" not in src:
        src = src.replace("</head>", CSS + "</head>", 1)

    def repl(m):
        global total
        ln = int(m.group(2))
        rec = lines.get(ln)               # only anchor lines carry a payload
        if not rec:
            return m.group(1)
        total += 1
        return m.group(1) + "\n" + box(rec)

    out = ROW_RE.sub(repl, src)
    open(h, "w").write(out)
    print(f"  {file.split('/')[-1]:24s} boxes injected: {sum(1 for ln in lines if ln)}")

print(f"\nTotal annotation boxes injected: {total}")
print(f"annotations.json written: {len(ann)} files")
json.dump({f: {str(k): v for k, v in d.items()} for f, d in ann.items()},
          open(os.path.join(os.path.dirname(HTML_ROOT), "annotations.json"), "w"),
          ensure_ascii=False, indent=1)
