import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowDown, ArrowUp, RefreshCw } from "lucide-react";
import { api, type FeedFilters, type Video } from "../lib/api";
import i18n from "../i18n";
import { notify, resolveVideo } from "../lib/notifications";
import VideoCard from "./VideoCard";
import PlayerModal from "./PlayerModal";

const PAGE = 60;

// The "Show" view filter, content-type filter and ordering live in the feed toolbar
// (above the cards), not the filter sidebar.
const SHOW_IDS = ["unwatched", "in_progress", "all", "watched", "hidden"];
const CONTENT = [
  { key: "includeNormal", label: "sidebar.content.normal" },
  { key: "includeShorts", label: "sidebar.content.shorts" },
  { key: "includeLive", label: "sidebar.content.live" },
] as const;
const rollSeed = () => Math.floor(Math.random() * 1_000_000_000);

// Ordering = a key + direction (like the Playlists page), mapped to the backend sort strings
// (which encode both). One entry per concept; a single arrow flips the direction.
type SortKey = "date" | "popular" | "duration" | "title" | "subscribers" | "priority" | "shuffle";
const SORT_KEYS: SortKey[] = ["date", "popular", "duration", "title", "subscribers", "priority", "shuffle"];
const SORT_MAP: Record<Exclude<SortKey, "shuffle">, { asc: string; desc: string }> = {
  date: { desc: "newest", asc: "oldest" },
  popular: { desc: "views", asc: "views_asc" },
  duration: { desc: "duration_desc", asc: "duration_asc" },
  title: { asc: "title", desc: "title_desc" },
  subscribers: { desc: "subscribers", asc: "subscribers_asc" },
  priority: { desc: "priority", asc: "priority_asc" },
};
const SORT_DEFAULT_DIR: Record<Exclude<SortKey, "shuffle">, "asc" | "desc"> = {
  date: "desc", popular: "desc", duration: "desc", title: "asc", subscribers: "desc", priority: "desc",
};
function parseSort(s: string): { key: SortKey; dir: "asc" | "desc" } {
  if (s === "shuffle") return { key: "shuffle", dir: "desc" };
  for (const k of Object.keys(SORT_MAP) as (keyof typeof SORT_MAP)[]) {
    if (SORT_MAP[k].asc === s) return { key: k, dir: "asc" };
    if (SORT_MAP[k].desc === s) return { key: k, dir: "desc" };
  }
  return { key: "date", dir: "desc" };
}
function buildSort(key: SortKey, dir: "asc" | "desc"): string {
  return key === "shuffle" ? "shuffle" : SORT_MAP[key][dir];
}

function matchesView(status: string, show: string): boolean {
  switch (show) {
    case "hidden":
      return status === "hidden";
    case "watched":
      return status === "watched";
    case "unwatched":
    case "in_progress":
      // (in_progress is further narrowed server-side by resume position; here we only
      // need to drop a card once it's optimistically marked watched/hidden.)
      return status !== "watched" && status !== "hidden";
    default:
      return status !== "hidden"; // all
  }
}

export default function Feed({
  filters,
  setFilters,
  view,
  canRead,
  isDemo = false,
  onOpenWizard,
}: {
  filters: FeedFilters;
  setFilters: (f: FeedFilters) => void;
  view: "grid" | "list";
  canRead: boolean;
  isDemo?: boolean;
  onOpenWizard: () => void;
}) {
  const { t } = useTranslation();
  const [overrides, setOverrides] = useState<Record<string, string>>({});
  const [savedOverrides, setSavedOverrides] = useState<Record<string, boolean>>({});
  // The open player: which video and where to start (null = resume from saved position).
  const [activeVideo, setActiveVideo] = useState<{ video: Video; startAt: number | null } | null>(
    null
  );
  const qc = useQueryClient();

  const openVideo = useCallback(
    (video: Video, startAt: number | null = null) => setActiveVideo({ video, startAt }),
    []
  );

  const query = useInfiniteQuery({
    queryKey: ["feed", filters],
    queryFn: ({ pageParam }) => api.feed(filters, pageParam as number, PAGE),
    initialPageParam: 0,
    getNextPageParam: (last) => (last.has_more ? last.offset + last.limit : undefined),
  });

  // Drop optimistic status overrides when filters change OR fresh server data
  // arrives — after a refetch the server is authoritative, so a stale override
  // (e.g. a video reverted to "new" from the notification center) won't keep it
  // filtered out of the current view.
  useEffect(() => {
    setOverrides({});
    setSavedOverrides({});
  }, [filters]);
  useEffect(() => {
    setOverrides({});
    setSavedOverrides({});
  }, [query.dataUpdatedAt]);

  const countQuery = useQuery({
    queryKey: ["feed-count", filters],
    queryFn: () => api.feedCount(filters),
    staleTime: 30_000,
  });

  const sentinel = useRef<HTMLDivElement | null>(null);
  const { hasNextPage, isFetchingNextPage, fetchNextPage } = query;
  useEffect(() => {
    const el = sentinel.current;
    if (!el) return;
    const io = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { rootMargin: "1500px" } // prefetch the next page well before the bottom is in view
    );
    io.observe(el);
    return () => io.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  // Keep the loaded videos in a ref so onState can stay referentially stable
  // (it needs the list only to look up a title for the notification). A stable
  // onState lets memo(VideoCard) skip re-rendering existing cards on page append.
  const loadedRef = useRef<Video[]>([]);

  const onState = useCallback(
    (id: string, status: string) => {
      setOverrides((o) => ({ ...o, [id]: status }));
      // Refetch once the server has the change so other views (e.g. Hidden) are in sync.
      // Announce the change only AFTER the server confirms it: a "Marked watched"/"Hidden"
      // notice (or resolving a stale one) is a claim of success, so firing it optimistically
      // would falsely report a change that the .catch below is about to roll back (e.g. when
      // the API is unreachable).
      api
        .setState(id, status)
        .then(() => {
          qc.invalidateQueries({ queryKey: ["feed"] });
          if (status === "hidden") {
            const v = loadedRef.current.find((x) => x.id === id);
            notify({
              message: v?.title
                ? i18n.t("feed.hiddenNamed", { title: v.title })
                : i18n.t("feed.hidden"),
              action: { label: i18n.t("feed.undo"), onClick: () => onState(id, "new") },
              meta: {
                kind: "video-hidden",
                videoId: id,
                title: v?.title ?? "this video",
                channelId: v?.channel_id ?? "",
                channelName: v?.channel_title ?? "",
              },
            });
          } else if (status === "watched") {
            const v = loadedRef.current.find((x) => x.id === id);
            notify({
              message: v?.title
                ? i18n.t("feed.markedWatchedNamed", { title: v.title })
                : i18n.t("feed.markedWatched"),
              action: { label: i18n.t("feed.unwatch"), onClick: () => onState(id, "new") },
              meta: {
                kind: "video-watched",
                videoId: id,
                title: v?.title ?? "this video",
                channelId: v?.channel_id ?? "",
                channelName: v?.channel_title ?? "",
              },
            });
          } else if (status === "new") {
            // Unhide / unwatch (from a card, the toast's Undo, or the center): quietly resolve
            // any stale hide/watch notice for this video so it doesn't linger with a dead action.
            resolveVideo(id);
          }
        })
        .catch(() =>
          // Server rejected the change — drop the optimistic override so the card
          // reverts to the real (unchanged) state instead of staying phantom-hidden.
          setOverrides((o) => {
            const next = { ...o };
            delete next[id];
            return next;
          })
        );
    },
    [qc]
  );

  const onResetState = useCallback(
    (id: string) => {
      // Reset to pristine (drop the whole VideoState row, incl. resume position) — the
      // un-watch toggle can't clear an in-progress position, this can.
      setOverrides((o) => ({ ...o, [id]: "new" }));
      api
        .clearState(id)
        .then(() => qc.invalidateQueries({ queryKey: ["feed"] }))
        .catch(() =>
          setOverrides((o) => {
            const next = { ...o };
            delete next[id];
            return next;
          })
        );
    },
    [qc]
  );

  const onChannelFilter = useCallback(
    (channelId: string, channelName: string) => {
      setFilters({ ...filters, channelId, channelName });
    },
    [filters, setFilters]
  );

  const onToggleSave = useCallback(
    (id: string, saved: boolean) => {
      setSavedOverrides((o) => ({ ...o, [id]: saved }));
      (saved ? api.watchLaterAdd(id) : api.watchLaterRemove(id))
        .then(() => {
          qc.invalidateQueries({ queryKey: ["playlists"] });
          qc.invalidateQueries({ queryKey: ["playlist"] });
        })
        .catch(() => setSavedOverrides((o) => ({ ...o, [id]: !saved })));
    },
    [qc]
  );

  const loaded: Video[] = (query.data?.pages ?? []).flatMap((p) => p.items);
  loadedRef.current = loaded;
  const items: Video[] = loaded
    .map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
    .map((v) => (savedOverrides[v.id] !== undefined ? { ...v, saved: savedOverrides[v.id] } : v))
    .filter((v) => matchesView(v.status, filters.show));

  if (query.isLoading) return <div className="p-8 text-muted">{t("feed.loading")}</div>;
  if (query.isError) return <div className="p-8 text-muted">{t("feed.loadError")}</div>;
  // Empty "my" feed with no YouTube access. The shared demo account can never connect
  // YouTube, so it gets a gentle nudge into the shared library instead of the connect
  // wizard; real users get the onboarding prompt. No toolbar (sort/type are meaningless).
  if (items.length === 0 && !canRead && filters.scope === "my")
    return (
        <div className="p-8 grid place-items-center text-center">
          <div className="max-w-sm">
            <h2 className="text-lg font-semibold">
              {isDemo ? t("feed.demoEmptyTitle") : t("feed.emptyTitle")}
            </h2>
            <p className="text-sm text-muted mt-2 mb-4">
              {isDemo ? t("feed.demoEmptyBody") : t("feed.emptyBody")}
            </p>
            {isDemo ? (
              <button
                onClick={() => setFilters({ ...filters, scope: "all" })}
                className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
              >
                {t("feed.browseLibrary")}
              </button>
            ) : (
              <>
                <button
                  onClick={onOpenWizard}
                  className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
                >
                  {t("feed.setUp")}
                </button>
                <button
                  onClick={() => setFilters({ ...filters, scope: "all" })}
                  className="block mx-auto mt-3 text-sm text-muted hover:text-accent transition"
                >
                  {t("feed.browseShared")}
                </button>
              </>
            )}
          </div>
        </div>
      );

  const { key: sortKey, dir: sortDir } = parseSort(filters.sort);

  const toolbar = (
    <div className="pb-3">
      <div className="flex items-center gap-1.5 flex-wrap mb-2.5">
        {SHOW_IDS.map((id) => (
          <button
            key={id}
            onClick={() => setFilters({ ...filters, show: id })}
            aria-pressed={filters.show === id}
            className={`text-xs px-3 py-1.5 rounded-full border transition ${
              filters.show === id
                ? "bg-accent text-accent-fg border-accent"
                : "border-border text-muted hover:text-fg hover:border-accent"
            }`}
          >
            {t("sidebar.show." + id)}
          </button>
        ))}
        <span className="mx-1 h-5 w-px bg-border" aria-hidden="true" />
        {CONTENT.map((c) => {
          const on = filters[c.key];
          return (
            <button
              key={c.key}
              onClick={() => setFilters({ ...filters, [c.key]: !on })}
              aria-pressed={on}
              className={`text-xs px-3 py-1.5 rounded-full border transition ${
                on
                  ? "bg-accent text-accent-fg border-accent"
                  : "border-border text-muted hover:text-fg hover:border-accent"
              }`}
            >
              {t(c.label)}
            </button>
          );
        })}
      </div>
      <div className="flex items-center gap-2 border-t border-border pt-2.5">
        <span className="text-sm text-muted">
          {countQuery.data
            ? t("feed.videoCount", {
                count: countQuery.data.count,
                formattedCount: countQuery.data.count.toLocaleString(),
              })
            : " "}
        </span>
        <div className="flex-1" />
        <span className="text-xs text-muted">{t("feed.sortLabel")}</span>
        <select
          value={sortKey}
          onChange={(e) => {
            const key = e.target.value as SortKey;
            const dir = key === "shuffle" ? "desc" : SORT_DEFAULT_DIR[key];
            setFilters({
              ...filters,
              sort: buildSort(key, dir),
              seed: key === "shuffle" ? rollSeed() : undefined,
            });
          }}
          className="bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent"
        >
          {SORT_KEYS.map((k) => (
            <option key={k} value={k}>
              {t("feed.sortKey." + k)}
            </option>
          ))}
        </select>
        {sortKey !== "shuffle" && (
          <button
            onClick={() =>
              setFilters({ ...filters, sort: buildSort(sortKey, sortDir === "asc" ? "desc" : "asc") })
            }
            title={sortDir === "asc" ? t("feed.dirAsc") : t("feed.dirDesc")}
            aria-label={sortDir === "asc" ? t("feed.dirAsc") : t("feed.dirDesc")}
            className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
          >
            {sortDir === "asc" ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
          </button>
        )}
        {sortKey === "shuffle" && (
          <button
            onClick={() => setFilters({ ...filters, seed: rollSeed() })}
            title={t("sidebar.reshuffle")}
            aria-label={t("sidebar.reshuffle")}
            className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
          >
            <RefreshCw className="w-4 h-4" />
          </button>
        )}
      </div>
    </div>
  );

  return (
    <div className="p-4">
      {toolbar}
      {items.length === 0 ? (
        <div className="py-16 text-center text-muted">{t("feed.noMatches")}</div>
      ) : view === "grid" ? (
        <div className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(260px,1fr))]">
          {items.map((v) => (
            <VideoCard
              key={v.id}
              video={v}
              view="grid"
              onState={onState}
              onToggleSave={onToggleSave}
              onChannelFilter={onChannelFilter}
              onResetState={onResetState}
              onOpen={openVideo}
            />
          ))}
        </div>
      ) : (
        <div className="max-w-4xl mx-auto flex flex-col gap-1">
          {items.map((v) => (
            <VideoCard
              key={v.id}
              video={v}
              view="list"
              onState={onState}
              onToggleSave={onToggleSave}
              onChannelFilter={onChannelFilter}
              onResetState={onResetState}
              onOpen={openVideo}
            />
          ))}
        </div>
      )}
      {activeVideo && (
        <PlayerModal
          video={activeVideo.video}
          startAt={activeVideo.startAt}
          onClose={() => setActiveVideo(null)}
          onState={onState}
        />
      )}
      <div ref={sentinel} className="h-10" />
      {isFetchingNextPage && (
        <div className="text-center text-muted py-4">{t("feed.loadingMore")}</div>
      )}
    </div>
  );
}
