// src/features/meetings/lesson-room.jsx
//
// Embedded Daily.co call UI that opens in a popup window via ?page=lesson.
// Hosts a daily-js callObject (NOT prebuilt iframe) so the page has direct
// access to every participant's audio track — that's what makes both-sides
// recording possible. Without this, the recorder.js mic-only fallback only
// captures the teacher's voice.
//
// URL params:
//   ?page=lesson&bookingId=<uuid>
//
// Page lifecycle:
//   1. Mount: fetch booking + meeting_url + Daily token via existing API.
//   2. Join Daily room with callObject.
//   3. Start mixed-audio recording (window.Recording.startMixed) — subscribes
//      to all remote audio tracks + local mic, mixes via Web Audio, uploads
//      checkpoints to Supabase Storage every 5 min via the existing pipeline.
//   4. Render minimal video-call UI: tile per participant, Mute/Camera/Leave.
//   5. Leave: stop recording + leave Daily room + close window.
//
// Public on window: LessonRoom (React component).
//
// Daily-js is loaded from CDN in index.html as a global (window.DailyIframe).

(function () {
  'use strict';

  // ── ParticipantTile ───────────────────────────────────────────────────────
  // Renders one participant's <video> + (for remotes) <audio>, and routes the
  // audio track into the shared Web Audio mixer.
  const ParticipantTile = ({ p, audioCtx, dest, wiredTracksRef }) => {
    const videoRef = React.useRef(null);
    const audioRef = React.useRef(null);

    // Re-run on EVERY participant-object change (Daily mutates nested
    // properties without changing their references, so depending only on
    // `persistentTrack` misses state flips like camera on/off).
    React.useEffect(() => {
      const v = p?.tracks?.video?.persistentTrack;
      if (videoRef.current) {
        const current = videoRef.current.srcObject;
        if (v) {
          // Only reattach if the underlying track actually changed.
          const same = current && current.getTracks && current.getTracks()[0] === v;
          if (!same) videoRef.current.srcObject = new MediaStream([v]);
        } else if (current) {
          videoRef.current.srcObject = null;
        }
      }
      const a = p?.tracks?.audio?.persistentTrack;
      if (!p.local && audioRef.current) {
        const current = audioRef.current.srcObject;
        if (a) {
          const same = current && current.getTracks && current.getTracks()[0] === a;
          if (!same) audioRef.current.srcObject = new MediaStream([a]);
        } else if (current) {
          audioRef.current.srcObject = null;
        }
      }
      if (a && audioCtx && dest && wiredTracksRef.current) {
        if (!wiredTracksRef.current.has(a)) {
          try {
            const src = audioCtx.createMediaStreamSource(new MediaStream([a]));
            src.connect(dest);
            wiredTracksRef.current.add(a);
          } catch (e) {
            console.warn('[lesson-room] audio wire failed:', e.message);
          }
        }
      }
    });

    const videoState = p?.tracks?.video?.state;
    const hasVideo  = !!p?.tracks?.video?.persistentTrack &&
                      videoState !== 'off' && videoState !== 'blocked';

    return (
      <div style={{
        position: 'relative',
        background: 'oklch(18% 0.03 265)',
        borderRadius: 12,
        overflow: 'hidden',
        aspectRatio: '16 / 9',
      }}>
        <video
          ref={videoRef}
          autoPlay
          playsInline
          muted={!!p.local}
          style={{
            width: '100%', height: '100%', objectFit: 'cover',
            background: 'oklch(18% 0.03 265)',
            display: hasVideo ? 'block' : 'none',
          }}
        />
        {!hasVideo && (
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', alignItems: 'center',
            justifyContent: 'center', color: '#fff',
            fontSize: 36, fontFamily: "'Plus Jakarta Sans', sans-serif",
            fontWeight: 600,
          }}>
            {(p.user_name || '?').slice(0, 1).toUpperCase()}
          </div>
        )}
        {!p.local && <audio ref={audioRef} autoPlay />}
        <div style={{
          position: 'absolute', bottom: 8, left: 8,
          background: 'oklch(0% 0 0 / 0.6)', color: '#fff',
          fontSize: 12, padding: '3px 8px', borderRadius: 6,
          fontFamily: "'Plus Jakarta Sans', sans-serif", fontWeight: 600,
        }}>
          {p.user_name || (p.local ? 'You' : 'Guest')}
          {p.local ? ' (you)' : ''}
        </div>
      </div>
    );
  };

  // ── ControlButton ─────────────────────────────────────────────────────────
  const ControlButton = ({ label, onClick, active, danger }) => (
    <button
      type="button"
      onClick={onClick}
      style={{
        padding: '10px 18px', borderRadius: 9, border: 'none', cursor: 'pointer',
        fontFamily: "'Plus Jakarta Sans', sans-serif",
        fontWeight: 600, fontSize: 14,
        background: danger
          ? 'oklch(58% 0.18 25)'
          : (active ? 'oklch(94% 0.01 265)' : 'oklch(56% 0.13 250)'),
        color: danger ? '#fff' : (active ? 'oklch(20% 0.04 265)' : '#fff'),
        minWidth: 110,
      }}
    >
      {label}
    </button>
  );

  // ── LessonRoom (main) ─────────────────────────────────────────────────────
  const LessonRoom = ({ bookingId }) => {
    const [state, setState] = React.useState('loading');   // loading|joining|in-call|ended|error
    const [errorMsg, setErrorMsg] = React.useState('');
    const [participants, setParticipants] = React.useState({});
    const [muted, setMuted] = React.useState(false);
    const [cameraOff, setCameraOff] = React.useState(false);
    const [recState, setRecState] = React.useState('idle'); // idle|recording|stopping|done|denied
    const [callerRole, setCallerRole] = React.useState(null); // 'instructor'|'admin'|'student'|null — gates admin-notes banner

    const callRef          = React.useRef(null);
    const audioCtxRef      = React.useRef(null);
    const destRef          = React.useRef(null);
    const wiredTracksRef   = React.useRef(new WeakSet());
    const recHandleRef     = React.useRef(null);

    // Initialise + join on mount.
    React.useEffect(() => {
      let cancelled = false;
      // Belt-and-suspenders resync — created inside the async IIFE below but
      // tracked in closure-scope variables here so the effect cleanup can
      // clear them on unmount (otherwise the interval keeps polling a
      // destroyed callObject every 2s for the rest of the page session).
      let resyncInterval = null;
      let resyncTimeout  = null;
      (async () => {
        try {
          if (!bookingId) throw new Error('Missing bookingId in URL.');
          if (typeof window.DailyIframe === 'undefined') {
            throw new Error('daily-js failed to load.');
          }

          // Sign in / refresh session: lesson page is a popup, but cookies +
          // localStorage are shared with the parent. window.supa should be
          // ready by the time this mounts (auth.jsx initialised it earlier).
          const { data: sess } = await window.supa.auth.getSession();
          const jwt = sess?.session?.access_token;
          if (!jwt) throw new Error('You need to sign in before joining.');

          // Resolve a tokenized Daily URL via the existing /api/meetings/token
          // route. This both verifies the caller owns the booking and embeds
          // their display name (no Daily prejoin name step needed).
          const r = await fetch('/api/meetings/token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
            body: JSON.stringify({ bookingId }),
          });
          const j = await r.json();
          if (!r.ok || !j.ok) throw new Error(j.error || `Token mint failed (HTTP ${r.status}).`);
          if (!j.url && !j.fallbackUrl) throw new Error('No meeting URL returned.');

          // Only ONE browser per lesson should run the recorder. The booking's
          // instructor is the canonical recorder; admins also record (so Joe's
          // own end-to-end tests still capture audio when he's the only one in
          // the room). Students just join the call without recording.
          // Also stored in state so the admin-notes banner can gate its
          // realtime subscription on "viewer is the booking's instructor".
          const callerRoleLocal = j.role || 'student';
          if (!cancelled) setCallerRole(callerRoleLocal);
          const shouldRecord = callerRoleLocal === 'instructor' || callerRoleLocal === 'admin';

          // Strip the ?t= token off the URL because callObject mode takes the
          // bare room URL + token separately.
          const u = new URL(j.url || j.fallbackUrl);
          const t = u.searchParams.get('t') || j.token || '';
          u.searchParams.delete('t');
          const roomUrl = u.toString();

          if (cancelled) return;

          setState('joining');

          // Create callObject (NOT iframe) so we have direct track access.
          const call = window.DailyIframe.createCallObject({
            subscribeToTracksAutomatically: true,
            dailyConfig: { useDevicePreferenceCookies: true },
          });
          callRef.current = call;

          // Web Audio mixer for the recorder. One AudioContext per page.
          audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)();
          destRef.current     = audioCtxRef.current.createMediaStreamDestination();

          // ── Participant events ──
          const refreshParticipants = () => {
            try {
              const ps = call.participants();
              setParticipants({ ...ps });
            } catch (_) {}
          };
          call.on('joined-meeting', refreshParticipants);
          call.on('participant-joined', refreshParticipants);
          call.on('participant-updated', refreshParticipants);
          call.on('participant-left', refreshParticipants);
          call.on('error',  (e) => {
            console.warn('[lesson-room] call error:', e);
            setErrorMsg(e?.errorMsg || 'Daily.co error.');
          });
          call.on('left-meeting', () => {
            if (!cancelled) setState('ended');
          });

          // daily-js validates token as a string when present — pass only when non-empty.
          const joinOpts = { url: roomUrl };
          if (t) joinOpts.token = t;
          await call.join(joinOpts);
          if (cancelled) return;

          // Immediately publish camera + mic — callObject mode does not turn
          // them on automatically the way createFrame (prebuilt) does.
          try { await call.setLocalAudio(true); } catch (_) {}
          try { await call.setLocalVideo(true); } catch (_) {}

          // Sync participants right after join (the 'joined-meeting' event
          // sometimes lands before our handler is wired). Then re-sync every
          // 2s for the first 30s as belt-and-suspenders.
          refreshParticipants();
          resyncInterval = setInterval(refreshParticipants, 2000);
          resyncTimeout  = setTimeout(() => {
            clearInterval(resyncInterval);
            resyncInterval = null;
          }, 30000);

          setState('in-call');

          // Resume AudioContext if suspended (autoplay policy).
          try { await audioCtxRef.current.resume(); } catch (_) {}

          // Kick off the mixed-audio recorder — only if this browser is the
          // designated recorder for the lesson (instructor or admin).
          if (shouldRecord && window.Recording && window.Recording.startMixed) {
            const h = await window.Recording.startMixed({
              bookingId,
              stream: destRef.current.stream,
            });
            if (h && h.ok) {
              recHandleRef.current = h;
              setRecState('recording');
            } else if (h?.reason === 'permission') {
              setRecState('denied');
            } else {
              // Greptile P2: surface non-permission start failures so the
              // instructor sees the lesson is uncaptured instead of finding
              // out after the fact in the Recordings tab.
              console.warn('[lesson-room] recorder start failed:', h?.reason, h?.err);
              setRecState('failed');
            }
          }
        } catch (e) {
          if (cancelled) return;
          setErrorMsg(e.message || String(e));
          setState('error');
        }
      })();

      return () => {
        cancelled = true;
        if (resyncInterval) clearInterval(resyncInterval);
        if (resyncTimeout)  clearTimeout(resyncTimeout);
        try { recHandleRef.current && recHandleRef.current.stop && recHandleRef.current.stop(); } catch (_) {}
        try { callRef.current && callRef.current.destroy && callRef.current.destroy(); } catch (_) {}
        try { audioCtxRef.current && audioCtxRef.current.close && audioCtxRef.current.close(); } catch (_) {}
      };
    }, [bookingId]);

    // Warn before tab/window close while the lesson is active. Modern
    // browsers ignore any custom message string and show their own dialog
    // (usually "Reload site? / Leave site? Changes you made may not be saved"
    // or similar). The handler is removed once the user clicks Leave so
    // the clean-close path doesn't show the warning.
    const guardLeaveRef = React.useRef(true);
    React.useEffect(() => {
      const onBeforeUnload = (ev) => {
        if (!guardLeaveRef.current) return;
        if (state !== 'in-call') return;
        ev.preventDefault();
        ev.returnValue = '';
        return '';
      };
      window.addEventListener('beforeunload', onBeforeUnload);
      return () => window.removeEventListener('beforeunload', onBeforeUnload);
    }, [state]);

    const handleLeave = async () => {
      // Disable the beforeunload guard — this is a clean intentional close.
      guardLeaveRef.current = false;
      setState('ending');
      setRecState('stopping');
      try { recHandleRef.current && (await recHandleRef.current.stop()); } catch (_) {}
      setRecState('done');
      try { callRef.current && (await callRef.current.leave()); } catch (_) {}
      // Close popup window immediately — no "Recording saved" panel.
      try { window.close(); } catch (_) {}
    };

    const handleMute = () => {
      const next = !muted;
      try { callRef.current && callRef.current.setLocalAudio(!next); } catch (_) {}
      setMuted(next);
    };

    const handleCamera = () => {
      const next = !cameraOff;
      try { callRef.current && callRef.current.setLocalVideo(!next); } catch (_) {}
      setCameraOff(next);
    };

    // ── Render ──
    if (state === 'loading' || state === 'joining') {
      return (
        <div style={fullPageDark}>
          <div style={{ color: '#fff', fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
            {state === 'loading' ? 'Loading lesson…' : 'Joining…'}
          </div>
        </div>
      );
    }
    if (state === 'error') {
      return (
        <div style={fullPageDark}>
          <div style={{
            background: '#fff', borderRadius: 14, padding: '24px 28px', maxWidth: 460,
            fontFamily: "'Plus Jakarta Sans', sans-serif",
          }}>
            <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 8, color: 'oklch(20% 0.04 265)' }}>
              Could not start the lesson
            </div>
            <div style={{ fontSize: 14, color: 'oklch(40% 0.04 265)', marginBottom: 16 }}>{errorMsg}</div>
            <button onClick={() => { try { window.close(); } catch (_) {} location.href = '/'; }}
              style={{
                padding: '10px 18px', borderRadius: 9, border: 'none',
                background: 'oklch(56% 0.13 250)', color: '#fff', cursor: 'pointer',
                fontWeight: 600,
              }}>Close</button>
          </div>
        </div>
      );
    }
    if (state === 'ended' || state === 'ending') {
      // No "Lesson ended / Recording saved" message — Joe wants the UI silent
      // about recording. handleLeave already calls window.close() shortly
      // after this state renders; this is just the brief visual state
      // between Leave-click and window close.
      return <div style={fullPageDark} />;
    }

    // Layout: top recording banner, centered participant tiles, bottom controls.
    // Tiles render video when camera is on and the initial letter otherwise.
    // ParticipantTile also routes each track into the Web Audio mixer (the
    // recorder is audio-only — video is for the lesson UX, not stored).
    const allParticipants = Object.values(participants);
    const remotes = allParticipants.filter(p => !p.local);
    const cols = allParticipants.length <= 1 ? 1 : 2;
    const tileMaxWidth = cols === 1 ? 520 : 880;

    // Surface recorder failures so the instructor knows audio is NOT being
    // captured. Per Joe's product call we don't show a positive "recording"
    // indicator (no banner during a healthy recording), but silent failures
    // are worse — the lesson finishes, no row appears in Recordings, no one
    // notices until much later. Show a small chip ONLY for failure states.
    const recErrorChip = (recState === 'denied' || recState === 'failed') && (
      <div style={{
        position: 'absolute', top: 14, left: 16,
        display: 'flex', alignItems: 'center', gap: 8,
        background: 'oklch(94% 0.08 25)', color: 'oklch(38% 0.18 25)',
        padding: '6px 12px', borderRadius: 999,
        fontFamily: "'Plus Jakarta Sans', sans-serif",
        fontSize: 12, fontWeight: 600,
      }}>
        <span style={{
          display: 'inline-block', width: 7, height: 7, borderRadius: '50%',
          background: 'oklch(58% 0.18 25)',
        }} />
        {recState === 'denied'
          ? 'Microphone blocked — lesson will not be recorded'
          : 'Recording unavailable — lesson will not be saved'}
      </div>
    );

    // Admin-notes banner — only the booking's assigned instructor sees
    // these. The component handles its own realtime subscription, dedup,
    // and dismiss. Lives in src/features/meetings/ui/meeting-note-banner.jsx
    // (extracted so this file stays under the 400-LOC cap).
    const NoteBanner = window.MeetingNotes && window.MeetingNotes.LessonNoteBanner;

    return (
      <div style={fullPageDark}>
        {recErrorChip}
        {NoteBanner && (
          <NoteBanner bookingId={bookingId} active={callerRole === 'instructor'} />
        )}
        <div style={{
          width: '100%', maxWidth: tileMaxWidth, padding: '60px 20px 110px',
          display: 'grid',
          gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
          gap: 14,
        }}>
          {allParticipants.length === 0 && (
            <div style={{
              color: 'oklch(78% 0.03 265)', fontSize: 14,
              fontFamily: "'Plus Jakarta Sans', sans-serif",
              textAlign: 'center', padding: '40px 0',
            }}>Connecting to the lesson…</div>
          )}
          {allParticipants.map(p => (
            <ParticipantTile
              key={p.session_id || (p.local ? 'local' : 'guest')}
              p={p}
              audioCtx={audioCtxRef.current}
              dest={destRef.current}
              wiredTracksRef={wiredTracksRef}
            />
          ))}
        </div>

        {remotes.length === 0 && allParticipants.length > 0 && (
          <div style={{
            position: 'absolute', top: 50, left: 0, right: 0, textAlign: 'center',
            color: 'oklch(78% 0.03 265)',
            fontFamily: "'Plus Jakarta Sans', sans-serif", fontSize: 13,
          }}>
            Waiting for the other participant to join…
          </div>
        )}

        <div style={{
          position: 'absolute', bottom: 22, left: 0, right: 0,
          display: 'flex', justifyContent: 'center', gap: 10,
        }}>
          <ControlButton label={muted ? 'Unmute' : 'Mute'} onClick={handleMute} active={muted} />
          <ControlButton label={cameraOff ? 'Camera on' : 'Camera off'} onClick={handleCamera} active={cameraOff} />
          <ControlButton label="Leave" onClick={handleLeave} danger />
        </div>
      </div>
    );
  };

  const fullPageDark = {
    position: 'fixed', inset: 0, background: 'oklch(14% 0.04 265)',
    display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column',
  };

  // Inject pulse keyframes if not already present (recorder.js may have).
  if (typeof document !== 'undefined' && !document.getElementById('mastery-rec-pulse-kf')) {
    const s = document.createElement('style');
    s.id = 'mastery-rec-pulse-kf';
    s.textContent = '@keyframes mastery-rec-pulse{0%,100%{opacity:1}50%{opacity:.35}}';
    document.head.appendChild(s);
  }

  window.LessonRoom = LessonRoom;
})();
