/**
 * Optimistic Updates Utility
 * FE-API-018: Utilities for optimistic UI updates
 *
 * Provides helpers for implementing optimistic updates in React components
 * with automatic rollback on error
 */

import { QueryClient } from '@tanstack/react-query';

/**
 * Options for optimistic update
 */
// Basic Generic Types
export interface OptimisticContext {
  previousValues: Array<{ queryKey: (string | number)[]; data: unknown }>;
}

/**
 * Options for optimistic update
 */
export interface OptimisticUpdateOptions<
  TData = unknown,
  TVariables = unknown,
> {
  /** QueryClient instance */
  queryClient: QueryClient;
  /** Query keys to update optimistically */
  queryKeys: (string | number)[][];
  /** Function to generate optimistic data from variables */
  optimisticData: (variables: TVariables) => TData;
  /** Function to update query data */
  updateQueryData?: (
    oldData: TData | undefined,
    variables: TVariables,
    queryClient: QueryClient,
  ) => TData;
  /** Function to rollback on error */
  rollback?: (variables: TVariables, queryClient: QueryClient) => void;
}

/**
 * Creates an optimistic update configuration for useMutation
 *
 * @example
 * ```typescript
 * const queryClient = useQueryClient();
 * const mutation = useMutation({
 *   mutationFn: updatePlaylist,
 *   ...createOptimisticUpdate({
 *     queryClient,
 *     queryKeys: [['playlist', playlistId], ['playlists']],
 *     optimisticData: (variables) => ({
 *       ...currentPlaylist,
 *       ...variables,
 *     }),
 *   }),
 * });
 * ```
 */
export function createOptimisticUpdate<TData = unknown, TVariables = unknown>(
  options: OptimisticUpdateOptions<TData, TVariables> & {
    queryClient: QueryClient;
  },
) {
  const { queryClient, queryKeys, optimisticData, updateQueryData, rollback } =
    options;

  return {
    onMutate: async (variables: TVariables): Promise<OptimisticContext> => {
      // Cancel outgoing refetches to avoid overwriting optimistic update
      // Snapshot previous values for rollback
      const previousValues: Array<{
        queryKey: (string | number)[];
        data: unknown;
      }> = [];

      // Cancel queries and snapshot previous values
      for (const queryKey of queryKeys) {
        await queryClient.cancelQueries({ queryKey });

        const previousData = queryClient.getQueryData(queryKey);
        previousValues.push({ queryKey, data: previousData });

        // Apply optimistic update
        if (updateQueryData) {
          queryClient.setQueryData<TData>(queryKey, (old) =>
            updateQueryData(old, variables, queryClient),
          );
        } else {
          // Default: replace with optimistic data
          queryClient.setQueryData<TData>(queryKey, optimisticData(variables));
        }
      }

      // Return context with previous values for rollback
      return { previousValues };
    },
    onError: (
      _error: Error,
      _variables: TVariables,
      context: OptimisticContext | undefined,
    ) => {
      if (context?.previousValues) {
        // Restore previous values
        for (const { queryKey, data } of context.previousValues) {
          queryClient.setQueryData(queryKey, data);
        }
      }

      // Call custom rollback if provided
      if (rollback) {
        rollback(_variables, queryClient);
      }
    },
    onSettled: () => {
      // Always refetch after error or success to ensure consistency
      for (const queryKey of queryKeys) {
        queryClient.invalidateQueries({ queryKey });
      }
    },
  };
}

/**
 * Helper for optimistic updates with array operations (add, remove, update)
 */
/**
 * Helper for optimistic updates with array operations (add, remove, update)
 */
export interface ArrayOptimisticUpdateOptions<
  TItem = unknown,
  TVariables = unknown,
  TData = unknown,
> {
  /** QueryClient instance */
  queryClient: QueryClient;
  /** Query keys to update */
  queryKeys: (string | number)[][];
  /** Query key that contains the array */
  arrayQueryKey: (string | number)[];
  /** Function to get the array from query data */
  getArray?: (data: TData) => TItem[];
  /** Operation type */
  operation: 'add' | 'remove' | 'update';
  /** Function to generate optimistic item */
  optimisticItem?: (variables: TVariables) => TItem;
  /** Function to match item for remove/update */
  matchItem?: (item: TItem, variables: TVariables) => boolean;
  /** Function to update item */
  updateItem?: (item: TItem, variables: TVariables) => TItem;
}

/**
 * Creates optimistic update for array operations (add, remove, update)
 *
 * @example
 * ```typescript
 * // Add item
 * const addMutation = useMutation({
 *   mutationFn: addTrackToPlaylist,
 *   ...createArrayOptimisticUpdate({
 *     queryKeys: [['playlist', playlistId]],
 *     arrayQueryKey: ['playlist', playlistId, 'tracks'],
 *     operation: 'add',
 *     optimisticItem: (variables) => ({
 *       id: `temp-${Date.now()}`,
 *       track_id: variables.trackId,
 *       ...variables,
 *     }),
 *   }),
 * });
 * // ...
 * ```
 */
/**
 * Helper to safely extract an array from unknown data
 */
function safeGetArray(data: unknown): unknown[] {
  if (Array.isArray(data)) return data;
  if (data && typeof data === 'object') {
    if (
      'items' in data &&
      Array.isArray((data as Record<string, unknown>).items)
    ) {
      return (data as Record<string, unknown>).items as unknown[];
    }
    // Check for tracks property common in this app
    if (
      'tracks' in data &&
      Array.isArray((data as Record<string, unknown>).tracks)
    ) {
      return (data as Record<string, unknown>).tracks as unknown[];
    }
  }
  return [];
}

/**
 * Creates optimistic update for array operations (add, remove, update)
 *
 * @example
 * ```typescript
 * // Add item
 * const addMutation = useMutation({
 *   mutationFn: addTrackToPlaylist,
 *   ...createArrayOptimisticUpdate({
 *     queryKeys: [['playlist', playlistId]],
 *     arrayQueryKey: ['playlist', playlistId, 'tracks'],
 *     operation: 'add',
 *     optimisticItem: (variables) => ({
 *       id: `temp-${Date.now()}`,
 *       track_id: variables.trackId,
 *       ...variables,
 *     }),
 *   }),
 * });
 * // ...
 * ```
 */
export function createArrayOptimisticUpdate<
  TItem = unknown,
  TVariables = unknown,
  TData = unknown,
>(options: ArrayOptimisticUpdateOptions<TItem, TVariables, TData>) {
  const {
    queryClient,
    queryKeys,
    arrayQueryKey,
    getArray = (data: unknown) => safeGetArray(data) as TItem[],
    operation,
    optimisticItem,
    matchItem,
    updateItem,
  } = options;

  return {
    onMutate: async (variables: TVariables): Promise<OptimisticContext> => {
      const previousValues: Array<{
        queryKey: (string | number)[];
        data: unknown;
      }> = [];

      // Cancel queries and snapshot
      for (const queryKey of queryKeys) {
        await queryClient.cancelQueries({ queryKey });
        const previousData = queryClient.getQueryData(queryKey);
        previousValues.push({ queryKey, data: previousData });
      }

      // Apply optimistic update to array
      for (const queryKey of queryKeys) {
        queryClient.setQueryData<TData>(queryKey, (old) => {
          if (!old) return old;

          // We treat old data as unknown initially
          const oldData = old as unknown;
          const array = getArray(oldData as TData);
          let newArray: TItem[];

          switch (operation) {
            case 'add':
              if (optimisticItem) {
                newArray = [...array, optimisticItem(variables)];
              } else {
                newArray = array;
              }
              break;

            case 'remove':
              if (matchItem) {
                newArray = array.filter(
                  (item: TItem) => !matchItem(item, variables),
                );
              } else {
                newArray = array;
              }
              break;

            case 'update':
              if (matchItem && updateItem) {
                newArray = array.map((item: TItem) =>
                  matchItem(item, variables)
                    ? updateItem(item, variables)
                    : item,
                );
              } else {
                newArray = array;
              }
              break;

            default:
              newArray = array;
          }

          // Update the array in the data structure
          if (Array.isArray(oldData)) {
            return newArray as unknown as TData;
          }

          if (oldData && typeof oldData === 'object') {
            const oldObj = oldData as Record<string, unknown>;
            if ('items' in oldObj) {
              return { ...oldObj, items: newArray } as unknown as TData;
            }
            if ('tracks' in oldObj) {
              return { ...oldObj, tracks: newArray } as unknown as TData;
            }
            // Fallback to arrayQueryKey path
            const lastKey = arrayQueryKey[arrayQueryKey.length - 1];
            if (lastKey) {
              return { ...oldObj, [lastKey]: newArray } as unknown as TData;
            }
          }

          return oldData as TData;
        });
      }

      return { previousValues };
    },
    onError: (
      _error: Error,
      _variables: TVariables,
      context: OptimisticContext | undefined,
    ) => {
      if (context?.previousValues) {
        for (const { queryKey, data } of context.previousValues) {
          queryClient.setQueryData(queryKey, data);
        }
      }
    },
    onSettled: () => {
      for (const queryKey of queryKeys) {
        queryClient.invalidateQueries({ queryKey });
      }
    },
  };
}

/**
 * Interface for entities that can be toggled (like/follow)
 */
interface ToggleableEntity {
  is_liked?: boolean;
  isLiked?: boolean;
  is_following?: boolean;
  isFollowing?: boolean;
  like_count?: number;
  likeCount?: number;
  follower_count?: number;
  followerCount?: number;
  [key: string]: unknown;
}

/**
 * Helper for toggle operations (like/unlike, follow/unfollow)
 */
export interface ToggleOptimisticUpdateOptions<TData = unknown> {
  /** QueryClient instance */
  queryClient: QueryClient;
  /** Query keys to update */
  queryKeys: (string | number)[][];
  /** Current state value */
  currentValue: boolean;
  /** Function to get the value from query data */
  getValue?: (data: TData) => boolean;
  /** Function to get the count from query data */
  getCount?: (data: TData) => number;
  /** New state value (opposite of currentValue) */
  newValue?: boolean;
  /** Whether to update count */
  updateCount?: boolean;
  /** Count increment/decrement */
  countDelta?: number;
}

/**
 * Creates optimistic update for toggle operations
 */
export function createToggleOptimisticUpdate<
  TVariables = unknown,
  TData = unknown,
>(options: ToggleOptimisticUpdateOptions<TData>) {
  const {
    queryClient,
    queryKeys,
    currentValue,
    getValue: _getValue,
    getCount: _getCount,
    newValue = !currentValue,
    updateCount = true,
    countDelta = 1,
  } = options;

  // No explicit getters used in the default implementation below, they operate on well-known properties directly.
  // We keep the parameters in options for future extensibility but ignore them in the body if not used.

  return {
    onMutate: async (_variables: TVariables): Promise<OptimisticContext> => {
      const previousValues: Array<{
        queryKey: (string | number)[];
        data: unknown;
      }> = [];

      for (const queryKey of queryKeys) {
        await queryClient.cancelQueries({ queryKey });
        const previousData = queryClient.getQueryData(queryKey);
        previousValues.push({ queryKey, data: previousData });

        queryClient.setQueryData<TData>(queryKey, (old) => {
          if (!old) return old;

          // Safe cast to ToggleableEntity for property access
          const entity = old as unknown as ToggleableEntity;
          const updated = { ...entity };

          // Update boolean value
          if ('is_liked' in updated) {
            updated.is_liked = newValue;
          }
          if ('isLiked' in updated) {
            updated.isLiked = newValue;
          }
          if ('is_following' in updated) {
            updated.is_following = newValue;
          }
          if ('isFollowing' in updated) {
            updated.isFollowing = newValue;
          }

          // Update count
          if (updateCount) {
            if ('like_count' in updated) {
              updated.like_count = Math.max(
                0,
                (updated.like_count || 0) + countDelta,
              );
            }
            if ('likeCount' in updated) {
              updated.likeCount = Math.max(
                0,
                (updated.likeCount || 0) + countDelta,
              );
            }
            if ('follower_count' in updated) {
              updated.follower_count = Math.max(
                0,
                (updated.follower_count || 0) + countDelta,
              );
            }
            if ('followerCount' in updated) {
              updated.followerCount = Math.max(
                0,
                (updated.followerCount || 0) + countDelta,
              );
            }
          }

          return updated as unknown as TData;
        });
      }

      return { previousValues };
    },
    onError: (
      _error: Error,
      _variables: TVariables,
      context: OptimisticContext | undefined,
    ) => {
      if (context?.previousValues) {
        for (const { queryKey, data } of context.previousValues) {
          queryClient.setQueryData(queryKey, data);
        }
      }
    },
    onSettled: () => {
      for (const queryKey of queryKeys) {
        queryClient.invalidateQueries({ queryKey });
      }
    },
  };
}
