/* global React, sbx */
// Courses + tee-slot availability + bookings (Phase B data layer).
//
// Powers the new golfer booking flow:
//   - Book tab: date-first availability list (near-you courses + open slots)
//   - Course detail: hero + Sandbox 9 + bookable slots
//   - Booking flow: reserve a slot for a 1v1/2v2 match
//   - My Rounds: a golfer's upcoming + past bookings
//
// Realtime on tee_slots + bookings so availability and "friends here"
// update live. Mirrors the conventions in events-data.jsx / social-data.jsx
// (unique channel name per hook instance, Object.assign(window, ...) export).

// ─── Geo helpers ──────────────────────────────────────────────────────
function haversineMiles(aLat, aLng, bLat, bLng) {
  if ([aLat, aLng, bLat, bLng].some(v => v == null)) return null;
  const toRad = d => (d * Math.PI) / 180;
  const R = 3958.8; // miles
  const dLat = toRad(bLat - aLat);
  const dLng = toRad(bLng - aLng);
  const lat1 = toRad(aLat), lat2 = toRad(bLat);
  const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(h));
}

// Browser geolocation with a graceful fallback to a fixed Miami center.
// Returns [coords, status]: status is 'pending' | 'ok' | 'denied' | 'unsupported'.
const MIAMI_FALLBACK = { lat: 25.7617, lng: -80.1918, fallback: true };
function useGeolocation() {
  const [coords, setCoords] = React.useState(null);
  const [status, setStatus] = React.useState('pending');

  React.useEffect(() => {
    if (!('geolocation' in navigator)) {
      setCoords(MIAMI_FALLBACK); setStatus('unsupported'); return;
    }
    let done = false;
    const ok = (pos) => {
      if (done) return; done = true;
      setCoords({ lat: pos.coords.latitude, lng: pos.coords.longitude, fallback: false });
      setStatus('ok');
    };
    const fail = () => {
      if (done) return; done = true;
      setCoords(MIAMI_FALLBACK); setStatus('denied');
    };
    navigator.geolocation.getCurrentPosition(ok, fail, { timeout: 8000, maximumAge: 600000 });
    // Hard fallback if the prompt is ignored.
    const t = setTimeout(fail, 9000);
    return () => clearTimeout(t);
  }, []);

  return [coords, status];
}

// ─── Date helpers ─────────────────────────────────────────────────────
// Local-day [start, end) ISO bounds for a Date.
function dayBounds(date) {
  const start = new Date(date); start.setHours(0, 0, 0, 0);
  const end = new Date(start); end.setDate(end.getDate() + 1);
  return [start.toISOString(), end.toISOString()];
}

// ─── Map a raw course row → camelCase course shape ────────────────────
// Real coordinates for a course: trust stored lat/lng only when real (the
// seeds ship 0/0 — "null island"); otherwise geocode the admin-entered
// address + city + state (Nominatim/OSM), cached per course on-device.
async function geocodeCourse(c) {
  const lat = Number(c.lat), lng = Number(c.lng);
  if (isFinite(lat) && isFinite(lng) && (Math.abs(lat) > 0.5 || Math.abs(lng) > 0.5)) return { lat, lng };
  const key = `spp_geocode_${c.id}`;
  try {
    const cc = JSON.parse(localStorage.getItem(key) || 'null');
    if (cc && isFinite(cc.lat) && isFinite(cc.lng)) return cc;
  } catch (_) { /* noop */ }
  const q = [c.address, c.city, c.state].filter(Boolean).join(', ');
  if (!q) return null;
  try {
    const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(q)}`);
    const j = await res.json();
    if (j && j[0]) {
      const cc = { lat: Number(j[0].lat), lng: Number(j[0].lon) };
      if (isFinite(cc.lat) && isFinite(cc.lng)) {
        try { localStorage.setItem(key, JSON.stringify(cc)); } catch (_) { /* noop */ }
        return cc;
      }
    }
  } catch (_) { /* geocoder unreachable */ }
  return null;
}

function mapCourse(r) {
  if (!r) return null;
  return {
    id: r.id,
    name: r.name,
    shortName: r.short_name,
    city: r.city,
    state: r.state,
    address: r.address,
    lat: r.lat,
    lng: r.lng,
    phone: r.phone,
    heroImg: r.hero_img,
    renderImg: r.render_img,
    description: r.description,
    holes: r.holes,
    par: r.par,
    realPar: r.real_par,
    realYardage: r.real_yardage,
    realRating: r.real_rating,
    realSlope: r.real_slope,
    realHoles: r.real_holes,
    status: r.status,
    suggestedPrice: r.suggested_price,
    sandboxTakePct: r.sandbox_take_pct,
  };
}

// ─── All active courses ───────────────────────────────────────────────
function useCourses() {
  const [courses, setCourses] = React.useState([]);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      const { data } = await sbx.from('courses')
        .select('*')
        .neq('status', 'inactive')
        .order('short_name');
      if (!cancelled) { setCourses((data || []).map(mapCourse)); setLoading(false); }
    })();
    return () => { cancelled = true; };
  }, []);

  return [courses, loading];
}

// ─── Availability for a given day: courses + their open slots ─────────
// Returns [list, loading], where each list item is:
//   { course, slots: [openSlot...], distanceMi }
// sorted by distance (nulls last). `slots` are future, open, type='open'.
function useAvailability(date, viewer) {
  const [list, setList] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const dayKey = date ? new Date(date).toDateString() : '';
  const vlat = viewer && viewer.lat, vlng = viewer && viewer.lng;

  const load = React.useCallback(async () => {
    setLoading(true);
    const [startISO, endISO] = dayBounds(date || new Date());
    const nowISO = new Date().toISOString();
    const lowerBound = startISO > nowISO ? startISO : nowISO; // hide past slots today

    const [{ data: courses }, { data: slots }] = await Promise.all([
      sbx.from('courses').select('*').eq('status', 'active').order('short_name'),
      sbx.from('tee_slots')
        .select('*')
        .eq('type', 'open')
        .eq('status', 'open')
        .gte('starts_at', lowerBound)
        .lt('starts_at', endISO)
        .order('starts_at'),
    ]);

    const slotsByCourse = {};
    (slots || []).forEach(s => { (slotsByCourse[s.course_id] = slotsByCourse[s.course_id] || []).push(s); });

    // Resolve REAL coordinates (geocoding the admin address when the stored
    // lat/lng is the 0/0 placeholder) so distances — and the closest-first
    // sort below — are true. Sequential on purpose: Nominatim rate limits,
    // and after the first visit every lookup is a local cache hit.
    const out = [];
    for (const c of (courses || [])) {
      const course = mapCourse(c);
      const coords = await geocodeCourse(c); // eslint-disable-line no-await-in-loop
      if (coords) { course.lat = coords.lat; course.lng = coords.lng; }
      const distanceMi = coords ? haversineMiles(vlat, vlng, coords.lat, coords.lng) : null;
      out.push({ course, slots: slotsByCourse[c.id] || [], distanceMi });
    }

    out.sort((a, b) => {
      if (a.distanceMi == null) return 1;
      if (b.distanceMi == null) return -1;
      return a.distanceMi - b.distanceMi;
    });

    setList(out);
    setLoading(false);
  }, [dayKey, vlat, vlng]); // eslint-disable-line react-hooks/exhaustive-deps

  React.useEffect(() => { load(); }, [load]);

  const channelName = React.useRef(`availability-${Math.random().toString(36).slice(2, 10)}`).current;
  React.useEffect(() => {
    const ch = sbx.channel(channelName)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'tee_slots' }, () => load())
      .on('postgres_changes', { event: '*', schema: 'public', table: 'bookings' }, () => load())
      .subscribe();
    return () => { sbx.removeChannel(ch); };
  }, [channelName, load]);

  return [list, loading];
}

// ─── Single course + its Sandbox 9 holes ──────────────────────────────
function useCourse(courseId) {
  const [course, setCourse] = React.useState(null);
  const [holes, setHoles] = React.useState([]);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let cancelled = false;
    if (!courseId) { setCourse(null); setHoles([]); setLoading(false); return; }
    (async () => {
      const [{ data: c }, { data: h }] = await Promise.all([
        sbx.from('courses').select('*').eq('id', courseId).maybeSingle(),
        sbx.from('course_holes').select('*').eq('course_id', courseId).order('hole_number'),
      ]);
      if (!cancelled) { setCourse(mapCourse(c)); setHoles(h || []); setLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [courseId]);

  return [course, holes, loading];
}

// ─── Open slots for one course on a given day (course detail) ─────────
function useCourseSlots(courseId, date) {
  const [slots, setSlots] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const dayKey = date ? new Date(date).toDateString() : '';

  const load = React.useCallback(async () => {
    if (!courseId) { setSlots([]); setLoading(false); return; }
    const [startISO, endISO] = dayBounds(date || new Date());
    const nowISO = new Date().toISOString();
    const lowerBound = startISO > nowISO ? startISO : nowISO;
    const { data } = await sbx.from('tee_slots')
      .select('*')
      .eq('course_id', courseId)
      .gte('starts_at', lowerBound)
      .lt('starts_at', endISO)
      .order('starts_at');
    setSlots(data || []);
    setLoading(false);
  }, [courseId, dayKey]); // eslint-disable-line react-hooks/exhaustive-deps

  React.useEffect(() => { load(); }, [load]);

  const channelName = React.useRef(`course-slots-${Math.random().toString(36).slice(2, 10)}`).current;
  React.useEffect(() => {
    if (!courseId) return;
    const ch = sbx.channel(channelName)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'tee_slots', filter: `course_id=eq.${courseId}` }, () => load())
      .on('postgres_changes', { event: '*', schema: 'public', table: 'bookings' }, () => load())
      .subscribe();
    return () => { sbx.removeChannel(ch); };
  }, [courseId, channelName, load]);

  return [slots, loading];
}

// ─── Friends booked, keyed by slot_id (for "friends here" on slots) ───
// Given a viewer + a list of slot ids, returns { slot_id: [friendProfile] }.
function useFriendsOnSlots(viewerId, slotIds) {
  const [bySlot, setBySlot] = React.useState({});
  const slotKey = (slotIds || []).slice().sort().join(',');

  const load = React.useCallback(async () => {
    if (!viewerId || !slotIds || slotIds.length === 0) { setBySlot({}); return; }
    const { data: follows } = await sbx.from('follows').select('following_id').eq('follower_id', viewerId);
    const friendIds = (follows || []).map(r => r.following_id);
    if (friendIds.length === 0) { setBySlot({}); return; }
    const { data: bks } = await sbx.from('bookings')
      .select(`slot_id, user_id, actor:profiles!bookings_user_id_fkey(id, handle, first_name, last_name, avatar_url)`)
      .in('slot_id', slotIds)
      .in('user_id', friendIds);
    const out = {};
    (bks || []).forEach(b => { if (b.actor) (out[b.slot_id] = out[b.slot_id] || []).push(b.actor); });
    setBySlot(out);
  }, [viewerId, slotKey]); // eslint-disable-line react-hooks/exhaustive-deps

  React.useEffect(() => { load(); }, [load]);
  return bySlot;
}

// ─── My bookings (upcoming + past) ────────────────────────────────────
function useMyBookings(userId) {
  const [upcoming, setUpcoming] = React.useState([]);
  const [past, setPast] = React.useState([]);
  const [loading, setLoading] = React.useState(true);

  const load = React.useCallback(async () => {
    if (!userId) { setUpcoming([]); setPast([]); setLoading(false); return; }
    const { data } = await sbx.from('bookings')
      .select(`
        *,
        partner:profiles!bookings_partner_id_fkey(id, handle, first_name, last_name, avatar_url),
        slot:tee_slots!bookings_slot_id_fkey(
          id, starts_at, price, type, title,
          course:courses!tee_slots_course_id_fkey(id, short_name, name, city, hero_img, render_img)
        )
      `)
      .eq('user_id', userId)
      .order('created_at', { ascending: false });

    const now = Date.now();
    const up = [], pa = [];
    (data || []).forEach(b => {
      const t = b.slot && b.slot.starts_at ? new Date(b.slot.starts_at).getTime() : 0;
      if (b.status === 'cancelled') return;
      if (t >= now && b.status !== 'completed') up.push(b); else pa.push(b);
    });
    up.sort((a, b) => new Date(a.slot.starts_at) - new Date(b.slot.starts_at)); // soonest first
    setUpcoming(up); setPast(pa); setLoading(false);
  }, [userId]);

  React.useEffect(() => { load(); }, [load]);

  const channelName = React.useRef(`my-bookings-${Math.random().toString(36).slice(2, 10)}`).current;
  React.useEffect(() => {
    if (!userId) return;
    const ch = sbx.channel(channelName)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'bookings', filter: `user_id=eq.${userId}` }, () => load())
      .subscribe();
    return () => { sbx.removeChannel(ch); };
  }, [userId, channelName, load]);

  return [upcoming, past, loading, load];
}

// ─── Matchup: the formed foursome (or 1v1) for a match ───────────────
// Returns [{ match, teamA:[profiles], teamB:[profiles] }, loading].
function useMatchup(matchId) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  const load = React.useCallback(async () => {
    if (!matchId) { setData(null); setLoading(false); return; }
    const { data: m } = await sbx.from('matches').select('*').eq('id', matchId).maybeSingle();
    if (!m) { setData(null); setLoading(false); return; }
    const ids = [m.player_a, m.player_a2, m.player_b, m.player_b2].filter(Boolean);
    const { data: profs } = await sbx.from('profiles')
      .select('id, handle, first_name, last_name, avatar_url, sbx')
      .in('id', ids);
    const byId = {}; (profs || []).forEach(p => { byId[p.id] = p; });
    const teamA = [m.player_a, m.player_a2].filter(Boolean).map(id => byId[id]).filter(Boolean);
    const teamB = [m.player_b, m.player_b2].filter(Boolean).map(id => byId[id]).filter(Boolean);
    // Per-player check-in state + the booked tee time (for the countdown + gate).
    const { data: bks } = await sbx.from('bookings')
      .select('user_id, status, tee_slots(starts_at)')
      .eq('match_id', matchId);
    const checkins = {}; let teeISO = null;
    (bks || []).forEach(b => {
      checkins[b.user_id] = b.status;
      if (b.tee_slots && b.tee_slots.starts_at) teeISO = b.tee_slots.starts_at;
    });
    setData({ match: m, teamA, teamB, checkins, teeISO });
    setLoading(false);
  }, [matchId]);

  React.useEffect(() => { load(); }, [load]);
  React.useEffect(() => {
    if (!matchId) return undefined;
    // matches is realtime-published; bookings isn't, so a 15s poll catches
    // teammates checking in. The local user reloads immediately on their own tap.
    const ch = sbx.channel(`matchup-${matchId.slice(0, 8)}`)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'matches', filter: `id=eq.${matchId}` }, () => load())
      .subscribe();
    const iv = setInterval(load, 15000);
    return () => { sbx.removeChannel(ch); clearInterval(iv); };
  }, [matchId, load]);
  return [data, loading, load];
}

// ─── Match detail: match + holes + players (for the summary screen) ───
function useMatchDetail(matchId) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  const load = React.useCallback(async () => {
    if (!matchId) { setData(null); setLoading(false); return; }
    const { data: m } = await sbx.from('matches').select('*').eq('id', matchId).maybeSingle();
    if (!m) { setData(null); setLoading(false); return; }
    const [{ data: holes }, { data: profs }, { data: pstats }] = await Promise.all([
      sbx.from('match_holes').select('*').eq('match_id', matchId).order('hole_number'),
      sbx.from('profiles').select('id, handle, first_name, last_name, avatar_url, sbx')
        .in('id', [m.player_a, m.player_a2, m.player_b, m.player_b2].filter(Boolean)),
      sbx.from('hole_player_stats').select('*').eq('match_id', matchId),
    ]);
    const byId = {}; (profs || []).forEach(p => { byId[p.id] = p; });
    // Per-player stats keyed by hole_number → [{player_id, fairway, gir, zone}]
    const playerStatsByHole = {};
    (pstats || []).forEach(s => { (playerStatsByHole[s.hole_number] = playerStatsByHole[s.hole_number] || []).push(s); });
    setData({
      match: m, holes: holes || [], byId, playerStatsByHole,
      teamA: [m.player_a, m.player_a2].filter(Boolean).map(i => byId[i]).filter(Boolean),
      teamB: [m.player_b, m.player_b2].filter(Boolean).map(i => byId[i]).filter(Boolean),
    });
    setLoading(false);
  }, [matchId]);

  React.useEffect(() => { load(); }, [load]);

  const channelName = React.useRef(`match-detail-${Math.random().toString(36).slice(2, 10)}`).current;
  React.useEffect(() => {
    if (!matchId) return;
    const ch = sbx.channel(channelName)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'matches', filter: `id=eq.${matchId}` }, () => load())
      .subscribe();
    return () => { sbx.removeChannel(ch); };
  }, [matchId, channelName, load]);

  return [data, loading];
}

async function confirmMatchResult(matchId) {
  if (!matchId) return;
  const { error } = await sbx.rpc('confirm_match_result', { p_match: matchId });
  if (error) throw new Error(error.message || 'Could not confirm.');
}

// ─── Mutations ────────────────────────────────────────────────────────
// Reserve a slot. Snapshots the slot price (reserve-only; no charge yet).
// Throws a friendly Error on the common failures.
async function createBooking({ slotId, userId, partnerId, matchType, price, needsPartner }) {
  if (!slotId || !userId) throw new Error('Missing slot or user.');
  const { data, error } = await sbx.from('bookings').insert({
    slot_id: slotId,
    user_id: userId,
    partner_id: partnerId || null,
    match_type: matchType === '2v2' ? '2v2' : '1v1',
    needs_partner: !!needsPartner,
    status: 'reserved',
    price_charged: price != null ? price : null,
  }).select('id').maybeSingle();
  if (error) {
    if (error.code === '23505') throw new Error("You've already booked this slot.");
    throw new Error(error.message || 'Could not complete booking.');
  }
  return data && data.id;
}

async function cancelBooking({ bookingId }) {
  if (!bookingId) return;
  const { error } = await sbx.from('bookings').delete().eq('id', bookingId);
  if (error) throw new Error(error.message || 'Could not cancel.');
}

// Check in at the course: seed the Sandbox 9 holes (if needed) and flip
// the match to active. Then the caller routes into the live scorecard.
async function startBookedMatch(matchId) {
  if (!matchId) return;
  const { error } = await sbx.rpc('start_booked_match', { p_match: matchId });
  if (error) throw new Error(error.message || 'Could not start the match.');
}

// Per-player check-in (opens 5 min before tee time). Flips the caller's booking
// to 'checked_in'; the match can start once everyone's in (or the tee passes).
async function checkInBooking(matchId) {
  if (!matchId) return;
  const { error } = await sbx.rpc('check_in_booking', { p_match: matchId });
  if (error) throw new Error(error.message || 'Could not check you in.');
}

// ─── Tee-time window waitlist ─────────────────────────────────────────
// One availability window per user/course/date ("I can tee off 4–7pm").
// Falls back to localStorage if the tee_waitlist table isn't migrated yet,
// so the flow still works on preview before the SQL runs.
const WL_LS = 'spp_tee_waitlist';
const wlKey = (u, c, d) => `${u}|${c}|${d}`;
function wlLocalGet(k) {
  try { return JSON.parse(localStorage.getItem(WL_LS) || '{}')[k] || null; } catch (_) { return null; }
}
function wlLocalSet(k, v) {
  try {
    const m = JSON.parse(localStorage.getItem(WL_LS) || '{}');
    if (v) m[k] = v; else delete m[k];
    localStorage.setItem(WL_LS, JSON.stringify(m));
  } catch (_) { /* private mode */ }
}

// undefined = loading · null = no window yet · { startMin, endMin }
function useTeeWaitlist(courseId, dateStr, userId) {
  const [row, setRow] = React.useState(undefined);
  const [rev, setRev] = React.useState(0);
  React.useEffect(() => {
    if (!courseId || !dateStr || !userId) { setRow(null); return undefined; }
    let on = true;
    (async () => {
      const { data, error } = await sbx.from('tee_waitlist').select('*')
        .eq('user_id', userId).eq('course_id', courseId).eq('play_date', dateStr).maybeSingle();
      if (!on) return;
      if (error) { setRow(wlLocalGet(wlKey(userId, courseId, dateStr))); return; }
      setRow(data ? { startMin: data.start_min, endMin: data.end_min, createdAt: data.created_at, quiet: !!data.quiet } : (wlLocalGet(wlKey(userId, courseId, dateStr)) || null));
    })();
    return () => { on = false; };
  }, [courseId, dateStr, userId, rev]);
  const refresh = React.useCallback(() => setRev(r => r + 1), []);
  return [row, refresh];
}

async function saveTeeWaitlist({ userId, courseId, dateStr, startMin, endMin, quiet }) {
  const base = {
    user_id: userId, course_id: courseId, play_date: dateStr,
    start_min: startMin, end_min: endMin, updated_at: new Date().toISOString(),
  };
  let { error } = await sbx.from('tee_waitlist')
    .upsert({ ...base, quiet: !!quiet }, { onConflict: 'user_id,course_id,play_date' });
  if (error) {
    // hub.sql (quiet column) not applied yet → retry without it.
    ({ error } = await sbx.from('tee_waitlist').upsert(base, { onConflict: 'user_id,course_id,play_date' }));
  }
  // Table not migrated yet (or offline) → keep the window on-device so the
  // user still sees their booked window when they reopen the card.
  if (error) wlLocalSet(wlKey(userId, courseId, dateStr), { startMin, endMin });
  else wlLocalSet(wlKey(userId, courseId, dateStr), null);
}

async function deleteTeeWaitlist({ userId, courseId, dateStr }) {
  try {
    await sbx.from('tee_waitlist').delete()
      .eq('user_id', userId).eq('course_id', courseId).eq('play_date', dateStr);
  } catch (_) { /* table may not exist yet */ }
  wlLocalSet(wlKey(userId, courseId, dateStr), null);
}

// All of MY upcoming waitlist windows across courses (for My Rounds).
// Merges backend rows with any on-device fallback entries; course names
// resolved in one query. → null = loading, else [{ courseId, courseName,
// dateStr, startMin, endMin }] soonest first.
function useMyWaitlist(userId) {
  const [rows, setRows] = React.useState(null);
  const [rev, setRev] = React.useState(0);
  React.useEffect(() => {
    if (!userId) { setRows([]); return undefined; }
    let on = true;
    (async () => {
      const p = (n) => String(n).padStart(2, '0');
      const d = new Date();
      const today = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;

      const byKey = {};
      const { data } = await sbx.from('tee_waitlist')
        .select('course_id, play_date, start_min, end_min, created_at')
        .eq('user_id', userId).gte('play_date', today).order('play_date');
      (data || []).forEach(r => {
        byKey[`${r.course_id}|${r.play_date}`] = {
          courseId: r.course_id, dateStr: r.play_date, startMin: r.start_min, endMin: r.end_min,
          createdAt: r.created_at,
        };
      });
      // On-device entries (pre-migration saves) not yet in the backend.
      try {
        const m = JSON.parse(localStorage.getItem(WL_LS) || '{}');
        for (const k in m) {
          const [u, c, ds] = k.split('|');
          if (u !== userId || ds < today || !m[k]) continue;
          const kk = `${c}|${ds}`;
          if (!byKey[kk]) byKey[kk] = { courseId: c, dateStr: ds, startMin: m[k].startMin, endMin: m[k].endMin };
        }
      } catch (_) { /* noop */ }

      const list = Object.values(byKey).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
      const cids = [...new Set(list.map(x => x.courseId))];
      if (cids.length) {
        const { data: cs } = await sbx.from('courses').select('id, short_name').in('id', cids);
        const nameOf = {}; (cs || []).forEach(c => { nameOf[c.id] = c.short_name; });
        list.forEach(x => { x.courseName = nameOf[x.courseId] || 'Course'; });
      }
      if (on) setRows(list);
    })();
    return () => { on = false; };
  }, [userId, rev]);
  const refresh = React.useCallback(() => setRev(r => r + 1), []);
  return [rows, refresh];
}

// ─── My tee assignments (waitlist → booked by the course) ────────────
// The manager board writes one row per real golfer when a foursome locks.
// This hook watches them: it also SELF-INSERTS a notification whenever an
// assignment appears or changes status (booked → "You're in!", rescheduled,
// cut), deduped in localStorage so each state change notifies once. Rows
// deleted on the manager side (demo wipe) just disappear — no notification.
const TA_SEEN = 'spp_seen_assignments';
function useMyTeeAssignments(userId) {
  // STAGE 1 of retiring tee_assignments: "You're in" rounds now derive from
  // BOOKINGS + MATCHES (single source of truth). Cuts still read tee_assignments
  // (a cut golfer has no booking) until they move to notifications. Output shape
  // is unchanged, so home.jsx is untouched.
  const [rows, setRows] = React.useState(null);
  const load = React.useCallback(async () => {
    if (!userId) { setRows([]); return; }
    const since = new Date(Date.now() - 6 * 3600e3).toISOString();

    // Seated rounds = my bookings with a formed match, not cancelled/completed,
    // on a recent/upcoming slot.
    const { data: myBks, error } = await sbx.from('bookings')
      .select('id, slot_id, match_id, status, slot:tee_slots!bookings_slot_id_fkey(starts_at, course_id, course:courses!tee_slots_course_id_fkey(short_name, city, state))')
      .eq('user_id', userId)
      .not('match_id', 'is', null)
      .not('status', 'in', '(cancelled,completed)');
    if (error) console.warn('[useMyTeeAssignments] bookings query failed:', error);
    const mine = (myBks || []).filter(b => b.slot && b.slot.starts_at >= since);

    let out = [];
    if (mine.length) {
      const slotIds = [...new Set(mine.map(b => b.slot_id))];
      const matchIds = [...new Set(mine.map(b => b.match_id))];
      const [{ data: coBks }, { data: mm }] = await Promise.all([
        sbx.from('bookings')
          .select('slot_id, user:profiles!bookings_user_id_fkey(id, first_name, last_name, handle, avatar_url, sbx)')
          .in('slot_id', slotIds).neq('status', 'cancelled'),
        sbx.from('matches').select('id, status').in('id', matchIds),
      ]);
      const mStatus = {}; (mm || []).forEach(m => { mStatus[m.id] = m.status; });
      const groupBySlot = {};
      (coBks || []).forEach(bk => {
        const p = bk.user; if (!p) return;
        const fa = (p.first_name || '').trim(), la = (p.last_name || '').trim();
        (groupBySlot[bk.slot_id] = groupBySlot[bk.slot_id] || []).push({
          id: p.id,
          name: [p.first_name, p.last_name].filter(Boolean).join(' ') || (p.handle ? '@' + String(p.handle).replace(/^@/, '') : 'Player'),
          initials: (fa || la) ? ((fa[0] || '') + (la[0] || '')).toUpperCase() : ((p.handle || '?').replace(/^@/, '')[0] || '?').toUpperCase(),
          avatar_url: p.avatar_url || null,
          sbx: p.sbx != null ? Number(p.sbx) : null,
          handle: p.handle, member: true,
        });
      });
      out = mine.map(bk => {
        const c = (bk.slot && bk.slot.course) || {};
        const st = mStatus[bk.match_id];
        return {
          id: bk.id, slotId: bk.slot_id, courseId: bk.slot && bk.slot.course_id, dateStr: null,
          teeISO: bk.slot.starts_at, status: 'booked',
          group: (groupBySlot[bk.slot_id] || []).slice(0, 4),
          courseName: c.short_name || 'your course',
          courseCity: [c.city, c.state].filter(Boolean).join(', '),
          matchId: bk.match_id, matchDone: st === 'completed', matchActive: st === 'active',
        };
      });
    }

    // Cuts (no booking exists) — still from tee_assignments for now.
    const { data: cuts } = await sbx.from('tee_assignments')
      .select('id, slot_id, course_id, tee_time, status, course:courses(short_name, city, state)')
      .eq('user_id', userId).eq('status', 'cut').gte('tee_time', since);
    (cuts || []).forEach(r => out.push({
      id: r.id, slotId: r.slot_id, courseId: r.course_id, dateStr: null,
      teeISO: r.tee_time, status: 'cut', group: [],
      courseName: (r.course && r.course.short_name) || 'your course',
      courseCity: r.course ? [r.course.city, r.course.state].filter(Boolean).join(', ') : '',
      matchId: null, matchDone: false, matchActive: false,
    }));

    out.sort((x, y) => new Date(x.teeISO) - new Date(y.teeISO));

    // One in-app notification per new round/cut.
    try {
      const seen = JSON.parse(localStorage.getItem(TA_SEEN) || '{}');
      let dirty = false;
      for (const a2 of out) {
        if (seen[a2.id] === a2.status) continue;
        const time = new Date(a2.teeISO).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
        const day = new Date(a2.teeISO).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
        const msg = a2.status === 'cut'
          ? { title: "Sorry — you didn't make the cut", body: `We couldn't seat you at ${a2.courseName} on ${day}. Your window stays on the waitlist for next time.` }
          : { title: "You're in! ⛳", body: `Your ${time} tee time at ${a2.courseName} on ${day} is booked — check Home to meet your group.` };
        await sbx.from('notifications').insert({ // eslint-disable-line no-await-in-loop
          user_id: userId, type: 'tee_assignment', title: msg.title, body: msg.body,
          data: { ref_id: a2.id, status: a2.status, course_id: a2.courseId },
        });
        seen[a2.id] = a2.status; dirty = true;
      }
      if (dirty) localStorage.setItem(TA_SEEN, JSON.stringify(seen));
    } catch (_) { /* notifications are best-effort */ }

    setRows(out);
  }, [userId]);

  React.useEffect(() => { load(); }, [load]);
  React.useEffect(() => {
    if (!userId) return undefined;
    const ch = sbx.channel(`tee-assign-${userId.slice(0, 8)}`)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'matches' }, () => load())
      .on('postgres_changes', { event: '*', schema: 'public', table: 'tee_assignments', filter: `user_id=eq.${userId}` }, () => load())
      .subscribe();
    const iv = setInterval(load, 30000);
    return () => { sbx.removeChannel(ch); clearInterval(iv); };
  }, [userId, load]);

  return [rows, load];
}

// Dismiss (delete) my own assignment row — used on the "didn't make the
// cut" card once the golfer has seen it.
async function dismissTeeAssignment(id) {
  try { await sbx.from('tee_assignments').delete().eq('id', id); } catch (_) { /* noop */ }
}

Object.assign(window, {
  haversineMiles, useGeolocation,
  useCourses, useAvailability, useCourse, useCourseSlots,
  useFriendsOnSlots, useMyBookings, useMatchup, useMatchDetail,
  createBooking, cancelBooking, startBookedMatch, checkInBooking, confirmMatchResult,
  geocodeCourse,
  useTeeWaitlist, saveTeeWaitlist, deleteTeeWaitlist, useMyWaitlist,
  useMyTeeAssignments, dismissTeeAssignment,
});
