"""Generate a FRESH random benchmark round — new designs, new web references.

Keeps the author versatile: instead of iterating on a fixed benchmark, each round
samples random cut x setting x carat x metal combos, web-searches a real product
photo for each (harness.reference's DuckDuckGo -> retailer og:image walk), and
writes eval/rounds/r<iter>.json in the benchmark schema.

  .cad-venv/bin/python harness/newround.py <iteration> [--n 5]

Then author each design (edit authored/<name>.py from the reference image) and run:
  BENCH_FILE=eval/rounds/r<iter>.json .cad-venv/bin/python harness/author.py <iter>
"""

from __future__ import annotations

import argparse
import json
import random
import sys
import time
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from harness.reference import _result_links, _og_image, UA  # noqa: E402

REFS = ROOT / "artifacts" / "eval-dashboard" / "refs"
ROUNDS = ROOT / "eval" / "rounds"

CUTS = ["round", "oval", "cushion", "elongated_cushion", "princess", "emerald",
        "pear", "marquise", "radiant", "asscher"]
ARCHETYPES = ["solitaire", "halo", "three_stone", "pavé-band solitaire", "bezel solitaire"]
METALS = ["18k_yellow_gold", "18k_white_gold", "14k_rose_gold", "platinum_950"]
CARATS = [0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0]


def _download(img_url: str, dest_base: Path) -> Path | None:
    req = urllib.request.Request(img_url, headers={"User-Agent": UA})
    try:
        with urllib.request.urlopen(req, timeout=25) as r:
            data = r.read(8_000_000)
    except Exception:
        return None
    if data[:3] == b"\xff\xd8\xff":
        ext = ".jpg"
    elif data[:8] == b"\x89PNG\r\n\x1a\n":
        ext = ".png"
    elif data[:4] == b"RIFF" and data[8:12] == b"WEBP":
        ext = ".webp"
    else:
        return None
    if len(data) < 8_000:                      # icons/trackers, not a packshot
        return None
    dest = dest_base.with_suffix(ext)
    dest.write_bytes(data)
    return dest


def find_reference(name: str, query: str) -> tuple[Path, str] | None:
    """Search the web for a product photo; download to refs/<name>.<ext>.

    DuckDuckGo throttles rapid-fire queries — pace searches and back off once on
    an empty result page before giving up.
    """
    links = _result_links(query)
    if not links:
        time.sleep(25)                          # throttle backoff, then one retry
        links = _result_links(query)
    for page in links:
        img = _og_image(page)
        if not img:
            continue
        dest = _download(img, REFS / name)
        if dest:
            return dest, page
    return None


def set_ref(iteration: int, name: str, image_url: str, page_url: str) -> int:
    """Record a hand-verified reference image for one design in the round file."""
    out = ROUNDS / f"r{iteration}.json"
    bench = json.loads(out.read_text())
    dest = _download(image_url, REFS / name)
    if not dest:
        print(f"download failed / not a raster image: {image_url}", file=sys.stderr)
        return 2
    for b in bench:
        if b["name"] == name:
            b["reference"] = str(dest.relative_to(ROOT))
            b["reference_page"] = page_url
            break
    else:
        print(f"no design named {name} in {out}", file=sys.stderr)
        return 3
    out.write_text(json.dumps(bench, indent=2, ensure_ascii=False))
    print(f"reference set: {name} -> {dest.relative_to(ROOT)}")
    return 0


def main() -> int:
    p = argparse.ArgumentParser()
    p.add_argument("iteration", type=int)
    p.add_argument("--n", type=int, default=5)
    p.add_argument("--set-ref", nargs=3, metavar=("NAME", "IMAGE_URL", "PAGE_URL"),
                   help="record a hand-verified reference for one design and exit")
    args = p.parse_args()

    REFS.mkdir(parents=True, exist_ok=True)
    ROUNDS.mkdir(parents=True, exist_ok=True)
    if args.set_ref:
        return set_ref(args.iteration, *args.set_ref)

    rng = random.Random(args.iteration)        # new sample per round, reproducible
    out = ROUNDS / f"r{args.iteration}.json"
    bench: list[dict] = json.loads(out.read_text()) if out.exists() else []
    seen: set[tuple[str, str]] = {(b["cut"], b["archetype"]) for b in bench}
    k = len(bench)
    while len(bench) < args.n and len(seen) < len(CUTS) * len(ARCHETYPES):
        cut, arch = rng.choice(CUTS), rng.choice(ARCHETYPES)
        if (cut, arch) in seen:
            continue
        seen.add((cut, arch))
        carat, metal = rng.choice(CARATS), rng.choice(METALS)
        name = f"r{args.iteration}-{k}-{cut}-{arch.replace(' ', '_').replace('é', 'e')}"
        pretty_metal = metal.replace("_", " ").replace("950", "").strip()
        query = f"{carat} carat {cut.replace('_', ' ')} {arch} engagement ring {pretty_metal}"
        print(f"[newround {args.iteration}] {name}: searching '{query}'", flush=True)
        found = find_reference(name, query)
        if not found:
            print("    no usable reference found; resampling", flush=True)
            time.sleep(6)                      # pace DDG between searches
            continue
        ref, page = found
        print(f"    reference: {ref.name}  (from {page[:80]})", flush=True)
        bench.append({
            "name": name, "cut": cut, "carat": carat, "standard": "US",
            "size": rng.choice([5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0]),
            "archetype": arch, "metal": metal, "prongs": 4,
            "band_width": round(rng.uniform(1.4, 2.0), 1),
            "band_thickness": round(rng.uniform(1.2, 1.6), 1),
            "reference": str(ref.relative_to(ROOT)),
            "reference_page": page,
            "query": query, "camera": "25:20",
        })
        k += 1
        out.write_text(json.dumps(bench, indent=2, ensure_ascii=False))
        time.sleep(6)                          # pace DDG between searches
    out.write_text(json.dumps(bench, indent=2, ensure_ascii=False))
    print(json.dumps({"round_file": str(out.relative_to(ROOT)),
                      "designs": [b["name"] for b in bench]}))
    return 0 if len(bench) >= args.n else 1


if __name__ == "__main__":
    raise SystemExit(main())
