"""50-run validation harness (no Claude). For every page with a regions.json in
the last N runs: detect rules -> dateline scan -> construct article boundaries ->
validate. Writes overlays for FAILING pages and a per-paper pass/fail report.

Usage: venv/bin/python3 _test_boundaries50.py [N=50] [out_dir=/tmp/bnd50]
"""
import json
import sys
import traceback
from collections import Counter
from pathlib import Path

import cv2
from PIL import Image

from _lines import detect_separator_lines
from _boundaries import construct_boundaries
from smart_extractor import find_article_starts_by_dateline

OUT = Path("output")
DL_CACHE = Path("/tmp/dl_cache")
DL_CACHE.mkdir(parents=True, exist_ok=True)


def cached_datelines(regs, png, paper, key):
    """Datelines depend only on the (fixed) regions + page image, so cache the
    Tesseract scan to disk — re-validation after a _boundaries.py edit is then
    instant (no OCR)."""
    cf = DL_CACHE / f"{key}.json"
    if cf.exists():
        return json.loads(cf.read_text())
    dls = find_article_starts_by_dateline(regs, png, paper)
    cf.write_text(json.dumps(dls))
    return dls


def paper_of_height(h):
    if 7040 <= h <= 7060:
        return "sakshi"
    if h in (6833, 6549):
        return "andhra_jyothi"
    if h == 6422:
        return "namaste_telangana"
    return f"h{h}"


def iou(a, b):
    ix = max(0, min(a[2], b[2]) - max(a[0], b[0]))
    iy = max(0, min(a[3], b[3]) - max(a[1], b[1]))
    inter = ix * iy
    if not inter:
        return 0.0
    ua = (a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter
    return inter / ua if ua else 0.0


def validate(boxes, rmap):
    issues = []
    for bx in boxes:
        db = rmap[bx["region_id"]]["bbox"]
        bb = bx["bbox"]
        if not (bb[0] <= db[0]+2 and bb[1] <= db[1]+2 and bb[2] >= db[2]-2 and bb[3] >= db[3]-2):
            issues.append(("no-contain", bx["region_id"]))
        if bb[2]-bb[0] < 60 or bb[3]-bb[1] < 60:
            issues.append(("degenerate", bx["region_id"]))
    for i in range(len(boxes)):
        for j in range(i+1, len(boxes)):
            if iou(boxes[i]["bbox"], boxes[j]["bbox"]) > 0.25:
                issues.append(("overlap", (boxes[i]["region_id"], boxes[j]["region_id"])))
    return issues


def draw(png, lines, boxes, out_path):
    img = cv2.imread(str(png))
    for L in lines["h"]:
        cv2.line(img, (L["x1"], L["y1"]), (L["x2"], L["y2"]), (0, 0, 255), 4)
    for L in lines["v"]:
        cv2.line(img, (L["x1"], L["y1"]), (L["x2"], L["y2"]), (255, 0, 0), 4)
    for bx in boxes:
        b = bx["bbox"]
        cv2.rectangle(img, (b[0], b[1]), (b[2], b[3]), (0, 180, 0), 8)
    cv2.imwrite(str(out_path), img)


def main(n=50, out_dir="/tmp/bnd50"):
    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    runs = sorted([d for d in OUT.iterdir() if d.is_dir()],
                  key=lambda d: d.stat().st_mtime, reverse=True)[:n]

    per_paper_pages = Counter()
    per_paper_fail = Counter()
    issue_kinds = Counter()
    fail_list = []
    total_pages = 0
    total_boxes = 0
    total_dls = 0

    for run in runs:
        pages = run / "pages"
        if not pages.exists():
            continue
        for reg_path in sorted(pages.glob("page_*.regions.json")):
            png = reg_path.with_name(reg_path.name.replace(".regions.json", ".png"))
            if not png.exists():
                continue
            try:
                regs = json.loads(reg_path.read_text())["regions"]
                rmap = {r["id"]: r for r in regs}
                paper = paper_of_height(Image.open(png).height)
                lines = detect_separator_lines(png)
                dls = cached_datelines(regs, str(png), paper,
                                       f"{run.name}_{reg_path.stem}")
                if not dls:
                    continue                      # no datelines -> nothing to bound
                boxes = construct_boundaries(regs, dls, lines, lines["size"])
                issues = validate(boxes, rmap)
            except Exception:
                issues = [("exception", reg_path.name)]
                boxes, dls, lines = [], [], {"h": [], "v": []}
                traceback.print_exc()

            total_pages += 1
            total_boxes += len(boxes)
            total_dls += len(dls)
            per_paper_pages[paper] += 1
            if issues:
                per_paper_fail[paper] += 1
                for k, _ in issues:
                    issue_kinds[k] += 1
                tag = f"{run.name}_{reg_path.stem}"
                fail_list.append((tag, paper, len(dls), len(boxes), issues))
                try:
                    draw(png, lines, boxes, out_dir / f"FAIL_{tag}.png")
                except Exception:
                    pass

    print("\n==================== BOUNDARY VALIDATION (last %d runs) ====================" % n)
    print(f"{'paper':<20}{'pages':>8}{'fail':>7}{'pass%':>8}")
    for paper in sorted(per_paper_pages):
        pg = per_paper_pages[paper]
        fl = per_paper_fail[paper]
        print(f"{paper:<20}{pg:>8}{fl:>7}{100*(pg-fl)/pg:>7.0f}%")
    tot = total_pages
    fl = sum(per_paper_fail.values())
    print(f"{'TOTAL':<20}{tot:>8}{fl:>7}{100*(tot-fl)/max(1,tot):>7.0f}%")
    print(f"\ndatelines total={total_dls}  boxes total={total_boxes}  "
          f"(boxes should equal datelines: {'OK' if total_dls==total_boxes else 'MISMATCH'})")
    print(f"issue kinds: {dict(issue_kinds)}")

    if fail_list:
        print(f"\n--- {len(fail_list)} failing page(s) (overlays in {out_dir}/FAIL_*.png) ---")
        for tag, paper, nd, nb, issues in fail_list[:60]:
            ic = Counter(k for k, _ in issues)
            print(f"  [{paper}] {tag}  dls={nd} boxes={nb}  {dict(ic)}")
    # machine-readable
    (out_dir / "report.json").write_text(json.dumps({
        "pages": total_pages, "fail": fl,
        "per_paper": {p: [per_paper_pages[p], per_paper_fail[p]] for p in per_paper_pages},
        "issue_kinds": dict(issue_kinds),
        "fails": [[t, p, nd, nb, [list(x) if isinstance(x, tuple) else x for _, x in iss]]
                  for t, p, nd, nb, iss in fail_list],
    }, indent=2))
    print(f"\nreport: {out_dir/'report.json'}")


if __name__ == "__main__":
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 50
    od = sys.argv[2] if len(sys.argv) > 2 else "/tmp/bnd50"
    main(n, od)
