"use client";

import { useEffect, useState } from "react";
import { Loader2, Server as ServerIcon } from "lucide-react";

import { apiFetch, ApiRequestError } from "@/lib/client/api";
import type { ResolvedSource, StreamingProviderDescriptor } from "@/services/providers/types";
import type { WatchKind } from "@/lib/types";

/**
 * The video surface.
 *
 * A cross-origin embed is driven by `<iframe>`, so FlixTV cannot read the frame's
 * `currentTime` or call `play()`/`pause()` on it. Everything about "how far in
 * are they?" therefore has to be answered without a playhead, and this component
 * is built around that constraint rather than around a guess.
 *
 * The one real fact it can supply is **that playback was about to begin**: a
 * source resolved, the URL passed the checks below, and the embed is loading.
 * That is what `onSourceReady` reports, and it is the whole basis of the Continue
 * Watching feature — see `use-watch-start.ts` and `db/queries/progress.ts`.
 *
 * There is no clock here, and adding one is the mistake to avoid. A wall clock
 * started by the visitor still advances while the video is paused, buffering, or
 * left open in a background tab, so a position derived from it is a guess — and
 * rendered as a progress bar it becomes a claim the visitor never made.
 */

type PlayerState =
  | { phase: "resolving" }
  | { phase: "ready"; source: ResolvedSource }
  | { phase: "unavailable"; message: string }
  | { phase: "error"; message: string };

interface PlayerResponse {
  source: ResolvedSource | null;
  available: boolean;
  providers: StreamingProviderDescriptor[];
  message?: string;
}

export interface VideoPlayerProps {
  kind: WatchKind;
  tmdbId: number;
  season?: number | null;
  episode?: number | null;
  title: string;
  providerId: string | null;
  /** Remember the server the visitor picked. */
  onProviderChosen?: (providerId: string) => void;
  /** Lets the page disable the `[ Server ▼ ]` control while a resolve is in flight. */
  onResolvingChange?: (resolving: boolean) => void;
  /**
   * Fired once per successful resolve: a playable source exists and the embed is
   * about to load. This is the only "playback is starting" signal the page can
   * truthfully produce, so it is what Continue Watching is built on.
   */
  onSourceReady?: () => void;
}

export function VideoPlayer({
  kind,
  tmdbId,
  season,
  episode,
  title,
  providerId,
  onProviderChosen,
  onResolvingChange,
  onSourceReady,
}: VideoPlayerProps) {
  const [state, setState] = useState<PlayerState>({ phase: "resolving" });

  const requestKey = `${kind}:${tmdbId}:${season ?? ""}:${episode ?? ""}`;

  /* ── Source resolution ──────────────────────────────────────────────────── */

  useEffect(() => {
    const controller = new AbortController();
    let cancelled = false;

    async function resolve() {
      setState({ phase: "resolving" });
      onResolvingChange?.(true);

      try {
        const data = await apiFetch<PlayerResponse>("/api/providers/stream", {
          method: "POST",
          signal: controller.signal,
          body: JSON.stringify({
            kind,
            tmdbId,
            season: season ?? undefined,
            episode: episode ?? undefined,
            providerId: providerId ?? undefined,
          }),
        });

        if (cancelled) return;
        onResolvingChange?.(false);

        if (!data.available || !data.source) {
          setState({
            phase: "unavailable",
            message: data.message ?? "No streaming server is configured for this title yet.",
          });
          return;
        }

        /*
          Last gate before the URL becomes an `<iframe src>`.

          The server already rejects a missing/zero/negative TMDB id and refuses a
          series without a season and episode, so in practice this never trips.
          It exists because this is the only place a malformed URL could do real
          damage: a config typo such as `{tmdb_id` left unclosed, or a template
          key misspelled as `{tmdbId}`, would otherwise load a real-looking but
          broken frame, and the visitor would see a dead player with no
          explanation and no way to tell it apart from a provider outage.

          So a source is only accepted if it is an absolute http(s) URL with
          every placeholder substituted and no `undefined`/`null`/`NaN` residue.
          Anything else is reported as a provider problem, which surfaces the
          server list and leaves the other servers selectable.
        */
        const problem = embedUrlProblem(data.source.url);
        if (problem) {
          onResolvingChange?.(false);
          setState({ phase: "error", message: problem });
          return;
        }

        onProviderChosen?.(data.source.providerId);
        setState({ phase: "ready", source: data.source });

        // Past every gate above: this source is real, absolute, and fully
        // substituted, so the embed is genuinely about to play something.
        onSourceReady?.();
      } catch (error) {
        if (cancelled) return;
        onResolvingChange?.(false);
        setState({
          phase: "error",
          message:
            error instanceof ApiRequestError
              ? error.message
              : "Could not reach the streaming server. Please try again.",
        });
      }
    }

    void resolve();
    return () => {
      cancelled = true;
      controller.abort();
    };
    // The `on*` reporting callbacks are excluded on purpose: the parent
    // re-creates them each render, so listing them would re-resolve the source in
    // an infinite loop. `onSourceReady` is a fire-and-forget notification, so the
    // page that owns it de-duplicates rather than relying on a stable identity.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [requestKey, providerId, season, episode, tmdbId, kind]);

  /*
    There is no watch clock here, deliberately — and now that is a live design
    constraint rather than a missing feature.

    This used to be a stopwatch the visitor started with a "Start timer" button
    (or Space/K), which counted wall-clock seconds and fed them to the progress
    API. All of it is gone: the button, the shortcut, the `tracking`/`elapsed`
    state, the per-second interval, and the pagehide/visibilitychange flush.

    It was not removed because a clock is hard to build. It was removed because
    the clock cannot be honest. A cross-origin embed is an opaque frame, so there
    is no `currentTime` to read and no `play()` to call. A stopwatch counts
    seconds on the *page*, and those seconds are not the same thing as seconds
    watched: the visitor may pause, buffer, scrub, or leave the tab open over
    lunch. Dividing that by the runtime produces a percentage that looks like a
    measurement and is a guess — and Continue Watching renders that guess as a
    filled bar next to a title, which is about as close to a false statement as
    this app can make.

    So the component reports only what it can actually know, via `onSourceReady`,
    and the Continue Watching rail is built on that. Nothing here polls, nothing
    ticks, and nothing claims a position.

    That also leaves the page with no keyboard shortcut. Space and K are unbound,
    and no replacement exists: a hidden binding that quietly started a clock
    would be the same feature in a disguise.
  */


  return (
    // `absolute inset-0` rather than a `flex` column: the surface has to fill the
    // stage exactly, and the server selector is a sibling overlay owned by
    // `WatchClient` — keeping it out of this box is what stops it from consuming
    // layout height and squeezing the video.
    <div className="absolute inset-0 overflow-hidden bg-black">
      {/*
        The video surface.

        Fills the stage edge to edge at every breakpoint — no rounding, no border,
        no margin. The stage behind it is black, so any letterboxing the embed
        cannot control blends into the page instead of showing a panel edge.
      */}
      <div className="relative size-full">
        {state.phase === "ready" ? (
          <>
            <iframe
              key={`${state.source.providerId}-${season ?? 0}-${episode ?? 0}`}
              src={state.source.url}
              title={`${title} — ${state.source.providerName}`}
              allow={
                state.source.allow ??
                "autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write"
              }
              referrerPolicy={state.source.referrerPolicy ?? "origin"}
              allowFullScreen
              /*
                No `sandbox` attribute, and not by omission.

                A sandbox was applied here, and it stopped the player working: a
                cross-origin embed that is sandboxed is refused by the upstream
                player rather than degraded. Vidbolt shows "Playback Disabled -
                this player cannot be embedded in a restricted sandbox
                environment" and asks for the attribute to be removed. The
                previous value already granted scripts, same-origin, presentation,
                forms and popups, so widening it cannot help; the providers simply
                want to run unsandboxed.

                `ResolvedSource` therefore has no `sandbox` field to set, so
                nothing can put one back here by accident. What still bounds the
                frame is in place: it lives in an `<iframe>` rather than the page,
                `allow` limits the features it may use, `allowFullScreen` and the
                `fullscreen` entry cover presentation, and `referrerPolicy`
                withholds the full referring URL from the upstream.
              */
              // Fills the stage and preserves the source's own aspect handling;
              // the black stage behind it absorbs any letterboxing.
              className="absolute inset-0 size-full border-0"
            />

            {state.source.notice ? (
              <p className="absolute inset-x-0 bottom-0 bg-black/85 px-4 py-2 text-center text-[12px] font-medium text-white/85">
                {state.source.notice}
              </p>
            ) : null}
          </>
        ) : (
          <Placeholder state={state} />
        )}
      </div>
    </div>
  );
}

/* ── sub-components ──────────────────────────────────────────────────────── */

/** Only the non-ready phases reach the placeholder. */
type PlaceholderState = Extract<PlayerState, { phase: "resolving" | "unavailable" | "error" }>;

function Placeholder({ state }: { state: PlaceholderState }) {
  if (state.phase === "resolving") {
    return (
      <div className="absolute inset-0 grid place-items-center bg-black">
        <div className="text-center">
          <Loader2 className="mx-auto size-7 animate-spin text-white/70" />
          <p className="mt-3 text-sm font-medium text-white/70">Finding a server…</p>
        </div>
      </div>
    );
  }

  return (
    <div className="absolute inset-0 grid place-items-center bg-linear-to-br from-surface-2 to-black p-6 text-center">
      <div className="max-w-md">
        <ServerIcon className="mx-auto size-7 text-ink-faint" />
        <p className="mt-3 text-sm font-semibold text-ink">
          {state.phase === "error" ? "Playback failed" : "Nothing to play yet"}
        </p>
        <p className="mt-2 text-[13px] leading-relaxed text-ink-muted">{state.message}</p>
      </div>
    </div>
  );
}

/* ── helpers ─────────────────────────────────────────────────────────────── */

/**
 * Why a resolved URL must not be put in an `<iframe src>`, or `null` when it is
 * safe to load.
 *
 * Four distinct ways a template can go wrong are collapsed into one check so the
 * caller does not have to enumerate them:
 *
 *   1. not an absolute http(s) URL at all (`javascript:`, a relative path, …)
 *   2. an unsubstituted `{placeholder}` — a misspelled key such as `{tmdbId}` or
 *      `{tmdb-id}` is left verbatim by `renderTemplate`, because it only knows
 *      its own `TEMPLATE_KEYS`
 *   3. a value that serialised as a JS non-value: `undefined`, `null`, `NaN`
 *   4. an empty value where the path needs one, e.g. `/tv//` from a missing
 *      season, which the path collapses but a viewer would read as a season 0
 */
function embedUrlProblem(raw: string): string | null {
  let url: URL;
  try {
    url = new URL(raw);
  } catch {
    return "This server returned an unusable embed address. Please try another server.";
  }

  if (url.protocol !== "https:" && url.protocol !== "http:") {
    return "This server returned an unsafe embed address. Please try another server.";
  }

  if (/\{[^}]*\}/.test(raw)) {
    return "This server's address was not fully resolved. Please try another server.";
  }

  if (/\b(?:undefined|null|NaN)\b/i.test(raw)) {
    return "This server's address was incomplete. Please try another server.";
  }

  // `//` anywhere in the path means a segment was substituted with an empty
  // string. The query string is exempt: a provider may legitimately use an
  // empty-valued flag.
  if (url.pathname.includes("//")) {
    return "This server's address was incomplete. Please try another server.";
  }

    return null;
  }

