// src/features/admin/ui/admin-schedule-live.jsx
//
// Live participant counter + class-status copy + admin Join button for the
// admin Schedule tab. Exposes:
//
//   window.AdminScheduleLive.useLivePresence(bookings)
//     React hook. Polls /api/meetings/presence every 15s while at least one
//     booking in the input list is inside its "live window" (start - 15min
//     through end + 60min). Returns a { [bookingId]: { count, participants } }
//     map plus a fetchedAt timestamp.
//
//   window.AdminScheduleLive.RowStatus({ booking, presence })
//     One-row status block: class status line + participant chips + Join.
//     Rendered inside each admin-schedule.jsx booking row.

(function () {
  'use strict';

  window.AdminScheduleLive = window.AdminScheduleLive || {};

  const POLL_MS = 15_000;
  const PRE_WINDOW_MS  = 15 * 60_000;   // start surfacing status 15 min before scheduled start
  const POST_WINDOW_MS = 60 * 60_000;   // keep surfacing for 60 min after scheduled end

  function endMs(booking) {
    const start = new Date(booking.scheduledAt).getTime();
    const dur   = Number(booking.durationMinutes) || 60;
    return start + dur * 60_000;
  }
  function startMs(booking) {
    return new Date(booking.scheduledAt).getTime();
  }
  function isInLiveWindow(booking, now) {
    if (!booking || !booking.scheduledAt) return false;
    if (booking.status === 'cancelled') return false;
    const s = startMs(booking);
    const e = endMs(booking);
    if (!Number.isFinite(s) || !Number.isFinite(e)) return false;
    return now >= s - PRE_WINDOW_MS && now <= e + POST_WINDOW_MS;
  }

  function fmtMin(ms) {
    const m = Math.max(0, Math.round(ms / 60000));
    if (m < 1) return 'less than a minute';
    if (m === 1) return '1 min';
    return m + ' min';
  }

  // POST /api/meetings/presence with the live-window booking ids. Returns the
  // server's { presence, fetchedAt } payload, or { presence: {} } on auth /
  // network failure (the UI degrades gracefully).
  async function fetchPresence(bookingIds) {
    if (!bookingIds.length) return { presence: {}, fetchedAt: Date.now() };
    let sess;
    try { sess = await window.supa.auth.getSession(); } catch (_) { sess = null; }
    const jwt = sess?.data?.session?.access_token;
    if (!jwt) return { presence: {}, fetchedAt: Date.now(), error: 'no_session' };
    let r;
    try {
      r = await fetch('/api/meetings/presence', {
        method:  'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
        body:    JSON.stringify({ bookingIds }),
      });
    } catch (err) {
      return { presence: {}, fetchedAt: Date.now(), error: 'network' };
    }
    if (!r.ok) return { presence: {}, fetchedAt: Date.now(), error: 'http_' + r.status };
    const j = await r.json().catch(() => ({}));
    return { presence: j.presence || {}, fetchedAt: j.fetchedAt || Date.now(), note: j.note };
  }

  // Coarse 30s ticker that drives the live-window MEMBERSHIP check at the
  // parent level. Membership only changes at minute granularity so 30s is
  // ample, and it avoids re-running AdminSchedule's filter/sort/group every
  // second. The fine-grained relative-time copy ticks inside each RowStatus.
  const PARENT_TICK_MS = 30_000;

  function useLivePresence(bookings) {
    const [tick, setTick]             = React.useState(0);
    const [presence, setPresence]     = React.useState({});
    const [fetchedAt, setFetchedAt]   = React.useState(null);

    React.useEffect(() => {
      const t = setInterval(() => setTick((n) => n + 1), PARENT_TICK_MS);
      return () => clearInterval(t);
    }, []);

    // Stable id list for the active window. Recomputed when the booking list
    // changes or when the slow tick advances. Sorted + joined so the useEffect
    // dep only changes when the SET of ids changes, not its order.
    const liveIdsKey = React.useMemo(() => {
      const now = Date.now();
      const ids = (bookings || [])
        .filter((b) => isInLiveWindow(b, now))
        .map((b) => b.id)
        .filter(Boolean);
      ids.sort();
      return ids.join(',');
    }, [bookings, tick]);

    // Poll presence every POLL_MS while there's at least one live booking.
    // Aborts the in-flight request on unmount or when the id set changes.
    React.useEffect(() => {
      const ids = liveIdsKey ? liveIdsKey.split(',') : [];
      if (ids.length === 0) {
        setPresence({});
        return;
      }
      let cancelled = false;
      let timer = null;
      const run = async () => {
        const { presence: p, fetchedAt: f } = await fetchPresence(ids);
        if (cancelled) return;
        setPresence(p);
        setFetchedAt(f);
        timer = setTimeout(run, POLL_MS);
      };
      run();
      return () => {
        cancelled = true;
        if (timer) clearTimeout(timer);
      };
    }, [liveIdsKey]);

    return { presence, fetchedAt };
  }

  // Open the Daily room for this booking with the admin's display name baked
  // into the token. /api/meetings/token already grants is_owner=true when the
  // caller is in admin_users. Popup-blocker safe via synchronous about:blank.
  async function adminJoin(booking) {
    const popup = window.open('about:blank', '_blank');
    if (!popup) {
      alert('Please allow popups for this site to join the meeting.');
      return;
    }
    // Land on the in-house lesson room at /?page=lesson&bookingId=<id> so
    // admin joins through the same code path as student + teacher (which
    // also goes there from join-button.jsx). The lesson room hosts the
    // daily-js callObject + admin-notes banner overlay. Admin sees no
    // banner (banner is gated on viewer = booking's instructor) but
    // shares the same page, which keeps observability consistent and
    // avoids two parallel join paths drifting.
    if (booking?.id) {
      try { popup.location.href = '/?page=lesson&bookingId=' + encodeURIComponent(booking.id); }
      catch (err) {
        try { popup.close(); } catch (_) {}
        alert('Could not open the meeting.');
      }
      return;
    }
    // No booking id — degraded path, navigate directly to Daily.
    try {
      const sess = await window.supa.auth.getSession();
      const jwt  = sess?.data?.session?.access_token;
      if (!jwt) throw new Error('Not signed in.');
      const r = await fetch('/api/meetings/token', {
        method:  'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
        body:    JSON.stringify({ bookingId: booking.id }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.ok) throw new Error(j.error || ('Token request failed (' + r.status + ')'));
      const url = j.url || j.fallbackUrl || booking.meetingLink || booking.meeting_url;
      if (!url) throw new Error('No meeting URL for this booking.');
      popup.location.href = url;
    } catch (err) {
      try { popup.close(); } catch (_) {}
      alert(err.message || 'Could not open the meeting.');
    }
  }

  function statusCopy(booking, count, now) {
    const s = startMs(booking);
    const e = endMs(booking);
    const isBefore = now < s;
    const isAfter  = now > e;
    if (isBefore) {
      return { kind: 'pre', text: 'Starts in ' + fmtMin(s - now) };
    }
    if (isAfter) {
      if (count >= 1) return { kind: 'overtime', text: 'In overtime · ' + fmtMin(now - e) + ' past end' };
      return { kind: 'ended', text: 'Ended ' + fmtMin(now - e) + ' ago' };
    }
    // In the scheduled window.
    const sinceStart = fmtMin(now - s);
    if (count === 0) return { kind: 'late', text: sinceStart + ' in · waiting for join' };
    if (count === 1) return { kind: 'one',  text: '1 waiting · started ' + sinceStart + ' ago' };
    return { kind: 'live', text: 'Live · started ' + sinceStart + ' ago' };
  }

  function ParticipantChip({ p, now }) {
    const joinedMs = p.joinTime ? p.joinTime * 1000 : null;
    const sinceJoin = joinedMs ? fmtMin(now - joinedMs) : null;
    return (
      <span style={{
        display:'inline-flex', alignItems:'center', gap:6,
        background:'#fff',
        border:'1px solid oklch(86% 0.05 150)',
        color:'oklch(28% 0.13 150)',
        borderRadius:14, padding:'3px 9px',
        fontSize:11, fontWeight:600,
        fontFamily:"'Plus Jakarta Sans', sans-serif",
      }}>
        <span style={{
          width:6, height:6, borderRadius:'50%',
          background:'oklch(58% 0.16 150)',
        }} />
        {p.userName || 'Guest'}
        {sinceJoin && <span style={{ color:'oklch(50% 0.05 150)', fontWeight:500 }}>· {sinceJoin}</span>}
      </span>
    );
  }

  // 30s tick on each RowStatus drives the per-row relative-time copy
  // ("Starts in 3 min", "Live · started 6 min ago"). Confined to the leaf
  // so the parent AdminSchedule does NOT re-render with it.
  const ROW_TICK_MS = 30_000;

  function RowStatus({ booking, presence }) {
    const [now, setNow] = React.useState(() => Date.now());
    React.useEffect(() => {
      const t = setInterval(() => setNow(Date.now()), ROW_TICK_MS);
      return () => clearInterval(t);
    }, []);

    if (!booking) return null;
    if (!isInLiveWindow(booking, now)) return null;

    const count = presence?.count || 0;
    const participants = presence?.participants || [];
    const status = statusCopy(booking, count, now);

    const tone = (() => {
      switch (status.kind) {
        case 'pre':      return { bg: 'oklch(96% 0.02 265)', border: 'oklch(88% 0.02 265)', fg: 'oklch(30% 0.08 265)' };
        case 'late':     return { bg: 'oklch(96% 0.07 80)',  border: 'oklch(86% 0.09 75)',  fg: 'oklch(34% 0.13 75)' };
        case 'one':      return { bg: 'oklch(96% 0.07 80)',  border: 'oklch(86% 0.09 75)',  fg: 'oklch(34% 0.13 75)' };
        case 'live':     return { bg: 'oklch(95% 0.06 150)', border: 'oklch(82% 0.08 150)', fg: 'oklch(28% 0.13 150)' };
        case 'overtime': return { bg: 'oklch(95% 0.06 150)', border: 'oklch(82% 0.08 150)', fg: 'oklch(28% 0.13 150)' };
        case 'ended':    return { bg: 'oklch(96% 0.01 265)', border: 'oklch(91% 0.01 265)', fg: 'oklch(46% 0.04 265)' };
        default:         return { bg: 'oklch(96% 0.01 265)', border: 'oklch(91% 0.01 265)', fg: 'oklch(46% 0.04 265)' };
      }
    })();

    const url = booking.meetingLink || booking.meeting_url || '';
    const joinDisabled = !url;

    return (
      <div style={{
        marginTop: 6,
        background: tone.bg,
        border: '1px solid ' + tone.border,
        borderRadius: 7,
        padding: '7px 10px',
        display: 'flex',
        alignItems: 'center',
        gap: 9,
        flexWrap: 'wrap',
        fontFamily: "'Plus Jakarta Sans', sans-serif",
      }}>
        <span style={{
          display: 'inline-flex', alignItems: 'center', gap: 6,
          fontSize: 11, fontWeight: 700, color: tone.fg,
        }}>
          {(status.kind === 'live' || status.kind === 'overtime') && (
            <span style={{
              width: 7, height: 7, borderRadius: '50%',
              background: 'oklch(58% 0.16 150)',
              boxShadow: '0 0 0 3px oklch(58% 0.16 150 / 0.25)',
            }} />
          )}
          {status.text}
        </span>
        <span style={{
          fontSize: 11, fontWeight: 700, color: tone.fg,
          background:'#fff', border:'1px solid ' + tone.border,
          borderRadius:14, padding:'3px 9px',
        }}>
          {count} {count === 1 ? 'participant' : 'participants'}
        </span>
        {participants.slice(0, 3).map((p, i) => (
          <ParticipantChip
            key={p.sessionId || `${p.userName || 'Guest'}@${p.joinTime != null ? p.joinTime : 'unknown'}-${i}`}
            p={p}
            now={now}
          />
        ))}
        {participants.length > 3 && (
          <span style={{ fontSize: 11, color: tone.fg }}>+{participants.length - 3} more</span>
        )}
        <span style={{ flex: 1 }} />
        {window.AdminMeetingNotes && window.AdminMeetingNotes.SendMeetingNoteButton && (
          <window.AdminMeetingNotes.SendMeetingNoteButton booking={booking} />
        )}
        <button
          onClick={() => adminJoin(booking)}
          disabled={joinDisabled}
          title={joinDisabled ? 'No meeting URL on this booking yet' : 'Join the Daily room as admin'}
          style={{
            background: joinDisabled ? 'oklch(92% 0.01 265)' : 'oklch(56% 0.13 250)',
            color: joinDisabled ? 'oklch(58% 0.02 265)' : '#fff',
            border: 'none',
            borderRadius: 6,
            padding: '5px 12px',
            fontSize: 11,
            fontWeight: 700,
            cursor: joinDisabled ? 'not-allowed' : 'pointer',
            fontFamily: "'Plus Jakarta Sans', sans-serif",
            whiteSpace: 'nowrap',
          }}
        >
          Join
        </button>
      </div>
    );
  }

  window.AdminScheduleLive.useLivePresence = useLivePresence;
  window.AdminScheduleLive.RowStatus       = RowStatus;
  window.AdminScheduleLive.isInLiveWindow  = isInLiveWindow;
})();
