import { Box, Loader, Stack, Text, ThemeIcon } from "@mantine/core";
import { IconPlugConnectedX } from "@tabler/icons-react";
import { useEffect, useRef, useState } from "react";
import classes from "./dashboard.module.css";

/**
 * THE sandboxed dashboard frame.
 *
 * Both viewers — the authenticated console viewer and the public share-link
 * viewer — render through this one component, so the security-critical iframe
 * configuration exists in exactly ONE place. Same discipline as the backend's
 * single serveDashboardAsset function: one implementation, two callers, no
 * chance of the two drifting apart.
 *
 * ─── THE SANDBOX ─────────────────────────────────────────────────────────────
 *
 * sandbox="allow-scripts" and NOTHING else.
 *
 * The omission of `allow-same-origin` is the entire security boundary. Without
 * it the browser assigns the frame an opaque, unique ("null") origin, which
 * means the untrusted bundle:
 *
 *   • has no access to the console's cookies, localStorage or sessionStorage —
 *     they belong to the console's origin, and the frame is not that origin;
 *   • cannot issue same-origin/credentialed requests as the signed-in user;
 *   • is separated from window.parent / window.top by the cross-origin barrier,
 *     so it can neither read nor script the console page that embeds it;
 *   • cannot cooperate with anything else on the page — every load gets a
 *     *fresh* opaque origin, so two frames can't even reach each other.
 *
 * Note the pairing: `allow-scripts` WITHOUT `allow-same-origin` is the safe
 * combination. Granting both together is equivalent to no sandbox at all — the
 * frame could simply reach up and remove its own sandbox attribute. That is
 * precisely why allow-same-origin must never be added here.
 *
 * Paired with the backend's `connect-src 'none'` CSP, the bundle can neither
 * reach the console's data nor send anything out: no fetch, no XHR, no
 * WebSocket, no sendBeacon. Isolation on the way in, no egress on the way out.
 *
 * Deliberately NOT granted:
 *   allow-same-origin ............ would collapse the whole boundary (above)
 *   allow-top-navigation ......... would let a bundle redirect the console away
 *   allow-popups ................. would let it open windows
 *   allow-popups-to-escape-sandbox would let those windows run unsandboxed
 *   allow-modals ................. would let it alert()/confirm() over the UI
 *   allow-forms .................. see below
 *
 * On allow-forms specifically: it is the only addition with a plausible
 * argument (a bundle with interactive controls), but `connect-src 'none'` plus
 * `form-action 'none'` mean a form has nowhere to submit to anyway. Client-side
 * interactivity works fine under allow-scripts alone. So we start with
 * allow-scripts only. If a real dashboard is ever demonstrated to need more,
 * that should be a deliberate, reviewed decision — not a quiet edit here.
 *
 * ─── NOTHING IS PASSED IN ────────────────────────────────────────────────────
 *
 * No token, session, cookie, user identity or config is handed to the frame,
 * by URL parameter or otherwise, and there is no postMessage channel in either
 * direction. The bundle is static and needs nothing from us. This is the
 * deliberate opposite of the KCGM iframe pattern's SSO handshake: here, silence
 * is the design. (The share token never reaches this component at all — the
 * public viewer resolves it into a serve URL before rendering.)
 */

export type DashboardFrameMode = "authed" | "public";

type Props = {
  /** Serve-proxy URL. A trailing slash is enforced below — see the comment. */
  serveSrc: string;
  mode: DashboardFrameMode;
  /** Accessible name for the frame; never rendered inside it. */
  title: string;
  /** Remounts the frame when it changes (e.g. an admin switching versions). */
  reloadKey?: string;
};

const DashboardFrame: React.FC<Props> = ({ serveSrc, mode, title, reloadKey }) => {
  const [state, setState] = useState<"loading" | "ready" | "error">("loading");
  const timeoutRef = useRef<number | null>(null);

  /*
   * The trailing slash matters and is easy to get wrong.
   *
   * A bundle references its assets relatively (`assets/app.js`). Those resolve
   * against the frame's document URL, so with a src of `…/serve` the browser
   * would ask for `…/assets/app.js` — one path segment too high, and every
   * asset 404s. With `…/serve/` it correctly asks for `…/serve/assets/app.js`.
   */
  const normalisedSrc = (() => {
    const [path, query] = serveSrc.split("?");
    const withSlash = path.endsWith("/") ? path : `${path}/`;
    return query ? `${withSlash}?${query}` : withSlash;
  })();

  useEffect(() => {
    setState("loading");
    // A cross-origin frame fires `load` for an error page too, and we cannot
    // read its status. Treat "still nothing after a while" as a failure so the
    // viewer doesn't spin forever on a dead link.
    timeoutRef.current = window.setTimeout(() => {
      setState((s) => (s === "loading" ? "error" : s));
    }, 20000);
    return () => {
      if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
    };
  }, [normalisedSrc, reloadKey]);

  return (
    <Box className={classes.frameShell}>
      {state === "loading" && (
        <Stack className={classes.frameOverlay} align="center" gap="xs">
          <Loader size="sm" />
          <Text size="xs" c="dimmed">
            Loading dashboard…
          </Text>
        </Stack>
      )}

      {state === "error" && (
        <Stack className={classes.frameOverlay} align="center" gap="xs">
          <ThemeIcon size={44} radius="xl" variant="light" color="gray">
            <IconPlugConnectedX size={22} />
          </ThemeIcon>
          <Text fw={600} size="sm">
            This dashboard couldn't be loaded
          </Text>
          <Text size="xs" c="dimmed" maw={340} ta="center">
            {mode === "public"
              ? "The link may have expired or been revoked."
              : "It may have been unpublished or removed. Try reloading, or pick another version."}
          </Text>
        </Stack>
      )}

      <iframe
        key={`${normalisedSrc}|${reloadKey ?? ""}`}
        className={classes.frame}
        src={normalisedSrc}
        title={title}
        // ── SECURITY BOUNDARY. Do not add allow-same-origin. See file header. ──
        sandbox="allow-scripts"
        referrerPolicy="no-referrer"
        loading="eager"
        onLoad={() => setState("ready")}
        // Cross-origin: fires only for network-level failures, but harmless.
        onError={() => setState("error")}
        style={{ visibility: state === "ready" ? "visible" : "hidden" }}
      />
    </Box>
  );
};

export default DashboardFrame;
