/**
 * Generic hook for loading and polling single-item data from an API endpoint.
 *
 * Composes useFetchData and adds polling and window-focus refetch semantics
 * for non-list endpoints that need periodic updates.
 */
import { useCallback, useEffect, useRef } from "react";
import { useFetchData } from "./useFetchData";
import { usePageVisibility } from "./usePageVisibility";
import type { FetchError } from "../types/api";

export interface UsePolledDataOptions<TResponse, TData> {
  /** Async function that accepts an AbortSignal for cancellation. */
  fetcher: (signal: AbortSignal) => Promise<TResponse>;
  /** Synchronous selector to extract data from response. */
  selector: (response: TResponse) => TData;
  /** Human-readable error message used as fallback if fetch fails. */
  errorMessage: string;
  /** Optional callback invoked after successful fetch with full response. */
  onSuccess?: (response: TResponse) => void;
  /** Initial data value. Defaults to null. */
  initialData?: TData;
  /** Polling interval in milliseconds. If unset, no periodic polling. */
  pollInterval?: number;
  /** If true, automatically refetch when browser window regains focus. Defaults to true. */
  refetchOnWindowFocus?: boolean;
  /**
   * If true, pause polling when the page is hidden (user switched to another tab).
   * Polling resumes immediately when the page becomes visible, with an immediate refresh.
   * Defaults to false for backward compatibility.
   */
  pauseWhenHidden?: boolean;
}

export interface UsePolledDataResult<TData> {
  /** The extracted data from the most recent successful fetch, or null if not yet fetched. */
  data: TData | null;
  /** True while a fetch is in-flight, false when complete. */
  loading: boolean;
  /** Typed error or null. Check `error?.type` to handle specific failure modes. */
  error: FetchError | null;
  /** Trigger a fresh fetch. Cancels any in-flight request first. */
  refresh: () => void;
}

/**
 * Load a single-item response and expose refresh semantics with polling support.
 *
 * Provides typed error handling through the `error` property, which is either
 * `null` or a discriminated `FetchError` union. Use the error's `type` field
 * to determine how to handle it:
 *
 * - `"api_error"`: Server returned HTTP error (check `status` for 401/403/50x)
 * - `"network_error"`: Network, DNS, or JSON parse failure
 * - `"abort_error"`: Request was cancelled (typically silently ignored by hook)
 *
 * When `pauseWhenHidden` is enabled, polling pauses when the page becomes hidden
 * and resumes immediately with a fresh fetch when the page becomes visible again.
 *
 * Polling uses drift correction: if a fetch takes longer than expected, the next poll
 * is scheduled to compensate, maintaining the desired polling interval.
 *
 * @param options - Configuration options
 * @returns Data, loading state, typed error, and refresh callback
 */
export function usePolledData<TResponse, TData>(
  options: UsePolledDataOptions<TResponse, TData>,
): UsePolledDataResult<TData> {
  const {
    fetcher,
    selector,
    errorMessage,
    onSuccess,
    initialData,
    pollInterval,
    refetchOnWindowFocus = true,
    pauseWhenHidden = false,
  } = options;

  const { data, loading, error, refresh } = useFetchData({
    fetcher,
    selector,
    errorMessage,
    onSuccess,
    initialData,
  });

  const refreshRef = useRef<(() => void) | null>(null);
  const timeoutIdRef = useRef<number | null>(null);
  const pollStartTimeRef = useRef<number | null>(null);
  const previousVisibilityRef = useRef<boolean | null>(null);
  const isVisible = usePageVisibility();
  const cancelledRef = useRef(false);
  const previousLoadingRef = useRef(true);

  useEffect(() => {
    refreshRef.current = refresh;
  }, [refresh]);

  /**
   * Schedule next poll with drift correction.
   * Calculates elapsed time since poll start and compensates the delay
   * to maintain the desired polling interval despite fetch duration variance.
   */
  const scheduleNextPoll = useCallback((nextDelay: number): void => {
    if (cancelledRef.current) return;

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

    const id = window.setTimeout((): void => {
      if (cancelledRef.current) return;
      pollStartTimeRef.current = performance.now();
      if (refreshRef.current) {
        refreshRef.current();
      }
    }, nextDelay);

    timeoutIdRef.current = id;
  }, []);

  // Monitor fetch completion and schedule next poll with drift correction
  useEffect(() => {
    if (!pollInterval) {
      return;
    }

    // Detect transition from loading=true to loading=false (fetch completed)
    if (previousLoadingRef.current && !loading && pollStartTimeRef.current !== null) {
      // Fetch completed, calculate elapsed time and schedule next poll
      const elapsed = performance.now() - pollStartTimeRef.current;
      const nextDelay = Math.max(0, pollInterval - elapsed);
      scheduleNextPoll(nextDelay);
    }

    previousLoadingRef.current = loading;
  }, [loading, pollInterval, scheduleNextPoll]);

  // Polling effect: set up self-scheduling polling if pollInterval is provided and page is visible
  useEffect(() => {
    if (!pollInterval) {
      return;
    }

    cancelledRef.current = false;

    // If pauseWhenHidden is enabled and page is hidden, don't start polling
    if (pauseWhenHidden && !isVisible) {
      return;
    }

    // Record when polling starts and schedule first poll immediately
    pollStartTimeRef.current = performance.now();
    const id = window.setTimeout((): void => {
      if (cancelledRef.current) return;
      pollStartTimeRef.current = performance.now();
      refreshRef.current?.();
    }, 0);

    timeoutIdRef.current = id;

    return (): void => {
      cancelledRef.current = true;
      if (timeoutIdRef.current !== null) {
        clearTimeout(timeoutIdRef.current);
        timeoutIdRef.current = null;
      }
    };
  }, [pollInterval, pauseWhenHidden, isVisible]);

  // Visibility effect: handle pause/resume when page visibility changes
  useEffect(() => {
    if (!pauseWhenHidden || !pollInterval) {
      return;
    }

    // Only refresh if this is a transition from hidden to visible, not on mount
    if (previousVisibilityRef.current === false && isVisible) {
      // Page became visible: clear pending timeout and refresh immediately
      if (timeoutIdRef.current !== null) {
        clearTimeout(timeoutIdRef.current);
        timeoutIdRef.current = null;
      }
      pollStartTimeRef.current = performance.now();
      refreshRef.current?.();
    }

    previousVisibilityRef.current = isVisible;
  }, [isVisible, pauseWhenHidden, pollInterval]);

  // Window focus: optional refetch on regain focus
  useEffect(() => {
    if (!refetchOnWindowFocus) return;

    const onFocus = (): void => {
      refreshRef.current?.();
    };

    window.addEventListener("focus", onFocus);
    return (): void => {
      window.removeEventListener("focus", onFocus);
    };
  }, [refetchOnWindowFocus]);

  return { data: data ?? null, loading, error, refresh };
}
