// gifts.jsx — v3 — Système d'inventaire, économie & cadeaux
//
// Refonte complète :
//   • Le MC ne peut offrir QUE les objets qu'il POSSÈDE
//   • Objets acquis via 5 canaux : quête, achat (boutique), trouvaille narrative,
//     reçu d'un PNJ, échange
//   • Économie : monnaie « pièces d'or » (générique, adaptée au scénario)
//   • Boutique contextuelle (marchande locale, taverne, scriptorium…)
//   • Catalogue séparé (référence visuelle, lecture seule)
//   • Compat avec l'inventaire de objectives.jsx (même clé localStorage)
//   • L'IA peut donner ou retirer des objets/or via annotations
//
// Annotations IA reconnues (en plus de [META] [CHECK] [ITEM] [CHOIX] déjà gérées) :
//   [GOLD: +N (raison)]     — accord ou retrait d'or
//   [RECEIVE: Nom de l'objet | from: Nom du PNJ | raison]   — un PNJ donne un objet au MC
//   [LOSE: Nom de l'objet]  — le MC perd un objet
//
// Stockage :
//   loisir-inventory-{charOrScenarioId}-{mcName}  (partagé avec objectives.jsx)
//   loisir-gold-{charOrScenarioId}-{mcName}        (nouveau, montant entier)

(function() {
'use strict';

// ═══════════════════════════════════════════════════════════════
// 1. CATALOGUE ENRICHI — prix, rareté, canaux d'acquisition
// ═══════════════════════════════════════════════════════════════

const GIFT_CATALOG = [
  // ─── Livres ────────────────────────────────────────────────────
  { id: 'livre-rare',        name: 'Livre rare',             emoji: '📖', category: 'livre',
    desc: 'Un ouvrage introuvable, relié cuir, aux pages dorées.',
    rarity: 'rare', price: 180, sources: ['shop', 'quest', 'found'] },
  { id: 'grimoire',          name: 'Grimoire ancien',        emoji: '📜', category: 'livre',
    desc: 'Un manuscrit ésotérique recouvert de formules effacées.',
    rarity: 'precious', price: 320, sources: ['shop', 'quest', 'received'] },
  { id: 'poesie',            name: 'Recueil de poésies',     emoji: '🪶', category: 'livre',
    desc: 'Vers écrits à la main sur vélin. Couverture en soie.',
    rarity: 'uncommon', price: 90, sources: ['shop', 'quest', 'received'] },
  { id: 'carte-historique',  name: 'Carte enluminée',        emoji: '🗺️', category: 'livre',
    desc: 'Parchemin d\'époque illustrant un territoire oublié.',
    rarity: 'rare', price: 220, sources: ['shop', 'quest'] },

  // ─── Bijoux ────────────────────────────────────────────────────
  { id: 'bijou-fin',         name: 'Bijou fin',              emoji: '💎', category: 'bijou',
    desc: 'Pendentif délicat, monture en argent, pierre translucide.',
    rarity: 'rare', price: 240, sources: ['shop', 'received'] },
  { id: 'broche-ancien',     name: 'Broche ancienne',        emoji: '✨', category: 'bijou',
    desc: 'Broche en émail et or, motif floral d\'époque.',
    rarity: 'precious', price: 380, sources: ['quest', 'found'] },
  { id: 'bague-cabochon',    name: 'Bague cabochon',         emoji: '💍', category: 'bijou',
    desc: 'Anneau en laiton patiné, pierre de lune enchâssée.',
    rarity: 'precious', price: 420, sources: ['quest', 'received'] },

  // ─── Fleurs ────────────────────────────────────────────────────
  { id: 'fleur-rare',        name: 'Fleur rare',             emoji: '🌸', category: 'fleur',
    desc: 'Orchidée noire à pétales nacrés, cultivée sous serre enchantée.',
    rarity: 'uncommon', price: 60, sources: ['shop', 'found'] },
  { id: 'bouquet-sauvage',   name: 'Bouquet sauvage',        emoji: '💐', category: 'fleur',
    desc: 'Assemblage rustique de lavande, camomille et bleuets des champs.',
    rarity: 'common', price: 15, sources: ['shop', 'found'], stackable: true },
  { id: 'pivoine-sechee',    name: 'Pivoine séchée',         emoji: '🌹', category: 'fleur',
    desc: 'Rose pâle séchée et conservée dans une cloche de verre.',
    rarity: 'uncommon', price: 80, sources: ['shop', 'received'] },

  // ─── Nourriture ────────────────────────────────────────────────
  { id: 'the-qualite',       name: 'Thé de qualité',         emoji: '🍵', category: 'nourriture',
    desc: 'Feuilles de premier rang importées, boîte laquée.',
    rarity: 'uncommon', price: 50, sources: ['shop'], stackable: true },
  { id: 'confiture-rare',    name: 'Confiture artisanale',   emoji: '🍓', category: 'nourriture',
    desc: 'Petits pots de fruits confits préparés par un maître confiseur.',
    rarity: 'uncommon', price: 70, sources: ['shop', 'received'], stackable: true },
  { id: 'nourriture-simple', name: 'Nourriture simple',      emoji: '🍞', category: 'nourriture',
    desc: 'Pain frais, fromage de campagne et quelques fruits de saison.',
    rarity: 'common', price: 10, sources: ['shop'], stackable: true },
  { id: 'gateau-fin',        name: 'Gâteau fin',             emoji: '🎂', category: 'nourriture',
    desc: 'Pâtisserie délicate décorée à la feuille d\'or.',
    rarity: 'rare', price: 140, sources: ['shop'] },
  { id: 'vin-millesime',     name: 'Vin millésimé',          emoji: '🍷', category: 'nourriture',
    desc: 'Bouteille cachetée d\'un grand cru oublié des caves royales.',
    rarity: 'rare', price: 200, sources: ['shop', 'quest'] },

  // ─── Artefacts ────────────────────────────────────────────────
  { id: 'tarot-ancien',      name: 'Carte de tarot ancienne', emoji: '🃏', category: 'artefact',
    desc: 'Lame unique peinte à la main, aura mystérieuse et encre qui pulse.',
    rarity: 'precious', price: 500, sources: ['quest', 'found'] },
  { id: 'cristal-lumiere',   name: 'Cristal de lumière',     emoji: '🔮', category: 'artefact',
    desc: 'Sphère translucide qui capte et diffracte la lumière en étoiles.',
    rarity: 'rare', price: 280, sources: ['shop', 'quest'] },
  { id: 'boite-musicale',    name: 'Boîte à musique',        emoji: '🎵', category: 'artefact',
    desc: 'Mécanisme délicat qui joue un air mélancolique en bois de rose.',
    rarity: 'rare', price: 320, sources: ['shop', 'quest', 'received'] },
  { id: 'encre-magique',     name: 'Encre enchantée',        emoji: '🖋️', category: 'artefact',
    desc: 'Flacon dont l\'encre change de couleur selon les émotions du scripteur.',
    rarity: 'precious', price: 450, sources: ['quest', 'received'] },
  { id: 'objet-vulgaire',    name: 'Objet vulgaire',         emoji: '🗑️', category: 'artefact',
    desc: 'Bibelot de pacotille sans goût ni valeur particulière.',
    rarity: 'common', price: 5, sources: ['shop', 'found'], stackable: true },

  // ─── Souvenirs ────────────────────────────────────────────────
  { id: 'souvenir-voyage',   name: 'Souvenir de voyage',     emoji: '🧭', category: 'souvenir',
    desc: 'Objet ramené d\'une cité lointaine, porteur d\'une histoire.',
    rarity: 'uncommon', price: 60, sources: ['shop', 'found'] },
  { id: 'portrait-miniature', name: 'Portrait miniature',     emoji: '🖼️', category: 'souvenir',
    desc: 'Médaillon ovale renfermant un portrait peint sur ivoire.',
    rarity: 'precious', price: 400, sources: ['quest', 'received'], unique: true },
  { id: 'lettre-personnelle', name: 'Lettre personnelle',    emoji: '✉️', category: 'souvenir',
    desc: 'Message manuscrit, cacheté d\'un sceau de cire rouge.',
    rarity: 'rare', price: 0, sources: ['quest', 'received'], unique: true },
  { id: 'ruban-souvenir',    name: 'Ruban souvenir',         emoji: '🎀', category: 'souvenir',
    desc: 'Brin de tissu brodé aux initiales du joueur, noué à la main.',
    rarity: 'rare', price: 0, sources: ['quest', 'received'], unique: true },
];

const CATALOG_BY_NAME = {};
GIFT_CATALOG.forEach(g => CATALOG_BY_NAME[g.name] = g);

// ═══════════════════════════════════════════════════════════════
// 2. STOCKAGE — inventaire & or
// ═══════════════════════════════════════════════════════════════

function inventoryKey(charId, mcName) {
  return `loisir-inventory-${charId}-${(mcName || 'anon').toLowerCase().replace(/\s+/g, '-')}`;
}
function goldKey(charId, mcName) {
  return `loisir-gold-${charId}-${(mcName || 'anon').toLowerCase().replace(/\s+/g, '-')}`;
}
function purseInitKey(charId, mcName) {
  return `loisir-gold-init-${charId}-${(mcName || 'anon').toLowerCase().replace(/\s+/g, '-')}`;
}

function loadInventory(charId, mcName) {
  try { return JSON.parse(localStorage.getItem(inventoryKey(charId, mcName)) || '[]'); } catch { return []; }
}
function saveInventory(charId, mcName, inv) {
  try { localStorage.setItem(inventoryKey(charId, mcName), JSON.stringify(inv)); } catch {}
}
function loadGold(charId, mcName) {
  // Ensure starting allowance the first time
  try {
    if (!localStorage.getItem(purseInitKey(charId, mcName))) {
      localStorage.setItem(goldKey(charId, mcName), String(STARTING_GOLD));
      localStorage.setItem(purseInitKey(charId, mcName), '1');
      return STARTING_GOLD;
    }
    const raw = parseInt(localStorage.getItem(goldKey(charId, mcName)) || '0', 10);
    return isNaN(raw) ? 0 : raw;
  } catch { return 0; }
}
function saveGold(charId, mcName, amount) {
  try { localStorage.setItem(goldKey(charId, mcName), String(Math.max(0, Math.floor(amount)))); } catch {}
}

const STARTING_GOLD = 120;

// ─── API publique pour les autres modules ────────────────────
function addItem(charId, mcName, { name, source = 'autre', sourceDetail = '', from = '' }) {
  const cat = CATALOG_BY_NAME[name];
  const inv = loadInventory(charId, mcName);
  // Stackable items: increment qty if same name+source
  if (cat?.stackable) {
    const existing = inv.find(it => it.name === name);
    if (existing) {
      existing.qty = (existing.qty || 1) + 1;
      saveInventory(charId, mcName, inv);
      return inv;
    }
  }
  // Unique items: don't double-add
  if (cat?.unique && inv.some(it => it.name === name)) return inv;
  inv.push({ name, source, sourceDetail, from, qty: 1, at: Date.now() });
  saveInventory(charId, mcName, inv);
  return inv;
}

function removeItem(charId, mcName, name) {
  const inv = loadInventory(charId, mcName);
  const idx = inv.findIndex(it => it.name === name);
  if (idx === -1) return inv;
  if ((inv[idx].qty || 1) > 1) {
    inv[idx].qty -= 1;
  } else {
    inv.splice(idx, 1);
  }
  saveInventory(charId, mcName, inv);
  return inv;
}

function hasItem(charId, mcName, name) {
  return loadInventory(charId, mcName).some(it => it.name === name && (it.qty || 1) > 0);
}

function addGold(charId, mcName, delta) {
  const cur = loadGold(charId, mcName);
  saveGold(charId, mcName, cur + delta);
  return cur + delta;
}
function spendGold(charId, mcName, amount) {
  const cur = loadGold(charId, mcName);
  if (cur < amount) return false;
  saveGold(charId, mcName, cur - amount);
  return true;
}

// ═══════════════════════════════════════════════════════════════
// 3. BOUTIQUE — quels items sont en vente, contextuel
// ═══════════════════════════════════════════════════════════════

// Stock par défaut : tout ce qui a 'shop' dans ses sources
function getShopStock(scenario) {
  // Pourrait être filtré selon le monde (ex. pas de boîte à musique dans le bunker apocalypse)
  // Pour l'instant : tout ce qui se vend, quel que soit le monde.
  return GIFT_CATALOG.filter(g => (g.sources || []).includes('shop'));
}

// Nom du marchand contextuel selon le scénario (cosmétique)
function getShopLabel(scenario, char) {
  if (!scenario && !char) return 'Marchand';
  const world = (scenario?.worldShort || '').toLowerCase();
  if (world.includes('cultivation') || world.includes('cour')) return 'Étal du marché';
  if (world.includes('donjon') || world.includes('royaume')) return 'Comptoir de la guilde';
  if (world.includes('vampire')) return 'Antiquaire des Wallach';
  if (world.includes('mafia') || world.includes('paris')) return 'Boutique du quartier';
  if (world.includes('académie') || world.includes('hôpital')) return 'Échoppe d\'à côté';
  if (world.includes('espace') || world.includes('apocalypse')) return 'Troc du bunker';
  return 'Marchand local';
}

// ═══════════════════════════════════════════════════════════════
// 4. EFFET D'UN CADEAU OFFERT (inchangé) — utilise giftPrefs
// ═══════════════════════════════════════════════════════════════

function getGiftEffect(gift, char) {
  const prefs = char?.giftPrefs;
  if (!prefs) return { delta: 1, reaction: 'neutral' };
  if (prefs.signature && gift.name === prefs.signature) return { delta: 15, reaction: 'signature' };
  if (Array.isArray(prefs.loved)    && prefs.loved.includes(gift.name))    return { delta: 8, reaction: 'loved' };
  if (Array.isArray(prefs.liked)    && prefs.liked.includes(gift.name))    return { delta: 4, reaction: 'liked' };
  if (Array.isArray(prefs.disliked) && prefs.disliked.includes(gift.name)) return { delta: -3, reaction: 'disliked' };
  return { delta: 1, reaction: 'neutral' };
}

function buildGiftPrompt(gift, effect, char, mcName) {
  const npcName = char?.name || 'le personnage';
  const giver = mcName || 'le joueur';
  const reactionLines = {
    signature: `${npcName} reconnut immédiatement l'objet. Quelque chose de profond se brisa dans son regard impassible. Iel reçut le cadeau avec des mains qui tremblaient imperceptiblement, comme si cela touchait à un souvenir enfoui. La scène était intime, chargée d'une signification que seul·e ${npcName} comprenait pleinement.`,
    loved:     `${npcName} parut visiblement touché·e. Son expression s'adoucit malgré iel — un sourire discret, un regard attardé sur le cadeau. Iel le reçut avec soin, presque avec tendresse, avant de reprendre une façade composée.`,
    liked:     `${npcName} accueillit le cadeau favorablement. Un signe d'approbation, bref mais sincère. « C'est… attentionné », dit-iel, peut-être plus chaleureusement qu'iel ne l'aurait voulu.`,
    neutral:   `${npcName} prit le cadeau avec politesse distante. Iel remercia d'un signe de tête, sans laisser paraître d'émotion particulière. Le geste était noté, rien de plus.`,
    disliked:  `${npcName} regarda le cadeau un instant, puis ${giver} avec une légère froideur. « Je… vois », dit-iel, sans chaleur. L'objet fut accepté mais posé de côté. Le malaise était palpable.`,
  };
  const reaction = reactionLines[effect.reaction] || reactionLines.neutral;
  return `[${giver} offre ${gift.name} (${gift.emoji}) à ${npcName}.]\n\n${reaction}\n\n[Continue la scène en incarnant ${npcName}. Réagis à ce geste de façon cohérente avec ta personnalité et votre relation actuelle. Maximum 4 phrases. Narration au passé, POV ${giver}.]`;
}

function giftHint(gift, char, repValue) {
  if (!char?.giftPrefs || repValue < 30) return null;
  const { reaction } = getGiftEffect(gift, char);
  if (reaction === 'signature') return { icon: '✦✦', color: 'var(--gold)', tip: 'Cadeau signature' };
  if (reaction === 'loved')     return { icon: '♡',   color: 'var(--pink)',  tip: 'Adoré' };
  if (reaction === 'liked')     return { icon: '↑',   color: '#7ab896',      tip: 'Apprécié' };
  if (reaction === 'disliked')  return { icon: '↓',   color: 'var(--rouge)', tip: 'Déplaît' };
  return null;
}

// ═══════════════════════════════════════════════════════════════
// 5. PARSER ANNOTATIONS IA — gold/receive/lose
// ═══════════════════════════════════════════════════════════════

// Intercept les réponses IA pour appliquer GOLD/RECEIVE/LOSE.
// Wraps window.claude.complete (additif au-dessus de improvements-v2).
function setupAnnotationInterceptor() {
  if (!window.claude || !window.claude.complete) {
    setTimeout(setupAnnotationInterceptor, 200);
    return;
  }
  if (window.claude.__loisirGiftPatched) return;
  window.claude.__loisirGiftPatched = true;
  const orig = window.claude.complete.bind(window.claude);

  window.claude.complete = async function(opts) {
    const raw = await orig(opts);
    try {
      const text = typeof raw === 'string' ? raw : (raw?.content || '');
      processGiftAnnotations(text);
    } catch (e) { console.warn('[gifts] annotation parsing error:', e); }
    return raw;
  };
  console.log('[Loisir] Gift / gold annotations enabled.');
}
setTimeout(setupAnnotationInterceptor, 1400);

// Detect active context (same heuristic as objectives.jsx)
function detectActiveContext() {
  try {
    let bestId = null, bestMc = null, bestVal = 0;
    for (const k of Object.keys(localStorage)) {
      const m = k.match(/^loisir-rep-([^-]+(?:-[^-]+)*?)-(.+)$/);
      if (!m) continue;
      const raw = localStorage.getItem(k);
      if (raw && raw.length > bestVal) {
        bestVal = raw.length;
        bestId = m[1];
        bestMc = m[2];
      }
    }
    return bestId && bestMc ? { id: bestId, mcName: bestMc } : null;
  } catch { return null; }
}

function processGiftAnnotations(text) {
  if (!text) return;
  const ctx = detectActiveContext();
  if (!ctx) return;
  const { id, mcName } = ctx;

  // [GOLD: +N (raison)] or [GOLD: -N (raison)]
  const goldMatches = [...text.matchAll(/\[GOLD:\s*([+\-]?\d+)\s*(?:\(([^)]+)\))?\]/gi)];
  goldMatches.forEach(m => {
    const delta = parseInt(m[1], 10);
    if (!isNaN(delta) && delta !== 0) {
      addGold(id, mcName, delta);
      pushToast(`${delta > 0 ? '+' : ''}${delta} pièces`, m[2] || '', delta > 0 ? '#c9a866' : '#a8324a');
    }
  });

  // [RECEIVE: Nom | from: PNJ | raison]
  const recvMatches = [...text.matchAll(/\[RECEIVE:\s*([^|\]]+?)(?:\s*\|\s*from:\s*([^|\]]+?))?(?:\s*\|\s*([^\]]+?))?\]/gi)];
  recvMatches.forEach(m => {
    const name = m[1].trim();
    const from = (m[2] || '').trim();
    const reason = (m[3] || '').trim();
    if (CATALOG_BY_NAME[name]) {
      addItem(id, mcName, { name, source: from ? 'reçu' : 'trouvaille', sourceDetail: reason, from });
      pushToast(`+ ${name}`, from ? `Reçu de ${from}` : 'Trouvé', '#7ab896');
    } else {
      // Add anyway with unknown name, for narrative consistency
      addItem(id, mcName, { name, source: from ? 'reçu' : 'trouvaille', sourceDetail: reason, from });
      pushToast(`+ ${name}`, from ? `Reçu de ${from}` : 'Trouvé', '#7ab896');
    }
  });

  // [LOSE: Nom]
  const loseMatches = [...text.matchAll(/\[LOSE:\s*([^\]]+?)\]/gi)];
  loseMatches.forEach(m => {
    const name = m[1].trim();
    if (hasItem(id, mcName, name)) {
      removeItem(id, mcName, name);
      pushToast(`− ${name}`, 'Objet perdu', '#a8324a');
    }
  });
}

function pushToast(title, sub, color) {
  const t = document.createElement('div');
  t.style.cssText = `
    position: fixed; top: 78px; right: 16px; z-index: 9200;
    padding: 10px 16px; background: rgba(13,7,9,0.94);
    border: 1px solid ${color}; color: var(--ink); border-radius: 10px;
    font-family: 'Manrope', sans-serif; font-size: 12px;
    backdrop-filter: blur(10px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
    animation: loisir-card-in 0.3s ease-out;
    max-width: 250px;
  `;
  t.innerHTML = `<div style="font-weight:600;color:${color}">${title}</div>${sub ? `<div style="font-size:10px;color:var(--ink-fade);margin-top:2px">${sub}</div>` : ''}`;
  document.body.appendChild(t);
  setTimeout(() => { t.style.transition = 'all 0.4s'; t.style.opacity = '0'; t.style.transform = 'translateY(-10px)'; }, 3200);
  setTimeout(() => t.remove(), 3700);
}

// ═══════════════════════════════════════════════════════════════
// 6. UI — Wallet, Possessions, Shop, Catalogue
// ═══════════════════════════════════════════════════════════════

function WalletBadge({ charId, mcName }) {
  const [gold, setGold] = React.useState(() => loadGold(charId, mcName));
  React.useEffect(() => {
    const id = setInterval(() => setGold(loadGold(charId, mcName)), 1500);
    return () => clearInterval(id);
  }, [charId, mcName]);
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 5,
      padding: '4px 10px',
      background: 'rgba(201,168,102,0.12)',
      border: '1px solid var(--gold)',
      borderRadius: 999,
      color: 'var(--gold)',
      fontFamily: 'Manrope', fontSize: 11, fontWeight: 600,
      letterSpacing: '0.04em',
    }}>
      <span style={{ fontSize: 12 }}>◉</span>
      <span style={{ fontVariantNumeric: 'tabular-nums' }}>{gold}</span>
    </div>
  );
}

const RARITY_COLOR = {
  common:   '#888',
  uncommon: '#7ab896',
  rare:     '#7a9bb8',
  precious: '#b298c0',
  unique:   '#e8a8b8',
};

function GiftCard({ gift, count, mode, onClick, selected, disabled, char, repValue }) {
  const hint = giftHint(gift, char, repValue);
  const rarityCol = RARITY_COLOR[gift.rarity] || '#888';
  const accent = char?.accent || 'var(--gold)';
  return (
    <button onClick={() => !disabled && onClick(gift)} disabled={disabled} title={gift.desc} style={{
      background: selected ? `${accent}22` : 'var(--surface)',
      border: `1px solid ${selected ? accent : 'var(--line-soft)'}`,
      borderTop: `3px solid ${rarityCol}`,
      borderRadius: 10,
      padding: '12px 8px 10px',
      cursor: disabled ? 'default' : 'pointer',
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', gap: 4,
      position: 'relative',
      opacity: disabled ? 0.4 : 1,
      transition: 'all 0.15s',
      textAlign: 'center',
      fontFamily: 'Manrope, sans-serif',
    }}>
      {hint && (
        <span title={hint.tip} style={{ position: 'absolute', top: 6, right: 7, fontSize: 10, color: hint.color }}>
          {hint.icon}
        </span>
      )}
      {count > 1 && (
        <span style={{ position: 'absolute', top: 6, left: 7, fontSize: 9, padding: '1px 5px', background: 'rgba(0,0,0,0.6)', color: 'var(--ink)', borderRadius: 999, fontWeight: 600 }}>
          ×{count}
        </span>
      )}
      <span style={{ fontSize: 26, lineHeight: 1, marginTop: 2 }}>{gift.emoji}</span>
      <span style={{ fontSize: 11, fontWeight: 500, color: selected ? accent : 'var(--ink)', lineHeight: 1.25, marginTop: 2 }}>
        {gift.name}
      </span>
      {mode === 'shop' && (
        <span style={{ fontSize: 10, color: 'var(--gold)', marginTop: 2, fontWeight: 600 }}>
          ◉ {gift.price}
        </span>
      )}
      {mode === 'catalog' && (
        <span style={{ fontSize: 9, color: rarityCol, marginTop: 2, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
          {gift.rarity}
        </span>
      )}
    </button>
  );
}

// ─── Vue Possessions (offrir) ─────────────────────────────────
function PossessionsView({ char, mcName, charId, repValue, onSelect, selected, mode }) {
  const inv = loadInventory(charId, mcName);
  // Group by name to show stacks
  const groups = {};
  inv.forEach(it => {
    if (!groups[it.name]) groups[it.name] = { name: it.name, count: 0, items: [], lastSource: it.source };
    groups[it.name].count += (it.qty || 1);
    groups[it.name].items.push(it);
    groups[it.name].lastSource = it.source;
  });
  const list = Object.values(groups);

  if (list.length === 0) {
    return (
      <div style={{ textAlign: 'center', padding: 30, color: 'var(--ink-fade)', fontStyle: 'italic', fontFamily: 'Cormorant Garamond, serif', fontSize: 15, lineHeight: 1.5 }}>
        Tu n'as encore rien à offrir.<br />
        <span style={{ fontSize: 12 }}>Accomplis des quêtes, achète à la boutique, ou trouve des objets dans le récit.</span>
      </div>
    );
  }

  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 10 }}>
      {list.map(g => {
        const gift = CATALOG_BY_NAME[g.name] || { name: g.name, emoji: '📦', desc: 'Objet personnel', rarity: 'common', category: 'autre' };
        return (
          <div key={g.name} style={{ position: 'relative' }}>
            <GiftCard
              gift={gift}
              count={g.count}
              mode={mode || 'inventory'}
              onClick={() => onSelect && onSelect(gift)}
              selected={selected?.name === g.name}
              char={char}
              repValue={repValue}
            />
            <div style={{ fontSize: 9, color: 'var(--ink-fade)', textAlign: 'center', marginTop: 4, lineHeight: 1.3 }}>
              {g.lastSource === 'quête'      && '🎯 quête'}
              {g.lastSource === 'reçu'        && '🎁 reçu'}
              {g.lastSource === 'trouvaille'  && '✨ trouvé'}
              {g.lastSource === 'achat'       && '◉ acheté'}
              {!['quête','reçu','trouvaille','achat'].includes(g.lastSource) && g.lastSource}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ─── Vue Boutique (acheter) ───────────────────────────────────
function ShopView({ char, mcName, charId, scenario, onAfterBuy }) {
  const [gold, setGold] = React.useState(() => loadGold(charId, mcName));
  const [confirmGift, setConfirmGift] = React.useState(null);
  const stock = getShopStock(scenario);
  const shopLabel = getShopLabel(scenario, char);
  const accent = char?.accent || 'var(--gold)';

  const buy = (gift) => {
    if (gold < gift.price) return;
    if (!spendGold(charId, mcName, gift.price)) return;
    addItem(charId, mcName, { name: gift.name, source: 'achat', sourceDetail: shopLabel });
    setGold(loadGold(charId, mcName));
    setConfirmGift(null);
    if (onAfterBuy) onAfterBuy(gift);
    pushToast(`+ ${gift.name}`, `Acheté — ${shopLabel}`, '#7ab896');
  };

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, padding: '8px 10px', background: 'rgba(241,230,207,0.04)', border: '1px solid var(--line-soft)', borderRadius: 8 }}>
        <span style={{ fontSize: 11, color: 'var(--ink-mute)', letterSpacing: '0.1em', textTransform: 'uppercase' }}>{shopLabel}</span>
        <span style={{ fontSize: 12, color: 'var(--gold)', fontWeight: 600 }}>◉ {gold}</span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 10 }}>
        {stock.map(g => {
          const canBuy = gold >= g.price;
          return (
            <GiftCard
              key={g.id}
              gift={g}
              mode="shop"
              onClick={() => canBuy && setConfirmGift(g)}
              disabled={!canBuy}
            />
          );
        })}
      </div>
      {confirmGift && (
        <div onClick={() => setConfirmGift(null)} style={{
          position: 'fixed', inset: 0, zIndex: 3000,
          background: 'rgba(8,5,10,0.85)', backdropFilter: 'blur(6px)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
        }}>
          <div onClick={e => e.stopPropagation()} style={{
            background: 'linear-gradient(180deg, var(--surface), var(--bg))',
            border: `1px solid ${accent}`,
            borderRadius: 14, padding: 22, maxWidth: 320, width: '100%',
            textAlign: 'center',
          }}>
            <div style={{ fontSize: 36, marginBottom: 8 }}>{confirmGift.emoji}</div>
            <div className="serif" style={{ fontSize: 18, fontWeight: 600, marginBottom: 4 }}>{confirmGift.name}</div>
            <div style={{ fontSize: 12, color: 'var(--ink-mute)', marginBottom: 16, fontStyle: 'italic' }}>{confirmGift.desc}</div>
            <div style={{ display: 'flex', gap: 8 }}>
              <button onClick={() => setConfirmGift(null)} style={{
                flex: 1, padding: '10px', background: 'transparent', border: '1px solid var(--line-soft)',
                color: 'var(--ink-mute)', borderRadius: 999, fontSize: 12, cursor: 'pointer', fontFamily: 'Manrope',
              }}>Annuler</button>
              <button onClick={() => buy(confirmGift)} style={{
                flex: 1, padding: '10px', background: accent, border: 'none',
                color: '#1c0e0a', borderRadius: 999, fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'Manrope',
              }}>Acheter ◉ {confirmGift.price}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Vue Catalogue (référence) ───────────────────────────────
function CatalogView({ char, repValue }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 10 }}>
      {GIFT_CATALOG.map(g => (
        <GiftCard
          key={g.id}
          gift={g}
          mode="catalog"
          onClick={() => {}}
          char={char}
          repValue={repValue}
        />
      ))}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════
// 7. MODAL UNIFIÉ — GiftModal v3 (onglets : Inventaire / Boutique / Catalogue)
// ═══════════════════════════════════════════════════════════════

function GiftModal({ char, mcName, rep, scenario, onGift, onClose }) {
  const charId = scenario?.id || char?.id || 'default';
  const repValue = typeof rep === 'number' ? rep : (rep?.value ?? 0);
  const [tab, setTab] = React.useState('inventory');
  const [selected, setSelected] = React.useState(null);
  const accent = char?.accent || 'var(--gold)';
  const canGift = repValue >= 6;

  const handleOffer = () => {
    if (!selected || !canGift) return;
    if (!hasItem(charId, mcName, selected.name)) {
      alert("Tu ne possèdes plus cet objet.");
      return;
    }
    const effect = getGiftEffect(selected, char);
    // Consume the item
    removeItem(charId, mcName, selected.name);
    onGift(selected, effect);
    onClose();
  };

  const tabs = [
    { id: 'inventory', label: 'Mes objets', icon: '◈' },
    { id: 'shop',      label: 'Boutique',   icon: '◉' },
    { id: 'catalog',   label: 'Catalogue',  icon: '✦' },
  ];

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 2000,
      background: 'rgba(13,7,9,0.88)',
      display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
      backdropFilter: 'blur(4px)',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: '100%', maxWidth: 480,
        background: 'var(--bg)',
        borderRadius: '20px 20px 0 0',
        border: '1px solid var(--line-soft)',
        borderBottom: 'none',
        maxHeight: '90vh',
        display: 'flex', flexDirection: 'column',
        overflow: 'hidden',
      }}>
        {/* Header */}
        <div style={{ padding: '18px 20px 12px', borderBottom: '1px solid var(--line-soft)', flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
            <div>
              <h2 style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: 22, fontWeight: 600, color: accent, margin: 0 }}>
                Cadeaux
              </h2>
              {char?.name && (
                <p style={{ margin: '2px 0 0', fontSize: 12, color: 'var(--ink-fade)' }}>
                  à {char.name}
                </p>
              )}
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <WalletBadge charId={charId} mcName={mcName} />
              <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--ink-mute)', fontSize: 22, cursor: 'pointer', lineHeight: 1, padding: 4 }}>×</button>
            </div>
          </div>

          {!canGift && (
            <div style={{ marginTop: 10, padding: '8px 12px', background: 'rgba(168,50,74,0.12)', border: '1px solid rgba(168,50,74,0.25)', borderRadius: 8, fontSize: 12, color: 'var(--rouge)' }}>
              Faveur insuffisante — atteins <strong>Remarqué·e</strong> avant d'offrir.
            </div>
          )}

          {/* Onglets */}
          <div style={{ display: 'flex', gap: 5, marginTop: 12 }}>
            {tabs.map(t => (
              <button key={t.id} onClick={() => { setTab(t.id); setSelected(null); }} style={{
                flex: 1, padding: '7px 0',
                borderRadius: 999,
                border: `1px solid ${tab === t.id ? accent : 'var(--line-soft)'}`,
                background: tab === t.id ? `${accent}22` : 'transparent',
                color: tab === t.id ? accent : 'var(--ink-mute)',
                fontSize: 11, fontWeight: 600, letterSpacing: '0.04em',
                cursor: 'pointer', whiteSpace: 'nowrap',
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
                fontFamily: 'Manrope, sans-serif',
              }}>
                <span>{t.icon}</span>
                <span>{t.label}</span>
              </button>
            ))}
          </div>
        </div>

        {/* Body */}
        <div style={{ flex: 1, overflowY: 'auto', padding: '14px 14px 12px' }}>
          {tab === 'inventory' && (
            <PossessionsView
              char={char}
              mcName={mcName}
              charId={charId}
              repValue={repValue}
              onSelect={canGift ? (g) => setSelected(g) : null}
              selected={selected}
              mode="inventory"
            />
          )}
          {tab === 'shop' && (
            <ShopView char={char} mcName={mcName} charId={charId} scenario={scenario} />
          )}
          {tab === 'catalog' && (
            <CatalogView char={char} repValue={repValue} />
          )}
        </div>

        {/* Footer : offrir (uniquement onglet inventaire) */}
        {tab === 'inventory' && (
          <div style={{
            padding: '12px 16px 28px',
            borderTop: '1px solid var(--line-soft)',
            flexShrink: 0,
            minHeight: 78,
            display: 'flex', flexDirection: 'column', gap: 10,
          }}>
            {selected ? (
              <>
                <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                  <span style={{ fontSize: 22 }}>{selected.emoji}</span>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>{selected.name}</div>
                    <div style={{ fontSize: 11, color: 'var(--ink-mute)', marginTop: 2 }}>{selected.desc}</div>
                  </div>
                </div>
                <button onClick={handleOffer} style={{
                  padding: '10px 0', background: `linear-gradient(135deg, ${accent}dd, ${accent}99)`,
                  border: 'none', borderRadius: 10, color: '#fff', fontSize: 13, fontWeight: 600,
                  cursor: 'pointer', letterSpacing: '0.03em',
                }}>
                  Offrir {selected.emoji} {selected.name}
                </button>
              </>
            ) : (
              <p style={{ textAlign: 'center', fontSize: 12, color: 'var(--ink-fade)', fontStyle: 'italic', margin: 0, paddingTop: 8 }}>
                Sélectionne un objet de ta possession à offrir
              </p>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════
// 8. EXPORTS
// ═══════════════════════════════════════════════════════════════

Object.assign(window, {
  // Catalogue
  GIFT_CATALOG, CATALOG_BY_NAME,
  // Économie
  loadGold, saveGold, addGold, spendGold, STARTING_GOLD,
  // Inventaire
  loadInventory, saveInventory, addItem, removeItem, hasItem,
  // Effet
  getGiftEffect, giftHint, buildGiftPrompt,
  // UI
  WalletBadge, GiftCard, PossessionsView, ShopView, CatalogView, GiftModal,
  // Boutique
  getShopStock, getShopLabel,
});

})();
