import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { captureCaught } from '@/lib/observability';
import { useHydrated } from '@/lib/hooks/useHydrated';

export interface PaginatedResponse<T> {
  data: T[];
  total: number;
}

export interface UsePaginatedQueryOptions<T> {
  /** Full API endpoint path, e.g. '/api/admin/deals/pending' */
  endpoint: string;
  /** Extra query params (filters). Changing these resets page to 1. */
  params?: Record<string, string>;
  /** Page size. Default: 20. */
  limit?: number;
  /** SSR-loaded first page data. */
  initialData: T[];
  /** SSR-loaded total count. */
  initialTotal: number;
  /** SSR-seeded page from ?page= (hydration-safe; matches server offset). */
  initialPage?: number;
  /** Key in the API response JSON that holds the data array, e.g. 'vendors'. */
  dataKey: string;
  /** When true, surfaces non-data/non-total top-level envelope keys in `meta`. */
  includeEnvelopeMeta?: boolean;
}

export interface UsePaginatedQueryResult<T> {
  data: T[];
  total: number;
  totalPages: number;
  page: number;
  isLoading: boolean;
  isInitialLoading: boolean;
  isRefetching: boolean;
  error: string | null;
  setPage: (page: number) => void;
  refetch: () => void;
  /** Patch the current rows in place (optimistic updates) without a refetch. */
  mutate: (updater: (rows: T[]) => T[]) => void;
  /** Present when `includeEnvelopeMeta` is true; undefined until the first fetch settles. */
  meta?: Record<string, unknown>;
}

/**
 * Shared pagination hook for admin list screens.
 *
 * Uses useState + useEffect (no React Query dependency - matches existing admin patterns).
 * initialData / initialTotal act as placeholderData: SSR values show immediately, and a
 * fetch always fires on mount so Playwright route mocks are never skipped.
 */
export function usePaginatedQuery<T>({
  endpoint,
  params = {},
  limit = 20,
  initialData,
  initialTotal,
  initialPage,
  dataKey,
  includeEnvelopeMeta = false,
}: UsePaginatedQueryOptions<T>): UsePaginatedQueryResult<T> {
  const hydrated = useHydrated();
  const readPageFromUrl = (): number => {
    const raw = new URLSearchParams(window.location.search).get('page');
    const n = Number(raw);
    return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
  };

  const writePageParam = useCallback((p: number) => {
    if (typeof window === 'undefined') return;
    const url = new URL(window.location.href);
    if (p <= 1) url.searchParams.delete('page');
    else url.searchParams.set('page', String(p));
    window.history.replaceState(window.history.state, '', url);
  }, []);

  const [page, setPageRaw] = useState(() => initialPage ?? 1);
  const [data, setData] = useState<T[]>(initialData);
  const [total, setTotal] = useState(initialTotal);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [refetchKey, setRefetchKey] = useState(0);
  const [meta, setMeta] = useState<Record<string, unknown> | undefined>(undefined);
  const [hasFetchedOnce, setHasFetchedOnce] = useState(false);

  const urlPage = hydrated && initialPage === undefined ? readPageFromUrl() : page;
  if (hydrated && initialPage === undefined && page !== urlPage) {
    setPageRaw(urlPage);
  }

  const prevParams = useRef(params);
  // Flipped true after the first fetch settles. Distinguishes the initial load
  // (full spinner OK) from any later refetch — including a refetch that returns
  // zero rows — which must NOT blank the page.
  const paramsString = JSON.stringify(params);
  const stableParams = useMemo(
    () => JSON.parse(paramsString) as Record<string, string>,
    [paramsString],
  );
  const totalPages = Math.max(1, Math.ceil(total / limit));

  // Reset page to 1 when filters change
  useEffect(() => {
    const currentStr = paramsString;
    const prevStr = JSON.stringify(prevParams.current);
    if (currentStr !== prevStr) {
      prevParams.current = stableParams;
      setPageRaw(1);
      writePageParam(1);
    }
  }, [paramsString, stableParams, writePageParam]);

  // Fetch data on page/params/refetchKey change (always fires — including first mount)
  useEffect(() => {
    const abortController = new AbortController();

    async function fetchPage() {
      setIsLoading(true);
      setError(null);

      const qs = new URLSearchParams({
        limit: String(limit),
        offset: String((page - 1) * limit),
        ...stableParams,
      });

      // Remove empty params
      for (const [key, val] of qs.entries()) {
        if (val === '' || val === 'undefined') qs.delete(key);
      }

      try {
        const res = await fetch(`${endpoint}?${qs.toString()}`, {
          signal: abortController.signal,
        });

        if (!res.ok) {
          const body = (await res.json().catch((err) => {
            captureCaught(err, { scope: 'lib.hooks.usePaginatedQuery', severity: 'info' });
            return {};
          })) as Record<string, unknown>;
          throw new Error((body.error as string) || `HTTP ${res.status}`);
        }

        const json: Record<string, unknown> = await res.json();
        if (abortController.signal.aborted) return;
        const items = (json[dataKey] as T[]) ?? [];
        const fetchedTotal = typeof json.total === 'number' ? (json.total as number) : items.length;

        setData(items);
        setTotal(fetchedTotal);
        if (includeEnvelopeMeta) {
          const { [dataKey]: _items, total: _total, ...envelopeMeta } = json;
          setMeta(envelopeMeta);
        }

        // If current page is empty and we're not on page 1, go back
        if (items.length === 0 && page > 1) {
          setPageRaw((p) => {
            const next = p - 1;
            writePageParam(next);
            return next;
          });
        }
      } catch (err) {
        if (!abortController.signal.aborted && (err as Error).name !== 'AbortError') {
          setError((err as Error).message);
        }
      } finally {
        if (!abortController.signal.aborted) {
          setHasFetchedOnce(true);
          setIsLoading(false);
        }
      }
    }

    fetchPage();

    return () => abortController.abort();
  }, [
    page,
    paramsString,
    refetchKey,
    endpoint,
    limit,
    dataKey,
    stableParams,
    includeEnvelopeMeta,
    writePageParam,
  ]);

  const setPage = useCallback(
    (newPage: number) => {
      const clamped = Math.max(1, newPage);
      setPageRaw(clamped);
      writePageParam(clamped);
    },
    [writePageParam],
  );

  const refetch = useCallback(() => {
    setRefetchKey((k) => k + 1);
  }, []);

  const mutate = useCallback((updater: (rows: T[]) => T[]) => {
    setData((rows) => updater(rows));
  }, []);

  // Full-height spinner only for the very first load with no rows yet.
  // data.length === 0 keeps SSR-seeded initialData on the busy path (no flash);
  // !hasFetchedOnce guarantees later zero-result refetches never re-blank.
  const isInitialLoading = isLoading && data.length === 0 && !hasFetchedOnce;
  const isRefetching = isLoading && !isInitialLoading;

  return {
    data,
    total,
    totalPages,
    page,
    isLoading,
    isInitialLoading,
    isRefetching,
    error,
    setPage,
    refetch,
    mutate,
    ...(includeEnvelopeMeta ? { meta } : {}),
  };
}
