// Gallery: category sections with masonry layout. Header shows count + accent.
// Clicking an image opens a fullscreen lightbox with the AI-generated alt text.

const { useState: _gus, useEffect: _gue, useMemo: _gum } = React;

function GallerySection({ tweaks }){
  const [shotCount, setShotCount] = _gus(window.ALL_SHOTS.length);
  // Same geo lookup the rail uses, so the headline and the clock can never
  // disagree. Defined in sidebar.jsx, which loads first.
  const city = window.useGeoCity()?.city ?? null;

  _gue(() => {
    const onPublish = () => setShotCount(window.ALL_SHOTS.length);
    window.addEventListener("dn:published", onPublish);
    return () => window.removeEventListener("dn:published", onPublish);
  }, []);

  return (
    <section id="work" data-screen-label="01 Work" className="section">
      {/* No eyebrow. The heading is the label — and where he is working right
          now is the one piece of information a visiting agency actually wants. */}
      <header className="work-head">
        <h2 className="display t-display" style={{margin:0}}>
          {city ? <>Work in <span className="work-city">{city}</span>.</> : "Work."}
        </h2>
        <p className="meta work-count">
          {shotCount} frames, {window.CATEGORIES.length} bodies of work
        </p>
      </header>

      <CategoryGalleries tweaks={tweaks} />
    </section>
  );
}

function CategoryGalleries({ tweaks }){
  const [openShot, setOpenShot] = _gus(null);
  const [tick, setTick] = _gus(0);

  _gue(() => {
    const onPublish = () => setTick(t => t + 1);
    window.addEventListener("dn:published", onPublish);
    return () => window.removeEventListener("dn:published", onPublish);
  }, []);

  const populated = window.CATEGORIES.filter(cat =>
    window.ALL_SHOTS.some(s => s.cat === cat.id)
  );

  if (populated.length === 0) {
    return (
      <div style={{
        padding:"80px 0", textAlign:"center",
        display:"flex", flexDirection:"column", alignItems:"center", gap:16,
      }}>
        <div className="meta" style={{color:"var(--fg-faint)"}}>No images published yet</div>
        <p className="t-body" style={{color:"var(--fg-soft)", maxWidth:"36ch", margin:0}}>
          No images published yet.
        </p>
      </div>
    );
  }

  return (
    <div style={{display:"flex", flexDirection:"column", gap:72}}>
      {populated.map((cat, idx) => (
        <CategoryBlock key={cat.id} cat={cat} idx={idx} density={tweaks.galleryDensity} onOpen={setOpenShot}/>
      ))}
      {openShot && <Lightbox shot={openShot} onClose={() => setOpenShot(null)}/>}
    </div>
  );
}

// The gallery column count follows the viewport, not just the density tweak:
// 3+ on a desktop, 2 on a tablet, 2 on a phone (a photographer's grid at one
// column is too sparse; at three, phone thumbnails were ~104px).
function useGalleryCols(density){
  const base = density === "compact" ? 4 : density === "loose" ? 2 : 3;
  const pick = (w) => w <= 700 ? Math.min(base, 2) : w <= 1100 ? Math.min(base, 3) : base;
  const [cols, setCols] = _gus(() => pick(typeof window !== "undefined" ? window.innerWidth : 1440));
  _gue(() => {
    const on = () => setCols(pick(window.innerWidth));
    window.addEventListener("resize", on, { passive: true });
    return () => window.removeEventListener("resize", on);
  }, [density]);
  return cols;
}

function CategoryBlock({ cat, idx, density, onOpen }){
  const cols = useGalleryCols(density);
  const shots = window.ALL_SHOTS.filter(s => s.cat === cat.id);
  if (!shots.length) return null;
  const ratios = shots.map(s => (s.ah || 5) / (s.aw || 4));
  return (
    <div className="cat-block">
      {/* Title and count on one baseline. The old "01 / 03" counter told the
          reader nothing they could not see; the frame count does. */}
      <div className="cat-head">
        <h3 className="display t-title" style={{margin:0}}>{cat.name}</h3>
        <span className="meta cat-count">
          <span className="dot" style={{background:cat.accent}}/>
          {shots.length} frames
        </span>
      </div>

      <p className="t-body measure cat-blurb">{cat.blurb}</p>

      <Masonry cols={cols} gap={12} ratios={ratios}>
        {shots.map((s, i) => (
          <button
            key={s.id}
            onClick={() => onOpen(s)}
            style={{display:"block", border:0, padding:0, background:"transparent", textAlign:"left"}}
          >
            <ShotImage file={s.previewUrl ? s : null} shot={s} accent={i === 0 ? cat.accent : null} eager={idx === 0 && i < 3} />
          </button>
        ))}
      </Masonry>
    </div>
  );
}

// `i % cols` dealt images across columns like cards, which both scrambled the
// reading order and left ragged columns whenever the aspect ratios differed.
// Placing each next image into whichever column is currently shortest keeps the
// order intact and evens the bottoms out.
function Masonry({cols=3, gap=16, ratios=[], children}){
  const arr = React.Children.toArray(children);
  const columns = Array.from({length: cols}, () => []);
  const heights = new Array(cols).fill(0);
  arr.forEach((child, i) => {
    let c = 0;
    for (let k = 1; k < cols; k++) if (heights[k] < heights[c]) c = k;
    columns[c].push(child);
    heights[c] += (ratios[i] || 1.25) + 0.05; // ratio is height per unit width
  });
  return (
    <div style={{display:"grid", gridTemplateColumns:`repeat(${cols}, 1fr)`, gap}}>
      {columns.map((col, i) => (
        <div key={i} style={{display:"flex", flexDirection:"column", gap}}>{col}</div>
      ))}
    </div>
  );
}

function Lightbox({shot, onClose}){
  React.useEffect(() => {
    const onKey = (e) => { if(e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow=""; };
  }, []);
  return (
    <div style={{
      position:"fixed", inset:0, zIndex:200,
      background:"rgba(8,8,7,.94)", color:"#F4F1EA",
      display:"grid", gridTemplateColumns:"1fr 340px", gap:0,
      animation:"fadeIn .35s ease",
    }} className="lightbox-inner">
      <style>{`@keyframes fadeIn{from{opacity:0}to{opacity:1}}`}</style>

      {/* image side */}
      <div onClick={(e) => { if(e.target === e.currentTarget) onClose(); }} style={{
        position:"relative", display:"flex", alignItems:"center", justifyContent:"center", padding:32, overflow:"hidden", cursor:"zoom-out",
      }}>
        <button onClick={onClose} className="lightbox-close-mobile meta" style={{
          position:"absolute", top:16, right:16,
          color:"rgba(255,255,255,.8)", display:"none", alignItems:"center", gap:6,
        }}>✕ Close</button>
        <img
          src={shot.previewUrl}
          alt={window.shotAlt(shot)}
          style={{maxHeight:"90vh", maxWidth:"100%", objectFit:"contain", display:"block"}}
        />
      </div>

      {/* meta side */}
      <div className="lightbox-meta" style={{
        padding:"40px 36px", borderLeft:"1px solid rgba(255,255,255,.12)",
        display:"flex", flexDirection:"column", gap:24,
      }}>
        <button onClick={onClose} className="meta" style={{
          alignSelf:"flex-end", color:"rgba(255,255,255,.7)",
          display:"flex", alignItems:"center", gap:8,
        }}>Close <span style={{fontSize:"var(--t-body)"}}>✕</span></button>

        <div className="meta" style={{color:"rgba(255,255,255,.5)"}}>{shot.code}</div>
        <h4 className="display t-sub" style={{margin:0}}>{shot.label}</h4>
        {shot.city && <div className="meta" style={{color:"rgba(255,255,255,.6)"}}>Shot in {shot.city}</div>}

      </div>
    </div>
  );
}

function Tag({label,value}){
  return (
    <div>
      <div className="meta" style={{color:"rgba(255,255,255,.45)"}}>{label}</div>
      <div style={{fontFamily:"var(--mono)", fontSize:"var(--t-body)", marginTop:4}}>{value}</div>
    </div>
  );
}

// ---------- Image analysis for alt text ----------

function _rgbToName(r,g,b){
  const max = Math.max(r,g,b), min = Math.min(r,g,b);
  const l = (max + min) / 2 / 255;
  const s = max === min ? 0 : (max - min) / (255 - Math.abs(2*max - 255));
  if (s < 0.10 || max - min < 22){
    if (l > 0.88) return "white";
    if (l > 0.65) return "light grey";
    if (l > 0.38) return "grey";
    if (l > 0.18) return "dark grey";
    return "black";
  }
  let h;
  const d = max - min;
  if (max === r) h = ((g - b) / d) % 6;
  else if (max === g) h = (b - r) / d + 2;
  else h = (r - g) / d + 4;
  h = h * 60; if (h < 0) h += 360;
  const isDark = l < 0.30;
  const isPale = l > 0.78 && s < 0.5;
  const prefix = isDark ? "dark " : isPale ? "pale " : "";
  if (h < 12)  return prefix + "red";
  if (h < 22)  return prefix + "warm red";
  if (h < 38)  return prefix + (l < 0.45 ? "brown" : "orange");
  if (h < 52)  return prefix + (l < 0.5 ? "ochre" : "yellow");
  if (h < 70)  return prefix + "yellow";
  if (h < 95)  return prefix + (l < 0.4 ? "olive" : "yellow-green");
  if (h < 160) return prefix + "green";
  if (h < 195) return prefix + "teal";
  if (h < 230) return prefix + (l < 0.4 ? "navy" : "blue");
  if (h < 265) return prefix + "blue";
  if (h < 290) return prefix + "purple";
  if (h < 330) return prefix + "magenta";
  return prefix + "red";
}

function _loadImg(src){
  return new Promise((res, rej) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.onload = () => res(img);
    img.onerror = rej;
    img.src = src;
  });
}

async function analyzeImage(src){
  if (!src) return null;
  let img;
  try { img = await _loadImg(src); }
  catch(e){ return null; }
  const W = 64, H = Math.max(8, Math.round(64 * (img.naturalHeight || 64) / (img.naturalWidth || 64)));
  const canvas = document.createElement("canvas");
  canvas.width = W; canvas.height = H;
  const ctx = canvas.getContext("2d");
  try { ctx.drawImage(img, 0, 0, W, H); }
  catch(e){ return null; }

  let data;
  try { data = ctx.getImageData(0, 0, W, H).data; }
  catch(e){ return null; }

  const buckets = new Map();
  let totalBright = 0, totalSat = 0, n = 0;
  for (let i = 0; i < data.length; i += 4){
    const r = data[i], g = data[i+1], b = data[i+2];
    const max = Math.max(r,g,b), min = Math.min(r,g,b);
    totalBright += (r+g+b)/3;
    totalSat += max === 0 ? 0 : (max - min) / max;
    n++;
    const key = (r>>4) + "_" + (g>>4) + "_" + (b>>4);
    const cur = buckets.get(key) || { r:0,g:0,b:0,c:0 };
    cur.r += r; cur.g += g; cur.b += b; cur.c += 1;
    buckets.set(key, cur);
  }
  const palette = [...buckets.values()]
    .sort((a,b) => b.c - a.c)
    .slice(0, 5)
    .map(p => ({
      r: Math.round(p.r/p.c), g: Math.round(p.g/p.c), b: Math.round(p.b/p.c),
      pct: p.c / n,
      name: _rgbToName(Math.round(p.r/p.c), Math.round(p.g/p.c), Math.round(p.b/p.c)),
    }));

  const seen = new Set();
  const namedColors = [];
  for (const p of palette){
    if (seen.has(p.name)) continue;
    seen.add(p.name);
    namedColors.push({ name: p.name, pct: p.pct });
    if (namedColors.length >= 3) break;
  }

  const avgBright = totalBright / n / 255;
  const avgSat = totalSat / n;
  const monochrome = avgSat < 0.10;

  let darkX = 0, darkY = 0, darkN = 0;
  for (let y = 0; y < H; y++){
    for (let x = 0; x < W; x++){
      const i = (y * W + x) * 4;
      const l = (data[i] + data[i+1] + data[i+2]) / 3;
      if (l < avgBright * 255 * 0.7){
        darkX += x; darkY += y; darkN++;
      }
    }
  }
  let framing = "centered";
  if (darkN > 0){
    const cx = (darkX / darkN) / W;
    const cy = (darkY / darkN) / H;
    const h = cx < 0.38 ? "left" : cx > 0.62 ? "right" : "center";
    const v = cy < 0.40 ? "upper" : cy > 0.66 ? "lower" : "middle";
    framing = `${v} ${h}`;
  }

  return {
    aspect: `${img.naturalWidth || W}:${img.naturalHeight || H}`,
    colors: namedColors,
    monochrome,
    brightness: avgBright < 0.30 ? "dark" : avgBright > 0.70 ? "bright" : "mid-tone",
    framing,
  };
}

window.analyzeImage = analyzeImage;

function generateAlt(shot){
  const subjects = {
    "fashion-editorial": [
      "woman in a black dress standing against a textured wall",
      "model in tailored coat looking off to the side",
      "woman wearing layered knitwear, hands in pockets",
      "model in silk slip dress on a plain backdrop",
    ],
    "talent": [
      "woman with dark hair looking at the camera",
      "man in a white shirt, soft window light on his face",
      "model standing front-facing on a neutral seamless",
      "profile shot of a woman against a grey background",
    ],
    "ecommerce": [
      "white linen shirt photographed on a model against a plain backdrop",
      "wool coat shown from the front, buttons fastened",
      "denim jacket on a model, sleeves rolled up",
      "knit sweater displayed on a figure, cropped at the waist",
    ],
  }[shot.cat] || ["photograph"];
  const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
  const sentence = pick(subjects);
  return sentence.charAt(0).toUpperCase() + sentence.slice(1) + ".";
}

async function _imageToBase64(src, maxDim = 768){
  try {
    const img = await _loadImg(src);
    const w0 = img.naturalWidth || img.width;
    const h0 = img.naturalHeight || img.height;
    if (!w0 || !h0) return null;
    const scale = Math.min(1, maxDim / Math.max(w0, h0));
    const W = Math.max(1, Math.round(w0 * scale));
    const H = Math.max(1, Math.round(h0 * scale));
    const c = document.createElement("canvas");
    c.width = W; c.height = H;
    const ctx = c.getContext("2d");
    ctx.drawImage(img, 0, 0, W, H);
    const url = c.toDataURL("image/jpeg", 0.82);
    const m = /^data:([^;]+);base64,(.+)$/.exec(url);
    if (!m) return null;
    return { mediaType: m[1], data: m[2] };
  } catch (e) {
    return null;
  }
}

async function classifyImageAI(file, catIds) {
  const prompt = [
    "You are classifying photography for a professional photographer's portfolio. Look at this image carefully and assign it to exactly one category.",
    "Reply with ONLY the category id — one word/phrase, nothing else.",
    "",
    "CATEGORY DEFINITIONS:",
    "",
    "fashion-editorial",
    "  The image IS the art. High-concept, styled, magazine-worthy. Model wears curated, intentional outfits.",
    "  Dramatic lighting, editorial composition, props, or conceptual framing. Feels like a fashion spread or cover.",
    "  Could be studio or location. The styling and visual story are the point.",
    "",
    "talent",
    "  Any portrait, personal study, agency digital, casting shot, or polaroid-style test.",
    "  Includes close intimate portraits, studio headshots, model digitals, comp card shots.",
    "  The person is the subject — not selling a garment, not a fashion concept.",
    "",
    "ecommerce",
    "  A specific garment or accessory IS the product being sold. Shot to show the item for retail.",
    "  Person is a 'hanger' for the clothing — clean, consistent, often white/light background.",
    "  Functional, not artistic. Also includes product still life (no person).",
    "",
    "DECISION RULES (apply in order):",
    "1. If it's high-concept, styled, editorial, artistic fashion → fashion-editorial",
    "2. If a person is wearing clothes shown for retail sale, or is a product still life → ecommerce",
    "3. Everything else with a human subject → talent",
    "",
    "Reply with one of: fashion-editorial, talent, ecommerce",
  ].join("\n");

  if (file.previewUrl && window.claude && window.claude.complete) {
    try {
      const encoded = await _imageToBase64(file.previewUrl, 512);
      if (encoded) {
        const text = await window.claude.complete({
          messages: [{
            role: "user",
            content: [
              { type: "image", source: { type: "base64", media_type: encoded.mediaType, data: encoded.data } },
              { type: "text", text: prompt },
            ],
          }],
        });
        const result = String(text || "").trim().toLowerCase().replace(/[^a-z-]/g, "");
        const match = (catIds || []).find(id => result === id || result.includes(id));
        if (match) return { cat: match, confidence: 0.93 };
      }
    } catch (e) {
      console.warn("classifyImageAI API error:", e && e.message || e);
    }
  }

  // Filename heuristics fallback
  const name = (file.name || "").toLowerCase();
  const hints = [
    { words: ["editorial", "fashion", "vogue", "lookbook", "cover", "styling", "look_"], cat: "fashion-editorial" },
    { words: ["ecomm", "sku", "on-figure", "retail", "product", "bottle", "still", "tabletop"], cat: "ecommerce" },
    { words: ["model", "digital", "polaroid", "test", "casting", "comp", "portrait", "headshot", "face", "head", "lifestyle", "bts"], cat: "talent" },
  ];
  for (const { words, cat } of hints) {
    if (words.some(w => name.includes(w))) return { cat, confidence: 0.70 };
  }

  const cats = catIds || window.CATEGORIES.map(c => c.id);
  return { cat: cats[Math.floor(Math.random() * cats.length)], confidence: 0.52 };
}

window.classifyImageAI = classifyImageAI;

async function generateAltAI(fileOrShot){
  const file = fileOrShot && fileOrShot.shot ? fileOrShot : null;
  const shot = file ? file.shot : fileOrShot;
  const src = (file && file.previewUrl) || (shot && window.getShotPreviewUrl && window.getShotPreviewUrl(shot));

  const instructions = [
    "Look at the image and write one short alt-text caption.",
    "",
    "Describe ONLY what is literally visible. Cover, in order:",
    "1. Subject — if a person, say woman / man / child / group (use what the image shows).",
    "2. Main items / clothing / objects in the frame.",
    "3. Dominant colors actually present in the image.",
    "4. The overall structure: pose or placement (lying, standing, close-up), and what's in the background.",
    "",
    "Hard rules:",
    "- One sentence, 12–24 words.",
    "- Plain and factual. No mood words ('striking', 'evocative', 'cinematic', 'dreamy', 'haunting').",
    "- No interpretation of feelings. Just what's in the picture.",
    "- Do NOT start with 'A photo of', 'Image of', 'Photograph of', 'This image'. Start with the subject.",
    "- Do not mention file name, year, or aspect ratio.",
    "- No quotes, no markdown. Just the sentence.",
    "",
    "Examples of the right tone:",
    "'Woman lying on a red background wearing gold earrings and a black slip dress.'",
    "'Man in a brown wool coat standing in front of a grey brick wall, looking left.'",
    "'Pair of black leather boots placed on a white linen sheet, photographed from above.'",
  ].join("\n");

  if (src) {
    try {
      const encoded = await _imageToBase64(src, 768);
      if (encoded && window.claude && window.claude.complete) {
        const text = await window.claude.complete({
          messages: [{
            role: "user",
            content: [
              { type: "image", source: { type: "base64", media_type: encoded.mediaType, data: encoded.data } },
              { type: "text", text: instructions },
            ],
          }],
        });
        const clean = String(text || "").trim().replace(/^["']|["']$/g, "");
        if (clean) return clean;
      }
    } catch (e) {}
  }

  let analysis = null;
  if (src) { try { analysis = await window.analyzeImage(src); } catch(e){} }
  const cat = window.CATEGORIES.find(c => c.id === (shot && shot.cat));
  const colorLine = analysis && analysis.colors && analysis.colors.length
    ? analysis.colors.map(c => `${c.name} (${Math.round(c.pct*100)}%)`).join(", ")
    : "unknown";
  const textPrompt = [
    instructions,
    "",
    "(You cannot see the image directly. Use these sampled facts as the only source of truth.)",
    `- Dominant colors: ${colorLine}`,
    `- Tone: ${analysis ? (analysis.monochrome ? "monochrome / black-and-white" : "color") : "unknown"}`,
    `- Brightness: ${analysis ? analysis.brightness : "unknown"}`,
    `- Subject position: ${analysis ? analysis.framing : "unknown"}`,
    `- Category hint (do not mention): ${cat ? cat.name : (shot && shot.cat) || "unknown"}`,
  ].join("\n");

  try {
    if (!window.claude || !window.claude.complete) throw new Error("no claude");
    const text = await window.claude.complete(textPrompt);
    const clean = String(text || "").trim().replace(/^["']|["']$/g, "");
    if (clean) return clean;
  } catch (e) {}

  return generateAlt(shot);
}

window.useMemo = React.useMemo;
Object.assign(window, { GallerySection, generateAlt, generateAltAI, Masonry, Lightbox });
