#!/usr/bin/env python3
"""Contact sheet per project for jonathanlooman.nl.

Scans the archive folders per project, makes 360px thumbnails into contact/<slug>/,
and writes contact-sheet.html: per project every image, a radio for HERO and a
checkbox for ROW. The Export button downloads selectie.json. Then:
    python3 contact-sheet.py --apply selectie.json
copies the chosen originals into img/<slug>/ at 1800px.

Read-only on the archive. Skips files under 40 KB (icons) and caps per project.
"""
import os, sys, json, subprocess, hashlib, html
from pathlib import Path

HERE = Path(__file__).resolve().parent
ARCH = Path("/Users/jonathan/SynologyDrive/Since Today/PORTFOLIO/Archive")
PROJ = Path("/Users/jonathan/SynologyDrive/Since Today/PROJECTEN")
B02 = Path("/Volumes/Backup_02"); B03 = Path("/Volumes/Backup_03")

SOURCES = {
  "hypergarden": [ARCH/"Project HyperGarden"],
  "visions-tv": [ARCH/"Project VISIONSTV"],
  "kruyd-platform": [Path.home()/"Desktop/Kruyd - Platform", PROJ/"Kruyd", ARCH/"Project Kruyd"],
  "blockchain-my-art": [ARCH/"Project BlockchainMyArt"],
  "bright": [ARCH/"Project Bright", ARCH/"Project BRIGHTday", ARCH/"Project BrightTv"],
  "landcent": [B02/"Project Archief 02/Landcent", Path("/Users/jonathan/SynologyDrive/Since Today/KANTOOR/Retainer/Landcent")],
  "caliper": [Path("/Users/jonathan/SynologyDrive/Since Today/WEBSITE/case-material-staging/caliper"), B02/"Project Archief 02/Capiler"],
  "kabk": [B03/"Projects/KABK", ARCH/"Workshop Workshop", ARCH/"Project Future office", ARCH/"Project IMD-openday"],
  "cid": [ARCH/"Project CID"],
  "border-sessions": [ARCH/"Project BorderSessions"],
  "big-brother-awards": [ARCH/"Project Big Brother Awards"],
  "powerhouse": [ARCH/"Project PowerHouse"],
  "clubhuis": [ARCH/"Project Clubhuis", PROJ/"Fresh Prespectives"],
  "logos-and-marks": [ARCH/"Logo Types", ARCH/"Project_letters"],
  "van-bostelen": [ARCH/"Project Van Bostelen"],
  "theater-aan-het-spui": [ARCH/"Project Theater aan het Spui"],
  "art-s-cool": [ARCH/"Project Art-S-Cool"],
  "rewire": [ARCH/"Project REWIRE"],
  "ademruimte": [ARCH/"Project AdemRuimte"],
  "hcak": [ARCH/"Project HCAK"],
  "teneo": [ARCH/"Project TENEO"],
  "worldstartup": [ARCH/"Project WorldStartup"],
  "ing-fabrique": [ARCH/"Project ING Annual Review"],
  "instant-reflex": [ARCH/"Project KIM"],
  "bgt": [ARCH/"Project BGT"],
}
EXT = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".JPG", ".JPEG", ".PNG", ".TIF"}
# Confidentiality, see vertrouwelijkheid.md. Shown on the sheet while choosing.
WARN = {
  "landcent": "Investor memorandum en fundingcijfers in deze map. Klant RESTING. Alleen merkwerk zonder bedragen.",
  "caliper": "Gezondheidsdata en Medicaid-programma's. Alleen de mock uit staging.",
  "kruyd-platform": "Interne hostnamen, poorten, chef- en kwekersnamen, bestellingen. Geen echte klantdata in beeld.",
  "bgt": "Gemeentenamen en gezinsgegevens. Alleen branding en de publieke site.",
  "bright": "Ongepubliceerd redactiemateriaal; RTL heeft een merkafdeling. Alleen wat gedrukt of gelanceerd is.",
  "blockchain-my-art": "Verenigingsnamen bij geldstromen, bezoekerssaldi. Geen saldo in beeld.",
  "kabk": "Studentnamen, beoordelingen, portfolio's. Instellings-AVG. Geen herkenbare studenten.",
  "worldstartup": "Deelnemende startups en hun aanvragen. Alleen wat publiek is.",
  "ing-fabrique": "Governance-materiaal. Alleen het gepubliceerde jaarverslag, Fabrique in de credits.",
  "instant-reflex": "Fotoboek van een derde: fotograaf crediteren.",
}
MIN_BYTES = 40_000
CAP = 320

def thumbs(slug, roots):
    out = HERE/"contact"/slug; out.mkdir(parents=True, exist_ok=True)
    items = []
    seen = set()
    for root in roots:
        if not root.exists(): continue
        for p in sorted(root.rglob("*")):
            if len(items) >= CAP: break
            if p.suffix not in EXT or not p.is_file(): continue
            if "node_modules" in p.parts or p.name.startswith("."): continue
            try: sz = p.stat().st_size
            except OSError: continue
            if sz < MIN_BYTES: continue
            h = hashlib.md5(str(p).encode()).hexdigest()[:10]
            if h in seen: continue
            seen.add(h)
            t = out/f"{h}.jpg"
            if not t.exists():
                r = subprocess.run(["sips", "-s", "format", "jpeg", "-Z", "360", str(p), "--out", str(t)],
                                   capture_output=True)
                if r.returncode != 0 or not t.exists(): continue
            items.append({"id": h, "src": str(p), "thumb": f"contact/{slug}/{h}.jpg", "bytes": sz})
    return items

def build():
    data = {}
    for slug, roots in SOURCES.items():
        data[slug] = thumbs(slug, roots)
        print(f"{slug:22s} {len(data[slug]):4d}", flush=True)
    (HERE/"contact"/"index.json").write_text(json.dumps(data, ensure_ascii=False))
    secs = []
    for slug, items in data.items():
        if not items: continue
        cells = "".join(
            f'<figure data-id="{i["id"]}" title="{html.escape(i["src"])}">'
            f'<img src="{i["thumb"]}" loading="lazy" alt="">'
            f'<div class="c"><label><input type="radio" name="hero-{slug}" value="{i["id"]}">hero</label>'
            f'<label><input type="checkbox" data-row="{slug}" value="{i["id"]}">rij</label></div></figure>'
            for i in items)
        w = WARN.get(slug)
        warn = f'<p class="warn">{html.escape(w)}</p>' if w else ''
        secs.append(f'<section id="{slug}"><h2>{slug} <small>{len(items)}</small></h2>{warn}<div class="grid">{cells}</div></section>')
    page = f'''<!DOCTYPE html><html lang="nl"><head><meta charset="utf-8"><title>Contactvel jonathanlooman.nl</title>
<style>
body{{margin:0;background:#FAF8F2;color:#1A1A1A;font:14px/1.4 -apple-system,Helvetica,sans-serif}}
header{{position:sticky;top:0;background:#FAF8F2;border-bottom:1px solid rgba(0,0,0,.08);padding:10px 20px;display:flex;gap:16px;align-items:center;z-index:5;flex-wrap:wrap}}
header nav a{{margin-right:10px;font:12px ui-monospace,Menlo;color:#6B6B6B;text-decoration:none}}
header button{{font:12px ui-monospace,Menlo;padding:8px 12px;border:1px solid #1A1A1A;background:#1A1A1A;color:#FAF8F2;cursor:pointer}}
header #count{{font:12px ui-monospace,Menlo;color:#6B6B6B}}
section{{padding:30px 20px 10px}}
h2{{font:600 20px/1 -apple-system,Helvetica;margin:0 0 14px}} h2 small{{font:12px ui-monospace,Menlo;color:#6B6B6B;margin-left:8px}}
.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(184px,1fr));gap:20px}}
figure{{margin:0;background:#E4E2DC;position:relative}}
figure img{{display:block;width:100%;aspect-ratio:1;object-fit:contain;background:#fff}}
figure .c{{display:flex;gap:10px;padding:6px 8px;font:12px ui-monospace,Menlo}}
figure:has(input[type=radio]:checked){{outline:3px solid #1A1A1A}}
figure:has(input[type=checkbox]:checked){{box-shadow:inset 0 0 0 3px #4c8a3f}}
figure:has(input[type=radio]:checked) .c label:first-child{{font-weight:700}}
.warn{{background:#F3E7C8;border-left:3px solid #B8860B;padding:8px 12px;margin:0 0 14px;font:12px/1.45 ui-monospace,Menlo;max-width:100ch}}
</style></head><body>
<header><strong>Contactvel</strong><span id="count">0 gekozen</span><button id="exp">Exporteer selectie.json</button>
<nav>{"".join(f'<a href="#{s}">{s}</a>' for s in data if data[s])}</nav></header>
{"".join(secs)}
<script>
var KEY='jl-selectie';
function state(){{var st={{}};document.querySelectorAll('section').forEach(function(sec){{var slug=sec.id;
  var h=sec.querySelector('input[type=radio]:checked');var row=[].map.call(sec.querySelectorAll('input[type=checkbox]:checked'),function(i){{return i.value}});
  if(h||row.length)st[slug]={{hero:h?h.value:null,row:row}};}});return st;}}
function save(){{var st=state();localStorage.setItem(KEY,JSON.stringify(st));
  var n=Object.values(st).reduce(function(a,p){{return a+(p.hero?1:0)+p.row.length}},0);document.getElementById('count').textContent=n+' gekozen';}}
document.addEventListener('change',save);
(function restore(){{try{{var st=JSON.parse(localStorage.getItem(KEY)||'{{}}');Object.keys(st).forEach(function(slug){{var sec=document.getElementById(slug);if(!sec)return;
  if(st[slug].hero){{var r=sec.querySelector('input[type=radio][value="'+st[slug].hero+'"]');if(r)r.checked=true;}}
  st[slug].row.forEach(function(v){{var c=sec.querySelector('input[type=checkbox][value="'+v+'"]');if(c)c.checked=true;}});}});save();}}catch(e){{}}}})();
document.getElementById('exp').onclick=function(){{var b=new Blob([JSON.stringify(state(),null,2)],{{type:'application/json'}});
  var a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='selectie.json';a.click();}};
</script></body></html>'''
    (HERE/"contact-sheet.html").write_text(page)
    print("contact-sheet.html written")

def apply(sel_path):
    sel = json.load(open(sel_path)); idx = json.load(open(HERE/"contact"/"index.json"))
    for slug, pick in sel.items():
        by = {i["id"]: i["src"] for i in idx.get(slug, [])}
        out = HERE/"img"/slug; out.mkdir(parents=True, exist_ok=True)
        for f in out.glob("hero.*"): f.unlink()
        for f in out.glob("row-*"): f.unlink()
        if pick.get("hero") and pick["hero"] in by:
            subprocess.run(["sips","-s","format","jpeg","-Z","2200",by[pick["hero"]],"--out",str(out/"hero.jpg")],capture_output=True)
        for n, hid in enumerate(pick.get("row", []), 1):
            if hid in by:
                subprocess.run(["sips","-s","format","jpeg","-Z","1800",by[hid],"--out",str(out/f"row-{n:02d}.jpg")],capture_output=True)
        print(slug, "hero" if pick.get("hero") else "-", len(pick.get("row", [])), "row")
    (HERE/"selectie.json").write_text(json.dumps(sel, indent=2, ensure_ascii=False))

if __name__ == "__main__":
    if len(sys.argv) > 2 and sys.argv[1] == "--apply": apply(sys.argv[2])
    else: build()
