// generator.jsx — REAL AI reasoning. No templates. No profiles.
//
// The user types arbitrary items and arbitrary priorities. We ask Claude to:
//   - reason about the items as they actually are in the world
//   - score each priority for each item (1..10)
//   - write category notes that reference the user's actual priorities
//   - produce a verdict that reflects priority weighting
//   - cite plausible sources, generate quotes, build a spec table
//
// Result is cached in-memory by (items + priorities + purpose) so re-renders
// are instant and consistent.

(function () {
  const cache = new Map();

  function cacheKey(answers) {
    const a = answers?.what?.a || '';
    const b = answers?.what?.b || '';
    const extras = (answers?.what?.extras || []).join('|');
    const priorities = (answers?.priorities || []).join('|');
    const purpose = answers?.purpose || '';
    const depth = answers?.depth || '';
    return [a, b, extras, priorities, purpose, depth].join('||').toLowerCase();
  }

  // Build a tight, deterministic prompt. We instruct strict JSON.
  function buildPrompt(answers) {
    const a = answers?.what?.a || 'A';
    const b = answers?.what?.b || 'B';
    const extras = answers?.what?.extras || [];
    const priorities = answers?.priorities && answers.priorities.length ? answers.priorities : ['Quality', 'Price', 'Reliability'];
    const purpose = answers?.purpose || 'general comparison';
    const items = [a, b, ...extras];
    const allItems = items.map((x, i) => `${i + 1}. ${x}`).join('\n');
    const currentMonth = new Date().toLocaleString('en-US', { month: 'short', year: 'numeric' });

    return `You are a calm, expert advisor producing a head-to-head comparison.

ITEMS BEING COMPARED:
${allItems}

USER'S PURPOSE: ${purpose}

USER'S PRIORITIES (most important first — weigh these heavily in the verdict):
${priorities.map((p, i) => `${i + 1}. ${p}`).join('\n')}

CURRENT DATE: ${currentMonth}

Use what you know about these specific items. Be concrete and reference their actual real-world strengths and weaknesses. If you genuinely don't know an item, reason from its name and any context. NEVER invent specs you'd need to look up — leave those as "—" if uncertain.

Citation recency:
- For fast-moving topics like operating systems, phones, AI models, apps, software, hardware, cloud, or EVs, prefer quote/source dates from the last 12-18 months.
- For those fast-moving topics, avoid old dates for current feature, pricing, compatibility, performance, security, or ecosystem claims.
- Older sources are okay only for background/history or slower-moving topics.

The user's priorities should drive the verdict. If "${priorities[0]}" is the top priority, the winner should be the item that genuinely wins on "${priorities[0]}", even if it loses on other dimensions.

Reply with ONLY valid JSON, no prose, matching this exact schema:

{
  "verdict": {
    "headline": "1 sentence — name the winner and the qualifier (max 90 chars)",
    "body": "2-3 sentences explaining WHY for THIS user given their priorities. Reference 2 of their actual priorities by name.",
    "winner": "<exact name of winning item, copied from above>",
    "confidence": 0.55-0.95
  },
  "categories": [
    // one entry per priority, in the order given. Score each item on this priority 1-10.
    // Then add 2-3 more categories you think are crucial for these items even though the user didn't list them.
    {
      "label": "<priority name or new category>",
      "userFlagged": true|false,
      "scores": { "<item name>": 7.5, ... }, // ALL items must have a score
      "note": "1 sentence comparing how the items differ on this — name the items, be concrete (max 140 chars)"
    }
  ],
  "specs": [
    // 4-7 rows. Real, well-known specs for these items. Use "—" if you don't know.
    { "label": "<spec name>", "values": { "<item name>": "<value>", ... } }
  ],
  "pros": {
    // For each item: 3-5 bullet points of genuine strengths relative to the others
    "<item name>": ["...", "...", "..."]
  },
  "cons": {
    "<item name>": ["...", "...", "..."]
  },
  "quotes": [
    // 4 plausible pull-quotes from real publications. Mark which item each is about. Dates should follow the citation recency rules above.
    { "about": "<item name>", "text": "<quote, max 140 chars>", "source": "<publication>", "date": "<Mon YYYY>" }
  ],
  "sources": [
    // 6-8 plausible web sources you'd cite. Use real publication hostnames. Dates should follow the citation recency rules above.
    { "host": "<hostname.com>", "title": "<article title>", "date": "<Mon YYYY>" }
  ],
  "recommendation": {
    "pick": "<exact name of winning item>",
    "pickFor": "<short phrase: 'your priorities and use case'>",
    "why": "1-2 sentences. Concrete. Reference their priorities.",
    "altPick": "1 sentence: pick the runner-up if X — name X concretely."
  }
}

CRITICAL RULES:
- Item names in scores/values/pros/cons/quotes MUST exactly match the names from the ITEMS list above.
- Every item must appear in every category's scores.
- Every item must have pros AND cons (no item is perfect).
- The winner of the verdict MUST score highest on the user's TOP priority "${priorities[0]}", OR you must explicitly explain in the body why a different item wins despite that.
- Be specific to THESE items. Do not produce generic advice.
- Output ONLY the JSON. No markdown, no prose before or after.`;
  }

  // Pull JSON out of the response defensively.
  function parseJson(text) {
    if (!text) return null;
    let t = text.trim();
    // Strip code fences if present
    t = t.replace(/^```json\s*/i, '').replace(/^```\s*/, '').replace(/```\s*$/, '');
    // Find first { and last }
    const first = t.indexOf('{');
    const last = t.lastIndexOf('}');
    if (first === -1 || last === -1) return null;
    try {
      return JSON.parse(t.slice(first, last + 1));
    } catch (e) {
      console.warn('[generator] JSON parse failed:', e, t.slice(0, 200));
      return null;
    }
  }

  // Fuzzy key lookup — finds the best match for `name` in an object's keys,
  // ignoring case, spaces, and punctuation. Returns the value or fallback.
  function fuzzyGet(obj, name, fallback) {
    if (!obj) return fallback;
    if (obj[name] !== undefined) return obj[name];
    const norm = (s) => s.toLowerCase().replace(/[\s\-_\.]+/g, '');
    const target = norm(name);
    for (const key of Object.keys(obj)) {
      if (norm(key) === target) return obj[key];
    }
    // Partial match — key contains or is contained by target
    for (const key of Object.keys(obj)) {
      const k = norm(key);
      if (k.includes(target) || target.includes(k)) return obj[key];
    }
    return fallback;
  }

  // Convert AI response into the shape the UI expects.
  function shape(answers, ai) {
    const a = answers?.what?.a || 'A';
    const b = answers?.what?.b || 'B';
    const extras = answers?.what?.extras || [];
    const items = [a, b, ...extras];
    const priorities = answers?.priorities && answers.priorities.length ? answers.priorities : ['Quality', 'Price'];

    // Categories — the UI today supports a/b winner; we'll preserve that for the
    // primary pair but expose all-item scores for future multi-column rendering.
    const cats = (ai.categories || []).slice(0, 8).map((c, i) => {
      const scores = c.scores || {};
      const aScore = num(fuzzyGet(scores, a, 7), 7);
      const bScore = num(fuzzyGet(scores, b, 7), 7);
      // For multi-item, use the highest-scoring item as winner
      const winnerSide = aScore >= bScore ? 'a' : 'b';
      return {
        id: (c.label || `cat${i}`).toLowerCase().replace(/\s+/g, '_'),
        label: c.label || `Category ${i + 1}`,
        a: round1(aScore),
        b: round1(bScore),
        scores: items.reduce((acc, it) => { acc[it] = round1(num(scores[it], 7)); return acc; }, {}),
        winner: winnerSide,
        userFlagged: !!c.userFlagged,
        note: c.note || `${winnerSide === 'a' ? a : b} edges on ${(c.label || '').toLowerCase()}.`,
      };
    });

    // Compute verdict scores from the categories using priority weighting (3x for user-flagged)
    const userSet = new Set(priorities);
    let aSum = 0, bSum = 0, total = 0;
    cats.forEach(c => {
      const w = userSet.has(c.label) ? 3 : 1;
      aSum += c.a * w;
      bSum += c.b * w;
      total += 10 * w;
    });
    const aPct = total ? Math.round((aSum / total) * 100) : 70;
    const bPct = total ? Math.round((bSum / total) * 100) : 70;

    // Specs: convert {label, values} to our 2-col {label, a, b} (preserve all values for future)
    const specs = (ai.specs || []).slice(0, 8).map(s => ({
      label: s.label || '',
      a: fuzzyGet(s.values, a, '—'),
      b: fuzzyGet(s.values, b, '—'),
      values: s.values || {},
    }));

    const pros = {
      a: fuzzyGet(ai.pros, a, []),
      b: fuzzyGet(ai.pros, b, []),
    };
    const cons = {
      a: fuzzyGet(ai.cons, a, []),
      b: fuzzyGet(ai.cons, b, []),
    };

    const quotes = (ai.quotes || []).slice(0, 6).map(q => ({
      side: fuzzyGet({ [a]: 'a', [b]: 'b' }, q.about, 'a'),
      text: q.text || '',
      source: q.source || 'The Verge',
      date: q.date || 'Apr 2026',
    }));

    const sources = (ai.sources || []).slice(0, 8).map(s => ({
      host: s.host || 'wikipedia.org',
      title: s.title || '',
      date: s.date || 'Apr 2026',
    }));

    const winnerName = ai.verdict?.winner || (aPct >= bPct ? a : b);
    const loserName = winnerName === a ? b : a;

    return {
      query: items.join(' vs '),
      a: { name: a, sub: answers?.purpose || 'Item A', score: aPct },
      b: { name: b, sub: answers?.purpose || 'Item B', score: bPct },
      items,
      context: {
        purpose: answers?.purpose || 'General comparison',
        priorities,
        audience: 'Based on your priorities',
      },
      verdict: {
        headline: ai.verdict?.headline || `${winnerName}, on the priorities you flagged.`,
        body: ai.verdict?.body || `${winnerName} aligns with what you said matters most.`,
        confidence: clamp(num(ai.verdict?.confidence, 0.75), 0.55, 0.95),
      },
      categories: cats,
      specs,
      pros,
      cons,
      quotes,
      sources,
      recommendation: {
        pick: ai.recommendation?.pick || winnerName,
        pickFor: ai.recommendation?.pickFor || 'your stated priorities',
        why: ai.recommendation?.why || `${winnerName} delivers more of what you said matters.`,
        altPick: ai.recommendation?.altPick || `Pick ${loserName} if its strengths matter more to you than the verdict suggests.`,
      },
    };
  }

  function num(v, fallback) {
    const n = Number(v);
    return Number.isFinite(n) ? n : fallback;
  }
  function round1(n) { return Math.round(n * 10) / 10; }
  function clamp(n, lo, hi) { return Math.min(hi, Math.max(lo, n)); }

  // Fallback shape used when the AI call fails.
  function fallback(answers) {
    const a = answers?.what?.a || 'A';
    const b = answers?.what?.b || 'B';
    const extras = answers?.what?.extras || [];
    const items = [a, b, ...extras];
    const priorities = answers?.priorities && answers.priorities.length ? answers.priorities : ['Quality', 'Price', 'Reliability'];
    const cats = priorities.slice(0, 5).map((p, i) => {
      const aScore = 7 + ((i % 2) ? 0.6 : -0.2);
      const bScore = 7 + ((i % 2) ? -0.4 : 0.5);
      const winnerSide = aScore >= bScore ? 'a' : 'b';
      return {
        id: p.toLowerCase().replace(/\s+/g, '_'),
        label: p,
        a: round1(aScore), b: round1(bScore),
        scores: { [a]: round1(aScore), [b]: round1(bScore) },
        winner: winnerSide,
        userFlagged: true,
        note: `Comparison of ${a} and ${b} on ${p.toLowerCase()} could not be loaded.`,
      };
    });
    return {
      query: items.join(' vs '),
      a: { name: a, sub: 'Item A', score: 72 },
      b: { name: b, sub: 'Item B', score: 70 },
      items,
      context: { purpose: answers?.purpose || '', priorities, audience: '—' },
      verdict: {
        headline: `${a} vs ${b} — couldn't reach the AI just now.`,
        body: `Try again in a moment. Your priorities and items are saved.`,
        confidence: 0.6,
      },
      categories: cats,
      specs: [],
      pros: { a: [], b: [] },
      cons: { a: [], b: [] },
      quotes: [],
      sources: [],
      recommendation: {
        pick: a, pickFor: 'your priorities',
        why: 'AI reasoning is temporarily unavailable.',
        altPick: `Try refreshing.`,
      },
    };
  }

  // Async: call Claude, return shaped data. Caches by inputs.
  async function generateResultsAsync(answers, opts = {}) {
    const key = cacheKey(answers);
    if (cache.has(key) && !opts.force) return cache.get(key);

    const prompt = buildPrompt(answers);
    let text = '';
    try {
      text = await window.claude.complete(prompt);
    } catch (e) {
      console.warn('[generator] claude.complete failed:', e);
      const fb = fallback(answers);
      cache.set(key, fb);
      return fb;
    }
    const ai = parseJson(text);
    if (!ai) {
      const fb = fallback(answers);
      cache.set(key, fb);
      return fb;
    }
    const shaped = shape(answers, ai);
    cache.set(key, shaped);
    return shaped;
  }

  // Sync: returns cached data if present (used by Results render).
  function generateResults(answers) {
    const key = cacheKey(answers);
    if (cache.has(key)) return cache.get(key);
    return fallback(answers);
  }

  function clearCache() { cache.clear(); }
  function hasCachedFor(answers) { return cache.has(cacheKey(answers)); }

  // ── Onboarding suggestion call ──────────────────────────────────────────
  // Used when the keyword detectProfile returns 'generic'. Asks the AI to
  // either route to a known profile, or return tailored chips with confidence.

  const SUGGEST_LS_KEY = 'versus.suggestCache';
  const SUGGEST_MAX = 30;

  // Load persisted cache on init.
  const suggestCache = new Map();
  try {
    const persisted = JSON.parse(localStorage.getItem(SUGGEST_LS_KEY) || '[]');
    if (Array.isArray(persisted)) persisted.forEach(([k, v]) => suggestCache.set(k, v));
  } catch {}

  function persistSuggestCache() {
    try {
      const entries = Array.from(suggestCache.entries()).slice(-SUGGEST_MAX);
      localStorage.setItem(SUGGEST_LS_KEY, JSON.stringify(entries));
    } catch {}
  }

  function suggestKey(items) {
    const a = (items?.a || '').trim().toLowerCase();
    const b = (items?.b || '').trim().toLowerCase();
    const extras = (items?.extras || []).map(s => (s || '').trim().toLowerCase()).join('|');
    return [a, b, extras].join('||');
  }

  function buildSuggestPrompt(items) {
    const a = items?.a || 'A';
    const b = items?.b || 'B';
    const extras = items?.extras || [];
    const all = [a, b, ...extras].map((x, i) => `${i + 1}. ${x}`).join('\n');
    const knownKeys = Object.keys(window.CATEGORY_PROFILES || {}).filter(k => k !== 'generic');

    return `You are helping classify a comparison so we can show the user the right priorities and purpose chips.

ITEMS BEING COMPARED:
${all}

KNOWN PROFILE KEYS: ${knownKeys.join(', ')}

Decide:
- If these items clearly fit one of the known profile keys, return that key.
- Otherwise return "custom".

Reply with ONLY valid JSON, always include all three fields:

{
  "profile": "<one known key from above>" or "custom",
  "purposes":   [{ "label": "<3-5 word phrase>", "confidence": 0.0-1.0 }],
  "priorities": [{ "label": "<1-3 word phrase>", "confidence": 0.0-1.0 }]
}

Rules:
- Tailor purposes and priorities to THESE specific items (not the broad category).
- Return 3-5 purposes and 8-12 priorities.
- confidence ≥ 0.7 = clearly relevant for these items (will be preselected)
- confidence 0.4-0.7 = relevant but optional (shown unselected)
- confidence < 0.4 = SKIP (don't return)
- Always include the chip arrays even when "profile" is a known key — they're a safety net.
- Output ONLY the JSON. No markdown, no prose.`;
  }

  function normalizeSuggest(ai) {
    if (!ai || typeof ai !== 'object') return null;
    const known = new Set(Object.keys(window.CATEGORY_PROFILES || {}));
    const purposes = (Array.isArray(ai.purposes) ? ai.purposes : [])
      .filter(p => p && typeof p.label === 'string' && Number(p.confidence) >= 0.4)
      .map(p => ({ label: p.label.trim(), confidence: Number(p.confidence) }))
      .sort((x, y) => y.confidence - x.confidence);
    const priorities = (Array.isArray(ai.priorities) ? ai.priorities : [])
      .filter(p => p && typeof p.label === 'string' && Number(p.confidence) >= 0.4)
      .map(p => ({ label: p.label.trim(), confidence: Number(p.confidence) }))
      .sort((x, y) => y.confidence - x.confidence);

    // 1. Valid known profile → use it
    if (typeof ai.profile === 'string' && known.has(ai.profile) && ai.profile !== 'generic') {
      return { kind: 'profile', key: ai.profile, purposes, priorities };
    }
    // 2. Custom or unknown classifier → use chips if any
    if (purposes.length || priorities.length) {
      return { kind: 'custom', purposes, priorities };
    }
    // 3. Nothing usable
    return null;
  }

  async function suggestProfileOrChips(items) {
    const key = suggestKey(items);
    if (suggestCache.has(key)) return suggestCache.get(key);

    let text = '';
    try {
      text = await window.claude.complete(buildSuggestPrompt(items));
    } catch (e) {
      console.warn('[generator] suggest call failed:', e);
      return null;
    }
    const ai = parseJson(text);
    const result = normalizeSuggest(ai);
    if (result) {
      suggestCache.set(key, result);
      persistSuggestCache();
    }
    return result;
  }

  window.generateResults = generateResults;
  window.generateResultsAsync = generateResultsAsync;
  window.versusClearCache = clearCache;
  window.versusHasCachedFor = hasCachedFor;
  window.suggestProfileOrChips = suggestProfileOrChips;
})();
