"use client";

import { useCallback, useState } from "react";

import { VideoPlayer } from "@/components/player/video-player";
import { ServerSelector } from "@/components/player/server-selector";
import { BackButton } from "@/components/player/back-button";
import { EpisodePanel } from "@/components/player/episode-panel";
import { useWatchStart } from "@/components/player/use-watch-start";
import { apiFetch } from "@/lib/client/api";
import { useSession } from "@/components/layout/session-provider";
import type { StreamingProviderDescriptor } from "@/services/providers/types";
import type { WatchKind, UserSettings } from "@/lib/types";

/**
 * Interactive half of the watch page.
 *
 * The page itself is a Server Component that has already fetched the title, the
 * episode list and the provider directory. Everything that reacts to a click —
 * the season selector, episode search, server switching, and the single
 * "started watching" record — lives here.
 *
 * The watch page is an isolated viewing surface, and that is enforced at three
 * levels:
 *
 *   1. There is no site chrome. `isCinematicRoute()` keeps both the top header
 *      and the bottom nav off this route, so the stage owns the whole viewport.
 *   2. Nothing on the page advertises the title or the surrounding catalogue.
 *      There is no title block, no My List, no Download, no Info and no detail
 *      link: a watch page is a place to watch something, not a page about it.
 *   3. The stage is a fixed, viewport-sized layer with `h-[100svh]` (so mobile
 *      browser chrome cannot crop it) and `overflow-hidden` (so nothing inside
 *      can introduce horizontal scroll). Letterboxing from the embed falls onto
 *      the stage's own black, not onto the page background.
 *
 * What remains is the player plus, for series only, the episode panel, and two
 * floating controls that overlay the video, stacked in one column on the left:
 * Back on top, the server selector beneath it.
 *
 * Back is a genuine "return to where I came from" control rather than a link to
 * a guessed destination — see `BackButton`. It does not contradict the
 * isolation: it is the one control whose whole purpose is to leave, and leaving
 * is what a visitor who opened the player from a search result expects to be
 * able to do.
 *
 * Episode changes go through the episode panel, which lists what is actually
 * published for the season. There is deliberately no previous/next pair: a
 * two-button strip along the bottom edge sits exactly where the provider's own
 * transport controls live, and it can only offer the episode either side of the
 * current one. The panel is the one place that shows the whole season, so it is
 * the only place an episode can be picked from.
 *
 * Stacking order, bottom to top:
 *   z-0   the iframe (inside `VideoPlayer`)
 *   z-20  Back and the server selector
 *   z-950 the episode panel, z-960 its toggle tab
 *   z-950 the episode panel, z-960 its toggle tab
 *   The provider's own controls live *inside* the iframe's own stacking context,
 *   so a control that needs to sit above them is painted here at z-20 — which is
 *   exactly why the stage creates no transform/filter on the iframe's ancestors.
 */


export interface EpisodeOption {
  season: number;
  episode: number;
  name: string;
  overview: string | null;
  stillPath: string | null;
  runtime: number | null;
}

export interface SeasonOption {
  season: number;
  name: string;
  episodeCount: number;
}

export function WatchClient({
  kind,
  tmdbId,
  title,
  posterPath,
  backdropPath,
  durationSeconds,
  providers,
  defaultProviderId,
  season,
  episode,
  episodes,
  seasons,
}: {
  kind: WatchKind;
  tmdbId: number;
  title: string;
  /**
   * Artwork for the Continue Watching card.
   *
   * A detail page's backdrop is the right choice over the show's poster because
   * the card is a 16:9 tile, and for an episode the series backdrop is a better
   * fit than any single episode's still. Both come from the Server Component, so
   * they are real TMDB paths rather than anything the client supplied.
   */
  posterPath?: string | null;
  backdropPath?: string | null;
  /** TMDB's runtime in seconds — a length, never a position. */
  durationSeconds?: number | null;
  providers: StreamingProviderDescriptor[];
  defaultProviderId: string | null;
  season: number;
  episode: number;
  /** Every episode in the current season. */
  episodes: EpisodeOption[];
  /** Every watchable season, for the season selector. */
  seasons?: SeasonOption[];
}) {
  const { settings, setSettings } = useSession();
  const [resolving, setResolving] = useState(true);

  // The server selection is owned here rather than inside `VideoPlayer` so the
  // `[ Server ▼ ]` control can float over the stage.
  const [providerId, setProviderId] = useState<string | null>(defaultProviderId);

  // Series and anime get the episode panel; a movie never does.
  const isSeries = kind !== "movie" && episodes.length > 0;
  const [panelOpen, setPanelOpen] = useState(false);

  /*
    The single write this page makes: "the visitor opened this episode".

    It fires from `onSourceReady`, which is the earliest moment a playable source
    genuinely exists — a resolved, validated URL about to load in the embed. There
    is no timer, no interval and no unload flush, because there is no playhead to
    sample: the page cannot read the embed's `currentTime`, and a wall clock would
    only measure how long the tab was open. See `use-watch-start.ts`.
  */
  const recordStart = useWatchStart({
    kind,
    tmdbId,
    season: isSeries ? season : null,
    episode: isSeries ? episode : null,
    title,
    posterPath,
    backdropPath,
    durationSeconds,
  });

  // Remember the server the visitor switched to, but never block playback on it.
  const onProviderChosen = useCallback(
    (chosen: string) => {
      if (!settings || settings.defaultStreamingProvider === chosen) return;
      void apiFetch<{ settings: UserSettings }>("/api/settings", {
        method: "PATCH",
        body: JSON.stringify({ defaultStreamingProvider: chosen }),
      })
        .then((next) => setSettings(next.settings))
        .catch(() => undefined);
    },
    [setSettings, settings],
  );

  const changeProvider = useCallback((id: string) => {
    setResolving(true);
    setProviderId(id);
  }, []);

  return (
    /*
      The stage. `h-[100svh] w-full` keeps the box in normal flow — so the page
      still has a document height for the back/forward cache — while the
      `.watch-stage` class in `globals.css` pins the visual surface to the
      viewport. Between the two, the video is exactly one screen tall and wide,
      with no margins, and nothing inside can produce a horizontal scrollbar.
    */
    <div className="watch-stage h-[100svh] w-full overflow-hidden bg-black text-white">
      {/* ── the video, filling the whole stage ─────────────────────────────── */}
      <div className="absolute inset-0 z-0">
        <VideoPlayer
          kind={kind}
          tmdbId={tmdbId}
          season={kind === "movie" ? null : season}
          episode={kind === "movie" ? null : episode}
          title={title}
          providerId={providerId}
          onProviderChosen={onProviderChosen}
          onResolvingChange={setResolving}
          onSourceReady={recordStart}
        />
      </div>

      {/*
        Top scrim: keeps the floating server selector legible over a bright frame
        without adding a solid bar that would shrink the video.
      */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-x-0 top-0 z-10 h-24 bg-linear-to-b from-black/80 via-black/30 to-transparent"
      />

      {/*
        The left-hand control stack: Back, with the server selector directly
        beneath it.

        One column rather than two floating corners, so the two controls that
        belong together read as one group: leave, then choose a source. The
        selector's list opens downward from here and is left-aligned to the
        trigger, which is why it does not need to know anything about the right
        side of the stage.

        `pointer-events-auto` because the top scrim is `pointer-events-none` so it
        cannot swallow clicks meant for the video underneath it — it spans the
        full width, so without this the whole top-left corner would be dead.
      */}
      <div className="pointer-events-auto absolute left-3 top-3 z-20 flex flex-col items-start gap-2 sm:left-5 sm:top-4">
        <BackButton />

        {/* ── server selector, only when servers exist ────────────────────── */}
        {/*
          `ServerSelector` returns `null` for an empty directory, so a deployment
          with no configured servers shows no server UI at all rather than an empty
          badge. The provider's own "nothing to play" message is rendered inside the
          player area instead.
        */}
        {providers.length > 0 ? (
          <ServerSelector
            providers={providers}
            value={providerId}
            onChange={changeProvider}
            busy={resolving}
          />
        ) : null}
      </div>

      {/* ── episode panel: series only ────────────────────────────────────── */}
      {isSeries ? (
        <EpisodePanel
          open={panelOpen}
          onToggle={setPanelOpen}
          tmdbId={tmdbId}
          title={title}
          season={season}
          episode={episode}
          episodes={episodes}
          seasons={seasons}
        />
      ) : null}
    </div>
  );
}
