/**
 * Tiny query + mutation hooks. Intentionally minimal — no caching, no devtools.
 * Add TanStack Query later if needs grow.
 */

import { useCallback, useEffect, useRef, useState } from 'react';

export interface QueryState<T> {
  data: T | undefined;
  error: Error | null;
  isLoading: boolean;
  refetch: () => Promise<void>;
}

export function useQuery<T>(
  fetcher: () => Promise<T>,
  deps: unknown[] = [],
): QueryState<T> {
  const [data, setData] = useState<T | undefined>(undefined);
  const [error, setError] = useState<Error | null>(null);
  const [isLoading, setLoading] = useState(true);
  const fetcherRef = useRef(fetcher);
  fetcherRef.current = fetcher;

  // Monotonic token identifying the most recent run(). Every invocation
  // captures its own token; when its promise resolves it only commits state
  // if it is STILL the latest run. Without this, a slow fetch that was
  // superseded by a newer one (deps changed mid-flight — e.g. the user typed
  // another character into a search box, or a poll fired during a refetch)
  // could resolve LAST and clobber the newer, correct result. That surfaced
  // as "the list shows rows for the previous filter." A mounted-ref guard
  // also prevents setState-after-unmount warnings.
  const seqRef = useRef(0);
  const mountedRef = useRef(true);
  useEffect(() => {
    return () => { mountedRef.current = false; };
  }, []);

  const run = useCallback(async () => {
    const seq = ++seqRef.current;
    // Only the latest run owns the shared loading/error/data state.
    setLoading(true);
    setError(null);
    try {
      const v = await fetcherRef.current();
      if (mountedRef.current && seq === seqRef.current) setData(v);
    } catch (e) {
      if (mountedRef.current && seq === seqRef.current) setError(e as Error);
    } finally {
      if (mountedRef.current && seq === seqRef.current) setLoading(false);
    }
  }, []);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  useEffect(() => { run(); }, deps);

  return { data, error, isLoading, refetch: run };
}

export interface MutationState<TArgs, TResult> {
  mutate: (args: TArgs) => Promise<TResult>;
  isLoading: boolean;
  error: Error | null;
  data: TResult | undefined;
  reset: () => void;
}

export function useMutation<TArgs, TResult>(
  fn: (args: TArgs) => Promise<TResult>,
  opts: { onSuccess?: (data: TResult) => void; onError?: (e: Error) => void } = {},
): MutationState<TArgs, TResult> {
  const [isLoading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [data, setData] = useState<TResult | undefined>(undefined);

  // Keep `fn` and `opts` in refs so `mutate` is stable across renders. The
  // previous implementation included `opts` in the deps of useCallback —
  // because callers usually pass a fresh object literal each render, `mutate`
  // changed identity on every render, defeating its purpose and re-firing any
  // effect that depended on it.
  const fnRef   = useRef(fn);
  const optsRef = useRef(opts);
  fnRef.current   = fn;
  optsRef.current = opts;

  const mutate = useCallback(async (args: TArgs) => {
    setLoading(true);
    setError(null);
    try {
      const v = await fnRef.current(args);
      setData(v);
      optsRef.current.onSuccess?.(v);
      return v;
    } catch (e) {
      const err = e as Error;
      setError(err);
      optsRef.current.onError?.(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  return { mutate, isLoading, error, data, reset: () => { setData(undefined); setError(null); } };
}
