'use client';

import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { useQuery, useQueryClient, type QueryKey } from '@tanstack/react-query';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';

import { apiFetch } from '@/lib/api/client';
import type { PaginatedResponse } from '@/types/api';
import {
  serializeFiltersToParams,
  deserializeFiltersFromParams,
  type FilterDefinition,
  type FilterValues,
} from '@/components/shared/filter-bar';

interface UsePaginatedQueryOptions {
  queryKey: QueryKey;
  endpoint: string;
  initialPage?: number;
  initialPageSize?: number;
  /** Default sort applied when the URL has no `sort` param. Lets a list
   *  surface advertise its preferred ordering (e.g. interests → most-
   *  recently-updated first) without forcing the rep to click a header
   *  on each visit. The active sort still serializes to / from the URL,
   *  so deep-links keep working. */
  initialSort?: { field: string; direction: 'asc' | 'desc' };
  filterDefinitions?: FilterDefinition[];
}

export function usePaginatedQuery<T>({
  queryKey,
  endpoint,
  initialPage = 1,
  initialPageSize = 25,
  initialSort,
  filterDefinitions = [],
}: UsePaginatedQueryOptions) {
  const searchParams = useSearchParams();
  const router = useRouter();
  const pathname = usePathname();
  const queryClient = useQueryClient();

  // Read initial state from URL
  const pageFromUrl = Number(searchParams.get('page')) || initialPage;
  const pageSizeFromUrl = Number(searchParams.get('limit')) || initialPageSize;
  const sortFieldFromUrl = searchParams.get('sort') ?? undefined;
  const sortOrderFromUrl = (searchParams.get('order') as 'asc' | 'desc') ?? 'desc';

  const [page, setPageState] = useState(pageFromUrl);
  const [pageSize, setPageSizeState] = useState(pageSizeFromUrl);
  const [sort, setSortState] = useState<{ field: string; direction: 'asc' | 'desc' } | undefined>(
    sortFieldFromUrl ? { field: sortFieldFromUrl, direction: sortOrderFromUrl } : initialSort,
  );
  const [filters, setFiltersState] = useState<FilterValues>(() =>
    deserializeFiltersFromParams(searchParams, filterDefinitions),
  );

  // Canonical query string for a given state. Sorted keys so two URLs
  // that carry the same params in a different order compare equal - this
  // is what lets the back/forward resync effect (H14) tell "the URL the
  // browser just restored" apart from "the URL we last wrote ourselves"
  // without thrashing on key ordering.
  const buildCanonicalQs = useCallback(
    (p: number, ps: number, s?: typeof sort, f?: FilterValues): string => {
      const params = new URLSearchParams();
      if (p !== 1) params.set('page', String(p));
      if (ps !== initialPageSize) params.set('limit', String(ps));
      if (s) {
        params.set('sort', s.field);
        params.set('order', s.direction);
      }
      if (f) {
        const filterParams = serializeFiltersToParams(f);
        filterParams.forEach((value, key) => params.set(key, value));
      }
      // Keep existing tab param
      const tab = searchParams.get('tab');
      if (tab) params.set('tab', tab);
      params.sort();
      return params.toString();
    },
    [searchParams, initialPageSize],
  );

  // Records the query string we last pushed via `router.replace`, so the
  // resync effect (H14) can skip params it itself authored and only react
  // to genuinely-external URL changes (Back/Forward).
  const lastWrittenQsRef = useRef<string | null>(null);

  // Sync state to URL
  const syncUrl = useCallback(
    (p: number, ps: number, s?: typeof sort, f?: FilterValues) => {
      const qs = buildCanonicalQs(p, ps, s, f);
      lastWrittenQsRef.current = qs;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      router.replace(`${pathname}${qs ? `?${qs}` : ''}` as any, { scroll: false });
    },
    [pathname, router, buildCanonicalQs],
  );

  function setPage(p: number) {
    setPageState(p);
    syncUrl(p, pageSize, sort, filters);
  }

  function setPageSize(ps: number) {
    setPageSizeState(ps);
    setPageState(1);
    syncUrl(1, ps, sort, filters);
  }

  function setSort(field: string, direction: 'asc' | 'desc') {
    const newSort = { field, direction };
    setSortState(newSort);
    setPageState(1);
    syncUrl(1, pageSize, newSort, filters);
  }

  function setFilter(key: string, value: unknown) {
    const newFilters = { ...filters };
    if (value === undefined || value === null) {
      delete newFilters[key];
    } else {
      newFilters[key] = value;
    }
    setFiltersState(newFilters);
    setPageState(1);
    syncUrl(1, pageSize, sort, newFilters);
  }

  function clearFilters() {
    setFiltersState({});
    setPageState(1);
    syncUrl(1, pageSize, sort, {});
  }

  /**
   * Atomically replace the entire filter set. Used by the saved-views
   * apply path - calling `clearFilters()` + N x `setFilter()` in a row
   * lost all but the last setFilter because each one reads the stale
   * `filters` closure and overwrites with `{...filters, key: val}`.
   * setAllFilters writes the whole object in one setState so the view
   * lands intact.
   */
  function setAllFilters(next: FilterValues) {
    setFiltersState(next);
    setPageState(1);
    syncUrl(1, pageSize, sort, next);
  }

  /**
   * Atomically apply a saved view's filters AND sort in a single URL
   * write. The saved-views apply path previously called `setAllFilters`
   * only, silently dropping the view's stored sort (H15). Splitting the
   * write into `setAllFilters` + `setSort` would also race: each reads a
   * stale closure and the second `syncUrl` clobbers the first. `applyView`
   * sets both pieces of state and emits ONE `syncUrl`, so the view lands
   * intact with its sort.
   *
   * `sort` is optional - a view saved without an explicit sort clears the
   * active sort back to the list's `initialSort` (handled by the consumer
   * passing `undefined`). The incoming `direction` is the loose `string`
   * shape from `SavedViewsDropdown`; we narrow non-'asc' to 'desc'.
   */
  function applyView({
    filters: nextFilters,
    sort: nextSort,
  }: {
    filters: FilterValues;
    sort?: { field: string; direction: string } | undefined;
  }) {
    const normalizedSort: { field: string; direction: 'asc' | 'desc' } | undefined = nextSort
      ? {
          field: nextSort.field,
          direction: nextSort.direction === 'asc' ? ('asc' as const) : ('desc' as const),
        }
      : initialSort;
    setFiltersState(nextFilters);
    setSortState(normalizedSort);
    setPageState(1);
    syncUrl(1, pageSize, normalizedSort, nextFilters);
  }

  /**
   * H14 - resync state FROM the URL on external navigation (Back/Forward).
   *
   * The slices above seed from the URL once (useState initializers) then
   * drive it one-way via `syncUrl` -> `router.replace`. Nothing pulled the
   * URL back into state, so Back/Forward moved the address bar but left the
   * list showing the previous page/sort/filters.
   *
   * Loop safety: every write we make records its canonical query string in
   * `lastWrittenQsRef`. Here we re-derive the canonical form of the params
   * the router is currently handing us and compare it against that ref. If
   * they match, this render was caused by our OWN write - bail before
   * touching state. We also compare against the canonical form of CURRENT
   * state and only call a setter for a slice that actually differs, so even
   * an external change that happens to equal current state is a no-op. Both
   * guards mean this effect cannot feed itself: it never calls `syncUrl`,
   * and it never setState's a value equal to what state already holds.
   */
  useEffect(() => {
    const incomingQs = buildCanonicalQs(
      Number(searchParams.get('page')) || initialPage,
      Number(searchParams.get('limit')) || initialPageSize,
      searchParams.get('sort')
        ? {
            field: searchParams.get('sort') as string,
            direction: (searchParams.get('order') as 'asc' | 'desc') ?? 'desc',
          }
        : initialSort,
      deserializeFiltersFromParams(searchParams, filterDefinitions),
    );

    // This params object is the one we just wrote - not an external nav.
    if (incomingQs === lastWrittenQsRef.current) return;

    // The state already matches the URL - nothing to do (also prevents a
    // redundant setState that could schedule an extra render).
    const currentQs = buildCanonicalQs(page, pageSize, sort, filters);
    if (incomingQs === currentQs) return;

    // Genuine external change (Back/Forward): pull each slice from the URL,
    // setting only the ones that drifted.
    const nextPage = Number(searchParams.get('page')) || initialPage;
    const nextPageSize = Number(searchParams.get('limit')) || initialPageSize;
    const nextSortField = searchParams.get('sort');
    const nextSort = nextSortField
      ? {
          field: nextSortField,
          direction: (searchParams.get('order') as 'asc' | 'desc') ?? 'desc',
        }
      : initialSort;
    const nextFilters = deserializeFiltersFromParams(searchParams, filterDefinitions);

    /* eslint-disable react-hooks/set-state-in-effect --
       Deliberate, guarded URL->state sync on Back/Forward: the URL is the
       external source of truth here. The two early-returns above ensure this
       runs ONLY on a genuine external navigation (never on our own write), and
       each setState is diff-guarded, so there is no cascading-render loop
       (audit H14). This is the documented external-store-subscription case the
       rule otherwise blocks. */
    if (nextPage !== page) setPageState(nextPage);
    if (nextPageSize !== pageSize) setPageSizeState(nextPageSize);
    if (nextSort?.field !== sort?.field || nextSort?.direction !== sort?.direction) {
      setSortState(nextSort);
    }
    if (JSON.stringify(nextFilters) !== JSON.stringify(filters)) {
      setFiltersState(nextFilters);
    }
    /* eslint-enable react-hooks/set-state-in-effect */
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [searchParams]);

  // Build query string for API
  const apiParams = useMemo(() => {
    const params = new URLSearchParams();
    params.set('page', String(page));
    params.set('limit', String(pageSize));
    if (sort) {
      params.set('sort', sort.field);
      params.set('order', sort.direction);
    }
    const filterParams = serializeFiltersToParams(filters);
    filterParams.forEach((value, key) => params.set(key, value));
    return params.toString();
  }, [page, pageSize, sort, filters]);

  const fullQueryKey = [...queryKey, apiParams];

  // Endpoints that already carry a query string (e.g. `/api/v1/documents?tab=eoi_queue`)
  // need our pagination params merged with `&`, not a second `?`. Without this guard
  // the URL becomes `…?tab=foo?page=1` and the API rejects it as 400.
  const separator = endpoint.includes('?') ? '&' : '?';

  const { data, isLoading, isFetching } = useQuery<PaginatedResponse<T>>({
    queryKey: fullQueryKey,
    queryFn: () => apiFetch<PaginatedResponse<T>>(`${endpoint}${separator}${apiParams}`),
  });

  const pagination = data?.pagination
    ? {
        page: data.pagination.page,
        pageSize: data.pagination.pageSize,
        total: data.pagination.total,
        totalPages: data.pagination.totalPages,
      }
    : { page, pageSize, total: 0, totalPages: 0 };

  /**
   * Optimistically removes an item from the cached list (e.g., after archive).
   */
  function optimisticRemove(id: string) {
    queryClient.setQueryData<PaginatedResponse<T>>(fullQueryKey, (old) => {
      if (!old) return old;
      return {
        ...old,
        data: old.data.filter((item) => (item as Record<string, unknown>).id !== id),
        pagination: {
          ...old.pagination,
          total: old.pagination.total - 1,
        },
      };
    });
  }

  return {
    data: data?.data ?? [],
    pagination,
    isLoading,
    isFetching,
    sort,
    setSort,
    page,
    setPage,
    pageSize,
    setPageSize,
    filters,
    setFilter,
    setAllFilters,
    applyView,
    clearFilters,
    optimisticRemove,
  };
}
