#!/usr/bin/env python3
"""Winnow Photos — the native Collections view generator (additive favorites).

Where winnow_view.py is the REDUCTIVE sort (keep good, set aside the rest), this is the
ADDITIVE collect: browse the WHOLE gallery by year, batch multi-select keepers, and file
them INTO named albums (a curated, shareable subset). Originals are never moved or touched.

This is Winnow's headline experience per the product brief: a gallery-scale favorites
manager, not a single-photo sorter. The defining moves are here — year sections, rubber-band
+ shift-range multi-select, "select all in <year>", and a one-tap "Add N to <album>" — so
curating hundreds of keepers across years is fast, the part everyone avoids.

The curated albums live under a dest root (Jason's personal build points it at the Dropbox
"Family Favorites" folder that the Fondly new tab consumes; the public build points it at
~/Pictures/<Brand> Collections). The Swift WKWebView shell loads the emitted file:// page and
files selections via the hvniz://winnowcollect IPC. A "Sort | Collect" toggle switches modes;| Collect" toggle switches modes;
the personal build also gets an "Update my Fondly tab" button (hvniz://winnowbake).

CLI:  python winnow_collect_view.py --dir FOLDER [--out OUT.html] [--dest ROOT] [--albums "Wedding,Chloe"]
"""
import os, sys, json, datetime

_HERE = os.path.dirname(os.path.abspath(__file__))
_SRC = os.path.dirname(_HERE)
for _p in (_HERE, _SRC):
    if _p not in sys.path:
        sys.path.insert(0, _p)

import winnow_photo as wp
try:
    import triage_gen as tg
    _furl = tg.furl
    _BASE = tg.BASE_DIR
except Exception:                       # standalone fallback
    import urllib.parse
    def _furl(p):
        return "file://" + urllib.parse.quote(os.path.abspath(p))
    _BASE = "/tmp"


def _thumb_url(it):
    """A file:// URL the WKWebView can render: the engine's cached thumbnail if present,
    else the original file."""
    t = it.get("thumb")
    if t:
        local = tg._local(t) if 'tg' in globals() else None
        if local and os.path.exists(local):
            return _furl(local)
        if t.startswith("file:"):
            return t
    return _furl(it["path"])


def _personal():
    """Optional, LOCAL-ONLY personalization read from ~/.winnow/personal.json. This file
    lives on the user's machine and is NEVER bundled into the shipped app, so Jason's
    Family-Favorites + Fondly wiring stays out of the public Winnow build (guardrail:
    Model B products ship generic; personal paths are the user's own config). Keys:
      collect_dest — root the album folders live under
      albums       — album names to seed the chips
      fondly       — show the "Update my Fondly tab" button (bool)
    Returns {} when absent. Cached for the process."""
    global _PERSONAL
    try:
        return _PERSONAL
    except NameError:
        pass
    p = os.path.expanduser("~/.winnow/personal.json")
    try:
        _PERSONAL = json.load(open(p)) if os.path.isfile(p) else {}
    except Exception:
        _PERSONAL = {}
    return _PERSONAL


def _default_dest():
    """Where curated albums live. Resolution order: env override, then the local personal
    config (Jason points it at the canonical Family Favorites folder), then a per-brand
    Collections folder in ~/Pictures for the public build."""
    env = os.environ.get("WINNOW_COLLECT_DEST") or _personal().get("collect_dest")
    if env:
        return os.path.abspath(os.path.expanduser(env))
    try:
        import brand as _b
        bn = _b.name()
    except Exception:
        bn = "Winnow"
    return os.path.expanduser("~/Pictures/%s Collections" % bn)


def _resolve_brand_name():
    """Same resolution order as _default_dest: the bundled brand.json is
    authoritative when present (multi-brand builds), else "Winnow Photos" for
    standalone/legacy CLI use."""
    try:
        import brand as _b
        return _b.name()
    except Exception:
        return "Winnow Photos"


def build_collect(folder, out_path=None, task=None, dest_root=None, albums=None,
                  fondly=None, progress=None, brand_name=None, scored=False):
    """Run the gallery manifest over `folder` and write the Collections HTML. Returns the
    {out, kb, note} dict triage_app.run expects.

    dest_root  — root the album folders live under (default: _default_dest()).
    albums     — album names to seed the chips with (existing albums are always merged in).
    fondly     — show the "Update my Fondly tab" button (Jason's personal config). Defaults
                 to env WINNOW_FONDLY=1."""
    dest_root = dest_root or _default_dest()
    if brand_name is None:
        brand_name = _resolve_brand_name()
    if albums is None:
        env_a = os.environ.get("WINNOW_COLLECT_ALBUMS", "")
        albums = [a.strip() for a in env_a.split(",") if a.strip()] or _personal().get("albums") or []
    if fondly is None:
        fondly = (os.environ.get("WINNOW_FONDLY", "") == "1") or bool(_personal().get("fondly"))

    man = wp.collect_manifest(folder, scored=scored, progress=progress)
    photos = man["photos"]

    # Existing album membership (by basename) so the gallery shows what's already filed.
    members = wp.album_members(dest_root)
    seed = list(dict.fromkeys([wp._safe_album(a) for a in albums if a.strip()]
                              + list(members.keys())))   # seed chips ∪ existing albums, order kept
    by_name_albums = {}
    for alb, names in members.items():
        for n in names:
            by_name_albums.setdefault(n, []).append(alb)

    items = []
    for p in photos:
        items.append({
            "name": p["name"],
            "url": _thumb_url(p),
            "year": p.get("year", "Undated"),
            "date": p.get("date", ""),
            "dims": p.get("dims", ""),
            "sharp": p.get("sharp"),                 # present only when scored=True
            "in": by_name_albums.get(p["name"], []),
        })

    payload = {
        "brand": brand_name,
        "stats": man["stats"],
        "years": man["years"],
        "albums": [{"name": a, "count": len(members.get(a, []))} for a in seed],
        "photos": items,
        "folder": os.path.basename(folder.rstrip("/")) or folder,
        "folder_path": os.path.abspath(folder),
        "dest_root": dest_root,
        "fondly": bool(fondly),
    }

    if not out_path:
        now = datetime.datetime.now()
        stamp = now.strftime("%Y-%m-%d at %H.%M") + now.strftime("%p").lower()
        os.makedirs(_BASE, exist_ok=True)
        out_path = os.path.join(_BASE, "%s %s (collect).html" % (stamp, task or payload["folder"]))

    htmltext = _HTML.replace("/*__DATA__*/null", json.dumps(payload))
    with open(out_path, "w") as f:
        f.write(htmltext)
    kb = os.path.getsize(out_path) / 1024.0
    note = "%d photos · %d years · %d albums" % (
        man["stats"]["photos"], man["stats"]["years"], len(seed))
    return {"out": out_path, "kb": kb, "note": note}


# ---------------------------------------------------------------- the page
_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Collect</title>
<style>
  :root{
    --ivory:#FBF7EE; --ivory-2:#F4EEDF; --paper:#FFFDF8;
    --gold:#C8A872; --gold-deep:#B8860B; --ink:#2B2722; --ink-2:#6B6457;
    --line:#E6DEC9; --ring:#D8CBA8;
    --good:#5C8A5A; --warn:#C9A227; --bad:#C06A4D;
    --shadow:0 3px 8px rgba(39,42,45,.08), 0 1px 24px rgba(39,42,45,.06);
    --shadow-hi:0 7px 18px rgba(39,42,45,.14), 0 2px 8px rgba(39,42,45,.10);
    --serif:"Cambria","Georgia","Times New Roman",serif;
    --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; --r:12px;
  }
  [data-skin="noir"]{
    --ivory:#16140F; --ivory-2:#1E1B15; --paper:#211D16; --gold:#C8A872; --gold-deep:#D9B873;
    --ink:#F2EBDA; --ink-2:#B6AC95; --line:#322C20; --ring:#3C3424;
    --shadow:0 3px 14px rgba(0,0,0,.45); --shadow-hi:0 8px 26px rgba(0,0,0,.55);
  }
  *{box-sizing:border-box} html,body{margin:0;height:100%}
  body{background:var(--ivory);color:var(--ink);font-family:var(--sans);font-size:14px;
    line-height:1.45;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none}
  .bar{height:54px;display:flex;align-items:center;gap:14px;padding:0 18px;background:var(--paper);
    border-bottom:1px solid var(--line);position:relative;z-index:6}
  .mark{width:26px;height:26px;flex:none}
  .mark circle{fill:none;stroke:var(--gold);stroke-width:1.4}
  .wordmark{font-family:var(--serif);font-size:19px;letter-spacing:.2px}
  .wordmark b{font-weight:600}
  .by{font-size:11px;color:var(--ink-2);letter-spacing:.3px;margin-top:1px}
  .grow{flex:1}
  .chip{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ink-2);
    border:1px solid var(--line);border-radius:999px;padding:5px 11px;background:var(--ivory)}
  .chip svg{width:12px;height:12px;opacity:.8}.lock{color:var(--gold-deep)}
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:999px;overflow:hidden;background:var(--ivory)}
  .seg button{border:0;background:transparent;color:var(--ink-2);font:inherit;font-size:12px;padding:6px 14px;cursor:pointer}
  .seg button.on{background:var(--ink);color:var(--paper);font-weight:600}
  .toggle{cursor:pointer;border:1px solid var(--line);background:var(--ivory);color:var(--ink-2);
    border-radius:999px;padding:5px 11px;font-size:11.5px}
  .toggle:hover{color:var(--ink);border-color:var(--ring)}
  /* album rail */
  .albrow{height:50px;display:flex;align-items:center;gap:10px;padding:0 18px;background:var(--paper);
    border-bottom:1px solid var(--line);position:relative;z-index:5;overflow-x:auto}
  .albrow .lbl{font-size:11.5px;color:var(--ink-2);letter-spacing:.3px;flex:none}
  .alb{flex:none;border:1px solid var(--line);background:var(--ivory);color:var(--ink);border-radius:999px;
    padding:7px 14px;cursor:pointer;font-size:13.5px;display:inline-flex;gap:7px;align-items:center;transition:.12s}
  .alb:hover{border-color:var(--ring)}
  .alb.on{background:linear-gradient(180deg,var(--gold),var(--gold-deep));color:#231f18;border-color:transparent;font-weight:600;box-shadow:var(--shadow)}
  .alb b{font-weight:600;font-variant-numeric:tabular-nums}.alb.on b{color:#231f18}
  .alb .ct{color:var(--ink-2);font-size:12px}.alb.on .ct{color:#4a3d1e}
  .newalb{flex:none;border:1px dashed var(--ring);background:transparent;color:var(--ink-2);border-radius:999px;
    padding:7px 13px;cursor:pointer;font-size:13px}
  .newalb:hover{color:var(--ink);border-color:var(--gold)}
  .scroller{height:calc(100% - 54px - 50px - 52px);overflow:auto;padding:8px 18px 28px}
  .ysec{margin-top:18px}
  .yhead{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:12px;padding:10px 2px 9px;
    background:linear-gradient(var(--ivory),var(--ivory) 70%,transparent);backdrop-filter:blur(2px)}
  .yhead h2{font-family:var(--serif);font-weight:500;font-size:22px;margin:0;letter-spacing:.3px}
  .yhead .yc{font-size:12px;color:var(--ink-2)}
  .yselect{font-size:12px;color:var(--gold-deep);cursor:pointer;border:1px solid var(--line);border-radius:999px;padding:3px 11px;background:var(--ivory)}
  .yselect:hover{border-color:var(--gold)}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:14px}
  .ph{position:relative;border-radius:var(--r);overflow:hidden;border:2px solid transparent;background:var(--ivory-2);
    aspect-ratio:1/1;cursor:pointer;box-shadow:var(--shadow);transition:transform .1s,border-color .1s}
  .ph img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}
  .ph:hover{transform:translateY(-2px);box-shadow:var(--shadow-hi)}
  .ph.sel{border-color:var(--gold)}
  .ph.sel::after{content:"\2713";position:absolute;left:8px;top:7px;width:22px;height:22px;border-radius:50%;
    background:linear-gradient(180deg,var(--gold),var(--gold-deep));color:#231f18;font-size:13px;font-weight:700;
    display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.3)}
  .ph .filed{position:absolute;right:7px;bottom:7px;display:flex;gap:4px;flex-wrap:wrap;justify-content:flex-end;max-width:80%}
  .ph .ftag{font-size:10px;font-weight:600;padding:2px 7px;border-radius:999px;background:rgba(92,138,90,.92);color:#fff;backdrop-filter:blur(3px)}
  .ph .blur{position:absolute;left:7px;bottom:7px;font-size:10px;font-weight:600;padding:2px 7px;border-radius:999px;background:rgba(192,106,77,.92);color:#fff}
  .marquee{position:fixed;border:1.5px solid var(--gold);background:rgba(200,168,114,.16);z-index:50;pointer-events:none;border-radius:4px;display:none}
  .empty{display:flex;align-items:center;justify-content:center;height:60%;color:var(--ink-2);font-family:var(--serif);font-size:18px;text-align:center;padding:40px}
  /* action dock */
  .dock{position:absolute;left:0;right:0;bottom:0;height:52px;display:flex;align-items:center;gap:14px;
    padding:0 18px;background:var(--paper);border-top:1px solid var(--line);z-index:6}
  .dock .selcount{font-size:13px;color:var(--ink-2)}.dock .selcount b{color:var(--ink);font-variant-numeric:tabular-nums}
  .dock .clearsel{font-size:12px;color:var(--gold-deep);cursor:pointer;text-decoration:underline;opacity:.85}
  .dock .clearsel:hover{opacity:1}
  .primary{background:linear-gradient(180deg,var(--gold),var(--gold-deep));color:#231f18;border:none;border-radius:10px;
    padding:10px 18px;font-size:13.5px;font-weight:600;cursor:pointer;box-shadow:var(--shadow)}
  .primary:disabled{opacity:.45;cursor:default;box-shadow:none}
  .primary.danger{background:linear-gradient(180deg,#D08C6F,var(--bad));color:#fff}
  .ghost{background:transparent;border:1px solid var(--line);color:var(--ink-2);border-radius:10px;padding:10px 15px;font-size:13px;cursor:pointer}
  .ghost:hover{color:var(--ink);border-color:var(--ring)}
  .toast{position:fixed;left:50%;bottom:66px;transform:translateX(-50%) translateY(20px);background:var(--ink);color:var(--paper);
    padding:11px 18px;border-radius:11px;font-size:13px;box-shadow:var(--shadow-hi);opacity:0;transition:.25s;z-index:80;pointer-events:none;max-width:80%}
  .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
</style>
</head>
<body data-skin="ivory">
  <div class="bar">
    <svg class="mark" viewBox="0 0 40 40"><circle cx="20" cy="20" r="6"/><circle cx="20" cy="20" r="12"/><circle cx="20" cy="20" r="18"/></svg>
    <div><div class="wordmark" id="wordmark"></div><div class="by" id="by">by Epiphani Studios</div></div>
    <span class="chip"><svg class="lock" viewBox="0 0 24 24" fill="currentColor"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm-3 8V6a3 3 0 1 1 6 0v3H9z"/></svg>100% on-device</span>
    <div class="grow"></div>
    <span class="chip" id="libname">Library</span>
    <div class="seg" role="tablist">
      <button id="seg-sort">Sort</button><button id="seg-collect" class="on">Collect</button>
    </div>
    <button class="toggle" id="skin">Noir</button>
  </div>
  <div class="albrow">
    <span class="lbl">FILE INTO</span>
    <div id="albs" style="display:flex;gap:10px;align-items:center"></div>
    <button class="newalb" id="newalb">+ New album</button>
  </div>
  <div class="scroller" id="scroller">
    <div id="sections"></div>
    <div class="empty" id="empty" style="display:none">No photos here yet.</div>
  </div>
  <div class="marquee" id="marquee"></div>
  <div class="dock">
    <span class="selcount"><b id="nsel">0</b> selected</span>
    <span class="clearsel" id="clearsel" style="display:none">clear</span>
    <div class="grow"></div>
    <button class="ghost" id="bake" style="display:none">Update my Fondly tab</button>
    <button class="primary" id="fileBtn" disabled>Add to album</button>
  </div>
  <div class="toast" id="toast"></div>
<script>
(function(){
  var DATA = /*__DATA__*/null;
  var $=function(s){return document.querySelector(s);};
  var photos = DATA.photos||[];
  var albums = (DATA.albums||[]).map(function(a){return {name:a.name,count:a.count||0};});
  var active = albums.length ? albums[0].name : "";
  var sel = {};                 // name -> true
  var lastClicked = null;       // for shift-range
  var order = [];               // flat photo order across sections (for shift-range)

  (function(){
    var name = DATA.brand || "";
    var sp = name.indexOf(" ");
    document.title = name + ": Collect";
    $("#wordmark").innerHTML = sp === -1 ? name
      : name.slice(0, sp) + " <b>" + name.slice(sp + 1) + "</b>";
  })();
  $("#libname").textContent = DATA.folder + " · " + (DATA.stats.photos||0) + " photos";
  if(DATA.fondly) $("#bake").style.display="";

  // ---- albums (the destination chips) ----
  function renderAlbums(){
    var box=$("#albs"); box.innerHTML="";
    if(!albums.length){ active=""; }
    albums.forEach(function(a){
      var el=document.createElement("button");
      el.className="alb"+(a.name===active?" on":"");
      el.innerHTML='<span>'+esc(a.name)+'</span><span class="ct">'+a.count+'</span>';
      el.onclick=function(){ active=a.name; renderAlbums(); updateDock(); };
      box.appendChild(el);
    });
  }
  $("#newalb").onclick=function(){
    var n=(window.prompt("Name the new album","")||"").trim();
    if(!n) return;
    if(!albums.some(function(a){return a.name===n;})) albums.push({name:n,count:0});
    active=n; renderAlbums(); updateDock();
  };

  // ---- sections by year ----
  function render(){
    var secWrap=$("#sections"); secWrap.innerHTML=""; order=[];
    var years = DATA.years && DATA.years.length ? DATA.years.map(function(y){return y.year;})
                : Array.from(new Set(photos.map(function(p){return p.year;})));
    if(!photos.length){ $("#empty").style.display="flex"; return; }
    years.forEach(function(yr){
      var inYear=photos.filter(function(p){return p.year===yr;});
      if(!inYear.length) return;
      var sec=document.createElement("div"); sec.className="ysec";
      var head=document.createElement("div"); head.className="yhead";
      head.innerHTML='<h2>'+esc(yr)+'</h2><span class="yc">'+inYear.length+' photos</span>';
      var ysel=document.createElement("span"); ysel.className="yselect"; ysel.textContent="Select all";
      ysel.onclick=function(){ var allSel=inYear.every(function(p){return sel[p.name];});
        inYear.forEach(function(p){ if(allSel) delete sel[p.name]; else sel[p.name]=true; });
        ysel.textContent=allSel?"Select all":"Deselect"; paint(); updateDock(); };
      head.appendChild(ysel); sec.appendChild(head);
      var grid=document.createElement("div"); grid.className="grid";
      inYear.forEach(function(p){
        var idx=order.length; order.push(p);
        var el=document.createElement("div"); el.className="ph"; el.dataset.name=p.name; el.dataset.idx=idx;
        var img=new Image(); img.loading="lazy"; img.src=p.url; img.alt=p.name;
        img.onerror=function(){el.style.background="linear-gradient(135deg,#cdbf9c,#9c8e6a)";};
        el.appendChild(img);
        if(p.sharp!=null && p.sharp<50){var b=document.createElement("span");b.className="blur";b.textContent="soft";el.appendChild(b);}
        var filed=document.createElement("div"); filed.className="filed"; el.appendChild(filed);
        el.onclick=function(e){ onPick(p, idx, e); };
        grid.appendChild(el);
      });
      sec.appendChild(grid); secWrap.appendChild(sec);
    });
    paint();
  }

  // selection paint (cheap: toggle classes only)
  function paint(){
    document.querySelectorAll(".ph").forEach(function(el){
      var p=photoByName(el.dataset.name);
      el.classList.toggle("sel", !!sel[el.dataset.name]);
      var filed=el.querySelector(".filed"); if(filed){ filed.innerHTML="";
        (p&&p["in"]||[]).forEach(function(al){var t=document.createElement("span");t.className="ftag";t.textContent=al;filed.appendChild(t);});
      }
    });
  }
  function photoByName(n){ for(var i=0;i<photos.length;i++) if(photos[i].name===n) return photos[i]; return null; }

  function onPick(p, idx, e){
    if(e.shiftKey && lastClicked!=null){
      var a=Math.min(lastClicked,idx), b=Math.max(lastClicked,idx);
      var want = !sel[p.name];
      for(var i=a;i<=b;i++){ if(want) sel[order[i].name]=true; else delete sel[order[i].name]; }
    } else if(e.metaKey || e.ctrlKey){
      if(sel[p.name]) delete sel[p.name]; else sel[p.name]=true;
    } else {
      if(sel[p.name]) delete sel[p.name]; else sel[p.name]=true;
    }
    lastClicked = idx;
    paint(); updateDock();
  }

  function selectedNames(){ return Object.keys(sel); }
  // True when EVERY selected photo is already filed in the active album → the action
  // flips to "Remove" (a clean add⇄remove toggle, like the picker this replaces).
  function selectionAllFiled(){
    var names=selectedNames(); if(!names.length||!active) return false;
    return names.every(function(nm){ var p=photoByName(nm); return p && (p["in"]||[]).indexOf(active)>=0; });
  }
  function dockOp(){ return selectionAllFiled() ? "remove" : "add"; }
  function updateDock(){
    var n=selectedNames().length;
    $("#nsel").textContent=n;
    $("#clearsel").style.display = n? "" : "none";
    var btn=$("#fileBtn");
    btn.disabled = !(n && active);
    var op=dockOp();
    btn.dataset.op=op;
    btn.classList.toggle("danger", op==="remove");
    btn.textContent = !(n&&active) ? (active? ("Add to "+active) : "Pick an album")
      : (op==="remove" ? ("Remove "+n+" from "+active) : ("Add "+n+" to "+active));
  }
  $("#clearsel").onclick=function(){ sel={}; lastClicked=null; paint(); updateDock(); };

  // ---- file the selection into the active album ----
  function signal(u){ window.__winnowLastSignal=u; location.href=u; }
  function nativeShell(){ return window.webkit || window.PagehawkNative; }
  $("#fileBtn").onclick=function(){
    var names=selectedNames(); if(!names.length || !active) return;
    var op=dockOp();
    if(!nativeShell()){  // browser preview
      if(op==="remove"){ toast("Preview only — in the app this removes "+names.length+" from “"+active+"”."); markUnfiled(names, active); }
      else { toast("Preview only — in the app this copies "+names.length+" into “"+active+"”."); markFiled(names, active); }
      return;
    }
    $("#fileBtn").disabled=true; $("#fileBtn").textContent=(op==="remove"?"Removing ":"Filing ")+names.length+"…";
    var b64=btoa(unescape(encodeURIComponent(JSON.stringify(names))));
    window.__pendingFile={names:names.slice(), album:active, op:op};
    signal("hvniz://winnowcollect?op="+op+"&dir="+encodeURIComponent(DATA.folder_path)
      +"&album="+encodeURIComponent(active)+"&dest="+encodeURIComponent(DATA.dest_root)
      +"&filesb64="+encodeURIComponent(b64));
  };
  // native shell calls back with the file/remove result
  window.winnowCollected=function(res){
    var pend=window.__pendingFile||{names:[],album:active,op:"add"};
    $("#fileBtn").disabled=false;
    if(!res||res.error){ updateDock(); toast("Could not "+(pend.op==="remove"?"remove":"file")+(res&&res.error?": "+res.error:"")+"."); return; }
    if(pend.op==="remove"){
      markUnfiled(pend.names, pend.album);
      var rn=(res.removed!=null?res.removed:pend.names.length);
      toast("Removed "+rn+" from “"+pend.album+"” ✓  Originals untouched.");
    } else {
      markFiled(pend.names, pend.album);
      var n=(res.filed!=null?res.filed:pend.names.length);
      toast("Added "+n+" to “"+pend.album+"” ✓  Originals untouched.");
    }
    sel={}; lastClicked=null; paint(); updateDock();
  };
  function markFiled(names, album){
    var added=0;
    names.forEach(function(nm){ var p=photoByName(nm); if(p){ p["in"]=p["in"]||[]; if(p["in"].indexOf(album)<0){p["in"].push(album);added++;} }});
    var a=albums.filter(function(x){return x.name===album;})[0]; if(a) a.count+=added; else albums.push({name:album,count:added});
    renderAlbums(); paint();
  }
  function markUnfiled(names, album){
    var removed=0;
    names.forEach(function(nm){ var p=photoByName(nm); if(p&&p["in"]){ var i=p["in"].indexOf(album); if(i>=0){p["in"].splice(i,1);removed++;} }});
    var a=albums.filter(function(x){return x.name===album;})[0]; if(a) a.count=Math.max(0,a.count-removed);
    renderAlbums(); paint();
  }

  // ---- update Fondly tab (personal build only) ----
  $("#bake").onclick=function(){
    if(!nativeShell()){ toast("Preview only — in the app this re-bakes the Fondly new tab."); return; }
    $("#bake").disabled=true; $("#bake").textContent="Updating…";
    signal("hvniz://winnowbake");
  };
  window.winnowBaked=function(res){
    $("#bake").disabled=false; $("#bake").textContent="Update my Fondly tab";
    toast(res&&res.ok ? ("Fondly now rotates "+(res.count||"your")+" photos ✓  Reload the new tab.") : ("Could not update Fondly"+(res&&res.err?": "+res.err:"")+"."));
  };

  // ---- mode toggle (Sort | Collect) ----| Collect) ----
  $("#seg-sort").onclick=function(){ if(!nativeShell()){ toast("Preview — Sort mode opens in the app."); return; } signal("hvniz://winnowmode?to=sort"); };
  $("#seg-collect").onclick=function(){ /* already here */ };

  // ---- marquee (rubber-band) multi-select ----
  (function(){
    var mq=$("#marquee"), scr=$("#scroller"), startX,startY,baseSel,dragging=false,addmode=true;
    scr.addEventListener("mousedown",function(e){
      if(e.target.closest(".ph")||e.target.closest("button")||e.target.closest(".yselect")) return;
      if(e.button!==0) return;
      dragging=true; startX=e.clientX; startY=e.clientY; addmode=!(e.altKey);
      baseSel=Object.assign({}, sel);
      mq.style.display="block"; place(e.clientX,e.clientY);
      e.preventDefault();
    });
    window.addEventListener("mousemove",function(e){ if(!dragging) return; place(e.clientX,e.clientY); hit(); });
    window.addEventListener("mouseup",function(){ if(!dragging) return; dragging=false; mq.style.display="none"; });
    function place(x,y){ var l=Math.min(x,startX),t=Math.min(y,startY),w=Math.abs(x-startX),h=Math.abs(y-startY);
      mq.style.left=l+"px"; mq.style.top=t+"px"; mq.style.width=w+"px"; mq.style.height=h+"px"; }
    function hit(){
      var r=mq.getBoundingClientRect();
      sel=Object.assign({}, baseSel);
      document.querySelectorAll(".ph").forEach(function(el){
        var b=el.getBoundingClientRect();
        var over=!(b.right<r.left||b.left>r.right||b.bottom<r.top||b.top>r.bottom);
        if(over){ if(addmode) sel[el.dataset.name]=true; else delete sel[el.dataset.name]; }
      });
      paint(); updateDock();
    }
  })();

  // keyboard: Cmd/Ctrl+A select all, Esc clear
  document.addEventListener("keydown",function(e){
    if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="a"){ e.preventDefault(); photos.forEach(function(p){sel[p.name]=true;}); paint(); updateDock(); }
    else if(e.key==="Escape"){ sel={}; lastClicked=null; paint(); updateDock(); }
  });

  $("#skin").onclick=function(){var b=document.body;var n=b.dataset.skin==="ivory"?"noir":"ivory";
    b.dataset.skin=n;$("#skin").textContent=n==="ivory"?"Noir":"Ivory";};

  var _tt=null;
  function toast(msg){ var t=$("#toast"); t.textContent=msg; t.classList.add("show");
    if(_tt)clearTimeout(_tt); _tt=setTimeout(function(){t.classList.remove("show");},3200); }
  function esc(s){ return (s+"").replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];}); }

  renderAlbums(); render(); updateDock();
})();
</script>
</body>
</html>
"""


if __name__ == "__main__":
    import argparse
    ap = argparse.ArgumentParser(description="Winnow Photos — Collections view generator")
    ap.add_argument("--dir", required=True)
    ap.add_argument("--out")
    ap.add_argument("--dest", help="root the album folders live under")
    ap.add_argument("--albums", help="comma-separated album names to seed the chips")
    ap.add_argument("--fondly", action="store_true", help="show the Update-Fondly button")
    ap.add_argument("--scored", action="store_true")
    a = ap.parse_args()
    if not os.path.isdir(a.dir):
        print("not a folder:", a.dir); sys.exit(1)
    albums = [x.strip() for x in (a.albums or "").split(",") if x.strip()] or None
    res = build_collect(a.dir, out_path=a.out, dest_root=a.dest, albums=albums,
                        fondly=a.fondly or None, scored=a.scored)
    print(json.dumps(res, indent=2))
