"""Build an UNBIASED random sample for measuring visible-mark RECALL.

Every earlier labelling round sampled where detectors FIRED, so images that every
detector missed were absent by construction and recall was unmeasurable. This round
samples at random within a provenance class and shows the labeller the corners where
a mark can physically be, so a MISSED mark is visible as such.

Design decisions that matter:

* SAMPLING FRAME is per provenance class, not the whole corpus. Recall is only
  meaningful against a denominator where the mark CAN occur: TC260 carriers for the
  ByteDance marks and the pill, Google-C2PA for the sparkle. Reporting one blended
  recall over all uploads would mostly measure how often each vendor appears.
* NATIVE RESOLUTION crops, never a downscaled whole image: a 220px preview destroys a
  faint mark (measured in an earlier round), which would inflate the miss count with
  the labeller's own blindness rather than the detector's.
* BOTH corners per image (top-left pill, bottom-right wordmark/strip/sparkle), so one
  pass adjudicates every registered mark instead of one mark per crop.
* The detector's verdict is NOT shown and is not in the sheet order -- the manifest
  holds it and must not be opened until labelling ends.
"""

from __future__ import annotations

import csv
import json
import random
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any

import cv2
import numpy as np

if TYPE_CHECKING:
    from numpy.typing import NDArray

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from remove_ai_watermarks.image_io import imread

CELL_W = 300
COLS, ROWS = 4, 3
PER = COLS * ROWS


def corner_strip(img: NDArray[Any]) -> NDArray[Any] | None:
    """Top-left and bottom-right corners stacked, at (near) native scale.

    Crop size is a fraction of the SHORT side because that is what marks scale with
    (China's GB 45438-2025 sizes the mandated label off the shortest side).
    """
    h, w = img.shape[:2]
    short = min(h, w)
    cw, ch = int(short * 0.46), int(short * 0.17)
    cw, ch = min(cw, w), min(ch, h)
    tl = img[0:ch, 0:cw]
    br = img[h - ch : h, w - cw : w]
    strip = np.vstack([tl, np.full((6, cw, 3), 90, np.uint8), br])
    s = min(1.0, CELL_W / strip.shape[1])  # never UPSCALE past native
    if s < 1.0:
        strip = cv2.resize(strip, (CELL_W, max(1, int(strip.shape[0] * s))), interpolation=cv2.INTER_AREA)
    return strip


def main() -> None:
    scan = Path(sys.argv[1])
    out = Path(sys.argv[2])
    n_tc260 = int(sys.argv[3]) if len(sys.argv) > 3 else 160
    n_google = int(sys.argv[4]) if len(sys.argv) > 4 else 80
    out.mkdir(parents=True, exist_ok=True)

    recs = [json.loads(line) for line in scan.open() if '"marks"' in line]
    seen: dict[tuple, dict] = {}
    for r in recs:  # exact-duplicate uploads share the whole NCC vector
        seen.setdefault(tuple(r.get("shape", ())) + tuple(sorted((k, m["conf"]) for k, m in r["marks"].items())), r)
    uniq = list(seen.values())

    tc = [r for r in uniq if r["cls"] == "tc260"]
    goog = [r for r in uniq if r["cls"] == "neg" and "Google" in r.get("platform", "")]
    rng = random.Random(2026)  # noqa: S311 -- sampling, not cryptography
    rng.shuffle(tc)
    rng.shuffle(goog)
    picked = [("tc260", r) for r in tc[:n_tc260]] + [("google", r) for r in goog[:n_google]]
    rng.shuffle(picked)

    manifest, cells = [], []
    for cls, r in picked:
        img = imread(r["path"])
        if img is None:
            continue
        strip = corner_strip(img)
        if strip is None:
            continue
        cells.append(strip)
        manifest.append(
            {
                "idx": len(cells) - 1,
                "uid": r["uid"],
                "cls": cls,
                "path": r["path"],
                "fired": "|".join(sorted(k for k, m in r["marks"].items() if m["strict"])),
                **{f"ncc_{k}": m["conf"] for k, m in r["marks"].items()},
            }
        )

    cell_h = max(c.shape[0] for c in cells)
    for si in range(0, len(cells), PER):
        chunk = cells[si : si + PER]
        sheet = np.full((ROWS * (cell_h + 26), COLS * (CELL_W + 8), 3), 40, np.uint8)
        for i, c in enumerate(chunk):
            rr, cc = divmod(i, COLS)
            y0, x0 = rr * (cell_h + 26) + 22, cc * (CELL_W + 8) + 4
            sheet[y0 : y0 + c.shape[0], x0 : x0 + c.shape[1]] = c
            cv2.putText(sheet, f"{si + i:03d}", (x0, y0 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
        cv2.imwrite(str(out / f"r{si // PER:02d}.png"), sheet)

    with (out / "MANIFEST_DO_NOT_OPEN.csv").open("w", newline="") as fh:
        w = csv.DictWriter(fh, fieldnames=list(manifest[0]))
        w.writeheader()
        w.writerows(manifest)
    print(f"cells={len(cells)}  sheets={(len(cells) + PER - 1) // PER} -> {out}")
    print("each cell = TOP-LEFT corner above, BOTTOM-RIGHT corner below, near native scale")


if __name__ == "__main__":
    main()
