import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";
import { getAccountRaw, LS, readAccount, setAccountRaw, writeAccount } from "../lib/storage";
import { parseChannelLayout, type ChannelLayout } from "../lib/channelLayout";
import { parseSortState, type SortState } from "../lib/columnSort";
import { useNavigation, useNavigationActions } from "./NavigationProvider";
import type { ChannelStatusFilter, ChannelsView } from "./Channels";

// Owns the Channels module's shared local state — the rail-less manager's name filter, status chip,
// active tab, the "drop a stale column filter" token, and the focus-a-channel intent — lifted out of
// App so the Channels page, the header search box, and the cross-module callers (the feed filter
// sidebar and the notifications inbox both "focus this channel in the manager") read it via hooks
// instead of prop-drilling through App. Mirrors FeedFiltersProvider (split State + Actions contexts,
// stable useCallback setters, one-line hooks; composed in main.tsx around <App/>).
//
// Nested INSIDE NavigationProvider so its actions can drive navigation: focusChannel and
// goToFullHistory both jump to the Channels page (and force the subscriptions tab). status / view are
// per-account persisted (localStorage); search / focus / the reset token are ephemeral. An account
// switch does a full reload, so init-from-storage is all the hydration the persisted ones need.

const STATUS_FILTERS: readonly ChannelStatusFilter[] = [
  "needs_full",
  "fully_synced",
  "hidden",
  "all",
];

function clampStatus(v: string | null): ChannelStatusFilter {
  return STATUS_FILTERS.includes(v as ChannelStatusFilter) ? (v as ChannelStatusFilter) : "all";
}
function clampView(v: string | null): ChannelsView {
  return v === "discovery" ? "discovery" : v === "blocked" ? "blocked" : "subscribed";
}

type ChannelsState = {
  search: string;
  statusFilter: ChannelStatusFilter;
  view: ChannelsView;
  // The subscribed table's sort, lifted out of DataTable so the card layout shares it.
  sort: SortState;
  // Bumped to tell the manager to drop a stale column filter when an intent sends the user to a
  // specific set (the header's "without full history" link).
  filtersResetToken: number;
  focusName: string | null;
  // Bumped every time a focus intent is (re-)issued, so re-focusing the same channel re-seeds the
  // manager's name filter even when the name is unchanged.
  focusToken: number;
};

interface ChannelsActions {
  setSearch: (q: string) => void;
  setStatusFilter: (f: ChannelStatusFilter) => void;
  setView: (v: ChannelsView) => void;
  setLayout: (l: ChannelLayout) => void;
  setSort: (s: SortState) => void;
  // "Focus this channel in the manager": jump to Channels (subscriptions tab) and seed its name
  // filter so the channel is isolated. Called from the feed sidebar and the notifications inbox.
  focusChannel: (name: string) => void;
  // Jump to the manager filtered to channels still missing full history (the header sync chip).
  goToFullHistory: () => void;
}

const StateContext = createContext<ChannelsState>({
  search: "",
  statusFilter: "all",
  view: "subscribed",
  sort: null,
  filtersResetToken: 0,
  focusName: null,
  focusToken: 0,
});
// The table/cards density gets its OWN context, deliberately narrow: App reads it (to decide the
// shell's top fade) and would otherwise re-render the whole app on every keystroke in the manager's
// search box, since the state context above changes with it.
const LayoutContext = createContext<ChannelLayout>("table");
const ActionsContext = createContext<ChannelsActions>({
  setSearch: () => {},
  setStatusFilter: () => {},
  setView: () => {},
  setLayout: () => {},
  setSort: () => {},
  focusChannel: () => {},
  goToFullHistory: () => {},
});

export function useChannels(): ChannelsState {
  return useContext(StateContext);
}
/** The manager's table/cards density on its own — subscribe to this instead of the whole state when
 * the layout is all you need (App does, for the shell's top fade). */
export function useChannelLayout(): ChannelLayout {
  return useContext(LayoutContext);
}
export function useChannelsActions(): ChannelsActions {
  return useContext(ActionsContext);
}

export function ChannelsProvider({ children }: { children: ReactNode }) {
  const { page } = useNavigation();
  const { setPage } = useNavigationActions();

  const [search, setSearchState] = useState("");
  const [statusFilter, setStatusFilterState] = useState<ChannelStatusFilter>(() =>
    clampStatus(getAccountRaw(LS.channelFilter)),
  );
  const [view, setViewState] = useState<ChannelsView>(() =>
    clampView(getAccountRaw(LS.channelsView)),
  );
  const [layout, setLayoutState] = useState<ChannelLayout>(() =>
    parseChannelLayout(getAccountRaw(LS.channelLayout)),
  );
  const [sort, setSortState] = useState<SortState>(() =>
    parseSortState(readAccount(LS.channelSort, null)),
  );
  const [filtersResetToken, setFiltersResetToken] = useState(0);
  const [focusName, setFocusName] = useState<string | null>(null);
  const [focusToken, setFocusToken] = useState(0);

  // Latest-value ref so the STABLE setView can compare against the current tab without depending on
  // it (a dep would recreate the action on every switch and re-render every actions consumer).
  // Written during render — the sanctioned pattern NavigationProvider uses for `pageRef`.
  const viewRef = useRef(view);
  viewRef.current = view;

  const setSearch = useCallback((q: string) => setSearchState(q), []);
  const setStatusFilter = useCallback((f: ChannelStatusFilter) => {
    setStatusFilterState(f);
    setAccountRaw(LS.channelFilter, f);
  }, []);
  // The active tab rides in history.state (`_chTab`), the same way the channel page (`_chan`) and the
  // live YouTube search (`_yt`) do — so Back steps back through the tabs you actually visited instead
  // of jumping straight out to the previous module.
  const setView = useCallback((v: ChannelsView) => {
    // Re-selecting the tab you are already on is not a navigation — bail before pushing, or clicking
    // the active tab (and goToFullHistory forcing "subscribed") stacks Back entries that do nothing.
    if (v === viewRef.current) return;
    setViewState(v);
    setAccountRaw(LS.channelsView, v);
    // Only own an entry while the manager is already on screen. Arriving from elsewhere
    // (focusChannel / goToFullHistory) lets setPage push the entry — the stamp effect labels it — so
    // one navigation never costs two Back presses.
    if (window.history.state?.sfPage === "channels") {
      // A FRESH state, like setPage's: carrying the old entry's `_chan`/`_yt` markers over would make
      // Back re-open the channel overlay or a spent search on top of the tab.
      window.history.pushState({ sfPage: "channels", _chTab: v }, "");
    }
  }, []);
  const setLayout = useCallback((l: ChannelLayout) => {
    setLayoutState(l);
    setAccountRaw(LS.channelLayout, l);
  }, []);
  const setSort = useCallback((s: SortState) => {
    setSortState(s);
    writeAccount(LS.channelSort, s);
  }, []);

  const focusChannel = useCallback(
    (name: string) => {
      setFocusName(name);
      setFocusToken((n) => n + 1); // re-seed even when the same channel is re-focused
      setView("subscribed"); // the name filter applies to the subscriptions table
      setPage("channels");
    },
    [setView, setPage],
  );
  const goToFullHistory = useCallback(() => {
    setStatusFilter("needs_full");
    setView("subscribed"); // the status filter applies to the subscriptions tab
    setFiltersResetToken((n) => n + 1); // drop any stale column filter hiding the rows
    setPage("channels");
  }, [setStatusFilter, setView, setPage]);

  // Clear the manager focus when leaving the Channels page so it doesn't re-apply on a later visit.
  useEffect(() => {
    if (page !== "channels") setFocusName(null);
  }, [page]);

  // Label the entry the manager was REACHED on (setPage pushes a bare { sfPage }), so stepping back
  // onto it later restores the tab it belonged to rather than leaving whatever tab is current.
  useEffect(() => {
    if (page !== "channels") return;
    const st = window.history.state || {};
    if (st._chTab === undefined) window.history.replaceState({ ...st, _chTab: view }, "");
  }, [page, view]);

  // Back/Forward across tab entries: restore the tab the entry carries.
  useEffect(() => {
    function onPop(e: PopStateEvent) {
      const tab = e.state?._chTab;
      if (!tab) return; // an entry outside the manager — leave the tab alone
      const next = clampView(tab as string);
      setViewState(next);
      setAccountRaw(LS.channelsView, next);
    }
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  const state = useMemo<ChannelsState>(
    () => ({ search, statusFilter, view, sort, filtersResetToken, focusName, focusToken }),
    [search, statusFilter, view, sort, filtersResetToken, focusName, focusToken],
  );
  const actions = useMemo<ChannelsActions>(
    () => ({
      setSearch,
      setStatusFilter,
      setView,
      setLayout,
      setSort,
      focusChannel,
      goToFullHistory,
    }),
    [setSearch, setStatusFilter, setView, setLayout, setSort, focusChannel, goToFullHistory],
  );

  return (
    <ActionsContext.Provider value={actions}>
      <LayoutContext.Provider value={layout}>
        <StateContext.Provider value={state}>{children}</StateContext.Provider>
      </LayoutContext.Provider>
    </ActionsContext.Provider>
  );
}
