/* global React, sbx, useDemoMode, rngFor, pageAll, chunk */
// Operations data layer — bookings and tee-sheet coverage for the WHOLE
// network, not one course at a time.
//
// The old admin modules made you pick a course from a dropdown before they
// showed you anything, which answers "what is happening at Melreese" but
// never "what is happening tonight". These read every partner course in one
// pass — slots once, bookings once, profiles once — and bucket in memory,
// the same shape useNetwork already uses.

// Local calendar-day keys. `new Date('2026-08-19')` parses as UTC midnight,
// which renders as the previous day anywhere west of Greenwich — so a day
// key is built from local components, and read back at local noon where no
// DST shift can move it.
function dayKeyOf(d) {
  const p = n => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
}
const dayFromKey = (key) => new Date(`${key}T12:00:00`);

// A tee time's seat count. The column is `capacity` — checked against the
// live schema, not inferred from calling code. Null falls back to a
// foursome, which is what a Sandbox tee time is.
const SLOT_COLS = 'id, course_id, starts_at, capacity, price, type, title, status, includes_cart';
const seatsOf = (slot) => (slot.capacity != null ? Number(slot.capacity) : 4);

// Booking lifecycle, in the order it actually happens. `settled` marks the
// states that are over — they stop counting towards a fill rate, because a
// seat that was played is not a seat still on sale.
const BOOKING_STATES = {
  reserved:   { label: 'Reserved',   live: true },
  checked_in: { label: 'Checked in', live: true },
  playing:    { label: 'Playing',    live: true },
  completed:  { label: 'Completed',  live: false },
  cancelled:  { label: 'Cancelled',  live: false, released: true },
  no_show:    { label: 'No-show',    live: false, released: true },
};
const BOOKING_STATE_KEYS = Object.keys(BOOKING_STATES);

// A booking that no longer holds a seat. Cancellations and no-shows put the
// seat back, so counting them as filled would overstate every fill rate.
const holdsSeat = (b) => !(BOOKING_STATES[b.status] || {}).released;

const OPS_RANGES = [
  { key: 'today',  label: 'Today',    from: 0,  to: 1 },
  { key: 'next7',  label: 'Next 7',   from: 0,  to: 7 },
  { key: 'next30', label: 'Next 30',  from: 0,  to: 30 },
  { key: 'past7',  label: 'Past 7',   from: -7, to: 0 },
];

// Day offsets → an ISO instant pair covering whole local days, because a
// tee sheet is read in the course's day, not in UTC.
function windowFor(range) {
  const start = new Date(); start.setHours(0, 0, 0, 0);
  const from = new Date(start); from.setDate(from.getDate() + range.from);
  const to = new Date(start); to.setDate(to.getDate() + range.to);
  return { fromISO: from.toISOString(), toISO: to.toISOString() };
}

// ─── Reads ────────────────────────────────────────────────────────────
async function fetchSlotWindow(courseIds, fromISO, toISO) {
  let out = [];
  for (const group of chunk(courseIds, 40)) {
    // eslint-disable-next-line no-await-in-loop
    const page = await pageAll(() => sbx.from('tee_slots').select(SLOT_COLS)
      .in('course_id', group).gte('starts_at', fromISO).lt('starts_at', toISO));
    out = out.concat(page);
  }
  return out;
}

async function fetchBookingsFor(slotIds) {
  let out = [];
  for (const group of chunk(slotIds)) {
    // eslint-disable-next-line no-await-in-loop
    const page = await pageAll(() => sbx.from('bookings').select('*').in('slot_id', group));
    out = out.concat(page);
  }
  return out;
}

async function fetchProfilesByIds(ids) {
  const by = {};
  for (const group of chunk(ids, 200)) {
    // eslint-disable-next-line no-await-in-loop
    const { data } = await sbx.from('profiles')
      .select('id, handle, first_name, last_name, avatar_url, tier').in('id', group);
    (data || []).forEach(p => { by[p.id] = p; });
  }
  return by;
}

// One window of the network's operations, resolved into rows the screens
// can render without further lookups.
async function loadOpsWindow(courses, fromISO, toISO) {
  const ids = courses.map(c => c.id);
  const courseById = Object.fromEntries(courses.map(c => [c.id, c]));
  if (!ids.length) return { slots: [], bookings: [] };

  const slots = await fetchSlotWindow(ids, fromISO, toISO);
  const bookings = slots.length ? await fetchBookingsFor(slots.map(s => s.id)) : [];
  const people = await fetchProfilesByIds([...new Set([
    ...bookings.map(b => b.user_id),
    ...bookings.map(b => b.partner_id),
  ].filter(Boolean))]);

  const slotById = Object.fromEntries(slots.map(s => [s.id, s]));
  const rows = bookings.map(b => {
    const slot = slotById[b.slot_id] || null;
    return {
      ...b,
      slot,
      course: slot ? courseById[slot.course_id] || null : null,
      user: people[b.user_id] || null,
      partner: b.partner_id ? people[b.partner_id] || null : null,
      // What this booking is worth: what was charged if it was recorded,
      // otherwise the slot's list price.
      value: b.price_charged != null ? Number(b.price_charged) : (slot ? Number(slot.price) || 0 : 0),
    };
  }).filter(r => r.slot && r.course);

  return { slots: slots.map(s => ({ ...s, course: courseById[s.course_id] || null })), rows };
}

// ─── Demo window ──────────────────────────────────────────────────────
// Seeded per course + day, never Math.random(), so a refresh shows the same
// tee sheet and the numbers never twitch between renders.
const DEMO_FIRST = ['Marco', 'Dani', 'Rob', 'Yaya', 'Tomas', 'Ilse', 'Kev', 'Nina', 'Andre', 'Sofia', 'Luis', 'Bea'];
const DEMO_LAST = ['Ferrer', 'Quiles', 'Alonso', 'Baptiste', 'Reyes', 'Okafor', 'Lindqvist', 'Navarro', 'Bright', 'Cruz'];

function demoOpsWindow(courses, range) {
  const start = new Date(); start.setHours(0, 0, 0, 0);
  const slots = [];
  const rows = [];

  courses.forEach((course) => {
    for (let d = range.from; d < range.to; d += 1) {
      const day = new Date(start); day.setDate(day.getDate() + d);
      const dayStr = dayKeyOf(day);
      const r = rngFor(`${course.id}:${dayStr}`, 'ops');
      // A course publishes an afternoon block most days and skips some.
      const publishes = r() > 0.12;
      if (!publishes) continue;
      const count = 6 + Math.floor(r() * 7);
      const firstMin = 15 * 60 + Math.floor(r() * 4) * 15;
      for (let i = 0; i < count; i += 1) {
        const at = new Date(day);
        at.setMinutes(firstMin + i * 15);
        const price = Math.round((course.suggested_price || 24) * (0.85 + r() * 0.4));
        const slot = {
          id: `demo-${course.id}-${dayStr}-${i}`,
          course_id: course.id, course,
          starts_at: at.toISOString(),
          capacity: 4, price, status: 'open', type: 'open', demo: true,
        };
        slots.push(slot);

        const filled = Math.min(4, Math.floor(r() * 5));
        for (let k = 0; k < filled; k += 1) {
          const fn = DEMO_FIRST[Math.floor(r() * DEMO_FIRST.length)];
          const ln = DEMO_LAST[Math.floor(r() * DEMO_LAST.length)];
          const roll = r();
          const status = d < 0
            ? (roll > 0.14 ? 'completed' : (roll > 0.06 ? 'no_show' : 'cancelled'))
            : (roll > 0.08 ? 'reserved' : 'cancelled');
          rows.push({
            id: `${slot.id}-b${k}`,
            slot_id: slot.id, slot, course,
            user_id: `demo-u-${fn}${ln}`,
            user: { id: `demo-u-${fn}${ln}`, first_name: fn, last_name: ln, handle: (fn + ln).toLowerCase(), tier: roll > 0.6 ? 'plus' : 'free' },
            partner: null, partner_id: null,
            match_type: r() > 0.55 ? '2v2' : '1v1',
            status, price_charged: price, value: price,
            created_at: new Date(at.getTime() - 864e5 * (1 + Math.floor(r() * 5))).toISOString(),
            demo: true,
          });
        }
      }
    }
  });

  return { slots, rows };
}

// ─── useOpsFeed ───────────────────────────────────────────────────────
// Every booking across the network inside a date window.
//   null while loading, then { rows, slots, totals, demo }
function useOpsFeed(courses, rangeKey) {
  const demo = useDemoMode();
  const [state, setState] = React.useState(null);
  const [nonce, setNonce] = React.useState(0);
  const reload = React.useCallback(() => setNonce(n => n + 1), []);
  const range = OPS_RANGES.find(r => r.key === rangeKey) || OPS_RANGES[0];
  const ready = !!courses;
  const sig = ready ? courses.map(c => c.id).join(',') : '';

  React.useEffect(() => {
    if (!ready) return undefined;
    let live = true;
    setState(null);
    (async () => {
      let out;
      try {
        const { fromISO, toISO } = windowFor(range);
        out = demo
          ? demoOpsWindow(courses, range)
          : await loadOpsWindow(courses, fromISO, toISO);
      } catch (e) {
        if (live) setState({ rows: [], slots: [], totals: emptyTotals(), demo, error: e.message });
        return;
      }
      if (!live) return;
      setState({ ...out, totals: opsTotals(out.slots, out.rows), demo });
    })();
    return () => { live = false; };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ready, sig, rangeKey, demo, nonce]);

  return [state, reload];
}

const emptyTotals = () => ({ bookings: 0, seats: 0, filled: 0, fill: null, value: 0, cancelled: 0, players: 0, courses: 0 });

function opsTotals(slots, rows) {
  const seats = slots.reduce((n, s) => n + seatsOf(s), 0);
  const held = rows.filter(holdsSeat);
  return {
    bookings: rows.length,
    seats,
    filled: held.length,
    fill: seats ? Math.min(1, held.length / seats) : null,
    value: held.reduce((n, r) => n + r.value, 0),
    cancelled: rows.length - held.length,
    players: new Set(held.map(r => r.user_id).filter(Boolean)).size,
    courses: new Set(slots.map(s => s.course_id)).size,
  };
}

// ─── useCoverage ──────────────────────────────────────────────────────
// The tee sheet as a grid: every course down the side, the next N days
// across. A published day shows how full it is; an empty one is the thing
// you are actually looking for.
function useCoverage(courses, days = 14) {
  const [feed, reload] = useOpsFeed(courses, 'next30');

  const grid = React.useMemo(() => {
    if (!feed || !courses) return null;
    const dates = [];
    const start = new Date(); start.setHours(0, 0, 0, 0);
    for (let i = 0; i < days; i += 1) {
      const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
      dates.push(dayKeyOf(d));
    }

    const cells = {};
    courses.forEach(c => {
      cells[c.id] = {};
      dates.forEach(d => { cells[c.id][d] = { slots: 0, seats: 0, filled: 0, value: 0 }; });
    });

    const dayOf = (iso) => dayKeyOf(new Date(iso));

    feed.slots.forEach(s => {
      const cell = cells[s.course_id] && cells[s.course_id][dayOf(s.starts_at)];
      if (!cell) return;
      cell.slots += 1;
      cell.seats += seatsOf(s);
    });
    feed.rows.forEach(r => {
      if (!holdsSeat(r)) return;
      const cell = cells[r.course.id] && cells[r.course.id][dayOf(r.slot.starts_at)];
      if (!cell) return;
      cell.filled += 1;
      cell.value += r.value;
    });

    const gaps = [];
    courses.forEach(c => dates.forEach(d => {
      if (!cells[c.id][d].slots) gaps.push({ course: c, date: d });
    }));

    return { dates, cells, gaps, demo: feed.demo };
  }, [feed, courses, days]);

  return [grid, reload];
}

Object.assign(window, {
  OPS_RANGES, BOOKING_STATES, BOOKING_STATE_KEYS, holdsSeat, seatsOf,
  useOpsFeed, useCoverage, loadOpsWindow, opsTotals, dayKeyOf, dayFromKey, SLOT_COLS,
});
