// sc-data.jsx — V2 shared store + seed data for SpotCrown prototype
//
// MODEL OVERVIEW
// ──────────────
// Two parallel modes share the same item catalog & people:
//
//   • CHAT mode (groups):  each group has one current crown holder, a spot
//     timeline, comments, and a per-group confirmation policy. Pending spots
//     accrue +/- votes from group members until they auto-confirm or expire.
//
//   • IN-PERSON mode (bilateral): for any (item, personA, personB) pair, the
//     crown is held by whoever last spotted the item with the OTHER person as
//     a witness — recorded via a "Peek Session" (QR + Wi-Fi Aware proximity).
//     Witnesses are people physically present; transfers are instant + final.
//
// Two "devices" (left = Emil, right = Ayaka) share this single store so
// activity cascades live across both phones during demo.

const SC = (() => {
  // ─── Avatar helper: tiny SVG portrait with initials + gradient ──────────
  const palette = [
    ['#F7D9B4','#E3A267'], ['#CDE4D8','#3E8C5A'], ['#E6D2EE','#8B5CB4'],
    ['#D6E3F0','#4A7FB0'], ['#F4C5C5','#C94A3A'], ['#F1E6B3','#B88A1F'],
    ['#CFE4E1','#2D7F7F'], ['#E9D7BF','#946A2B'],
  ];
  function avatar(seed, initials){
    const [a,b] = palette[Math.abs([...seed].reduce((h,c)=>h*31+c.charCodeAt(0),0)) % palette.length];
    const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'>
      <defs><linearGradient id='g${seed}' x1='0' y1='0' x2='1' y2='1'>
        <stop offset='0' stop-color='${a}'/><stop offset='1' stop-color='${b}'/>
      </linearGradient></defs>
      <rect width='80' height='80' rx='40' fill='url(#g${seed})'/>
      <text x='40' y='47' text-anchor='middle' font-family='Inter,system-ui' font-weight='600' font-size='28' fill='white' letter-spacing='-.02em'>${initials}</text>
    </svg>`;
    return 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg);
  }

  // ─── Decorative item "photos" via SVG ───────────────────────────────────
  function itemArt(emoji, tint){
    const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'>
      <defs><linearGradient id='b' x1='0' y1='0' x2='0' y2='1'>
        <stop offset='0' stop-color='${tint[0]}'/><stop offset='1' stop-color='${tint[1]}'/></linearGradient></defs>
      <rect width='120' height='120' rx='18' fill='url(#b)'/>
      <text x='60' y='82' text-anchor='middle' font-size='62'>${emoji}</text>
    </svg>`;
    return 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg);
  }
  function fakePhoto(emoji, seed){
    const tints = [
      ['#EADFC6','#C9A66B'], ['#D7E4CE','#6B9A72'], ['#E4D7E9','#8B6BA5'],
      ['#D8E2EE','#6B84A5'], ['#EED5CB','#B57058'], ['#F0E6C0','#AF8B3A'],
    ];
    const [a,b] = tints[Math.abs([...(seed||emoji)].reduce((h,c)=>h*31+c.charCodeAt(0),0)) % tints.length];
    const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'>
      <defs>
        <linearGradient id='bg' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='${a}'/><stop offset='1' stop-color='${b}'/></linearGradient>
        <pattern id='st' width='14' height='14' patternUnits='userSpaceOnUse' patternTransform='rotate(30)'>
          <rect width='14' height='14' fill='url(#bg)'/>
          <rect width='7' height='14' fill='rgba(255,255,255,0.08)'/>
        </pattern>
      </defs>
      <rect width='200' height='200' fill='url(#st)'/>
      <circle cx='100' cy='105' r='52' fill='rgba(0,0,0,0.12)'/>
      <text x='100' y='126' text-anchor='middle' font-size='70'>${emoji}</text>
    </svg>`;
    return 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg);
  }

  // ─── People ─────────────────────────────────────────────────────────────
  const people = {
    emil:   { id:'emil',   name:'Emil',         first:'Emil',   avatar: avatar('emil','EM'),   me:true },
    ayaka:  { id:'ayaka',  name:'Ayaka Müller', first:'Ayaka',  avatar: avatar('ayaka','AM') },
    sam:    { id:'sam',    name:'Sam Nguyen',   first:'Sam',    avatar: avatar('sam','SN') },
    kat:    { id:'kat',    name:'Kat Parker',   first:'Kat',    avatar: avatar('kat','KP') },
    luke:   { id:'luke',   name:'Luke Turner',  first:'Luke',   avatar: avatar('luke','LT') },
    mika:   { id:'mika',   name:'Mika Ito',     first:'Mika',   avatar: avatar('mika','MI') },
    jen:    { id:'jen',    name:'Jen Alvarez',  first:'Jen',    avatar: avatar('jen','JA') },
    taro:   { id:'taro',   name:'Taro Sato',    first:'Taro',   avatar: avatar('taro','TS') },
  };

  // ─── Item catalog ──────────────────────────────────────────────────────
  const items = [
    { id:'rooster',   name:'Rooster figurine',   title:'Cockmaster',    emoji:'🐓', region:'Original',  art: itemArt('🐓', ['#F5D7B8','#E08A3A']) },
    { id:'tanuki',    name:'Tanuki statue',      title:'Tanuki master', emoji:'🦝', region:'Japan',     art: itemArt('🦝', ['#EADBC0','#8C6B3D']) },
    { id:'maneki',    name:'Maneki-neko',        title:'Lucky one',     emoji:'🐱', region:'Japan',     art: itemArt('🐱', ['#F6DCDC','#CF8080']) },
    { id:'torii',     name:'Torii gate',         title:'Gatekeeper',    emoji:'⛩️', region:'Japan',     art: itemArt('⛩️',['#F1CFC4','#B05238']) },
    { id:'daruma',    name:'Daruma doll',        title:'Goal-getter',   emoji:'🔴', region:'Japan',     art: itemArt('🎎',['#F7CFC0','#B84C38']) },
    { id:'matcha',    name:'Matcha bowl',        title:'Matcha monarch',emoji:'🍵', region:'Japan',     art: itemArt('🍵', ['#DDEBC9','#6B9452']) },
    { id:'stolper',   name:'Stolperstein',       title:'Rememberer',    emoji:'🟨', region:'Europe',    art: itemArt('🧱', ['#EFE1B8','#8C6E2B']) },
    { id:'phone',     name:'Red phone booth',    title:'Ring-ringer',   emoji:'📞', region:'Europe',    art: itemArt('📞', ['#F2C6C0','#AA3A2A']) },
    { id:'azulejo',   name:'Azulejo tile',       title:'Tile master',   emoji:'🔷', region:'Europe',    art: itemArt('🔷', ['#D5E3EF','#3E6E99']) },
    { id:'lovelock',  name:'Love lock',          title:'Lock keeper',   emoji:'🔒', region:'Worldwide', art: itemArt('🔒', ['#E9D7C3','#8E6B3B']) },
    { id:'tuktuk',    name:'Tuk-tuk',            title:'Tuk-tuk titan', emoji:'🛺', region:'Worldwide', art: itemArt('🛺', ['#F3D9B8','#C17A2A']) },
    // In-person-only items (often hyperlocal / Mika-Emil signature)
    { id:'cat',       name:'Black cat',          title:'Cat charmer',   emoji:'🐈‍⬛', region:'Anywhere', art: itemArt('🐈‍⬛',['#E5D8C2','#5C5048']) },
    { id:'piano',     name:'Street piano',       title:'Piano whisperer',emoji:'🎹',region:'Worldwide', art: itemArt('🎹', ['#E0E0E6','#5C5C7A']) },
  ];
  const itemById = Object.fromEntries(items.map(i=>[i.id,i]));

  // ─── Time helpers ──────────────────────────────────────────────────────
  const now = Date.now();
  const DAY = 86400000, HOUR = 3600000, MIN = 60000;
  const ago = d => now - d*DAY;

  // ─── CHAT MODE: groups + score-based pending spots ─────────────────────
  // Each spot has confirmations: a map of personId → +1 / -1 / 0.
  // policy.minApprovals:   net score required to auto-confirm
  // policy.autoConfirmAfter: ms until pending auto-confirms if net ≥ 0
  // policy.takebackWindow:  ms during which the spotter can rescind
  // policy.photoRequired:   if true, default policy needs +N photo confirmations
  // admins: array of person ids — auto-pass spots immediately when they vote +.
  const groups = [
    {
      id:'rooster-hunters', name:'Rooster Hunters', itemId:'rooster',
      titleOverride:null, members:['ayaka','emil','sam','kat','luke'],
      ownerId:'mika', adminIds:['mika','luke'],
      holderId:'ayaka', heldSince: ago(2), streak: 7,
      policy:{ photoRequired:true, minApprovals:2, autoConfirmAfter: 24*HOUR, takebackWindow: 60*MIN },
      spots:[
        { id:'s1', byId:'ayaka', at: ago(2),  photoSeed:'r1', takenFrom:'luke', status:'confirmed', votes:{}, comments:[
          {id:'c-1', byId:'kat',  at: ago(2)+30*MIN, text:'Roosters everywhere in Lisbon I swear 🐓'},
          {id:'c-2', byId:'luke', at: ago(2)+45*MIN, text:'I had it for one (1) hour 😩'},
        ]},
        { id:'s2', byId:'luke',  at: ago(3),  photoSeed:'r2', takenFrom:'kat',   status:'confirmed', votes:{}, comments:[]},
        { id:'s3', byId:'kat',   at: ago(7),  photoSeed:'r3', takenFrom:'sam',   status:'confirmed', votes:{}, comments:[]},
        { id:'s4', byId:'sam',   at: ago(11), photoSeed:'r4', takenFrom:'luke',  status:'confirmed', votes:{}, comments:[]},
        { id:'s5', byId:'luke',  at: ago(15), photoSeed:'r5', takenFrom:null,    status:'confirmed', votes:{}, comments:[]},
      ],
      pendingSpots:[],
    },
    {
      id:'coffee-roosters', name:'Coffee Roosters', itemId:'rooster',
      titleOverride:null, members:['emil','mika','jen'],
      ownerId:'emil', adminIds:['emil'],
      holderId:'emil', heldSince: ago(4), streak: 4,
      policy:{ photoRequired:false, minApprovals:1, autoConfirmAfter: 24*HOUR, takebackWindow: 60*MIN },
      spots:[
        { id:'s6', byId:'emil', at: ago(4), photoSeed:'c1', takenFrom:'mika', status:'confirmed', votes:{}, comments:[
          {id:'c-3', byId:'mika', at: ago(4)+10*MIN, text:'wow ok ok 👀'},
        ]},
        { id:'s7', byId:'mika', at: ago(9), photoSeed:'c2', takenFrom:'jen',  status:'confirmed', votes:{}, comments:[]},
      ],
      pendingSpots:[],
    },
    {
      id:'tanuki-hunters', name:'Tanuki Hunters', itemId:'tanuki',
      titleOverride:'Tanuki Master', members:['emil','ayaka','sam','taro'],
      ownerId:'taro', adminIds:['taro'],
      holderId:'ayaka', heldSince: ago(3), streak: 3,
      policy:{ photoRequired:true, minApprovals:2, autoConfirmAfter: 24*HOUR, takebackWindow: 60*MIN },
      spots:[
        { id:'s8', byId:'ayaka', at: ago(3), photoSeed:'t1', takenFrom:'emil', status:'confirmed', votes:{}, comments:[]},
        { id:'s9', byId:'emil',  at: ago(8), photoSeed:'t2', takenFrom:'taro', status:'confirmed', votes:{}, comments:[]},
        { id:'s10',byId:'taro',  at: ago(14),photoSeed:'t3', takenFrom:null,   status:'confirmed', votes:{}, comments:[]},
      ],
      // Live pending: Sam is trying to claim. Awaits +2 net.
      pendingSpots:[
        { id:'p1', byId:'sam', at: now-12*MIN, photoSeed:'pending1',
          status:'pending', votes:{ taro:1 }, comments:[
            {id:'c-4', byId:'taro', at: now-10*MIN, text:'looks legit, in Asakusa right?'},
            {id:'c-5', byId:'sam',  at: now-9*MIN,  text:'yep, by the temple gate'},
          ]
        }
      ],
    },
    {
      id:'memory-walkers', name:'Memory Walkers', itemId:'stolper',
      titleOverride:'Rememberer', members:['emil','kat','mika'],
      ownerId:'mika', adminIds:['mika'],
      holderId:'kat', heldSince: ago(1), streak: 1,
      policy:{ photoRequired:true, minApprovals:2, autoConfirmAfter: 24*HOUR, takebackWindow: 60*MIN },
      spots:[
        { id:'s11', byId:'kat',  at: ago(1), photoSeed:'m1', takenFrom:'emil', status:'confirmed', votes:{}, comments:[]},
        { id:'s12', byId:'emil', at: ago(6), photoSeed:'m2', takenFrom:'mika', status:'confirmed', votes:{}, comments:[]},
      ],
      pendingSpots:[],
    },
    {
      id:'matcha-kings', name:'Matcha Kings', itemId:'matcha',
      titleOverride:null, members:['emil','jen','luke'],
      ownerId:'emil', adminIds:['emil'],
      holderId:'emil', heldSince: ago(22), streak: 22,
      policy:{ photoRequired:false, minApprovals:1, autoConfirmAfter: 24*HOUR, takebackWindow: 60*MIN },
      spots:[
        { id:'s13', byId:'emil', at: ago(22), photoSeed:'mk1', takenFrom:'jen', status:'confirmed', votes:{}, comments:[]},
      ],
      pendingSpots:[],
    },
  ];

  // ─── IN-PERSON MODE: bilateral state ───────────────────────────────────
  // Per (itemId, pair) the crown is held by one of the two; transfers happen
  // via Peek sessions where the other physically witnesses it.
  // pairKey is sorted so [a,b] == [b,a].
  function pairKey(a,b){ return [a,b].sort().join('::'); }

  // Each entry: items: { itemId: { holderId, heldSince, streak, transfers:[{at,byId,photo?,comment?}] } }
  const inperson = {
    [pairKey('emil','ayaka')]: {
      itemId_byId:{},
      items: {
        rooster: { holderId:'emil', heldSince: ago(0.2), streak: 11, transfers:[
          { id:'ip1', byId:'emil', at: ago(0.2), takenFrom:'ayaka', comment:'haha got you at the bakery', photo:false },
          { id:'ip2', byId:'ayaka', at: ago(2),  takenFrom:'emil',  comment:null, photo:true },
          { id:'ip3', byId:'emil', at: ago(5),   takenFrom:'ayaka', comment:'breakfast spot', photo:false },
        ]},
        cat: { holderId:'ayaka', heldSince: ago(1), streak: 4, transfers:[
          { id:'ip4', byId:'ayaka', at: ago(1), takenFrom:'emil', comment:'on the windowsill again', photo:false },
          { id:'ip5', byId:'emil',  at: ago(3), takenFrom:'ayaka', comment:null, photo:false },
        ]},
        piano: { holderId:'ayaka', heldSince: ago(7), streak: 1, transfers:[
          { id:'ip6', byId:'ayaka', at: ago(7), takenFrom:null, comment:'first one!', photo:true },
        ]},
      }
    },
    [pairKey('emil','mika')]: {
      items: {
        rooster: { holderId:'mika', heldSince: ago(0.5), streak: 3, transfers:[
          { id:'ip7', byId:'mika', at: ago(0.5), takenFrom:'emil', comment:null, photo:false },
        ]},
        tanuki: { holderId:'emil', heldSince: ago(8),    streak: 12, transfers:[
          { id:'ip8', byId:'emil', at: ago(8), takenFrom:'mika', comment:'sneaky', photo:false },
        ]},
        maneki: { holderId:'mika', heldSince: ago(2),    streak: 1, transfers:[
          { id:'ip9', byId:'mika', at: ago(2), takenFrom:'emil', comment:null, photo:true },
        ]},
      }
    },
    [pairKey('emil','sam')]: {
      items: {
        torii: { holderId:'sam', heldSince: ago(4), streak: 2, transfers:[
          { id:'ip10', byId:'sam', at: ago(4), takenFrom:'emil', comment:'kyoto run', photo:true },
        ]},
      }
    },
    [pairKey('emil','luke')]: {
      items: {
        rooster: { holderId:'luke', heldSince: ago(6), streak: 1, transfers:[
          { id:'ip11', byId:'luke', at: ago(6), takenFrom:'emil', comment:null, photo:false },
        ]},
      }
    },
    [pairKey('ayaka','mika')]: {
      items: {
        cat: { holderId:'mika', heldSince: ago(0.7), streak: 2, transfers:[
          { id:'ip12', byId:'mika', at: ago(0.7), takenFrom:'ayaka', comment:'hahahaha', photo:false },
        ]},
      }
    },
  };

  // ─── Activity feed (cross-mode) ────────────────────────────────────────
  // kind: 'crown' | 'pending' | 'inperson'
  function buildActivity(groups, inperson){
    const out = [];
    groups.forEach(g=>{
      g.spots.forEach(s=>{
        out.push({
          id: g.id+'-'+s.id, mode:'chat', groupId:g.id, groupName:g.name,
          itemId:g.itemId, byId:s.byId, takenFrom:s.takenFrom,
          at:s.at, photoSeed:s.photoSeed, kind:'crown',
        });
      });
      g.pendingSpots.forEach(p=>{
        out.push({
          id: g.id+'-'+p.id, mode:'chat', groupId:g.id, groupName:g.name,
          itemId:g.itemId, byId:p.byId, at:p.at, photoSeed:p.photoSeed, kind:'pending',
          score: Object.values(p.votes||{}).reduce((s,v)=>s+v,0),
          needed: g.policy.minApprovals,
        });
      });
    });
    Object.entries(inperson).forEach(([key, pair])=>{
      const [a,b] = key.split('::');
      Object.entries(pair.items).forEach(([itemId, st])=>{
        st.transfers.forEach(t=>{
          out.push({
            id:'ip-'+key+'-'+itemId+'-'+t.id, mode:'inperson', pairKey:key,
            withId: (t.byId===a?b:a),
            itemId, byId:t.byId, takenFrom:t.takenFrom,
            at:t.at, kind:'inperson', comment:t.comment, photo:t.photo,
          });
        });
      });
    });
    return out.sort((a,b)=>b.at-a.at);
  }

  // ─── Reactive store ────────────────────────────────────────────────────
  const listeners = new Set();
  let state = {
    meId:'emil',
    deviceBId:'ayaka',
    people, items, itemById,
    groups,
    inperson,
    activity: buildActivity(groups, inperson),
    // Active peek session (when one user opens "Spot in person")
    peekSession: null,
    /* peekSession shape:
       {
         id, hostId, itemId, startedAt, endsAt,
         witnesses: [{ personId, joinedAt, status:'present'|'confirmed' }],
         status: 'advertising' | 'confirming' | 'completed' | 'cancelled',
         comment: '', photo: false,
       }
    */
  };
  function subscribe(fn){ listeners.add(fn); return ()=>listeners.delete(fn); }
  function set(updater){
    state = typeof updater==='function' ? updater(state) : {...state, ...updater};
    listeners.forEach(fn=>fn(state));
  }
  function get(){ return state; }

  // ─── ACTIONS ───────────────────────────────────────────────────────────

  // CHAT: spot creates pending entry in selected groups (auto-confirms if admin
  // or policy.minApprovals===0). Pending spots show up in activity + group.
  function spotChat(userId, itemId, groupIds, photoSeed, comment){
    set(s=>{
      const newActivity = [];
      const groups = s.groups.map(g=>{
        if (!groupIds.includes(g.id)) return g;
        if (g.itemId !== itemId) return g;
        const isAdmin = g.adminIds.includes(userId);
        const needsApproval = !isAdmin && g.policy.minApprovals > 0;
        if (!needsApproval){
          // Direct confirm
          const newSpot = {
            id:'n'+Date.now()+'-'+g.id, byId:userId, at:Date.now(),
            photoSeed, takenFrom:g.holderId, status:'confirmed',
            votes:{}, comments: comment ? [{id:'sc'+Date.now(), byId:userId, at:Date.now(), text:comment}] : [],
          };
          newActivity.push({
            id:g.id+'-'+newSpot.id, mode:'chat', groupId:g.id, groupName:g.name,
            itemId:g.itemId, byId:userId, takenFrom:g.holderId,
            at:newSpot.at, photoSeed, kind:'crown',
          });
          return {...g, holderId:userId, heldSince:Date.now(), streak:1, spots:[newSpot, ...g.spots]};
        } else {
          const pending = {
            id:'pp'+Date.now()+'-'+g.id, byId:userId, at:Date.now(),
            photoSeed, status:'pending',
            votes: {}, // no auto-vote — spotter not counted
            comments: comment ? [{id:'sc'+Date.now(), byId:userId, at:Date.now(), text:comment}] : [],
          };
          newActivity.push({
            id:g.id+'-'+pending.id, mode:'chat', groupId:g.id, groupName:g.name,
            itemId:g.itemId, byId:userId,
            at:pending.at, photoSeed, kind:'pending',
            score:0, needed:g.policy.minApprovals,
          });
          return {...g, pendingSpots:[pending, ...g.pendingSpots]};
        }
      });
      return {...s, groups, activity:[...newActivity, ...s.activity]};
    });
  }
  // Vote on a pending spot. score is +1 / -1 / 0 (withdraw).
  function voteOnPending(groupId, pendingId, voterId, score){
    set(s=>{
      const groups = s.groups.map(g=>{
        if (g.id !== groupId) return g;
        const pendingSpots = g.pendingSpots.map(p=>{
          if (p.id !== pendingId) return p;
          const votes = {...p.votes};
          if (score===0) delete votes[voterId];
          else votes[voterId] = score;
          return {...p, votes};
        });
        // Check if the pending one tipped over
        const pp = pendingSpots.find(p=>p.id===pendingId);
        const net = Object.values(pp.votes).reduce((s,v)=>s+v,0);
        const isAdminVote = g.adminIds.includes(voterId) && score>0;
        if (net >= g.policy.minApprovals || isAdminVote){
          // Promote to confirmed
          const newSpot = {
            id:pp.id.replace('pp','sn'), byId:pp.byId, at:pp.at,
            photoSeed:pp.photoSeed, takenFrom:g.holderId, status:'confirmed',
            votes:pp.votes, comments:pp.comments,
          };
          const remaining = pendingSpots.filter(p=>p.id!==pendingId);
          return {...g, holderId:pp.byId, heldSince:pp.at, streak:1,
                  spots:[newSpot, ...g.spots], pendingSpots:remaining};
        }
        if (net <= -g.policy.minApprovals){
          // Rejected — drop the pending
          return {...g, pendingSpots: pendingSpots.filter(p=>p.id!==pendingId)};
        }
        return {...g, pendingSpots};
      });
      // Rebuild activity
      const inp = s.inperson;
      return {...s, groups, activity: buildActivity(groups, inp)};
    });
  }
  // Add a comment to a chat spot (works for confirmed and pending).
  function addComment(groupId, spotId, byId, text){
    if (!text || !text.trim()) return;
    const c = { id:'c'+Date.now(), byId, at:Date.now(), text:text.trim() };
    set(s=>{
      const groups = s.groups.map(g=>{
        if (g.id!==groupId) return g;
        const spots = g.spots.map(sp=>sp.id===spotId?{...sp, comments:[...sp.comments, c]}:sp);
        const pendingSpots = g.pendingSpots.map(sp=>sp.id===spotId?{...sp, comments:[...sp.comments, c]}:sp);
        return {...g, spots, pendingSpots};
      });
      return {...s, groups};
    });
  }
  function createGroup({ name, itemId, titleOverride, memberIds, ownerId, policy }){
    set(s=>{
      const id = name.toLowerCase().replace(/\s+/g,'-')+'-'+Math.floor(Math.random()*999);
      const g = { id, name, itemId, titleOverride:titleOverride||null,
        members: memberIds, ownerId, adminIds:[ownerId],
        holderId:null, heldSince:null, streak:0,
        policy: policy||{ photoRequired:true, minApprovals:1, autoConfirmAfter: 24*HOUR, takebackWindow: 60*MIN },
        spots:[], pendingSpots:[] };
      return {...s, groups:[g, ...s.groups]};
    });
  }
  function updateGroupPolicy(groupId, patch){
    set(s=>({
      ...s,
      groups: s.groups.map(g=>g.id===groupId?{...g, policy:{...g.policy, ...patch}}:g)
    }));
  }
  function setDeviceBUser(id){ set({deviceBId:id}); }

  // IN-PERSON: peek session
  function startPeek(hostId, itemId){
    const id = 'peek-'+Date.now();
    const startedAt = Date.now();
    const endsAt = startedAt + 60*1000;
    set(s=>({...s, peekSession:{
      id, hostId, itemId, startedAt, endsAt,
      witnesses:[], status:'advertising', comment:'', photo:false,
    }}));
    return id;
  }
  function joinPeek(witnessId){
    set(s=>{
      if (!s.peekSession) return s;
      if (s.peekSession.witnesses.find(w=>w.personId===witnessId)) return s;
      return {...s, peekSession:{
        ...s.peekSession,
        witnesses: [...s.peekSession.witnesses, {personId:witnessId, joinedAt:Date.now(), status:'present'}],
      }};
    });
  }
  function confirmPeek(witnessId){
    set(s=>{
      if (!s.peekSession) return s;
      return {...s, peekSession:{
        ...s.peekSession,
        witnesses: s.peekSession.witnesses.map(w=>w.personId===witnessId?{...w, status:'confirmed'}:w),
      }};
    });
  }
  function setPeekComment(text){
    set(s=> s.peekSession ? {...s, peekSession:{...s.peekSession, comment:text}} : s);
  }
  function setPeekPhoto(yes){
    set(s=> s.peekSession ? {...s, peekSession:{...s.peekSession, photo:yes}} : s);
  }
  function cancelPeek(){
    set(s=>({...s, peekSession:null}));
  }
  // Finalize: for each confirmed witness, transfer the bilateral crown for this item
  function finalizePeek(){
    set(s=>{
      const ps = s.peekSession;
      if (!ps) return s;
      const itemId = ps.itemId;
      const hostId = ps.hostId;
      const at = Date.now();
      const inp = {...s.inperson};
      const newActivity = [];
      ps.witnesses.filter(w=>w.status==='confirmed').forEach(w=>{
        const key = pairKey(hostId, w.personId);
        const pair = inp[key] || { items:{} };
        const prev = pair.items[itemId];
        const transfer = {
          id:'ipn'+Date.now()+'-'+w.personId, byId:hostId, at,
          takenFrom: prev ? prev.holderId : null,
          comment: ps.comment || null,
          photo: ps.photo,
        };
        const nextItems = {...pair.items, [itemId]:{
          holderId:hostId, heldSince:at, streak: (prev && prev.holderId===hostId ? prev.streak+1 : 1),
          transfers: [transfer, ...(prev?prev.transfers:[])],
        }};
        inp[key] = {...pair, items:nextItems};
        newActivity.push({
          id:'ip-'+key+'-'+itemId+'-'+transfer.id, mode:'inperson', pairKey:key,
          withId:w.personId, itemId, byId:hostId, takenFrom: prev?prev.holderId:null,
          at, kind:'inperson', comment:ps.comment||null, photo:ps.photo,
        });
      });
      return {
        ...s, inperson:inp, peekSession:null,
        activity: [...newActivity, ...s.activity],
      };
    });
  }

  return {
    subscribe, set, get, avatar, itemArt, fakePhoto, pairKey,
    actions:{
      spotChat, voteOnPending, addComment, createGroup, updateGroupPolicy, setDeviceBUser,
      startPeek, joinPeek, confirmPeek, setPeekComment, setPeekPhoto, cancelPeek, finalizePeek,
    },
  };
})();

// Small shared hook
function useStore(){
  const [s, setS] = React.useState(SC.get());
  React.useEffect(()=>SC.subscribe(setS), []);
  return s;
}

// Format helpers
function fmtDaysAgo(t){
  const d = Math.max(0, Math.floor((Date.now()-t)/86400000));
  if (d===0){
    const h = Math.floor((Date.now()-t)/3600000);
    if (h<1){
      const m = Math.max(1, Math.floor((Date.now()-t)/60000));
      return `${m}m ago`;
    }
    return `${h}h ago`;
  }
  if (d===1) return 'yesterday';
  if (d<7) return `${d}d ago`;
  return `${Math.floor(d/7)}w ago`;
}
function fmtHeld(since){
  if (!since) return 'vacant';
  const d = Math.floor((Date.now()-since)/86400000);
  if (d===0){
    const h = Math.floor((Date.now()-since)/3600000);
    if (h<1){
      const m = Math.max(1, Math.floor((Date.now()-since)/60000));
      return m+'m';
    }
    return `${h}h`;
  }
  return `${d} day${d===1?'':'s'}`;
}
// In-person: items I hold across all my pairs / they hold against me
function inpersonForUser(s, userId){
  const out = []; // [{ itemId, withId, holderId, heldSince, streak, transfers }]
  Object.entries(s.inperson).forEach(([key, pair])=>{
    const [a,b] = key.split('::');
    if (a!==userId && b!==userId) return;
    const withId = a===userId ? b : a;
    Object.entries(pair.items).forEach(([itemId, st])=>{
      out.push({ pairKey:key, itemId, withId, ...st });
    });
  });
  return out;
}
function inpersonItemsHeld(s, userId){
  // Count: per item, how many pairs is the user the holder
  const map = {};
  inpersonForUser(s, userId).forEach(r=>{
    if (r.holderId===userId){
      map[r.itemId] = (map[r.itemId]||0)+1;
    }
  });
  return map;
}

Object.assign(window, { SC, useStore, fmtDaysAgo, fmtHeld, inpersonForUser, inpersonItemsHeld });
