// b/analyze.jsx — the data layer.
//
// Two jobs now:
//   1) analyzeImage()  → POST /api/briklab (Groq vision) to GUESS the bricks on
//      a photo. Only a starting point — the user edits it in the app.
//   2) findSets()      → match the user's (edited) inventory against the bundled
//      sets-pool.json (real Rebrickable data) IN THE BROWSER. No API key needed.
//
// Instruction links point to the official LEGO / Rebrickable pages — we never
// re-draw copyrighted instructions in-app.

// ---- catalog + pool (bundled static assets, fetched once) ------------------
let _catalog = null, _pool = null;
function loadJSON(url) { return fetch(url).then(r => { if (!r.ok) throw new Error(url + ' ' + r.status); return r.json(); }); }
async function loadCatalog() { if (!_catalog) { _catalog = await loadJSON('parts-catalog.json'); window.CATALOG = _catalog; } return _catalog; }
async function loadPool() { if (!_pool) { _pool = await loadJSON('sets-pool.json'); } return _pool; }

// AI returns loose english color names → map onto catalog color keys.
const AI_COLOR = {
  red: 'red', blue: 'blue', white: 'white', black: 'black', yellow: 'yellow',
  green: 'green', lime: 'lime', orange: 'orange', brown: 'brown', tan: 'tan',
  grey: 'lgray', gray: 'lgray', lightgrey: 'lgray', lightgray: 'lgray',
  darkgrey: 'dgray', darkgray: 'dgray', clear: 'white',
};
const normKey = (s = '') => String(s).toLowerCase().replace(/\s+/g, ' ').trim();

// ---- robust JSON extraction (for the AI reply) -----------------------------
function parseJSON(text) {
  if (typeof text !== 'string') return text;
  let s = text.trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();
  const i = s.indexOf('{');
  if (i < 0) throw new Error('no json');
  let depth = 0, end = -1, inStr = false, esc = false;
  for (let k = i; k < s.length; k++) {
    const c = s[k];
    if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; }
    else if (c === '"') inStr = true;
    else if (c === '{') depth++;
    else if (c === '}') { depth--; if (depth === 0) { end = k; break; } }
  }
  return JSON.parse(s.slice(i, end < 0 ? undefined : end + 1));
}

// ---- 1) photo → seed inventory ---------------------------------------------
// Returns { source, items } where items = [{ part, color, count }] using
// catalog keys. The user edits these before matching.
async function analyzeImage(image, mediaType) {
  const cat = await loadCatalog();
  let raw = null, source = 'manual';
  try {
    const r = await fetch('/api/briklab', {
      method: 'POST', headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ task: 'inventory', image, mediaType }),
    });
    if (r.ok) { raw = await r.json(); source = 'api'; }
  } catch (e) { /* no backend / no key — fall back to manual entry */ }
  if (!raw || raw.error) return { source: 'manual', items: [] };

  // map the loose guess onto catalog rows
  const shapeKeys = (raw.shapes || [])
    .map(s => cat.shapeGuess[normKey(s)] || cat.shapeGuess[normKey(s).replace('x', '×')])
    .filter(Boolean);
  const defaultPart = shapeKeys[0] || 'brick-2x4';
  const items = (raw.colors || [])
    .map(c => ({ part: defaultPart, color: AI_COLOR[normKey(c.color)] || null, count: Math.max(1, Math.round(c.count || 1)) }))
    .filter(it => it.color);
  return { source, items };
}

// ---- 2) inventory → matching real sets -------------------------------------
const diffForParts = (n) => n < 40 ? 1 : n < 90 ? 2 : n < 180 ? 3 : n < 320 ? 4 : 5;

function userMapFrom(items, cat) {
  const partRb = Object.fromEntries(cat.parts.map(p => [p.key, p.rb]));
  const m = new Map();
  for (const it of items) {
    const rb = partRb[it.part];
    if (!rb || !it.color || !it.count) continue;
    const key = rb + '|' + it.color;
    m.set(key, (m.get(key) || 0) + Number(it.count));
  }
  return m;
}

function scoreSet(set, userMap) {
  let need = 0, have = 0, missTotal = 0;
  const missing = [];
  for (const part of set.p) {
    need += part.q;
    const own = userMap.get(part.p + '|' + (part.c || '')) || 0;
    const got = Math.min(own, part.q);
    have += got;
    if (got < part.q) { const lack = part.q - got; missTotal += lack; missing.push({ name: part.n, q: lack, price: 3 }); }
  }
  return { match: need ? Math.round((have / need) * 100) : 0, missing, missTotal };
}

function instructionsUrl(set) {
  const num = String(set.id).split('-')[0];
  if (/^\d+$/.test(num)) return 'https://www.lego.com/en-us/service/building-instructions/' + num;
  return set.url || 'https://rebrickable.com/build/';
}

async function findSets(items) {
  const [cat, pool] = [await loadCatalog(), await loadPool()];
  const userMap = userMapFrom(items, cat);
  const projects = pool.sets.map(set => {
    const { match, missing, missTotal } = scoreSet(set, userMap);
    return {
      id: set.id, name: set.name, year: set.year, cat: 'Officielt sæt' + (set.year ? ' · ' + set.year : ''),
      diff: diffForParts(set.parts), parts: set.parts, match,
      palette: (set.palette && set.palette.length ? set.palette : ['#c8483b', '#26241f', '#9a988f']).slice(0, 3),
      blurb: 'Officielt LEGO-sæt' + (set.year ? ' fra ' + set.year : '') + ' med ' + set.parts + ' dele.',
      tip: missTotal ? ('Du har ' + match + '% af delene – mangler ' + missTotal + ' klodser.') : 'Du har alle delene 🎉',
      missing: missing.slice(0, 12), missTotal,
      img: set.img || '', url: set.url || '', instr: instructionsUrl(set),
    };
  });
  projects.sort((a, b) => (b.match - a.match) || (a.parts - b.parts));
  return { placeholder: !!pool.placeholder, projects: projects.slice(0, 14) };
}

// ---- client-side image capture / compression ------------------------------
function fileToCompressedBase64(file, maxDim = 1024, quality = 0.82) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const url = URL.createObjectURL(file);
    img.onload = () => {
      URL.revokeObjectURL(url);
      const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
      const w = Math.round(img.width * scale), h = Math.round(img.height * scale);
      const c = document.createElement('canvas'); c.width = w; c.height = h;
      c.getContext('2d').drawImage(img, 0, 0, w, h);
      resolve({ base64: c.toDataURL('image/jpeg', quality).split(',')[1], mediaType: 'image/jpeg',
        preview: c.toDataURL('image/jpeg', quality) });
    };
    img.onerror = reject; img.src = url;
  });
}
function videoFrameToBase64(video, maxDim = 1024, quality = 0.82) {
  const scale = Math.min(1, maxDim / Math.max(video.videoWidth, video.videoHeight));
  const w = Math.round(video.videoWidth * scale), h = Math.round(video.videoHeight * scale);
  const c = document.createElement('canvas'); c.width = w; c.height = h;
  c.getContext('2d').drawImage(video, 0, 0, w, h);
  return { base64: c.toDataURL('image/jpeg', quality).split(',')[1], mediaType: 'image/jpeg',
    preview: c.toDataURL('image/jpeg', quality) };
}

Object.assign(window, {
  analyzeImage, findSets, instructionsUrl, loadCatalog,
  fileToCompressedBase64, videoFrameToBase64,
});
