"use client";

import { useCallback, useState } from "react";
import { Download, Loader2, Server } from "lucide-react";

import { DropdownMenu, type DropdownItem } from "@/components/ui/dropdown";
import { apiFetch, ApiRequestError } from "@/lib/client/api";
import { cn } from "@/lib/utils";
import type { WatchKind } from "@/lib/types";

/**
 * Download button.
 *
 * FlixTV does not proxy, re-sign or otherwise circumvent a third-party
 * download protection. It asks the server which authorised destination the
 * operator configured — via `POST /api/providers/download`, which reads
 * `config/download-services.ts` and the signed-in visitor's preferred download
 * provider — and hands that URL to the browser, which opens it in a new tab.
 *
 * **No URL is ever built here.** The component sends `kind` + `tmdbId` (+ season
 * and episode for series) and receives a finished URL back. All template
 * rendering lives in `services/providers`, so the detail page, the API and the
 * watch page can never disagree about how a link is built.
 *
 * With one configured source the click goes straight there. With several, the
 * click opens a menu of provider names and the visitor's choice is passed to the
 * server as a `providerId`, which resolves *that* provider specifically.
 */

/**
 * The serialisable slice of a download descriptor.
 *
 * Declared here rather than imported from `services/providers/downloads` so the
 * client bundle can never reach a `server-only` module, and so this component
 * depends on exactly the three fields it renders.
 */
export interface DownloadProviderOption {
  id: string;
  name: string;
  badge?: string;
}

/**
 * The Download trigger's styling, shared by the movie control and the TV modal's
 * trigger so the two cannot drift apart visually.
 *
 * Extracted as a function rather than a constant because the size is a prop, and
 * it carries the same reasoning the movie button has always documented: these
 * classes mirror `AddToListButton`'s `size="lg"` so the Download button is
 * pixel-identical to the "+ My List" button it sits beside. It is deliberately
 * not routed through the `Button` primitive, whose `lg` size is tuned for the
 * wider "Watch now" primary action and would be visibly taller.
 */
export function downloadTriggerClass(size: "lg" | "icon", className?: string): string {
  return cn(
    "inline-flex items-center justify-center rounded-full font-semibold",
    "ring-1 ring-white/20 backdrop-blur-sm transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]",
    "bg-black/55 text-white hover:bg-white/92 hover:text-black",
    "active:scale-95 disabled:opacity-60",
    size === "lg" && "h-11 gap-2 px-4 text-sm",
    size === "icon" && "size-10",
    className,
  );
}

interface DownloadResponse {
  available: boolean;
  providers: unknown[];
  message?: string;
  source?: { url: string; providerId: string; providerName: string } | null;
}

export function DownloadButton({
  kind,
  tmdbId,
  season,
  episode,
  defaultProviderId,
  providers,
  size = "lg",
  className,
}: {
  kind: WatchKind;
  tmdbId: number;
  season?: number | null;
  episode?: number | null;
  defaultProviderId?: string | null;
  /**
   * The download sources configured for this kind of title, resolved on the
   * server by the page. Zero disables the control, one opens directly, several
   * show a picker.
   */
  providers: DownloadProviderOption[];
  /**
   * `lg` is the detail-page action pill: a labelled button that sits inline
   * beside "Watch now" and "+ My List". `icon` is the square icon-only form.
   */
  size?: "lg" | "icon";
  className?: string;
}) {
  const [pending, setPending] = useState(false);
  const [message, setMessage] = useState<string | null>(null);
  const [tone, setTone] = useState<"error" | "info">("info");

  /*
    Guards, applied before anything is sent.

    `tmdbId` is the only value that reaches a URL, so it is the only thing that
    could produce a link containing "undefined". A non-integer or non-positive id
    means the page was handed a title it could not really resolve, and the honest
    result is a disabled control rather than a plausible-looking dead link.

    Series additionally require a season *and* an episode: the TV templates are
    `.../tv/{tmdb_id}/{season}/{episode}` and `TemplateStreamingProvider` refuses
    to render without both. Checking here tells the visitor the episode is unknown
    instead of letting the server reject the request afterwards.
  */
  const hasTmdbId = Number.isInteger(tmdbId) && tmdbId > 0;
  const isSeries = kind !== "movie";
  const hasEpisode =
    !isSeries ||
    (Number.isInteger(season) && season! > 0 && Number.isInteger(episode) && episode! > 0);

  const configured = providers.length > 0;
  const canDownload = hasTmdbId && hasEpisode && configured;

  const unavailableHint = !configured
    ? "Download unavailable: no download server is configured"
    : !hasTmdbId
      ? "Download unavailable: this title has no TMDB id"
      : "Download unavailable: pick a season and episode first";

  const showMenu = providers.length > 1;

  /** Ask the server for a URL, optionally pinned to one provider, and open it. */
  const resolveAndOpen = useCallback(
    async (providerId: string) => {
      const data = await apiFetch<DownloadResponse>("/api/providers/download", {
        method: "POST",
        body: JSON.stringify({
          kind,
          tmdbId,
          season: season ?? undefined,
          episode: episode ?? undefined,
          providerId,
        }),
      });

      if (!data.available || !data.source?.url) {
        setTone("info");
        setMessage(data.message ?? "No download destination is configured for this title.");
        return;
      }

      // New tab, and `noopener` so the destination can never reach back into
      // FlixTV's window. This is the project's existing download behaviour.
      window.open(data.source.url, "_blank", "noopener,noreferrer");
    },
    [episode, kind, season, tmdbId],
  );

  const start = async (providerId: string) => {
    if (pending || !canDownload) return;
    setPending(true);
    setMessage(null);
    try {
      await resolveAndOpen(providerId);
    } catch (error) {
      setTone("error");
      setMessage(
        error instanceof ApiRequestError
          ? error.message
          : "Could not reach the download service.",
      );
    } finally {
      setPending(false);
    }
  };

  /*
    With a single source there is nothing to choose, so the button resolves and
    opens immediately. An explicit `defaultProviderId` (the visitor's saved
    preference) wins when it is one of the configured sources; otherwise the
    first configured source is used, which is the same order the settings page
    lists them in.
  */
  const only = providers[0];
  const directProviderId =
    defaultProviderId && providers.some((p) => p.id === defaultProviderId)
      ? defaultProviderId
      : (only?.id ?? "");

  const items: DropdownItem[] = providers.map((p) => ({
    id: p.id,
    label: p.name,
    icon: <Server className="size-4" />,
    hint: p.badge,
    onSelect: () => void start(p.id),
  }));

  const trigger = (toggle?: () => void, open?: boolean) => (
    <button
      type="button"
      onClick={() => {
        // Open the picker when there is a choice to make; otherwise go direct.
        if (showMenu) toggle?.();
        else void start(directProviderId);
      }}
      disabled={pending || !canDownload}
      aria-haspopup={showMenu ? "menu" : undefined}
      aria-expanded={showMenu ? Boolean(open) : undefined}
      aria-label="Download"
      aria-busy={pending}
      title={canDownload ? "Download" : unavailableHint}
      className={downloadTriggerClass(size, className)}
    >

      {/*
        The label stays "Download" in every state. Swapping it for "Preparing…"
        would make the control resize mid-click and would leave a label in the
        DOM that is not the one the visitor aimed at; the spinner plus
        `aria-busy` already say that work is in flight.
      */}
      {pending ? (
        <Loader2 className="size-4 shrink-0 animate-spin" />
      ) : (
        <Download className="size-4 shrink-0" />
      )}
      {size === "lg" ? <span>Download</span> : null}
    </button>
  );

  /*
    The message is absolutely positioned rather than stacked in flow.

    The detail hero's action row is `flex items-center`, so a message that took up
    layout space would grow the row and push "Watch now" and "+ My List" out of
    alignment the moment a download failed. Anchoring it to the button keeps the
    row exactly one button high in every state.
  */
  return (
    <div className="relative inline-flex">
      {showMenu ? (
        <DropdownMenu
          label="Download providers"
          align="start"
          items={items}
          className="pointer-events-auto"
          panelClassName="min-w-[220px]"
          header={
            <p className="text-[11px] font-bold uppercase tracking-[0.14em] text-ink-faint">
              Download
            </p>
          }
        >
          {({ toggle, open }) => trigger(toggle, open)}
        </DropdownMenu>
      ) : (
        trigger()
      )}

      {message ? (
        <p
          role="status"
          className={cn(
            "absolute top-full left-0 z-10 mt-2 w-max max-w-[220px] text-[11px] leading-snug",
            tone === "error" ? "text-brand-400" : "text-ink-muted",
          )}
        >
          {message}
        </p>
      ) : null}
    </div>
  );
}
