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

export type AutoSaveStatus = "idle" | "saving" | "saved" | "error";

interface UseAutoSaveOptions<T> {
  /** Debounce delay in ms. Default: 800. */
  debounceMs?: number;
  /** Equality check. Default: JSON.stringify comparison. */
  isEqual?: (a: T, b: T) => boolean;
}

interface UseAutoSaveResult {
  status: AutoSaveStatus;
  errorText: string | null;
  retry: () => void;
}

const defaultIsEqual = <T>(a: T, b: T): boolean =>
  JSON.stringify(a) === JSON.stringify(b);

export function useAutoSave<T>(
  value: T,
  save: (value: T) => Promise<void>,
  options?: UseAutoSaveOptions<T>,
): UseAutoSaveResult {
  const [status, setStatus] = useState<AutoSaveStatus>("idle");
  const [errorText, setErrorText] = useState<string | null>(null);

  // Refs to avoid stale closures — option refs are intentionally NOT in deps.
  const debounceRef = useRef(options?.debounceMs ?? 800);
  const isEqualRef = useRef<(a: T, b: T) => boolean>(
    options?.isEqual ?? defaultIsEqual,
  );
  const savedValueRef = useRef<T>(value);
  const latestValueRef = useRef<T>(value);
  const isSavingRef = useRef<boolean>(false);
  const pendingSaveRef = useRef<boolean>(false);
  const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const saveRef = useRef(save);

  // Keep mutable refs up to date without triggering effects.
  saveRef.current = save;
  latestValueRef.current = value;

  const performSave = useCallback(async (valueToSave: T): Promise<void> => {
    if (isSavingRef.current) {
      pendingSaveRef.current = true;
      return;
    }

    isSavingRef.current = true;
    pendingSaveRef.current = false;
    setStatus("saving");
    setErrorText(null);

    try {
      await saveRef.current(valueToSave);
      savedValueRef.current = valueToSave;
      setStatus("saved");

      if (savedTimerRef.current !== null) {
        clearTimeout(savedTimerRef.current);
      }
      savedTimerRef.current = setTimeout(() => {
        setStatus("idle");
        savedTimerRef.current = null;
      }, 2000);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      setErrorText(msg);
      setStatus("error");
    } finally {
      isSavingRef.current = false;

      // If a pending save was queued while we were saving, run it now.
      if (pendingSaveRef.current as boolean) {
        pendingSaveRef.current = false;
        await performSave(latestValueRef.current);
      }
    }
  }, []);

  // Debounce on value change. Only `value` and the stable `performSave` are
  // deps — option refs are read directly to avoid re-running the effect when
  // the caller recreates option callbacks.
  useEffect(() => {
    if (isEqualRef.current(value, savedValueRef.current)) {
      return;
    }

    if (debounceTimerRef.current !== null) {
      clearTimeout(debounceTimerRef.current);
    }

    debounceTimerRef.current = setTimeout(() => {
      debounceTimerRef.current = null;
      void performSave(latestValueRef.current);
    }, debounceRef.current);
  }, [value, performSave]);

  // Cleanup timers on unmount.
  useEffect(() => {
    return (): void => {
      if (debounceTimerRef.current !== null) {
        clearTimeout(debounceTimerRef.current);
      }
      if (savedTimerRef.current !== null) {
        clearTimeout(savedTimerRef.current);
      }
    };
  }, []);

  const retry = useCallback(() => {
    void performSave(latestValueRef.current);
  }, [performSave]);

  return { status, errorText, retry };
}
