"""Build BLIND contact sheets for hand-labelling relaxation additions.

Crops are centered on the DETECTED REGION (not the corner), padded by ~0.9x the mark
size, and resized to 240px with INTER_NEAREST -- a downscaled preview destroys a faint
mark, so nothing here may smooth. The manifest is written to a separate file that must
NOT be read until labelling is finished.

Each sheet mixes three strata in shuffled order:
  add    - the relaxation additions whose precision we are measuring
  pos    - strict-consistent detections (a mark is really there): labeller sensitivity
  clean  - verified-clean negatives (no mark can be there): labeller specificity
The two control strata are what make a low measured precision trustworthy.
"""

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

import cv2
import numpy as np
from numpy.typing import NDArray

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

CELL = 240
COLS, ROWS = 6, 3
PER = COLS * ROWS


def region_for(path: str, key: str) -> tuple[int, int, int, int] | None:
    """Re-detect to recover the mark bbox (Candidate carries no region)."""
    img = imread(path)
    if img is None:
        return None
    mark = next(m for m in wr._REGISTRY if m.key == key)
    d = mark.detect(img, provenance=True)
    return d.region if d.region else None


def crop(path: str, region: tuple[int, int, int, int] | None, pad_factor: float = 0.9) -> NDArray[Any] | None:
    img = imread(path)
    if img is None:
        return None
    h, w = img.shape[:2]
    if region:
        x, y, rw, rh = region
    else:
        return None
    px, py = int(rw * pad_factor), int(rh * pad_factor)
    x0, y0 = max(0, x - px), max(0, y - py)
    x1, y1 = min(w, x + rw + px), min(h, y + rh + py)
    c = img[y0:y1, x0:x1]
    if c.size == 0:
        return None
    s = CELL / max(c.shape[0], c.shape[1])
    return cv2.resize(c, (max(1, int(c.shape[1] * s)), max(1, int(c.shape[0] * s))), interpolation=cv2.INTER_NEAREST)


def main() -> None:
    with open(sys.argv[1]) as fh:
        items = json.load(fh)  # [{uid,path,key,stratum,conf}]
    outdir = Path(sys.argv[2])
    outdir.mkdir(parents=True, exist_ok=True)
    random.Random(1234).shuffle(items)  # noqa: S311 -- sheet ordering, not cryptography

    manifest = []
    cells = []
    for it in items:
        reg = region_for(it["path"], it["key"])
        c = crop(it["path"], reg)
        if c is None:
            continue
        cells.append((it, c))

    for si in range(0, len(cells), PER):
        chunk = cells[si : si + PER]
        sheet = np.full((ROWS * (CELL + 26), COLS * (CELL + 8), 3), 40, np.uint8)
        for i, (it, c) in enumerate(chunk):
            r, col = divmod(i, COLS)
            y0 = r * (CELL + 26) + 22
            x0 = col * (CELL + 8) + 4
            sheet[y0 : y0 + c.shape[0], x0 : x0 + c.shape[1]] = c
            label = f"{si + i:04d}"  # index ONLY -- no stratum, no confidence
            cv2.putText(sheet, label, (x0, y0 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
            manifest.append({"idx": si + i, **it})
        cv2.imwrite(str(outdir / f"sheet_{si // PER:03d}.png"), sheet)

    with open(outdir / "MANIFEST_DO_NOT_OPEN.csv", "w", newline="") as fh:
        w = csv.DictWriter(fh, fieldnames=list(manifest[0]))
        w.writeheader()
        w.writerows(manifest)
    print(f"sheets={(len(cells) + PER - 1) // PER} cells={len(cells)} -> {outdir}")


if __name__ == "__main__":
    main()
