const { useState, useEffect, useMemo, useRef } = React;

/* ================= CRATEWISE — v1 =================
   rules backbone + additive AI · never edits source
============================================================ */

const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Archivo:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
:root{
  --ink:#0A0F12; --surf:#10181E; --raise:#15212A; --line:#1D2E36; --line2:#27414C;
  --teal:#35E0C6; --teal-dim:#1E8577; --txt:#E8F1F2; --mut:#7D9096; --dim:#54666C;
  --e1:#3B6FB5; --e2:#3AA6C9; --e3:#35E0C6; --e4:#A98CF2; --e5:#E4649E; --amber:#F2B84B;
}
.cw-root{min-height:100vh;background:var(--ink);color:var(--txt);font-family:'Archivo',system-ui,sans-serif;font-size:14px;line-height:1.5;}
.cw-root *{box-sizing:border-box;}
.cw-wrap{max-width:1060px;margin:0 auto;padding:0 20px 80px;}
.cw-serif{font-family:'Cormorant Garamond',Georgia,serif;}
.cw-mono{font-family:'IBM Plex Mono',monospace;}
.cw-refract{height:1px;background:linear-gradient(90deg,var(--teal) 0%,#7FD4F0 30%,var(--e4) 62%,var(--e5) 85%,var(--amber) 100%);opacity:.85;}
.cw-header{display:flex;align-items:center;gap:14px;padding:22px 0 18px;}
.cw-wordmark{font-size:21px;letter-spacing:.34em;font-weight:600;text-transform:uppercase;}
.cw-sub{color:var(--mut);font-size:12px;letter-spacing:.18em;text-transform:uppercase;margin-top:2px;}
.cw-tabs{display:flex;gap:4px;margin:18px 0 26px;border:1px solid var(--line);border-radius:10px;padding:4px;width:fit-content;background:var(--surf);}
.cw-tab{border:0;background:transparent;color:var(--mut);font:inherit;font-weight:600;font-size:13px;padding:8px 16px;border-radius:7px;cursor:pointer;letter-spacing:.02em;}
.cw-tab:hover{color:var(--txt);}
.cw-tab.on{background:var(--raise);color:var(--teal);}
.cw-card{background:var(--surf);border:1px solid var(--line);border-radius:12px;padding:18px;}
.cw-card+.cw-card{margin-top:14px;}
.cw-h{font-size:12px;letter-spacing:.22em;text-transform:uppercase;color:var(--mut);margin:0 0 12px;font-weight:600;}
.cw-big{font-size:30px;font-weight:600;line-height:1.1;}
.cw-grid{display:grid;gap:14px;}
@media(min-width:760px){.cw-grid.c2{grid-template-columns:1fr 1fr}.cw-grid.c4{grid-template-columns:repeat(4,1fr)}}
.cw-btn{border:1px solid var(--line2);background:var(--raise);color:var(--txt);font:inherit;font-weight:600;font-size:13px;padding:9px 16px;border-radius:9px;cursor:pointer;transition:border-color .15s;}
.cw-btn:hover{border-color:var(--teal-dim);}
.cw-btn.pri{background:var(--teal);border-color:var(--teal);color:#062A24;}
.cw-btn.pri:hover{filter:brightness(1.07);}
.cw-btn.sm{padding:5px 10px;font-size:12px;border-radius:7px;}
.cw-btn:disabled{opacity:.45;cursor:default;}
.cw-btn:focus-visible,.cw-tab:focus-visible{outline:2px solid var(--teal);outline-offset:2px;}
.cw-chip{display:inline-flex;align-items:center;justify-content:center;min-width:34px;padding:2px 7px;border-radius:6px;font-size:11.5px;font-weight:500;border:1px solid var(--line2);color:var(--mut);}
.cw-echip{border:0;color:#08211D;font-weight:600;}
.cw-row{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:9px;}
.cw-row:hover{background:var(--raise);}
.cw-track-name{font-weight:600;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.cw-track-artist{color:var(--mut);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.cw-icon-btn{border:0;background:transparent;color:var(--dim);cursor:pointer;font-size:14px;padding:4px 6px;border-radius:6px;}
.cw-icon-btn:hover{color:var(--teal);background:var(--raise);}
.cw-icon-btn.on{color:var(--amber);}
.cw-bar{height:8px;border-radius:4px;background:var(--raise);overflow:hidden;}
.cw-bar>div{height:100%;border-radius:4px;}
.cw-focus-item{display:flex;gap:12px;align-items:baseline;padding:10px 0;border-top:1px solid var(--line);}
.cw-focus-item:first-of-type{border-top:0;}
.cw-num{font-size:15px;color:var(--teal);min-width:22px;}
.cw-note{color:var(--mut);font-size:12.5px;}
.cw-drop{border:1px dashed var(--line2);border-radius:14px;padding:56px 24px;text-align:center;cursor:pointer;transition:border-color .2s;}
.cw-drop:hover{border-color:var(--teal-dim);}
.cw-alt{border-top:1px solid var(--line);margin:4px 0 6px;padding-top:6px;}
.cw-alt .cw-row{padding:6px 10px;cursor:pointer;}
.cw-toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--raise);border:1px solid var(--line2);padding:10px 18px;border-radius:10px;font-size:13px;z-index:50;box-shadow:0 8px 30px rgba(0,0,0,.5);}
.cw-ai{white-space:pre-wrap;font-size:13.5px;line-height:1.65;color:var(--txt);}
.cw-table{width:100%;border-collapse:collapse;font-size:12.5px;}
.cw-table th{text-align:left;color:var(--dim);font-weight:600;letter-spacing:.08em;text-transform:uppercase;font-size:10.5px;padding:6px 8px;border-bottom:1px solid var(--line);}
.cw-table td{padding:7px 8px;border-bottom:1px solid var(--line);}
select.cw-sel,input.cw-inp{background:var(--raise);border:1px solid var(--line2);color:var(--txt);font:inherit;border-radius:8px;padding:8px 10px;}
@media(prefers-reduced-motion:reduce){.cw-root *{transition:none!important;animation:none!important;}}
`;

/* ---------------- constants ---------------- */
const EN_COLORS = { 0:"#3A4A50", 1:"var(--e1)", 2:"var(--e2)", 3:"var(--e3)", 4:"var(--e4)", 5:"var(--e5)" };
const TAXONOMY = [
  "House - Deep","House - Tech House","House - Progressive","House - Melodic","House - Funky / Disco","House - Afro","House - Vocal / Anthems","House - Peak Time",
  "Techno - Melodic","Techno - Peak Time / Driving","Techno - Hard","Techno - Minimal / Deep","Techno - Industrial","Techno - Hypnotic / Groove","Techno - Dub / Ambient",
  "Lab - Hip Hop","Lab - Future Bass","Lab - Hard Dance","Tools"
];
const ARC_COLOR = { "Build up":"Green", "Build down":"Blue", "Peak Time":"Red", "DJ tool":"Purple" };

const SITUATIONS = {
  lucent: {
    id:"lucent", name:"Tuesday", sub:"Tuesday 8–10pm · intimate room", bpm:[118,130], gt:["Tuesday"],
    buckets:[
      {id:"starters", role:"Core Starters", n:8, e:[1.6,2.6], tagW:{"Build up":0.8,"Build down":0.8}, instr:0.8, desc:"Low, warm, room-filling openers"},
      {id:"anchors", role:"Anchors", n:6, e:[2.6,3.4], played:1.6, desc:"Proven center-of-set tracks"},
      {id:"momentum", role:"Momentum Boosters", n:9, e:[3,4.2], tagW:{"Build up":2}, fresh:0.5, desc:"Lifts that keep the floor leaning in"},
      {id:"pivot", role:"Pivots", n:7, e:[2.6,3.6], versatile:1.6, desc:"Key-versatile bridges between moods"},
      {id:"peak", role:"Peak", n:7, e:[3.8,4.6], tagW:{"Peak Time":2.5}, vocal:0.8, desc:"Tuesday ceiling — euphoric, not slammed"},
      {id:"resets", role:"Groove Resets", n:7, e:[2,3.2], tagW:{"Build down":2.5}, instr:1, desc:"Release valves after peaks"},
    ],
  },
  saturday: {
    id:"saturday", name:"Saturday", sub:"10pm–12am · main floor → second floor", bpm:[120,134], gt:["Main Floor","Second Floor"],
    buckets:[
      {id:"starters", role:"Core Starters", n:6, e:[2,3], tagW:{"Build up":0.8}, gt:["Main Floor"], desc:"Arrival groove while the floor fills"},
      {id:"anchors", role:"Anchors", n:6, e:[3,3.8], played:1.6, gt:["Main Floor"], desc:"Dependable main-floor movers"},
      {id:"momentum", role:"Momentum Boosters", n:10, e:[3.4,4.4], tagW:{"Build up":2}, gt:["Main Floor","Second Floor"], fresh:0.5, desc:"The climb toward peak"},
      {id:"peak", role:"Peak Energy", n:11, e:[4.2,5], tagW:{"Peak Time":2.5}, vocal:1.2, gt:["Second Floor"], desc:"Full-floor payoff — anthems welcome"},
      {id:"resets", role:"Groove Resets", n:8, e:[2.8,3.6], tagW:{"Build down":2.5}, instr:1, desc:"Cool-downs that keep bodies moving"},
      {id:"handover", role:"Handover", n:3, e:[3.4,4.2], versatile:2.2, desc:"Common-key exits for the next DJ"},
    ],
  },
  tiktok: {
    id:"clip", name:"Short Clip", sub:"2–5 min · one idea per clip", bpm:[95,175], gt:[],
    buckets:[
      {id:"hooks", role:"Hooks", n:5, e:[3.8,5], vocal:1.5, desc:"Front-loaded moments that stop the scroll"},
      {id:"flips", role:"Flips", n:5, e:[0,5], lab:2.5, desc:"Genre experiments & contrast partners"},
      {id:"lands", role:"Lands", n:4, e:[2.4,3.6], desc:"Resolutions that button the clip"},
    ],
  },
};

/* ---------------- utilities ---------------- */
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function xmlEscape(s){return String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
function parseCamelot(k){const m=/^\s*(\d{1,2})\s*(A|B)\s*$/i.exec(k||"");if(!m)return null;const n=+m[1];if(n<1||n>12)return null;return{n,l:m[2].toUpperCase()};}
function keyCompat(a,b){if(!a||!b)return 0;const da=Math.min(Math.abs(a.n-b.n),12-Math.abs(a.n-b.n));if(a.l===b.l&&da===0)return 3;if(a.l!==b.l&&da===0)return 2;if(a.l===b.l&&da===1)return 2;if(a.l===b.l&&da===2)return .5;return 0;}
function normName(t){return(t.name+"|"+t.artist).toLowerCase().replace(/\(.*?\)|\[.*?\]/g,"").replace(/feat\..*/,"").replace(/[^a-z0-9|]+/g," ").trim();}
function fmtDate(iso){try{return new Date(iso).toLocaleDateString(undefined,{month:"short",day:"numeric"});}catch{return"";}}
function locToPath(loc){try{return decodeURIComponent((loc||"").replace(/^file:\/\/localhost\//,"").replace(/^file:\/\//,""));}catch{return loc;}}
function downloadFile(name,content,mime){const b=new Blob([content],{type:mime||"text/plain"});const u=URL.createObjectURL(b);const a=document.createElement("a");a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),4000);}
function csv(rows){return rows.map(r=>r.map(c=>`"${String(c??"").replace(/"/g,'""')}"`).join(",")).join("\r\n");}

/* ---------------- parsing ---------------- */
function parseLibrary(xmlText){
  const doc=new DOMParser().parseFromString(xmlText,"text/xml");
  if(doc.querySelector("parsererror"))throw new Error("Not valid XML — export from rekordbox: File → Export Collection in xml format.");
  const els=[...doc.querySelectorAll("COLLECTION > TRACK")];
  if(!els.length)throw new Error("No tracks found in COLLECTION.");
  const tracks=els.map(el=>{
    const raw={};for(const n of el.getAttributeNames())raw[n]=el.getAttribute(n);
    const comments=raw.Comments||"";
    const m=/\/\*([\s\S]*?)\*\//.exec(comments);
    const tags=m?m[1].split("/").map(s=>s.trim()).filter(Boolean):[];
    const marks=[...el.querySelectorAll("POSITION_MARK")];
    const hotCues=marks.filter(x=>+(x.getAttribute("Num")??-1)>=0).length;
    const energy=Math.round((+raw.Rating||0)/51);
    const bpm=+raw.AverageBpm||0;
    const totalTime=+raw.TotalTime||0;
    const genre=(raw.Genre||"").trim();
    const isTool=tags.includes("DJ tool")||tags.includes("Acapella")||/loop samples|scratching/i.test(genre)||(totalTime>0&&totalTime<=35);
    const vocal=tags.some(t=>/^vocal$|^good vocal$/i.test(t))?true:(tags.some(t=>/instrumental/i.test(t))?false:null);
    return{id:raw.TrackID,name:raw.Name||"(untitled)",artist:raw.Artist||"",genre,bpm,totalTime,
      key:parseCamelot(raw.Tonality),keyStr:raw.Tonality||"",energy,tags,comments,hotCues,
      playCount:+raw.PlayCount||0,dateAdded:raw.DateAdded||"",location:raw.Location||"",isTool,vocal,raw};
  });
  return tracks;
}

/* ---------------- health ---------------- */
function computeHealth(tracks){
  const playable=tracks.filter(t=>!t.isTool);
  const missingEnergy=playable.filter(t=>t.energy===0);
  const missingKey=playable.filter(t=>!t.key);
  const needsGenre=playable.filter(t=>!TAXONOMY.includes(t.genre));
  const lowCues=playable.filter(t=>t.hotCues<2);
  const lowCuesSit=lowCues.filter(t=>t.tags.some(x=>["Tuesday","Main Floor","Second Floor"].includes(x)));
  const groups={};playable.forEach(t=>{const k=normName(t);(groups[k]=groups[k]||[]).push(t);});
  const duplicates=Object.values(groups).filter(g=>g.length>1);
  const eHist=[0,0,0,0,0,0];playable.forEach(t=>eHist[t.energy]++);
  const focus=[
    {n:missingEnergy.length,label:"tracks missing an energy rating",why:"Energy drives every slot the engine ranks — this is the gap that blurs everything downstream.",pct:missingEnergy.length/playable.length},
    {n:lowCues.length,label:`tracks with fewer than 2 hot cues (${lowCuesSit.length} of them situation-tagged)`,why:"Reactive CDJ work needs a mix-in point and a payoff point at minimum. Start with the situation-tagged ones.",pct:lowCues.length/playable.length},
    {n:missingKey.length,label:"tracks with no key analysis",why:"Invisible to harmonic ranking until analyzed in rekordbox.",pct:missingKey.length/playable.length},
    {n:needsGenre.length,label:"tracks outside taxonomy v0.1",why:"Run the Migration tab — the engine drafts genres, you approve.",pct:needsGenre.length/playable.length},
    {n:duplicates.length,label:"possible duplicate pairs",why:"Same title + artist after normalization. Verify before culling.",pct:0},
  ].filter(f=>f.n>0);
  return{playable,missingEnergy,missingKey,needsGenre,lowCues,lowCuesSit,duplicates,eHist,focus,
    energyCov:1-missingEnergy.length/playable.length,keyCov:1-missingKey.length/playable.length,
    cueReady:playable.filter(t=>t.hotCues>=2).length/playable.length};
}

/* ---------------- playlist engine ---------------- */
function neighborDensity(tracks){
  const counts={};tracks.forEach(t=>{if(t.key){const k=t.key.n+t.key.l;counts[k]=(counts[k]||0)+1;}});
  const dens={};let max=1;
  tracks.forEach(t=>{if(!t.key)return;let d=0;for(const[k,c]of Object.entries(counts)){const kk=parseCamelot(k);if(keyCompat(t.key,kk)>=2)d+=c;}dens[t.id]=d;max=Math.max(max,d);});
  return id=>(dens[id]||0)/max;
}
function labness(t){
  let s=0;
  if(t.tags.includes("Other"))s+=1.6;
  if(t.tags.length===0)s+=0.6;
  if(t.bpm&&(t.bpm<116||t.bpm>136))s+=1.2;
  if(/hip hop|future bass|hard dance|lab -/i.test(t.genre))s+=1.4;
  return Math.min(s,3);
}
function scoreTrack(t,bucket,sit,dens,rnd){
  let s=0;
  const[e0,e1]=bucket.e,mid=(e0+e1)/2;
  if(!bucket.lab){if(t.energy===0)s-=1.8;else{s+=2.2-Math.abs(t.energy-mid)*1.6;if(t.energy>=Math.floor(e0)&&t.energy<=Math.ceil(e1))s+=0.6;}}
  const gtTags=bucket.gt||sit.gt;
  if(gtTags.length&&t.tags.some(x=>gtTags.includes(x)))s+=2.2;
  if(bucket.tagW)for(const[tag,w]of Object.entries(bucket.tagW))if(t.tags.includes(tag))s+=w;
  if(bucket.vocal&&t.vocal===true)s+=bucket.vocal*0.8;
  if(bucket.instr&&t.vocal===false)s+=bucket.instr*0.8;
  if(bucket.played&&t.playCount>0)s+=Math.min(bucket.played,1+t.playCount*0.3);
  if(bucket.versatile&&t.key)s+=dens(t.id)*bucket.versatile;
  if(bucket.lab)s+=labness(t)*bucket.lab;
  if(t.bpm){const[b0,b1]=sit.bpm;if(t.bpm>=b0&&t.bpm<=b1)s+=0.5;else if(!bucket.lab)s-=Math.min(2.5,Math.abs(t.bpm-(t.bpm<b0?b0:b1))/8);}
  if(bucket.fresh&&t.dateAdded){const days=(Date.now()-new Date(t.dateAdded))/864e5;if(days<75)s+=bucket.fresh;}
  s+=rnd()*0.9;
  return s;
}
function buildCrate(tracks,sitId,pinned,seed){
  const sit=SITUATIONS[sitId];
  const pool=tracks.filter(t=>!t.isTool);
  const dens=neighborDensity(pool);
  const rnd=mulberry32(seed);
  const used=new Set();
  const crate={};
  for(const b of sit.buckets){
    const pins=(pinned[b.id]||[]).filter(id=>pool.some(t=>t.id===id));
    pins.forEach(id=>used.add(id));
    const ranked=pool.filter(t=>!used.has(t.id)).map(t=>({t,s:scoreTrack(t,b,sit,dens,rnd)})).sort((a,b2)=>b2.s-a.s);
    const chosen=[...pins];
    for(const{t}of ranked){if(chosen.length>=b.n)break;chosen.push(t.id);used.add(t.id);}
    const byId=Object.fromEntries(pool.map(t=>[t.id,t]));
    chosen.sort((x,y)=>{const a=byId[x],c=byId[y];const ka=a.key?a.key.l+String(a.key.n).padStart(2,"0"):"Z99";const kb=c.key?c.key.l+String(c.key.n).padStart(2,"0"):"Z99";return ka<kb?-1:ka>kb?1:a.bpm-c.bpm;});
    crate[b.id]=chosen;
  }
  return crate;
}
function alternativesFor(tracks,sitId,bucketId,crate,seed){
  const sit=SITUATIONS[sitId];const b=sit.buckets.find(x=>x.id===bucketId);
  const pool=tracks.filter(t=>!t.isTool);
  const dens=neighborDensity(pool);
  const rnd=mulberry32(seed+7);
  const used=new Set(Object.values(crate).flat());
  return pool.filter(t=>!used.has(t.id)).map(t=>({t,s:scoreTrack(t,b,sit,dens,rnd)})).sort((a,c)=>c.s-a.s).slice(0,8).map(x=>x.t);
}

/* ---------------- exports ---------------- */
function crateOrder(sit,crate){return sit.buckets.flatMap(b=>crate[b.id]||[]);}
function buildRekordboxXML(name,orderedTracks){
  const trackXml=orderedTracks.map(t=>{
    const attrs=Object.entries(t.raw).map(([k,v])=>`${k}="${xmlEscape(v)}"`).join(" ");
    return`    <TRACK ${attrs}/>`;}).join("\n");
  const refs=orderedTracks.map(t=>`        <TRACK Key="${xmlEscape(t.id)}"/>`).join("\n");
  return`<?xml version="1.0" encoding="UTF-8"?>\n<DJ_PLAYLISTS Version="1.0.0">\n  <PRODUCT Name="rekordbox" Version="7.2.14" Company="AlphaTheta"/>\n  <COLLECTION Entries="${orderedTracks.length}">\n${trackXml}\n  </COLLECTION>\n  <PLAYLISTS>\n    <NODE Type="0" Name="ROOT" Count="1">\n      <NODE Name="${xmlEscape(name)}" Type="1" KeyType="0" Entries="${orderedTracks.length}">\n${refs}\n      </NODE>\n    </NODE>\n  </PLAYLISTS>\n</DJ_PLAYLISTS>`;
}
function buildM3U8(orderedTracks){
  return"#EXTM3U\n"+orderedTracks.map(t=>`#EXTINF:${t.totalTime||-1},${t.artist} - ${t.name}\n${locToPath(t.location)}`).join("\n")+"\n";
}
function buildTextSetlist(sit,crate,byId){
  let out=`${sit.name} — contingency crate · ${new Date().toLocaleDateString()}\n`;
  for(const b of sit.buckets){out+=`\n■ ${b.role}\n`;(crate[b.id]||[]).forEach(id=>{const t=byId[id];out+=`  ${t.artist} — ${t.name}  [${t.keyStr||"?"} · ${t.bpm?Math.round(t.bpm):"?"} · E${t.energy||"?"}]\n`;});}
  return out;
}

/* ---------------- storage ---------------- */
async function loadStored(key){try{const v=localStorage.getItem(key);return v?JSON.parse(v):null;}catch{return null;}}
async function saveStored(key,val){try{localStorage.setItem(key,JSON.stringify(val));return true;}catch{return false;}}
function snapshotOf(tracks){const byId={};tracks.forEach(t=>byId[t.id]=[t.energy,t.keyStr,t.tags.length,t.hotCues]);return{at:Date.now(),n:tracks.length,byId};}
function diffSnapshot(base,tracks){
  if(!base)return null;
  const cur=new Set(tracks.map(t=>t.id));
  const added=tracks.filter(t=>!base.byId[t.id]);
  const removed=Object.keys(base.byId).filter(id=>!cur.has(id)).length;
  let changed=0;tracks.forEach(t=>{const b=base.byId[t.id];if(b&&(b[0]!==t.energy||b[1]!==t.keyStr||b[2]!==t.tags.length||b[3]!==t.hotCues))changed++;});
  return{at:base.at,added,removed,changed};
}

/* ---------------- AI ---------------- */
async function callClaude(prompt){
  const res=await fetch("https://api.anthropic.com/v1/messages",{method:"POST",headers:{"Content-Type":"application/json"},
    body:JSON.stringify({model:"claude-sonnet-4-6",max_tokens:1000,messages:[{role:"user",content:prompt}]})});
  const data=await res.json();
  if(data.error)throw new Error(data.error.message||"AI call failed");
  return(data.content||[]).filter(x=>x.type==="text").map(x=>x.text).join("\n");
}
function crateSummary(sit,crate,byId){
  return sit.buckets.map(b=>`${b.role} (target E${b.e[0]}–${b.e[1]}):\n`+(crate[b.id]||[]).map(id=>{const t=byId[id];
    return`  ${t.artist} — ${t.name} [${t.keyStr||"?"}, ${t.bpm?Math.round(t.bpm):"?"}bpm, E${t.energy||"?"}${t.vocal===true?", vocal":t.vocal===false?", instr":""}${t.tags.filter(x=>["Build up","Build down","Peak Time"].includes(x)).map(x=>", "+x).join("")}]`;}).join("\n")).join("\n");
}

/* ---------------- small components ---------------- */
function Octahedron({size=30}){
  return(<svg width={size} height={size} viewBox="0 0 40 40" aria-hidden="true">
    <defs><linearGradient id="cwpr" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stopColor="#35E0C6"/><stop offset="55%" stopColor="#A98CF2"/><stop offset="100%" stopColor="#E4649E"/>
    </linearGradient></defs>
    <path d="M20 3 L35 20 L20 37 L5 20 Z" fill="none" stroke="url(#cwpr)" strokeWidth="1.6"/>
    <path d="M5 20 H35 M20 3 V37" stroke="url(#cwpr)" strokeWidth="0.7" opacity="0.55"/>
  </svg>);
}
function EChip({e}){
  return<span className="cw-chip cw-echip cw-mono" style={{background:EN_COLORS[e||0],color:e?"#08211D":"#B9C6C9"}}>{e?`E${e}`:"E?"}</span>;
}
function KChip({t}){return<span className="cw-chip cw-mono">{t.keyStr||"—"}</span>;}
function BChip({t}){return<span className="cw-chip cw-mono">{t.bpm?Math.round(t.bpm):"—"}</span>;}
function Toast({msg}){return msg?<div className="cw-toast" role="status">{msg}</div>:null;}

function TrackRow({t,gtTags,children}){
  const gt=t.tags.some(x=>gtTags&&gtTags.includes(x));
  return(<div className="cw-row">
    <div style={{flex:1,minWidth:0}}>
      <div className="cw-track-name">{t.name}{gt&&<span title="gig-proven for this situation" style={{color:"var(--teal)",marginLeft:6}}>●</span>}{t.vocal===true&&<span className="cw-note" style={{marginLeft:6}}>V</span>}</div>
      <div className="cw-track-artist">{t.artist||"—"}</div>
    </div>
    <KChip t={t}/><BChip t={t}/><EChip e={t.energy}/>
    {children}
  </div>);
}

/* ---------------- upload screen ---------------- */
function Upload({onLoad,cache,notice}){
  const ref=useRef(null);
  const read=f=>{const r=new FileReader();r.onload=()=>onLoad(r.result,f.name);r.readAsText(f);};
  return(<div style={{paddingTop:48,textAlign:"center"}}>
    <div style={{display:"flex",justifyContent:"center",marginBottom:18}}><Octahedron size={54}/></div>
    <div className="cw-serif" style={{fontSize:34,fontWeight:600,letterSpacing:".02em"}}>Library Engine</div>
    <div className="cw-sub" style={{marginBottom:34}}>Cratewise · rules first · never edits your files</div>
    <div className="cw-drop" role="button" tabIndex={0} onClick={()=>ref.current.click()} onKeyDown={e=>e.key==="Enter"&&ref.current.click()}
      onDragOver={e=>e.preventDefault()} onDrop={e=>{e.preventDefault();const f=e.dataTransfer.files[0];if(f)read(f);}}>
      <div style={{fontWeight:600,fontSize:15}}>Drop your rekordbox XML export</div>
      <div className="cw-note" style={{marginTop:6}}>File → Export Collection in xml format · MyTags flow in via Comments</div>
      <input ref={ref} type="file" accept=".xml,text/xml" style={{display:"none"}} onChange={e=>{const f=e.target.files[0];if(f)read(f);}}/>
    </div>
    {cache&&<div style={{marginTop:18}}>
      <button className="cw-btn" onClick={()=>onLoad(null,null,true)}>Resume cached library · {cache.n} tracks · saved {fmtDate(cache.at)}</button>
    </div>}
    {notice&&<div className="cw-note" style={{marginTop:14,color:"var(--e5)"}}>{notice}</div>}
  </div>);
}

/* ---------------- dashboard (Pulse) ---------------- */
function Pulse({tracks,health,drift,onBaseline,baselineAt,toast}){
  const h=health;
  const colorRows=useMemo(()=>{
    const rows=[];tracks.forEach(t=>{const tag=Object.keys(ARC_COLOR).find(x=>t.tags.includes(x));if(tag)rows.push([t.name,t.artist,ARC_COLOR[tag],tag]);});
    return rows;},[tracks]);
  const stat=(label,val,sub)=>(<div className="cw-card"><div className="cw-h">{label}</div><div className="cw-big cw-mono">{val}</div>{sub&&<div className="cw-note" style={{marginTop:4}}>{sub}</div>}</div>);
  return(<div>
    <div className="cw-grid c4">
      {stat("Playable tracks",h.playable.length,`${tracks.length-h.playable.length} tools & acapellas set aside`)}
      {stat("Energy coverage",Math.round(h.energyCov*100)+"%",`${h.missingEnergy.length} untagged`)}
      {stat("Key coverage",Math.round(h.keyCov*100)+"%",`${h.missingKey.length} unanalyzed`)}
      {stat("Cue-ready",Math.round(h.cueReady*100)+"%","2+ hot cues")}
    </div>

    {drift&&<div className="cw-card" style={{marginTop:14,borderColor:"var(--teal-dim)"}}>
      <div className="cw-h">Since baseline · {fmtDate(drift.at)}</div>
      <div style={{display:"flex",gap:26,flexWrap:"wrap"}} className="cw-mono">
        <span>+{drift.added.length} added</span><span>−{drift.removed} removed</span><span>{drift.changed} tracks changed (energy / key / tags / cues)</span>
      </div>
      <div style={{marginTop:10}}><button className="cw-btn sm" onClick={onBaseline}>Set current scan as new baseline</button></div>
    </div>}
    {!drift&&<div className="cw-card" style={{marginTop:14}}>
      <div className="cw-h">Drift tracking</div>
      <div className="cw-note">{baselineAt?`Baseline saved ${fmtDate(baselineAt)} — matches current scan.`:"No baseline yet. Save one and every future upload gets a since-last-scan report."}</div>
      <div style={{marginTop:10}}><button className="cw-btn sm" onClick={onBaseline}>Save baseline</button></div>
    </div>}

    <div className="cw-grid c2" style={{marginTop:14}}>
      <div className="cw-card">
        <div className="cw-h">Focus queue — where attention pays next</div>
        {h.focus.map((f,i)=>(<div className="cw-focus-item" key={i}>
          <span className="cw-num cw-mono">{String(i+1).padStart(2,"0")}</span>
          <div><div style={{fontWeight:600}}>{f.n} {f.label}</div><div className="cw-note">{f.why}</div></div>
        </div>))}
        {!h.focus.length&&<div className="cw-note">Nothing burning. Go make music.</div>}
      </div>
      <div>
        <div className="cw-card">
          <div className="cw-h">Energy spread (playable)</div>
          {[1,2,3,4,5].map(e=>(<div key={e} style={{display:"flex",alignItems:"center",gap:10,margin:"7px 0"}}>
            <span className="cw-mono cw-note" style={{width:24}}>E{e}</span>
            <div className="cw-bar" style={{flex:1}}><div style={{width:(h.eHist[e]/Math.max(...h.eHist.slice(1),1)*100)+"%",background:EN_COLORS[e]}}/></div>
            <span className="cw-mono cw-note" style={{width:30,textAlign:"right"}}>{h.eHist[e]}</span>
          </div>))}
          <div className="cw-note" style={{marginTop:6}}>E? (untagged): {h.eHist[0]}</div>
        </div>
        <div className="cw-card">
          <div className="cw-h">Color policy · D5</div>
          <div className="cw-note">Color = arc function, one dimension only. Green Build up · Blue Build down · Red Peak · Purple Tool. {colorRows.length} tracks have a suggested color; apply in rekordbox from the checklist.</div>
          <div style={{marginTop:10}}><button className="cw-btn sm" onClick={()=>{downloadFile("cratewise_color_checklist.csv",csv([["Track","Artist","Color","From tag"],...colorRows]),"text/csv");toast("Color checklist downloaded");}}>Download color checklist (CSV)</button></div>
        </div>
      </div>
    </div>

    {h.duplicates.length>0&&<div className="cw-card" style={{marginTop:14}}>
      <div className="cw-h">Possible duplicates</div>
      {h.duplicates.slice(0,8).map((g,i)=>(<div key={i} className="cw-note" style={{padding:"4px 0"}}>{g.map(t=>`${t.artist} — ${t.name}`).join("  ·  ")}</div>))}
    </div>}
  </div>);
}

/* ---------------- builder ---------------- */
function Builder({tracks,toast}){
  const[sitId,setSitId]=useState("lucent");
  const[seed,setSeed]=useState(1);
  const[pinned,setPinned]=useState({});
  const[crate,setCrate]=useState(null);
  const[swapOpen,setSwapOpen]=useState(null); // {bucketId, trackId}
  const[ai,setAi]=useState({loading:false,text:null,err:null});
  const sit=SITUATIONS[sitId];
  const byId=useMemo(()=>Object.fromEntries(tracks.map(t=>[t.id,t])),[tracks]);

  const generate=(newSeed)=>{const s=newSeed??seed;setSeed(s);setCrate(buildCrate(tracks,sitId,pinned,s));setAi({loading:false,text:null,err:null});setSwapOpen(null);};
  useEffect(()=>{setPinned({});setCrate(null);setAi({loading:false,text:null,err:null});},[sitId]);

  const togglePin=(bId,id)=>{setPinned(p=>{const cur=new Set(p[bId]||[]);cur.has(id)?cur.delete(id):cur.add(id);return{...p,[bId]:[...cur]};});};
  const doSwap=(bId,oldId,newId)=>{setCrate(c=>({...c,[bId]:c[bId].map(x=>x===oldId?newId:x)}));setSwapOpen(null);};
  const remove=(bId,id)=>{setCrate(c=>({...c,[bId]:c[bId].filter(x=>x!==id)}));setPinned(p=>({...p,[bId]:(p[bId]||[]).filter(x=>x!==id)}));};

  const exportXML=()=>{const ord=crateOrder(sit,crate).map(id=>byId[id]);const nm=`Cratewise ${sit.name} ${new Date().toISOString().slice(0,10)}`;downloadFile(nm.replace(/\s+/g,"_")+".xml",buildRekordboxXML(nm,ord),"text/xml");toast("Rekordbox XML downloaded — import via the rekordbox xml bridge");};
  const exportM3U=()=>{const ord=crateOrder(sit,crate).map(id=>byId[id]);downloadFile(`Cratewise_${sitId}_${Date.now()}.m3u8`,buildM3U8(ord),"audio/x-mpegurl");toast("M3U8 downloaded");};
  const copyText=async()=>{await navigator.clipboard.writeText(buildTextSetlist(sit,crate,byId));toast("Setlist copied");};

  const review=async()=>{
    setAi({loading:true,text:null,err:null});
    try{
      const text=await callClaude(
`You are the library partner for a house & techno DJ (reactive, phrase-aligned mixing; Camelot-aware).
Situation: ${sit.name} — ${sit.sub}. BPM lane ${sit.bpm[0]}–${sit.bpm[1]}.
Below is a CONTINGENCY CRATE (buckets you pick from live, not a fixed sequence).

${crateSummary(sit,crate,byId)}

Respond in plain text, under 260 words, direct tone, no flattery:
1) Three sharpest strengths (one line each).
2) Three flow or coverage risks (key clumping, energy holes, vocal stacking, thin buckets).
3) Two concrete moves using only tracks already listed (move X from bucket A to B, or drop X because Y).
4) One digging gap: what this crate is missing that you should buy next.`);
      setAi({loading:false,text,err:null});
    }catch(e){setAi({loading:false,text:null,err:e.message});}
  };

  return(<div>
    <div style={{display:"flex",gap:10,flexWrap:"wrap",alignItems:"center"}}>
      {Object.values(SITUATIONS).map(s=>(<button key={s.id} className={"cw-btn"+(s.id===sitId?" pri":"")} onClick={()=>setSitId(s.id)}>{s.name}</button>))}
      <div style={{flex:1}}/>
      <button className="cw-btn pri" onClick={()=>generate(crate?seed+1:seed)}>{crate?"Regenerate (keeps pins)":"Generate crate"}</button>
    </div>
    <div className="cw-note" style={{margin:"10px 2px 0"}}>{sit.sub} · BPM {sit.bpm[0]}–{sit.bpm[1]} · ● = gig-proven for this situation · pins survive regeneration</div>

    {crate&&<div style={{marginTop:16}}>
      <div className="cw-card" style={{display:"flex",gap:10,flexWrap:"wrap",alignItems:"center"}}>
        <span className="cw-h" style={{margin:0,flex:1}}>Exports</span>
        <button className="cw-btn sm" onClick={exportXML}>Rekordbox XML</button>
        <button className="cw-btn sm" onClick={exportM3U}>M3U8</button>
        <button className="cw-btn sm" onClick={copyText}>Copy setlist text</button>
        <button className="cw-btn sm" onClick={review} disabled title="AI review isn't enabled in this build yet">AI review</button>
      </div>
      {(ai.text||ai.err)&&<div className="cw-card" style={{marginTop:14,borderColor:"var(--line2)"}}>
        <div className="cw-h">Partner notes</div>
        {ai.err?<div className="cw-note" style={{color:"var(--e5)"}}>Review failed: {ai.err}. The crate stands on rules alone — try again later.</div>:<div className="cw-ai">{ai.text}</div>}
      </div>}
      {sit.buckets.map(b=>(<div className="cw-card" key={b.id} style={{marginTop:14}}>
        <div style={{display:"flex",alignItems:"baseline",gap:12,marginBottom:6}}>
          <div className="cw-serif" style={{fontSize:19,fontWeight:600}}>{b.role}</div>
          <span className="cw-chip cw-mono">E{b.e[0]}–{b.e[1]}</span>
          <span className="cw-note" style={{flex:1}}>{b.desc}</span>
          <span className="cw-note cw-mono">{(crate[b.id]||[]).length}/{b.n}</span>
        </div>
        {(crate[b.id]||[]).map(id=>{const t=byId[id];const isPin=(pinned[b.id]||[]).includes(id);const open=swapOpen&&swapOpen.b===b.id&&swapOpen.t===id;
          return(<div key={id}>
            <TrackRow t={t} gtTags={b.gt||sit.gt}>
              <button className={"cw-icon-btn"+(isPin?" on":"")} title={isPin?"Unpin":"Pin (survives regenerate)"} onClick={()=>togglePin(b.id,id)}>⏍</button>
              <button className="cw-icon-btn" title="Swap for an alternative" onClick={()=>setSwapOpen(open?null:{b:b.id,t:id})}>⇄</button>
              <button className="cw-icon-btn" title="Remove" onClick={()=>remove(b.id,id)}>✕</button>
            </TrackRow>
            {open&&<div className="cw-alt">
              <div className="cw-note" style={{padding:"0 10px 4px"}}>Ranked alternatives — click to swap in</div>
              {alternativesFor(tracks,sitId,b.id,crate,seed).map(a=>(<div key={a.id} onClick={()=>doSwap(b.id,id,a.id)}>
                <TrackRow t={a} gtTags={b.gt||sit.gt}/></div>))}
            </div>}
          </div>);})}
      </div>))}
    </div>}
    {!crate&&<div className="cw-card" style={{marginTop:16,textAlign:"center",padding:"40px 20px"}}>
      <div className="cw-note">Pick a situation and generate. You get a bucketed contingency crate — ordered by key within each bucket — not a rigid sequence. Swap, pin, regenerate until it feels like a set.</div>
    </div>}
  </div>);
}

/* ---------------- migration ---------------- */
function Migration({tracks,health,toast}){
  const[scope,setScope]=useState("needs");
  const[sugs,setSugs]=useState({});
  const[run,setRun]=useState({on:false,done:0,total:0,err:null});
  const list=scope==="needs"?health.needsGenre:health.playable;
  const runBatches=async()=>{
    const todo=list.filter(t=>!sugs[t.id]);
    setRun({on:true,done:0,total:todo.length,err:null});
    const out={...sugs};
    try{
      for(let i=0;i<todo.length;i+=25){
        const batch=todo.slice(i,i+25);
        const prompt=`Classify each DJ track into EXACTLY ONE of these genres:\n${TAXONOMY.join(" | ")}\n\nMelodic-blur tiebreak: swing/bounce in the drums → House; straight drive → Techno. Short one-shots, loops, FX → Tools.\nRespond ONLY with a JSON array, no prose, no markdown fences: [{"id":"...","g":"..."}]\n\nTracks:\n${batch.map(t=>`${t.id} | ${t.artist} — ${t.name} | ${t.bpm?Math.round(t.bpm)+"bpm":"?"} | current: ${t.genre||"none"} | tags: ${t.tags.join(", ")||"none"}`).join("\n")}`;
        const text=await callClaude(prompt);
        const clean=text.replace(/```json|```/g,"").trim();
        const arr=JSON.parse(clean.slice(clean.indexOf("["),clean.lastIndexOf("]")+1));
        arr.forEach(r=>{if(r.id&&TAXONOMY.includes(r.g))out[r.id]=r.g;});
        setSugs({...out});setRun(r=>({...r,done:Math.min(i+25,todo.length)}));
      }
      setRun(r=>({...r,on:false}));toast("Genre suggestions complete");
    }catch(e){setSugs({...out});setRun({on:false,done:0,total:0,err:e.message});}
  };
  const exportCsv=()=>{
    const rows=[["Track","Artist","Current genre","Suggested genre"]];
    list.forEach(t=>{if(sugs[t.id])rows.push([t.name,t.artist,t.genre,sugs[t.id]]);});
    downloadFile("cratewise_genre_migration_checklist.csv",csv(rows),"text/csv");toast("Checklist downloaded — apply in rekordbox, then re-export");
  };
  const nDone=list.filter(t=>sugs[t.id]).length;
  return(<div>
    <div className="cw-card">
      <div className="cw-h">Genre migration — taxonomy v0.1</div>
      <div className="cw-note">The engine drafts one genre per track (AI, batched); you approve and apply inside rekordbox. Nothing here edits your files. This layer isn't enabled in this build yet.</div>
      <div style={{display:"flex",gap:10,marginTop:12,flexWrap:"wrap",alignItems:"center"}}>
        <select className="cw-sel" value={scope} onChange={e=>setScope(e.target.value)}>
          <option value="needs">Outside taxonomy ({health.needsGenre.length})</option>
          <option value="all">All playable ({health.playable.length})</option>
        </select>
        <button className="cw-btn pri" onClick={runBatches} disabled title="Not enabled in this build yet">Draft genres</button>
        <button className="cw-btn" onClick={exportCsv} disabled={!nDone}>Download checklist ({nDone})</button>
      </div>
      {run.err&&<div className="cw-note" style={{marginTop:8,color:"var(--e5)"}}>Stopped early: {run.err}. Suggestions so far are kept — run again to continue.</div>}
    </div>
    {nDone>0&&<div className="cw-card" style={{marginTop:14,maxHeight:420,overflow:"auto"}}>
      <table className="cw-table"><thead><tr><th>Track</th><th>Artist</th><th>Current</th><th>Suggested</th></tr></thead>
      <tbody>{list.filter(t=>sugs[t.id]).map(t=>(<tr key={t.id}><td>{t.name}</td><td className="cw-note">{t.artist}</td><td className="cw-note">{t.genre||"—"}</td><td style={{color:"var(--teal)"}}>{sugs[t.id]}</td></tr>))}</tbody></table>
    </div>}
  </div>);
}

/* ---------------- app ---------------- */
function App(){
  const[tracks,setTracks]=useState(null);
  const[fileName,setFileName]=useState("");
  const[tab,setTab]=useState("pulse");
  const[cacheInfo,setCacheInfo]=useState(null);
  const[baseline,setBaseline]=useState(null);
  const[notice,setNotice]=useState(null);
  const[msg,setMsg]=useState(null);
  const toast=t=>{setMsg(t);setTimeout(()=>setMsg(null),3200);};

  useEffect(()=>{(async()=>{
    const c=await loadStored("cratewise_library_cache");if(c)setCacheInfo({n:c.tracks.length,at:c.at});
    const b=await loadStored("cratewise_baseline");if(b)setBaseline(b);
  })();},[]);

  const onLoad=async(text,name,fromCache)=>{
    try{
      let ts;
      if(fromCache){const c=await loadStored("cratewise_library_cache");if(!c)throw new Error("Cache unavailable.");ts=c.tracks;setFileName("cached library");}
      else{ts=parseLibrary(text);setFileName(name);
        const ok=await saveStored("cratewise_library_cache",{at:Date.now(),tracks:ts});
        if(ok)setCacheInfo({n:ts.length,at:Date.now()});}
      setTracks(ts);setNotice(null);
    }catch(e){setNotice(e.message);}
  };
  const health=useMemo(()=>tracks?computeHealth(tracks):null,[tracks]);
  const drift=useMemo(()=>{if(!tracks||!baseline)return null;const d=diffSnapshot(baseline,tracks);return d&&(d.added.length||d.removed||d.changed)?d:null;},[tracks,baseline]);
  const saveBaseline=async()=>{const s=snapshotOf(tracks);const ok=await saveStored("cratewise_baseline",s);if(ok){setBaseline(s);toast("Baseline saved");}else toast("Could not save baseline");};

  return(<div className="cw-root">
    <style>{CSS}</style>
    <div className="cw-wrap">
      <div className="cw-header">
        <Octahedron/>
        <div style={{flex:1}}>
          <div className="cw-wordmark cw-serif">Cratewise</div>
          <div className="cw-sub">Library Engine{tracks?` · ${fileName} · ${tracks.length} tracks`:""}</div>
        </div>
        {tracks&&<button className="cw-btn sm" onClick={()=>{setTracks(null);setTab("pulse");}}>Load different export</button>}
      </div>
      <div className="cw-refract"/>
      {!tracks&&<Upload onLoad={onLoad} cache={cacheInfo} notice={notice}/>}
      {tracks&&<>
        <div className="cw-tabs" role="tablist">
          {[["pulse","Pulse"],["builder","Builder"],["migration","Migration"]].map(([id,label])=>(
            <button key={id} role="tab" aria-selected={tab===id} className={"cw-tab"+(tab===id?" on":"")} onClick={()=>setTab(id)}>{label}</button>))}
        </div>
        {tab==="pulse"&&<Pulse tracks={tracks} health={health} drift={drift} onBaseline={saveBaseline} baselineAt={baseline&&baseline.at} toast={toast}/>}
        {tab==="builder"&&<Builder tracks={tracks} toast={toast}/>}
        {tab==="migration"&&<Migration tracks={tracks} health={health} toast={toast}/>}
      </>}
    </div>
    <Toast msg={msg}/>
  </div>);
}

ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
