// src/features/meetings/ui/join-button.jsx
//
// "Join meeting" CTA. Renders only when a booking has a meeting_url.
// Always enabled when a URL exists — the prejoin UI inside Daily.co is
// the right place for the user to check mic/camera before entering the room.
//
// Usage:
//   <Meetings.ui.JoinButton booking={booking} />
//   <Meetings.ui.JoinButton booking={booking} meetingUrl={resolved} />
//
// `booking` must include: { scheduled_at } (others ignored).
// `meetingUrl` (optional) overrides booking.meeting_url — use this when the
// caller already resolved the (booking → assignment) fallback chain via
// Meetings.db.resolveForBooking. When not passed, falls back to
// booking.meeting_url for direct callers.
//
// Permission pre-flight:
//   On click, this component probes camera+mic permission state via
//   window.Meetings.permission before opening the Daily popup. If permission
//   was never asked it shows a "please click Allow" warning modal. If
//   permission is blocked it shows plain-English help steps inside that
//   modal. We intentionally do NOT show a standing "having trouble?" link
//   under every Join button — that read as over-prompting and made users
//   suspect something was broken even when nothing was.
//
//   Popup-blocker safe: we open about:blank synchronously on the click event,
//   then either navigate it to the meeting (granted/unsupported) or close it
//   (denied/prompt, where a subsequent user gesture opens a fresh popup).
//
// Audio recording side-effect:
//   When the user clicks the (enabled) button, in addition to opening the
//   external meeting URL, this kicks off window.Recording.start({ bookingId })
//   so the in-tab mic captures the audio for transcription. This runs
//   silently — no visible banner. Earlier versions surfaced a "Recording
//   audio — keep this tab open" line, but in practice Daily.co's popup
//   grabs the mic first, our Recording.start() then briefly returns a
//   denied result, and the banner flashed "Mic permission denied" for a
//   split second on every Join click. The banner is gone; recording still
//   happens when the browser cooperates, and quietly no-ops when it doesn't.

(function () {
  'use strict';

  window.Meetings = window.Meetings || {};
  window.Meetings.ui = window.Meetings.ui || {};

  const JoinButton = ({ booking, meetingUrl, compact = false }) => {
    // Re-render every 30s while mounted so time-based state changes happen
    // without a parent refresh (kept even though join is always-enabled, in
    // case callers rely on the periodic refresh for other data).
    const [, force] = React.useReducer(x => x + 1, 0);
    React.useEffect(() => {
      const t = setInterval(force, 30 * 1000);
      return () => clearInterval(t);
    }, []);

    // Active recording handle. Kept in a ref to avoid unnecessary re-renders.
    // No state mirror — the recording runs silently with no UI feedback per
    // Joe's call to drop the "Recording audio" banner.
    const recRef = React.useRef(null);

    // Active live-captions handle. Independent of the recording — captions
    // are gated by an admin master switch (admin_settings.live_captions_enabled)
    // and may silently no-op when the feature is off or the browser can't.
    const capRef = React.useRef(null);
    const [capOn, setCapOn] = React.useState(false);

    // Permission modal state.
    // 'none'          — no modal showing
    // 'pre-prompt'    — about to ask; show the "click Allow" pre-warning
    // 'denied-help'   — cam/mic blocked; show browser-specific help
    // 'standalone-help' — user opened help via the link below the button
    //                    (variant retained for callers that still pass it,
    //                    though the in-component link has been removed)
    const [permModal, setPermModal] = React.useState('none');

    // Popup opened synchronously in handlePrePromptContinue so the
    // subsequent async token fetch has somewhere to navigate.
    const pendingPopup = React.useRef(null);

    // Guard against double-clicks during the async probe window. Without this,
    // two rapid clicks open two about:blank popups that both navigate to the
    // meeting URL — placing the user in the same Daily.co room twice with echo.
    const probeInFlight = React.useRef(false);

    // On unmount: finalize any active recording (uploads the final chunk
    // via stop() rather than discarding it via cancel(), since there is no
    // longer a user-facing End-recording button to do that explicitly) and
    // close any orphaned popup that was opened in handlePrePromptContinue
    // but whose promise hasn't resolved yet (e.g. parent component
    // unmounted mid-getUserMedia).
    React.useEffect(() => {
      return () => {
        const h = recRef.current;
        if (h && h.stop) {
          // Fire-and-forget — the await would race the unmount.
          try { h.stop(); } catch (_) {}
          recRef.current = null;
        }
        const c = capRef.current;
        if (c && c.cancel) {
          try { c.cancel(); } catch (_) {}
          capRef.current = null;
        }
        const p = pendingPopup.current;
        if (p && !p.closed) {
          try { p.close(); } catch (_) {}
          pendingPopup.current = null;
        }
      };
    }, []);

    if (!booking) return null;
    const url = meetingUrl || booking.meeting_url || booking.meetingLink || '';
    if (!url) return null;
    const bookingId = booking.id || booking.bookingId || null;

    const joinable = !!url;
    // A per-student Google Meet room (the assignment.meeting_url that
    // resolveForBooking now prefers) opens in a new tab and runs entirely in
    // Google's own UI — it does NOT use the in-house Daily lesson room, the
    // cam/mic pre-prompt, or the in-tab recorder/captions (Google Meet handles
    // its own device check and recording).
    const isGoogleMeet = /meet\.google\.com/i.test(url);
    const provider = window.Meetings.util.providerOf({ ...booking, meeting_url: url }) || 'manual';

    const baseBtn = {
      display: 'inline-flex',
      alignItems: 'center',
      gap: 8,
      padding: compact ? '7px 14px' : '10px 18px',
      borderRadius: 9,
      border: 'none',
      fontFamily: "'Plus Jakarta Sans', sans-serif",
      fontWeight: 600,
      fontSize: compact ? 12 : 14,
      cursor: 'pointer',
      transition: 'background 0.15s',
      background: 'oklch(56% 0.13 250)',
      color: '#fff',
      opacity: 1,
    };

    async function tryStartRecording() {
      if (!bookingId) return;
      if (!window.Recording || !window.Recording.support?.canRecord()) return;
      if (recRef.current) return;
      const handle = await window.Recording.start({ bookingId });
      if (handle && handle.ok) {
        recRef.current = handle;
      }
      // No UI feedback on denied / unsupported — fail quiet. The lesson
      // still happens in Daily.co; transcription just doesn't capture.
    }

    // Live captions: webkitSpeechRecognition runs in this parent tab on the
    // signed-in user's mic. Tagged speaker_role via the booking → profiles
    // lookup inside window.Captions.start. Gated on admin_settings.live_captions_enabled
    // (is_live_captions_enabled() SECURITY DEFINER helper) — if the flag is
    // off, start() returns null and the badge stays hidden. onTerminated
    // clears the badge if the recognizer permanently stops (e.g. mic denied).
    async function tryStartCaptions() {
      if (!bookingId) return;
      if (!window.Captions || !window.Captions.support?.canCaption()) return;
      if (capRef.current) return;
      const handle = await window.Captions.start({
        bookingId,
        onTerminated: () => {
          capRef.current = null;
          setCapOn(false);
        },
      });
      if (handle && handle.ok) {
        capRef.current = handle;
        setCapOn(true);
      }
    }

    // Navigate an already-open popup to the in-house lesson room
    // (/?page=lesson&bookingId=<id>). That page hosts daily-js callObject +
    // Web Audio mixer + MediaRecorder, so audio capture (both sides) and
    // upload happen entirely inside it — Plan A. Live captions, if enabled,
    // still run in this parent tab via window.Captions.
    //
    // Manual-link bookings (no uuid) fall back to the raw URL so the meeting
    // still opens — they just won't be recorded. Google Meet rooms ALSO use the
    // raw URL (and skip in-tab captions): the lesson happens in Google's tab,
    // so the in-house Daily lesson room would just open an empty parallel room.
    function navigatePopupToMeeting(popup) {
      // Kick off captions in this parent tab. Recording is no longer called
      // from this file — it happens entirely inside the lesson popup now.
      // Google Meet rooms skip this: the audio lives in Google's own tab.
      if (!isGoogleMeet) tryStartCaptions();
      const finalUrl = (bookingId && !isGoogleMeet)
        ? `/?page=lesson&bookingId=${encodeURIComponent(bookingId)}`
        : url;
      if (popup && !popup.closed) {
        try { popup.location.href = finalUrl; }
        catch (_) {}
      }
    }

    // "Continue" inside the pre-prompt modal IS the user gesture. We open
    // the popup synchronously here, then fire getUserMedia which triggers
    // the browser's native permission dialog. On grant we navigate the popup;
    // on deny we close it and show the help steps.
    function handlePrePromptContinue() {
      pendingPopup.current = window.open('about:blank', '_blank');
      setPermModal('none');

      window.Meetings.permission.requestPermission().then(result => {
        const popup = pendingPopup.current;
        pendingPopup.current = null;
        if (result === 'granted' || result === 'error') {
          // 'error' covers hardware-missing (NotFoundError) and other non-denial
          // failures — proceed to the lesson room, which surfaces its own
          // no-device message. Showing our "blocked" help here would mislead
          // users with no webcam into thinking they need to unblock something.
          navigatePopupToMeeting(popup);
        } else {
          if (popup && !popup.closed) { try { popup.close(); } catch (_) {} }
          setPermModal('denied-help');
        }
      }).catch(() => {
        // requestPermission() rejected unexpectedly — close any orphaned popup
        // and leave the modal closed so the user can re-trigger via Join.
        const popup = pendingPopup.current;
        pendingPopup.current = null;
        if (popup && !popup.closed) { try { popup.close(); } catch (_) {} }
      });
    }

    // "Try again" inside the denied-help modal re-probes. If the user fixed
    // the setting, probe() now returns 'granted' and we can open the meeting.
    // Note: probe() is async so we cannot open a popup inside the callback;
    // instead we show the pre-prompt modal whose Continue button IS a user
    // gesture. For the 'granted' case we just tell the user to click Join
    // again — this is the simplest, most reliable UX for this audience.
    function handleRetry() {
      setPermModal('none');
      window.Meetings.permission.probe().then(state => {
        if (state === 'denied') {
          // Still blocked — keep showing help.
          setPermModal('denied-help');
        } else {
          // 'granted', 'prompt', or 'unsupported' — all mean we can proceed.
          // Show the pre-prompt one more time so the user has the user-gesture
          // on Continue to open the popup. (For 'granted' this skips straight
          // through because requestPermission() will return immediately.)
          setPermModal('pre-prompt');
        }
      }).catch(() => {
        // probe() rejected unexpectedly — restore the help modal so the user
        // still has a visible path forward instead of an empty screen.
        setPermModal('denied-help');
      });
    }

    // Main click handler.
    // Strategy: open about:blank synchronously (preserves the user gesture so
    // the popup isn't blocked). Then run the async probe:
    //   granted / unsupported → navigate the already-open popup.
    //   denied → close the popup, show help.
    //   prompt → close the popup, show pre-prompt modal. The "Continue" click
    //            in that modal becomes the next user gesture that opens a fresh
    //            popup via handlePrePromptContinue.
    const onClick = (e) => {
      if (!joinable) { e.preventDefault(); return; }

      // Google Meet rooms just open the real meet.google.com link in a new tab.
      // No cam/mic pre-prompt (Google runs its own device check) and no in-house
      // Daily lesson room. This is the per-student Meet link from the assignment.
      if (isGoogleMeet) {
        window.open(url, '_blank', 'noopener,noreferrer');
        return;
      }

      // Ignore double-clicks while a probe is in flight. Without this guard,
      // two rapid clicks open two popups that both navigate to the meeting URL,
      // placing the user in the same lesson room twice.
      if (probeInFlight.current) return;

      // Graceful fallback when permission module hasn't loaded (race condition).
      if (!window.Meetings || !window.Meetings.permission) {
        const popup = window.open('about:blank', '_blank');
        navigatePopupToMeeting(popup);
        return;
      }

      probeInFlight.current = true;
      const popup = window.open('about:blank', '_blank');
      // Track the popup in the shared ref so the unmount-cleanup effect
      // can close it if the component tears down mid-probe (e.g. parent
      // re-render collapses the booking card while probe() is resolving).
      pendingPopup.current = popup;

      window.Meetings.permission.probe().then(state => {
        probeInFlight.current = false;
        // The resolve branch takes ownership of the popup — clear the ref so
        // unmount cleanup won't close a popup we've already navigated.
        if (pendingPopup.current === popup) pendingPopup.current = null;
        if (state === 'granted' || state === 'unsupported') {
          navigatePopupToMeeting(popup);
        } else if (state === 'denied') {
          if (popup && !popup.closed) { try { popup.close(); } catch (_) {} }
          setPermModal('denied-help');
        } else {
          // 'prompt' — close the placeholder popup; the Continue button will
          // open a fresh popup as the new user gesture.
          if (popup && !popup.closed) { try { popup.close(); } catch (_) {} }
          setPermModal('pre-prompt');
        }
      }).catch(() => {
        // probe() rejected unexpectedly — reset the guard and close the orphaned
        // blank tab. Fall through to the prompt flow on the next click.
        probeInFlight.current = false;
        if (pendingPopup.current === popup) pendingPopup.current = null;
        if (popup && !popup.closed) { try { popup.close(); } catch (_) {} }
      });
    };

    const label = provider === 'zoom' ? 'Join Zoom meeting' : 'Join meeting';

    return (
      <span style={{ display: 'inline-flex', alignItems: 'flex-start' }}>
        <button
          type="button"
          onClick={onClick}
          title={url}
          disabled={!joinable}
          style={baseBtn}
        >
          <span style={{
            display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
            background: joinable ? '#fff' : 'oklch(50% 0.04 265)',
          }} />
          {label}
        </button>

        {/* Hotfix c7f3708 dropped the standalone help link and the
            RecordingBanner (recording moved into the lesson popup; the
            banner caused a teacher-side flash when Daily.co grabbed the
            mic). Live captions keep their own subtle pulsing pill — gated
            on the admin_settings flag via Captions.start() so it stays
            hidden until Joe flips the toggle. */}
        {capOn && (
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 8,
            fontFamily: "'Plus Jakarta Sans', sans-serif",
            fontSize: compact ? 11 : 12,
            color: 'oklch(55% 0.12 150)',
          }}>
            <span style={{
              display: 'inline-block', width: 7, height: 7, borderRadius: '50%',
              background: 'oklch(55% 0.12 150)',
              animation: 'mastery-rec-pulse 1.4s ease-in-out infinite',
            }} />
            <span>Live captions on</span>
          </div>
        )}

        {permModal !== 'none' && (() => {
          const PermissionHelp = window.Meetings && window.Meetings.ui && window.Meetings.ui.PermissionHelp;
          if (!PermissionHelp) return null;
          return (
            <PermissionHelp
              variant={permModal}
              onContinue={handlePrePromptContinue}
              onRetry={handleRetry}
              onClose={() => setPermModal('none')}
            />
          );
        })()}
      </span>
    );
  };

  window.Meetings.ui.JoinButton = JoinButton;
})();
