import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react";
import { useTranslation } from "react-i18next";
import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowLeft, CheckCircle2, Info, ListPlus, Play } from "lucide-react";
import {
  api,
  EMPTY_PLEX_FILTERS,
  plexFilterCount,
  type PlexCard,
  type PlexFilters,
  type PlexPerson,
} from "../lib/api";
import { useDebounced } from "../lib/useDebounced";
import { useHistorySubview } from "../lib/history";
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";

// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
const PlexPlayer = lazy(() => import("./PlexPlayer"));
const PlexInfo = lazy(() => import("./PlexInfo"));
const PlexPlaylistView = lazy(() => import("./PlexPlaylistView"));

type Sub =
  | { kind: "grid" }
  | { kind: "show"; id: string }
  | { kind: "player"; id: string; queue?: string[] }
  | { kind: "info"; id: string }
  | { kind: "playlist"; id: number };

// The Plex module's content area: browse/search the mirrored library as poster cards, drill from a
// show into seasons/episodes. Library scope / watch-state / sort live in the left PlexSidebar; the
// search term comes from the shared top Header search box (q). A show drill-down rides history
// (useHistorySubview) so browser Back returns to the grid. Playback lands in P2 — a card announces
// for now. Rendered by App for page==="plex" (its own nav module).

type Props = {
  q: string;
  onClearSearch: () => void;
  library: string;
  show: string;
  sort: string;
  filters: PlexFilters;
  setFilters: (f: PlexFilters) => void;
  // The sidebar opens a playlist by setting this; PlexBrowse turns it into a history subview + clears it.
  openPlaylist?: number | null;
  onPlaylistOpened?: () => void;
};

const PAGE = 40;

function dur(n?: number | null): string {
  if (!n) return "";
  const h = Math.floor(n / 3600);
  const m = Math.floor((n % 3600) / 60);
  return h ? `${h}h ${m}m` : `${m}m`;
}

export default function PlexBrowse({
  q,
  onClearSearch,
  library,
  show,
  sort,
  filters,
  setFilters,
  openPlaylist,
  onPlaylistOpened,
}: Props) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const dq = useDebounced(q.trim(), 350);
  const sub = useHistorySubview<Sub>({ kind: "grid" });

  // Sidebar asked to open a playlist → push it as a subview (browser Back returns to the grid), then
  // clear the signal so it doesn't re-open.
  useEffect(() => {
    if (openPlaylist != null) {
      sub.open({ kind: "playlist", id: openPlaylist });
      onPlaylistOpened?.();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [openPlaylist]);

  // Opening a show/player replaces the grid entirely (the component returns early below), so the
  // scroll container resets to the top. Remember where the grid was scrolled when leaving it, and
  // restore it when we come back — so the browser/mouse Back from the player lands on the same card
  // instead of the top of the library. The scroller is App's <main> (the page's overflow-y-auto).
  const scrollRef = useRef(0);
  // Same idea for the info page: "Browse collection" (onFilter) leaves the info view for a filtered
  // grid, and browser Back returns to this same info page — restore where the user had scrolled it
  // (down to a collection strip) instead of snapping to the top. Reset on a fresh info open so a
  // different title's info starts at the top.
  const infoScrollRef = useRef(0);
  const scroller = () => document.querySelector("main");
  useLayoutEffect(() => {
    const el = scroller();
    if (!el) return;
    if (sub.view.kind === "grid" && scrollRef.current) el.scrollTop = scrollRef.current;
    else if (sub.view.kind === "info" && infoScrollRef.current) el.scrollTop = infoScrollRef.current;
  }, [sub.view.kind]);

  const browseQ = useInfiniteQuery({
    queryKey: ["plex-browse", library, dq, sort, show, filters],
    enabled: !!library && sub.view.kind === "grid",
    initialPageParam: 0,
    queryFn: ({ pageParam }) =>
      api.plexBrowse({
        library,
        q: dq || undefined,
        sort,
        show,
        filters,
        offset: pageParam as number,
        limit: PAGE,
      }),
    getNextPageParam: (last) => {
      const seen = last.offset + last.items.length;
      return seen < last.total ? seen : undefined;
    },
  });

  const items = browseQ.data?.pages.flatMap((p) => p.items) ?? [];
  const total = browseQ.data?.pages[0]?.total ?? 0;

  // Cast/crew whose name matches the current search → virtual cards above the grid; clicking one
  // adds that person to the filter. Only meaningful for movie libraries (the endpoint returns [] else).
  const peopleQ = useQuery({
    queryKey: ["plex-people", library, dq],
    queryFn: () => api.plexPeople(library, dq),
    enabled: !!library && dq.length >= 2 && sub.view.kind === "grid",
  });
  const people = peopleQ.data?.people ?? [];
  function addPerson(p: PlexPerson) {
    const key = p.kind === "director" ? "directors" : "actors";
    const cur = filters[key];
    if (!cur.includes(p.name)) setFilters({ ...filters, [key]: [...cur, p.name] });
    onClearSearch(); // switch from the name search to the person filter (clears the search box)
  }

  // Infinite scroll: auto-load the next page when the sentinel scrolls into view.
  const sentinel = useRef<HTMLDivElement>(null);
  const { hasNextPage, isFetchingNextPage, fetchNextPage } = browseQ;
  useEffect(() => {
    const el = sentinel.current;
    if (!el) return;
    const io = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
      },
      { rootMargin: "600px" },
    );
    io.observe(el);
    return () => io.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  function onCard(card: PlexCard) {
    scrollRef.current = scroller()?.scrollTop ?? 0; // remember grid position for the trip back
    if (card.type === "show") sub.open({ kind: "show", id: card.id });
    else sub.open({ kind: "player", id: card.id });
  }
  function onInfo(card: PlexCard) {
    scrollRef.current = scroller()?.scrollTop ?? 0;
    infoScrollRef.current = 0; // fresh info page starts at the top
    sub.open({ kind: "info", id: card.id });
  }
  async function toggleWatched(card: PlexCard) {
    await api.plexSetState(card.id, card.status === "watched" ? "new" : "watched");
    qc.invalidateQueries({ queryKey: ["plex-browse"] });
  }

  if (sub.view.kind === "player") {
    return (
      <Suspense fallback={<div className="fixed inset-0 z-50 bg-black" />}>
        <PlexPlayer itemId={sub.view.id} queue={sub.view.queue} onClose={sub.back} />
      </Suspense>
    );
  }
  if (sub.view.kind === "playlist") {
    const pid = sub.view.id;
    return (
      <Suspense fallback={<p className="p-8 text-muted text-sm">{t("plex.loading")}</p>}>
        <PlexPlaylistView
          playlistId={pid}
          onBack={sub.back}
          onPlay={(itemRk, queue) => sub.open({ kind: "player", id: itemRk, queue })}
        />
      </Suspense>
    );
  }
  if (sub.view.kind === "info") {
    const infoId = sub.view.id;
    return (
      <PlexInfoView
        id={infoId}
        library={library}
        onBack={sub.back}
        onPlay={() => sub.open({ kind: "player", id: infoId })}
        onPlayItem={(id) => sub.open({ kind: "player", id })}
        onFilter={(patch) => {
          // Clicking a metadata chip sets that filter and shows the filtered grid. Array filters
          // (genres/people/studios) UNION with what's already set (so you can stack people); scalars
          // replace. We push a fresh grid entry (not history.back) so browser Back returns to this
          // info page instead of leaving the Plex module.
          const merged = { ...filters } as unknown as Record<string, unknown>;
          for (const [k, v] of Object.entries(patch)) {
            const cur = merged[k];
            if (Array.isArray(v) && Array.isArray(cur)) {
              merged[k] = Array.from(new Set([...(cur as unknown[]), ...(v as unknown[])]));
            } else {
              merged[k] = v;
            }
          }
          setFilters(merged as unknown as PlexFilters);
          infoScrollRef.current = scroller()?.scrollTop ?? 0; // restore on Back to this info page
          sub.open({ kind: "grid" });
        }}
      />
    );
  }
  if (sub.view.kind === "show") {
    return (
      <PlexShowView
        showId={sub.view.id}
        onBack={sub.back}
        onPlay={(ep) => sub.open({ kind: "player", id: ep.id })}
      />
    );
  }

  return (
    <div className="p-4 max-w-[1600px] mx-auto">
      <p className="text-xs text-muted mb-3">
        {browseQ.isLoading ? " " : dq ? t("plex.searchCount", { count: total }) : t("plex.count", { count: total })}
      </p>

      {/* Virtual person cards for a name search — click to add them to the filter. */}
      {people.length > 0 && (
        <div className="mb-4">
          <p className="text-[11px] uppercase tracking-wide text-muted mb-2">{t("plex.people.match")}</p>
          <div className="flex flex-wrap gap-2">
            {people.map((p) => {
              const added = (p.kind === "director" ? filters.directors : filters.actors).includes(p.name);
              return (
                <button
                  key={`${p.kind}:${p.name}`}
                  onClick={() => addPerson(p)}
                  disabled={added}
                  className={`flex items-center gap-2.5 rounded-xl border p-1.5 pr-3 text-left transition ${
                    added ? "border-accent bg-accent/10" : "border-border bg-card hover:border-accent"
                  }`}
                >
                  <div className="h-11 w-11 shrink-0 overflow-hidden rounded-full border border-border bg-surface">
                    {p.photo ? (
                      <img src={p.photo} alt="" className="h-full w-full object-cover" />
                    ) : (
                      <div className="grid h-full w-full place-items-center text-sm opacity-40">
                        {p.name.charAt(0)}
                      </div>
                    )}
                  </div>
                  <div className="min-w-0">
                    <div className="truncate text-sm font-medium">{p.name}</div>
                    <div className="text-xs text-muted">
                      {t(`plex.people.${p.kind}`)} · {t("plex.people.count", { count: p.count })}
                      {added ? ` · ${t("plex.people.added")}` : ""}
                    </div>
                  </div>
                </button>
              );
            })}
          </div>
        </div>
      )}

      {browseQ.isLoading ? (
        <p className="text-muted text-sm">{t("plex.loading")}</p>
      ) : items.length === 0 ? (
        dq && plexFilterCount(filters) > 0 ? (
          // A search that comes up empty WHILE filters are active is usually the filters, not the
          // query — say so and offer a one-click escape, instead of a bare "No matches".
          <div className="flex flex-wrap items-center gap-3 text-sm text-muted">
            <span>{t("plex.noMatchesFiltered", { count: plexFilterCount(filters) })}</span>
            <button
              onClick={() => setFilters(EMPTY_PLEX_FILTERS)}
              className="rounded-full border border-border bg-card px-3 py-1 text-xs font-medium transition hover:border-accent hover:text-accent"
            >
              {t("plex.filter.clearAll")}
            </button>
          </div>
        ) : (
          <p className="text-muted text-sm">{dq ? t("plex.noMatches") : t("plex.empty")}</p>
        )
      ) : (
        <div className="grid grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-3">
          {items.map((c) => (
            <PlexPosterCard
              key={c.id}
              card={c}
              onOpen={() => onCard(c)}
              onInfo={() => onInfo(c)}
              onToggleWatched={toggleWatched}
            />
          ))}
        </div>
      )}

      <div ref={sentinel} className="h-8" />
      {isFetchingNextPage && <p className="text-center text-xs text-muted pb-4">{t("plex.loading")}</p>}
    </div>
  );
}

const PLAYABLE_TINT: Record<string, string> = {
  direct: "bg-emerald-500",
  remux: "bg-amber-500",
  transcode: "bg-orange-600",
};

function PlexPosterCard({
  card,
  onOpen,
  onInfo,
  onToggleWatched,
}: {
  card: PlexCard;
  onOpen: () => void;
  onInfo: () => void;
  onToggleWatched: (c: PlexCard) => void;
}) {
  const { t } = useTranslation();
  const inProgress = (card.position_seconds ?? 0) > 0 && card.status !== "watched";
  const isPlayable = card.type !== "show"; // movies/episodes play; shows open the drill-down
  const pct =
    inProgress && card.duration_seconds
      ? Math.min(100, Math.round(((card.position_seconds ?? 0) / card.duration_seconds) * 100))
      : 0;
  return (
    <div
      className="group text-left"
      // content-visibility keeps off-screen posters cheap to render (smoother long-list scroll).
      style={{ contentVisibility: "auto", containIntrinsicSize: "auto 300px" } as CSSProperties}
    >
      <div
        onClick={onOpen}
        role="button"
        tabIndex={0}
        onKeyDown={(e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            onOpen();
          }
        }}
        className="relative aspect-[2/3] rounded-xl overflow-hidden bg-card border border-border cursor-pointer"
      >
        <img
          src={card.thumb}
          alt=""
          loading="lazy"
          decoding="async"
          className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
        />
        {/* Hover affordance: what a click does (play / resume / open show). */}
        <div className="absolute inset-0 bg-black/45 opacity-0 group-hover:opacity-100 transition grid place-items-center">
          {isPlayable ? (
            <div className="flex flex-col items-center gap-1 text-white">
              <Play className="w-10 h-10" fill="currentColor" />
              <span className="text-xs font-medium">{inProgress ? t("plex.resume") : t("plex.play")}</span>
            </div>
          ) : (
            <span className="text-white text-xs font-medium">{t("plex.openShow")}</span>
          )}
        </div>
        {/* Quick watched toggle (movies/episodes), on hover. */}
        {isPlayable && (
          <button
            onClick={(e) => {
              e.stopPropagation();
              onToggleWatched(card);
            }}
            title={card.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
            className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition"
          >
            <CheckCircle2 className={`w-4 h-4 ${card.status === "watched" ? "text-emerald-400" : "text-white"}`} />
          </button>
        )}
        {/* Media info page (movies/episodes). */}
        {isPlayable && (
          <button
            onClick={(e) => {
              e.stopPropagation();
              onInfo();
            }}
            title={t("plex.info.title")}
            className="absolute bottom-1.5 left-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition"
          >
            <Info className="w-4 h-4 text-white" />
          </button>
        )}
        {/* Watched badge when not hovering (the toggle replaces it on hover). */}
        {card.status === "watched" && (
          <span className="absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded group-hover:opacity-0">
            {t("plex.watched")}
          </span>
        )}
        {card.type === "show" && card.season_count != null && (
          <span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded">
            {t("plex.seasons", { count: card.season_count })}
          </span>
        )}
        {card.playable && (
          <span
            className={`absolute top-1.5 left-1.5 w-2 h-2 rounded-full ${PLAYABLE_TINT[card.playable] ?? "bg-gray-400"}`}
            title={t(`plex.playable.${card.playable}`)}
          />
        )}
        {pct > 0 && (
          <div className="absolute bottom-0 inset-x-0 h-1 bg-black/40">
            <div className="h-full bg-accent" style={{ width: `${pct}%` }} />
          </div>
        )}
      </div>
      <div className="mt-1.5 text-sm font-medium line-clamp-2 leading-tight">{card.title}</div>
      <div className="text-xs text-muted">{[card.year, dur(card.duration_seconds)].filter(Boolean).join(" · ")}</div>
    </div>
  );
}

function PlexInfoView({
  id,
  library,
  onBack,
  onPlay,
  onPlayItem,
  onFilter,
}: {
  id: string;
  library: string;
  onBack: () => void;
  onPlay: () => void;
  onPlayItem: (id: string) => void;
  onFilter: (patch: Partial<PlexFilters>) => void;
}) {
  const { t } = useTranslation();
  const q = useQuery({ queryKey: ["plex-item", id], queryFn: () => api.plexItem(id) });
  return (
    <div className="w-[90%] max-w-[1600px] mx-auto">
      <div className="p-4 pb-0">
        <button
          onClick={onBack}
          className="glass-card glass-hover inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs"
        >
          <ArrowLeft className="w-4 h-4" />
          {t("plex.backToLibrary")}
        </button>
      </div>
      {q.isLoading || !q.data ? (
        <p className="p-8 text-muted text-sm">{t("plex.loading")}</p>
      ) : (
        <Suspense fallback={<p className="p-8 text-muted text-sm">{t("plex.loading")}</p>}>
          <PlexInfo
            detail={q.data}
            variant="page"
            library={library}
            onPlay={onPlay}
            onPlayItem={onPlayItem}
            onFilter={onFilter}
            onStateChange={() => q.refetch()}
          />
        </Suspense>
      )}
    </div>
  );
}

function PlexShowView({
  showId,
  onBack,
  onPlay,
}: {
  showId: string;
  onBack: () => void;
  onPlay: (c: PlexCard) => void;
}) {
  const { t } = useTranslation();
  const q = useQuery({ queryKey: ["plex-show", showId], queryFn: () => api.plexShow(showId) });
  const d = q.data;
  // "Add to playlist" dialog target (single episode / whole season / whole show); null = closed.
  const [addTarget, setAddTarget] = useState<PlexAddTarget | null>(null);
  const allEpisodeKeys = (d?.seasons ?? []).flatMap((se) => se.episodes.map((e) => e.id));

  return (
    <div className="p-4 max-w-[1200px] mx-auto">
      <button
        onClick={onBack}
        className="glass-card glass-hover inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs mb-4"
      >
        <ArrowLeft className="w-4 h-4" />
        {t("plex.backToLibrary")}
      </button>

      {q.isLoading || !d ? (
        <p className="text-muted text-sm">{t("plex.loading")}</p>
      ) : (
        <>
          <div className="flex gap-4 mb-6">
            <img
              src={d.show.thumb}
              alt=""
              className="w-32 sm:w-40 aspect-[2/3] object-cover rounded-xl border border-border shrink-0"
            />
            <div className="min-w-0">
              <h1 className="text-xl font-semibold">{d.show.title}</h1>
              {d.show.year && <p className="text-sm text-muted">{d.show.year}</p>}
              {d.show.summary && (
                <p className="text-sm text-muted mt-2 line-clamp-6 leading-relaxed">{d.show.summary}</p>
              )}
              {allEpisodeKeys.length > 0 && (
                <button
                  onClick={() => setAddTarget({ kind: "group", ratingKeys: allEpisodeKeys, title: d.show.title })}
                  className="glass-card glass-hover mt-3 inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium"
                >
                  <ListPlus className="w-4 h-4" />
                  {t("plex.playlist.addShow")}
                </button>
              )}
            </div>
          </div>

          {d.seasons.map((se) => (
            <div key={se.id} className="mb-6">
              <div className="mb-2 flex items-center gap-2">
                <h2 className="text-sm font-semibold">{se.title}</h2>
                {se.episodes.length > 0 && (
                  <button
                    onClick={() =>
                      setAddTarget({
                        kind: "group",
                        ratingKeys: se.episodes.map((e) => e.id),
                        title: `${d.show.title} — ${se.title}`,
                      })
                    }
                    title={t("plex.playlist.addSeason")}
                    aria-label={t("plex.playlist.addSeason")}
                    className="rounded p-1 text-muted hover:text-accent"
                  >
                    <ListPlus className="w-4 h-4" />
                  </button>
                )}
              </div>
              <div className="flex flex-col divide-y divide-border/60">
                {se.episodes.map((ep) => {
                  const inProgress = (ep.position_seconds ?? 0) > 0 && ep.status !== "watched";
                  return (
                    <div key={ep.id} className="flex items-center gap-3 py-2 group">
                      <button onClick={() => onPlay(ep)} className="flex min-w-0 flex-1 items-center gap-3 text-left">
                        <div className="relative w-28 aspect-video rounded-lg overflow-hidden bg-card border border-border shrink-0">
                          <img src={ep.thumb} alt="" loading="lazy" className="w-full h-full object-cover" />
                          {ep.status === "watched" && (
                            <span className="absolute top-1 right-1 text-[9px] bg-black/70 text-white px-1 rounded">
                              ✓
                            </span>
                          )}
                          {inProgress && <div className="absolute bottom-0 inset-x-0 h-0.5 bg-accent" />}
                        </div>
                        <div className="min-w-0 flex-1">
                          <div className="text-sm font-medium truncate">
                            <span className="text-muted mr-1.5">{ep.episode_number}.</span>
                            {ep.title}
                          </div>
                          {ep.summary && (
                            <div className="text-xs text-muted line-clamp-2 mt-0.5">{ep.summary}</div>
                          )}
                          <div className="text-[11px] text-muted mt-0.5">{dur(ep.duration_seconds)}</div>
                        </div>
                      </button>
                      <button
                        onClick={() => setAddTarget({ kind: "single", ratingKey: ep.id, title: ep.title })}
                        title={t("plex.playlist.addEpisode")}
                        aria-label={t("plex.playlist.addEpisode")}
                        className="shrink-0 rounded p-1.5 text-muted opacity-0 transition hover:text-accent group-hover:opacity-100"
                      >
                        <ListPlus className="w-4 h-4" />
                      </button>
                    </div>
                  );
                })}
              </div>
            </div>
          ))}
        </>
      )}

      {addTarget && <PlexPlaylistAdd target={addTarget} onClose={() => setAddTarget(null)} />}
    </div>
  );
}
