"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { getCurrentUserID, getToken } from "@enode/core/api/storage";
import { useTranslation } from "@enode/core/translations/hooks";
import { overlayPortalHost } from "@enode/ui/containment";
import { CloseXIcon, SpinnerIcon } from "@enode/ui/icons";
import { Sheet } from "@enode/ui/sheet";
import { syncReminders } from "../../../reminders/sync";
import { markCheckInSubmitted } from "./check-in-status";
import { setCheckedIn } from "./check-in-store";
import { buildQuestionnaireUrl, resolveServiceOrigin } from "./service-url";

// The check-in as it appears before training: a full-screen sheet the athlete
// fills in, ending in their own summary and a Submit. Completing it is what lets
// the workout start (`onCompleted`).
//
// The questionnaire itself is NO LONGER part of this app — it is served by the
// questionnaire service and rendered in the iframe below (see
// apps/portal/src/questionnaire-service/). Only the chrome is local, which is
// why the screen looks exactly as it did when the flow lived here.
//
// Closing the sheet (X, Escape, hardware Back) cancels the start: the station
// stays as it was, so the check-in can't trap someone who opened a workout by
// mistake.
//
// ── Why the iframe lives OUTSIDE the Sheet ──────────────────────────────────
// Sheet unmounts its children when closed, and the service is a whole web app:
// loading it on open costs a fetch, ~900KB of JS, and a hydration pass, which
// the athlete would spend staring at an empty drawer. So the frame is mounted
// for the whole session in a hidden, always-present overlay — it loads while
// the athlete is doing something else, and opening the sheet only reveals it.
//
// The overlay is portaled to the same host as Sheet (document.body) so the two
// share a stacking context, and it is offset by the measured header height so
// the sheet's own title stays visible above it. Moving an iframe in the DOM
// reloads it, which is exactly what this arrangement exists to avoid — so the
// node is never re-parented, only shown and hidden.

/**
 * How long to wait for the service's `ready` before giving up and letting the
 * workout start anyway.
 *
 * FAIL-OPEN, and deliberately so: this sheet gates EVERY start, and the app is
 * built to train offline (see docs/offline-and-sync.md). A questionnaire behind
 * a URL must never be able to stop an athlete from training — losing a check-in
 * is the cheaper failure by a wide margin. Only runs while the sheet is open
 * and the frame has not reported in; a preloaded frame never starts it.
 */
const READY_TIMEOUT_MS = 4000;

/**
 * Delay before reloading a used frame, so the blank first paint of the fresh
 * questionnaire can't be seen on the way out. The frame is already hidden by
 * then (`setReady(false)`); this only keeps the remount from landing in the
 * same beat as the close. It used to guard against flashing through the sheet's
 * close animation, which no longer exists (`slideFrom="none"`).
 */
const RELOAD_DELAY_MS = 600;

/** Fallback offset for the first frame after opening, before the real header is
 *  measured — close to the header's natural height, so nothing jumps. */
const HEADER_FALLBACK_PX = 108;

// Mirrors the service's own contract (questionnaire-service/host-bridge.ts).
type ServiceEvent =
  | { event: "ready" }
  | { event: "authenticated"; userId: string }
  | { event: "submitted"; id: string; payload: unknown }
  | { event: "cancelled" };

export function WellnessCheckInSheet({
  open,
  subject,
  onCancel,
  onCompleted,
}: {
  open: boolean;
  /**
   * Whose check-in this is. Omit for the signed-in user; pass a participant to
   * let a coach answer for an athlete who never logs in.
   *
   * The coach's token still authorises the write — this only selects the
   * subject, which the backend attributes the measurements to.
   */
  subject?: { id: string; name?: string | null } | null;
  onCancel: () => void;
  onCompleted: () => void;
}) {
  const { t, locale } = useTranslation();
  const frameRef = useRef<HTMLIFrameElement | null>(null);
  const subjectId = subject?.id ?? getCurrentUserID() ?? "";
  // Only when answering for someone else — the header names them, because
  // mis-attribution is this feature's failure mode and is otherwise invisible.
  const subjectName = subject?.name?.trim() || null;

  // Bumped after a finished check-in so the next workout gets a fresh
  // questionnaire — and, because the frame reloads immediately, a preloaded one.
  const [generation, setGeneration] = useState(0);
  const [ready, setReady] = useState(false);
  const [headerHeight, setHeaderHeight] = useState(HEADER_FALLBACK_PX);

  const serviceOrigin = useMemo(
    () => new URL(resolveServiceOrigin()).origin,
    [],
  );

  // Built as soon as the page mounts — NOT gated on `open`, which is what makes
  // the frame preload. Guarded on `window` because the app is a static export
  // and this module is evaluated during prerender.
  const src = useMemo(
    () =>
      typeof window === "undefined"
        ? null
        : buildQuestionnaireUrl({ id: "wellness", audience: "athlete", locale }),
    [locale],
  );

  // Latest callback, so the listener below subscribes once per frame.
  const completedRef = useRef(onCompleted);
  useEffect(() => {
    completedRef.current = onCompleted;
  });

  // The service's half of the contract. Mounted for the session, not just while
  // the sheet is open, so a `ready` that arrives during preload is not missed.
  useEffect(() => {
    function onMessage(event: MessageEvent) {
      // The only thing standing between a forged message and a started workout.
      if (event.origin !== serviceOrigin) return;
      const message = event.data as ServiceEvent | undefined;
      if (!message?.event) return;

      if (message.event === "ready") {
        setReady(true);
        // Hand over the session so the service can act as this user. Sent only
        // after `ready`, and only to the service's origin — never as a URL
        // parameter, which would leak it into history and access logs.
        const token = getToken();
        if (token) {
          // Two different things: the TOKEN authorises the write (always the
          // signed-in user), the USER ID selects whose check-in it is. The
          // backend attributes the measurements to the id, which is what lets a
          // coach answer for an athlete who has no login.
          frameRef.current?.contentWindow?.postMessage(
            { type: "auth", token, userId: subjectId },
            serviceOrigin,
          );
        }
        return;
      }

      if (message.event === "submitted") {
        // Note the check-in for the reminder. The server is what actually
        // decides (see check-in-status.ts) — this only bridges the gap until the
        // submit reaches it. Deliberately here and not in `onCompleted`: the
        // fail-open timeout below also completes the sheet, and a check-in that
        // never happened must not suppress tomorrow's reminder.
        // Recorded against the SUBJECT, not the signed-in user — otherwise a
        // coach checking in an athlete would clear their own badge and leave
        // the athlete's showing.
        if (subjectId) {
          // Two consumers, told directly rather than through each other: the
          // reminder's session hint, and the training view's badge store. The
          // submit goes through an outbox, so a refetch would race the write
          // and leave the badge showing.
          markCheckInSubmitted(subjectId);
          setCheckedIn(subjectId);
          void syncReminders();
        }
        completedRef.current();
        // Hide it straight away, then reload once the sheet has animated out.
        setReady(false);
        window.setTimeout(
          () => setGeneration((value) => value + 1),
          RELOAD_DELAY_MS,
        );
      }
    }

    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
    // `subjectId` is a dependency: the handler posts it on `ready`, so a stale
    // closure would authorise the frame as the previous athlete.
  }, [serviceOrigin, subjectId]);

  // Fail-open. Only armed when the athlete is actually waiting — a frame that
  // preloaded successfully is already `ready`, so this never runs.
  useEffect(() => {
    if (!open || ready) return;
    const timer = window.setTimeout(
      () => completedRef.current(),
      READY_TIMEOUT_MS,
    );
    return () => window.clearTimeout(timer);
  }, [open, ready]);

  const showFrame = open && ready;

  return (
    <>
      <Sheet
        open={open}
        onClose={onCancel}
        // No enter animation. The questionnaire below is portalled into a
        // separate, non-animated overlay, so a sliding surface meant the header
        // travelled up alone past an already-stationary questionnaire — which
        // read as the title animating in on its own.
        slideFrom="none"
        ariaLabel={t("Daily check-in")}
        zIndex={50}
        className="flex flex-col bg-background pt-[env(safe-area-inset-top)]"
      >
        <header
          // Measured in a ref callback rather than an effect: the overlay below
          // is a sibling of this element (different portal), so it can't inherit
          // the offset through layout.
          //
          // HEIGHT, not `bottom`: a height is the one measurement that stays
          // meaningful wherever the surface happens to be, which kept this
          // correct back when the Sheet still slid in from `translate-y-full`
          // and the element was off-screen at mount. The safe-area inset it sits
          // below is added back on the overlay itself, which is the one thing a
          // height can't carry across the portal boundary.
          ref={(node) => {
            if (node) setHeaderHeight(node.getBoundingClientRect().height);
          }}
          className="flex shrink-0 items-start justify-between gap-3 px-6 pt-6 pb-3 ios:px-4"
        >
          <div className="min-w-0 space-y-1">
            <h1 className="heading-large">{t("Daily check-in")}</h1>
            {/* Answering for someone else is the one case that must never be
                ambiguous — the answers are attributed to them, and nothing else
                on screen would say so. */}
            {subjectName && (
              <p className="body-prominent text-foreground-default">
                {t("For {name}", { name: subjectName })}
              </p>
            )}
            <p className="body-small text-foreground-muted">
              {subjectName
                ? t(
                    "Five quick questions — answer as they feel right now. 5 is best.",
                  )
                : t(
                    "Five quick questions before today's training — answer from how you feel right now. 5 is best.",
                  )}
            </p>
          </div>
          <button
            type="button"
            onClick={onCancel}
            aria-label={t("Close")}
            className="flex size-10 shrink-0 items-center justify-center rounded-full bg-surface-base text-foreground-default shadow ring-1 ring-surface-stroke hover:bg-surface-base-muted"
          >
            <CloseXIcon className="size-5" />
          </button>
        </header>

        {/* Only seen when the frame wasn't preloaded in time — normally the
            questionnaire is already there when the sheet opens. */}
        {ready ? null : (
          <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3">
            <SpinnerIcon className="size-6 animate-spin text-foreground-muted" />
            <span className="body-small text-foreground-muted">
              {t("Loading your check-in…")}
            </span>
          </div>
        )}
      </Sheet>

      {src
        ? createPortal(
            <div
              // Transparent and inset below the header, so the sheet's title
              // shows through above the questionnaire. Hidden — not unmounted —
              // whenever the sheet is closed or the frame isn't ready: a hidden
              // iframe still loads, which is the whole point.
              className="fixed inset-0"
              style={{
                zIndex: 51,
                // The safe-area inset has to be added explicitly. This overlay
                // is `fixed inset-0` from the VIEWPORT top, while the header it
                // sits below is inside a Sheet padded by
                // `pt-[env(safe-area-inset-top)]` — so offsetting by the header
                // height alone starts the questionnaire one status-bar height
                // too high, straight over the title.
                paddingTop: `calc(env(safe-area-inset-top) + ${headerHeight}px)`,
                // The bottom inset belongs here too, not inside the frame: the
                // service's footer (Next / Back) is its last flex child, so
                // without this the buttons sit under the iOS home indicator.
                // `env()` is only reliable in the top-level document, which is
                // exactly why the host owns both safe areas.
                paddingBottom: "env(safe-area-inset-bottom)",
                visibility: showFrame ? "visible" : "hidden",
                // NEVER on the wrapper. It spans the whole viewport, so its
                // transparent top strip — the part you see the sheet's title and
                // close button THROUGH — would swallow every tap aimed at them,
                // leaving the X visible but dead. Input belongs to the iframe
                // alone, which is the only part that actually renders anything.
                pointerEvents: "none",
              }}
              aria-hidden={!showFrame}
            >
              <iframe
                ref={frameRef}
                // Remounting on `generation` is what reloads a used
                // questionnaire — same URL, fresh document.
                // The SUBJECT is part of the key: retargeting the questionnaire
                // must remount it. Re-posting `auth` alone would leave any
                // answers already entered attached to the new athlete.
                key={`${subjectId}:${generation}`}
                src={src}
                title={t("Daily check-in")}
                className="h-full w-full border-0"
                style={{ pointerEvents: showFrame ? "auto" : "none" }}
              />
            </div>,
            overlayPortalHost(),
          )
        : null}
    </>
  );
}
