/* global React */
// Ghost tee-time emulation for the course-partner portal.
//
// "▶ Emulate" on a tee time fabricates a foursome of ghost players who book
// that slot and play a full round on the live map — each hole takes 10
// seconds (tee → fairway logged → on the green → holed), 9 holes ≈ 90
// seconds. Everything lives in this in-memory store: nothing touches
// Supabase, nothing is logged anywhere, refresh and it's gone.
//
// A slot whose tee time is still in the future queues in "Coming soon"
// until its time arrives, then walks onto the course — so managers can
// stage several emulations to see a realistic queue + live map at once.

const GHOST_HOLE_MS = 10000; // 10s per hole
const GHOST_HOLES = 9;       // × 9 holes = 90s of emulation
const GHOST_LINGER_MS = 30000; // keep a finished group visible for a bit

// Fake roster the emulator draws foursomes from. `member` drives the SPP
// membership mark in the Coming soon list.
const GHOST_ROSTER = [
  { first: 'Marcus',  last: 'Bell',      sbx: 4.812, member: true  },
  { first: 'Tina',    last: 'Alvarez',   sbx: 5.204, member: true  },
  { first: 'Jordan',  last: 'Whitfield', sbx: 3.911, member: false },
  { first: 'Priya',   last: 'Nair',      sbx: 4.455, member: true  },
  { first: 'Sam',     last: 'Okafor',    sbx: 6.030, member: true  },
  { first: 'Leah',    last: 'Grossman',  sbx: 3.542, member: false },
  { first: 'Diego',   last: 'Fuentes',   sbx: 4.988, member: true  },
  { first: 'Casey',   last: 'Tran',      sbx: 5.617, member: true  },
  { first: 'Noah',    last: 'Lindqvist', sbx: 4.101, member: false },
  { first: 'Amara',   last: 'Diallo',    sbx: 5.873, member: true  },
  { first: 'Reid',    last: 'Calloway',  sbx: 3.760, member: true  },
  { first: 'Yuki',    last: 'Tanaka',    sbx: 6.245, member: true  },
  { first: 'Georgia', last: 'Marsh',     sbx: 4.333, member: false },
  { first: 'Owen',    last: 'Petrov',    sbx: 5.090, member: true  },
  { first: 'Zoe',     last: 'Barros',    sbx: 4.702, member: true  },
  { first: 'Felix',   last: 'Nakamura',  sbx: 3.988, member: true  },
];

const GHOST = { groups: [], listeners: new Set() };
function ghostNotify() { GHOST.listeners.forEach(fn => { try { fn(); } catch (_) {} }); }

// Mode is a pure function of headcount (adaptive match modes — see
// docs/ADAPTIVE_MATCH_MODES_DESIGN.md): 2 → 1v1, 3 → 1v1v1, 4 → 2v2 scramble.
// Not live in production booking yet (dark-launched behind a flag), but the
// demo emulator can already show all three since it's purely in-memory.
function modeForCount(n) { return n === 2 ? '1v1' : n === 3 ? '1v1v1' : '2v2'; }

// Fabricate a group on this slot. Starts at the tee time (or now if the tee
// time already passed) — future slots queue up in "Coming soon".
// `seatPlayers` (optional) carries the tile's ACTUAL group — real booked
// golfers + the matchmaking fill — so the live map walks the same faces the
// manager sees on the tee-time tile. Missing seats top up from the roster.
// `count` (2/3/4, default 4) sets the target group size — real tee-time
// tiles always pass 4 (today's booking flow is 2v2-only in production); the
// standalone demo button lets a manager pick 2/3/4 to show off 1v1/1v1v1/2v2.
function ghostEmulateSlot(slot, nine, seatPlayers, count = 4) {
  const teeMs = new Date(slot.starts_at).getTime();
  const target = Math.max(2, Math.min(4, count));
  const players = (seatPlayers || []).slice(0, target).map(p => ({
    id: p.id,
    name: p.name,
    initials: p.initials,
    avatar_url: p.avatar_url || null,
    sbx: p.sbx != null ? p.sbx : null,
    member: p.member !== false,
  }));
  if (players.length < target) {
    const used = new Set(players.map(p => p.name));
    [...GHOST_ROSTER].sort(() => Math.random() - 0.5).forEach(p => {
      const name = `${p.first[0]}. ${p.last}`;
      if (players.length >= target || used.has(name)) return;
      used.add(name);
      players.push({
        id: `ghost-${p.first}-${p.last}`,
        name,
        initials: (p.first[0] + p.last[0]).toUpperCase(),
        avatar_url: null,
        sbx: p.sbx,
        member: p.member,
      });
    });
  }
  GHOST.groups.push({
    id: `ghost-${Math.random().toString(36).slice(2, 9)}`,
    ghost: true,
    slotId: slot.id,
    teeISO: slot.starts_at,
    nine: nine === 'back' ? 'back' : 'front',
    startAt: Math.max(Date.now(), teeMs),
    mode: modeForCount(players.length),
    players,
  });
  ghostNotify();
}

function ghostClearAll() { GHOST.groups = []; ghostNotify(); }

// Where a ghost group is at time `now`:
//   queued (before its tee time) → playing {holeIdx 0-8, stage} → finished → gone
// Stage thirds inside each 10s hole mirror the real tracking events:
//   tee (walking up) → fairway (all 4 logged their fairway shot) → green.
function ghostStateAt(g, now) {
  const el = now - g.startAt;
  if (el < 0) return { phase: 'queued' };
  if (el >= GHOST_HOLES * GHOST_HOLE_MS) {
    return { phase: el >= GHOST_HOLES * GHOST_HOLE_MS + GHOST_LINGER_MS ? 'gone' : 'finished' };
  }
  const holeIdx = Math.floor(el / GHOST_HOLE_MS);
  const inHole = el - holeIdx * GHOST_HOLE_MS;
  const stage = inHole < GHOST_HOLE_MS / 3 ? 'tee' : inHole < (GHOST_HOLE_MS * 2) / 3 ? 'fairway' : 'green';
  return { phase: 'playing', holeIdx, stage };
}

// Subscribe to the store + a ticker that animates progress and sweeps out
// long-finished groups. Returns the raw group list (derive state with
// ghostStateAt).
function useGhostGroups() {
  const [, force] = React.useReducer(x => x + 1, 0);
  React.useEffect(() => {
    GHOST.listeners.add(force);
    return () => GHOST.listeners.delete(force);
  }, []);
  React.useEffect(() => {
    const iv = setInterval(() => {
      if (!GHOST.groups.length) return;
      const now = Date.now();
      const alive = GHOST.groups.filter(g => ghostStateAt(g, now).phase !== 'gone');
      if (alive.length !== GHOST.groups.length) GHOST.groups = alive;
      force();
    }, 500);
    return () => clearInterval(iv);
  }, []);
  return GHOST.groups;
}

// ─── Ghost waitlist demand ────────────────────────────────────────────
// Fabricated "requested tee time" windows so the manager can demo the
// demand panel + auto-fill without real golfers. In-memory, per date.
const GHOST_DEMAND = { byDate: {}, listeners: new Set() };
function demandNotify() { GHOST_DEMAND.listeners.forEach(fn => { try { fn(); } catch (_) {} }); }

// Windows span the whole day — morning and midday golfers exist too, not
// just twilight (weighted toward twilight, Sandbox's core slot).
const DEMAND_WINDOWS = [
  // Morning
  [7 * 60, 9 * 60], [7 * 60 + 30, 10 * 60], [8 * 60, 11 * 60], [9 * 60, 12 * 60],
  // Midday
  [11 * 60, 13 * 60], [12 * 60, 14 * 60], [12 * 60, 16 * 60], [13 * 60 + 30, 15 * 60 + 30],
  // Twilight (double-weighted — the core Sandbox slot)
  [17 * 60, 19 * 60], [16 * 60, 17 * 60], [17 * 60, 20 * 60],
  [18 * 60, 19 * 60], [16 * 60, 20 * 60], [16 * 60 + 30, 18 * 60 + 30],
  [17 * 60, 19 * 60], [16 * 60, 20 * 60],
];

const overlapsWindow = (a, b) => a[0] < b[1] && b[0] < a[1];

// Add a handful of ghost golfers to the waitlist for a date — every click
// stacks MORE demand on top of what's already there. Replaces the stored
// array (never mutates in place) so React memos see each addition. ~60% of
// member roster picks land on the priority (Sandbox+) list.
//
// `allowedWindows` (optional): the manager's currently-selected Allowed Tee
// Time Window(s) on the Tee Times panel, as [startMin,endMin] pairs. When
// given, fabricated demand leans toward windows that overlap what's actually
// being offered (75% of the time) — a few still land outside it, since real
// waitlist demand isn't perfectly aligned to what's live either.
function ghostEmulateDemand(dateStr, allowedWindows) {
  // For today, don't fabricate demand for windows that have already passed —
  // drop any window ending before now and clamp starts up to the current
  // local time (rounded to 30 min). Future dates use the full day.
  const now = new Date();
  const p2 = n => String(n).padStart(2, '0');
  const todayString = `${now.getFullYear()}-${p2(now.getMonth() + 1)}-${p2(now.getDate())}`;
  const floor = Math.ceil((now.getHours() * 60 + now.getMinutes()) / 30) * 30;
  let windows = DEMAND_WINDOWS;
  if (dateStr === todayString) {
    windows = DEMAND_WINDOWS.filter(w => w[1] > floor).map(w => [Math.max(w[0], floor), w[1]]);
    if (!windows.length) { demandNotify(); return; } // nothing left to play today
  }
  const biased = (allowedWindows && allowedWindows.length)
    ? windows.filter(w => allowedWindows.some(aw => overlapsWindow(w, aw)))
    : [];
  // Never queue the same fabricated person twice in one day — repeat clicks
  // used to redraw from the full roster with no memory of who's already
  // waiting, so the same ghost could turn up in two different windows (or
  // twice in the same one) and read as a duplicate account.
  const already = new Set((GHOST_DEMAND.byDate[dateStr] || []).map(e => e.user.name));
  const available = GHOST_ROSTER.filter(p => !already.has(`${p.first[0]}. ${p.last}`));
  if (!available.length) { demandNotify(); return; } // whole roster is already queued today
  const picked = [...available].sort(() => Math.random() - 0.5).slice(0, 4 + Math.floor(Math.random() * 3));
  const fresh = picked.map((p, i) => {
    const pool = biased.length && Math.random() < 0.75 ? biased : windows;
    const w = pool[Math.floor(Math.random() * pool.length)];
    return {
      id: `gd-${Math.random().toString(36).slice(2, 9)}`,
      ghost: true,
      startMin: w[0], endMin: w[1],
      priority: p.member && Math.random() < 0.6,
      sbx: p.sbx,
      createdAt: Date.now() + i,
      user: {
        name: `${p.first[0]}. ${p.last}`,
        initials: (p.first[0] + p.last[0]).toUpperCase(),
        avatar_url: null, sbx: p.sbx, member: p.member,
      },
    };
  });
  GHOST_DEMAND.byDate[dateStr] = [...(GHOST_DEMAND.byDate[dateStr] || []), ...fresh];
  demandNotify();
}
function ghostClearDemand(dateStr) {
  if (dateStr) delete GHOST_DEMAND.byDate[dateStr]; else GHOST_DEMAND.byDate = {};
  demandNotify();
}
const GHOST_DEMAND_EMPTY = []; // stable identity → no memo churn when a date has no demand
function useGhostDemand(dateStr) {
  const [, force] = React.useReducer(x => x + 1, 0);
  React.useEffect(() => {
    GHOST_DEMAND.listeners.add(force);
    return () => GHOST_DEMAND.listeners.delete(force);
  }, []);
  return GHOST_DEMAND.byDate[dateStr] || GHOST_DEMAND_EMPTY;
}

// ─── Front 9 / Back 9 per tee time ────────────────────────────────────
// Demo-level persistence (localStorage) so the manager can mark live tee
// times as front or back nine without a schema change. Drives which line
// of the live map a group walks and its hole numbering (1-9 vs 10-18).
const NINE_KEY = 'spp_slot_nine';
function getSlotNine(slotId) {
  try { return (JSON.parse(localStorage.getItem(NINE_KEY) || '{}'))[slotId] === 'back' ? 'back' : 'front'; }
  catch (_) { return 'front'; }
}
function setSlotNine(slotId, nine) {
  try {
    const m = JSON.parse(localStorage.getItem(NINE_KEY) || '{}');
    m[slotId] = nine;
    localStorage.setItem(NINE_KEY, JSON.stringify(m));
  } catch (_) { /* private mode — toggle just won't persist */ }
}

// Per-day front/back-nine designation shown to golfers (which nine the day's
// tee times play). Defaults from whichever nine the admin measured.
const DAYNINE_KEY = 'spp_day_nine';
function getDayNine(courseId, dateStr) {
  try { const v = (JSON.parse(localStorage.getItem(DAYNINE_KEY) || '{}'))[`${courseId}|${dateStr}`]; return v === 'back' ? 'back' : v === 'front' ? 'front' : null; }
  catch (_) { return null; }
}
function setDayNine(courseId, dateStr, v) {
  try { const m = JSON.parse(localStorage.getItem(DAYNINE_KEY) || '{}'); m[`${courseId}|${dateStr}`] = v; localStorage.setItem(DAYNINE_KEY, JSON.stringify(m)); }
  catch (_) { /* private mode */ }
}

// ─── Tee-time config template + per-date overrides ────────────────────
// "Apply to next 10 days" writes a shared per-course template (interval,
// price, cart, allowed windows). Saving a SPECIFIC date's changes writes an
// override for that date only — overrides always win over the template, so
// a manager can customize one day without it getting clobbered by a later
// template apply (or by just navigating back to that date).
const TT_TEMPLATE_KEY = 'spp_tt_template';
function loadTTTemplate(courseId) {
  try { return (JSON.parse(localStorage.getItem(TT_TEMPLATE_KEY) || '{}'))[courseId] || null; }
  catch (_) { return null; }
}
function saveTTTemplate(courseId, cfg) {
  try {
    const m = JSON.parse(localStorage.getItem(TT_TEMPLATE_KEY) || '{}');
    m[courseId] = cfg;
    localStorage.setItem(TT_TEMPLATE_KEY, JSON.stringify(m));
  } catch (_) { /* private mode */ }
}
const TT_OVERRIDE_KEY = 'spp_tt_override';
function loadTTOverride(courseId, dateStr) {
  try { return (JSON.parse(localStorage.getItem(TT_OVERRIDE_KEY) || '{}'))[`${courseId}|${dateStr}`] || null; }
  catch (_) { return null; }
}
function saveTTOverride(courseId, dateStr, cfg) {
  try {
    const m = JSON.parse(localStorage.getItem(TT_OVERRIDE_KEY) || '{}');
    m[`${courseId}|${dateStr}`] = cfg;
    localStorage.setItem(TT_OVERRIDE_KEY, JSON.stringify(m));
  } catch (_) { /* private mode */ }
}
// Un-customize a date: it goes back to following the shared template. Used
// when a bulk apply deliberately overwrites the date the manager is looking
// at — leaving the override behind would keep flagging it as "customized"
// and make later bulk applies skip a day that now matches the template.
function clearTTOverride(courseId, dateStr) {
  try {
    const m = JSON.parse(localStorage.getItem(TT_OVERRIDE_KEY) || '{}');
    delete m[`${courseId}|${dateStr}`];
    localStorage.setItem(TT_OVERRIDE_KEY, JSON.stringify(m));
  } catch (_) { /* private mode */ }
}

Object.assign(window, {
  getDayNine, setDayNine,
  ghostEmulateSlot, ghostClearAll, ghostStateAt, useGhostGroups, modeForCount,
  ghostEmulateDemand, ghostClearDemand, useGhostDemand,
  getSlotNine, setSlotNine, GHOST_ROSTER,
  loadTTTemplate, saveTTTemplate, loadTTOverride, saveTTOverride, clearTTOverride,
});
