/**
 * Optimistic Updates Utility
 * FE-API-018: Utilities for optimistic UI updates
 * Edge 6.2: Enhanced with conflict handling for data conflicts
 * Edge 3.1: Enhanced with concurrent update handling
 *
 * Provides helpers for implementing optimistic updates in React components
 * with automatic rollback on error and conflict resolution
 *
 * Concurrent Update Handling:
 * - React Query automatically queues mutations, but multiple mutations on the same
 *   resource can still run in parallel
 * - `cancelQueries` in `onMutate` prevents refetches from overwriting optimistic updates
 * - `setQueryData` is atomic, so optimistic updates are safe even if multiple mutations
 *   update the same query key simultaneously
 * - Conflict handling (409 errors) ensures server-side conflicts are detected and handled
 * - For critical resources, consider using `mutateAsync` and awaiting before triggering
 *   the next mutation to ensure sequential execution
 */

import { QueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { parseApiError } from './apiErrorHandler';
import { logger } from './logger';
import toast from './toast';

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

/**
 * Edge 6.2: Check if an error is a conflict error (HTTP 409)
 */
function isConflictError(error: unknown): boolean {
  if (error instanceof AxiosError) {
    return error.response?.status === 409;
  }
  // Check if it's an ApiError with code 409
  const apiError = parseApiError(error);
  const code = apiError.code;
  // Handle both number and string codes (code can be number or string)
  if (code === 409) {
    return true;
  }
  // Handle string codes
  if (typeof code === 'string' && code === '409') {
    return true;
  }
  // Fallback: convert to string and compare
  return String(code) === '409';
}

/**
 * Edge 6.2: Get conflict message from error
 */
function getConflictMessage(error: unknown): string {
  const apiError = parseApiError(error);
  if (apiError.message && apiError.message.toLowerCase().includes('conflict')) {
    return apiError.message;
  }
  return 'Une modification simultanée a été détectée. Les données ont été mises à jour.';
}

/**
 * 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;
  /** Edge 6.2: Function to handle conflicts (409 errors) - if not provided, uses default rollback */
  onConflict?: (
    error: unknown,
    variables: TVariables,
    context: OptimisticContext | undefined,
    queryClient: QueryClient,
  ) => void;
  /** Edge 6.2: Whether to show conflict message to user (default: true) */
  showConflictMessage?: boolean;
}

/**
 * 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,
    onConflict,
    showConflictMessage = true,
  } = options;

  return {
    onMutate: async (variables: TVariables): Promise<OptimisticContext> => {
      // Edge 3.1: Handle concurrent state updates
      // Cancel outgoing refetches to avoid overwriting optimistic update
      // This prevents race conditions when multiple mutations happen simultaneously
      // Snapshot previous values for rollback
      const previousValues: Array<{
        queryKey: (string | number)[];
        data: unknown;
      }> = [];

      // Cancel queries and snapshot previous values
      // Edge 3.1: cancelQueries ensures that any in-flight refetches are cancelled,
      // preventing them from overwriting our optimistic update
      for (const queryKey of queryKeys) {
        await queryClient.cancelQueries({ queryKey });

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

        // Edge 3.1: Apply optimistic update atomically
        // setQueryData is atomic, so even if multiple mutations update the same
        // query key simultaneously, the last one wins (which is correct behavior)
        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,
    ) => {
      // Edge 6.2: Handle conflicts (409 errors) specially
      const isConflict = isConflictError(error);

      if (isConflict && onConflict) {
        // Use custom conflict handler if provided
        try {
          onConflict(error, variables, context, queryClient);
        } catch (conflictError) {
          logger.error('[OptimisticUpdate] Error in conflict handler', {
            error: conflictError instanceof Error ? conflictError.message : String(conflictError),
          });
          // Fall back to default rollback on conflict handler error
          if (context?.previousValues) {
            for (const { queryKey, data } of context.previousValues) {
              queryClient.setQueryData(queryKey, data);
            }
          }
        }
      } else {
        // Default error handling: rollback optimistic update
        if (context?.previousValues) {
          // Restore previous values
          for (const { queryKey, data } of context.previousValues) {
            queryClient.setQueryData(queryKey, data);
          }
        }

        // Edge 6.2: Show conflict message to user
        if (isConflict && showConflictMessage) {
          const conflictMessage = getConflictMessage(error);
          toast.error(conflictMessage, {
            duration: 5000,
          });
          logger.warn('[OptimisticUpdate] Conflict detected', {
            error: error instanceof Error ? error.message : String(error),
            variables: typeof variables === 'object' ? variables : undefined,
          });
        }

        // 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;
  /** Edge 6.2: Function to handle conflicts (409 errors) */
  onConflict?: (
    error: unknown,
    variables: TVariables,
    context: OptimisticContext | undefined,
    queryClient: QueryClient,
  ) => void;
  /** Edge 6.2: Whether to show conflict message to user (default: true) */
  showConflictMessage?: boolean;
}

/**
 * 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,
    onConflict,
    showConflictMessage = true,
  } = options;

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

      // Edge 3.1: Cancel queries and snapshot to prevent concurrent update conflicts
      for (const queryKey of queryKeys) {
        await queryClient.cancelQueries({ queryKey });
        const previousData = queryClient.getQueryData(queryKey);
        previousValues.push({ queryKey, data: previousData });
      }

      // Edge 3.1: Apply optimistic update to array atomically
      // setQueryData is atomic, preventing race conditions in concurrent updates
      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,
    ) => {
      // Edge 6.2: Handle conflicts (409 errors) specially
      const isConflict = isConflictError(error);

      if (isConflict && onConflict) {
        // Use custom conflict handler if provided
        try {
          onConflict(error, variables, context, queryClient);
        } catch (conflictError) {
          logger.error('[OptimisticUpdate] Error in conflict handler', {
            error: conflictError instanceof Error ? conflictError.message : String(conflictError),
          });
          // Fall back to default rollback on conflict handler error
          if (context?.previousValues) {
            for (const { queryKey, data } of context.previousValues) {
              queryClient.setQueryData(queryKey, data);
            }
          }
        }
      } else {
        // Default error handling: rollback optimistic update
        if (context?.previousValues) {
          for (const { queryKey, data } of context.previousValues) {
            queryClient.setQueryData(queryKey, data);
          }
        }

        // Edge 6.2: Show conflict message to user
        if (isConflict && showConflictMessage) {
          const conflictMessage = getConflictMessage(error);
          toast.error(conflictMessage, {
            duration: 5000,
          });
          logger.warn('[OptimisticUpdate] Conflict detected', {
            error: error instanceof Error ? error.message : String(error),
            variables: typeof variables === 'object' ? variables : undefined,
          });
        }
      }
    },
    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<
  TVariables = unknown,
  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;
  /** Edge 6.2: Function to handle conflicts (409 errors) */
  onConflict?: (
    error: unknown,
    variables: TVariables,
    context: OptimisticContext | undefined,
    queryClient: QueryClient,
  ) => void;
  /** Edge 6.2: Whether to show conflict message to user (default: true) */
  showConflictMessage?: boolean;
}

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

      // Edge 3.1: Cancel queries and snapshot to prevent concurrent update conflicts
      for (const queryKey of queryKeys) {
        await queryClient.cancelQueries({ queryKey });
        const previousData = queryClient.getQueryData(queryKey);
        previousValues.push({ queryKey, data: previousData });

        // Edge 3.1: Apply optimistic update atomically
        // setQueryData is atomic, preventing race conditions in concurrent updates
        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,
    ) => {
      // Edge 6.2: Handle conflicts (409 errors) specially
      const isConflict = isConflictError(error);

      if (isConflict && onConflict) {
        // Use custom conflict handler if provided
        try {
          onConflict(error, variables, context, queryClient);
        } catch (conflictError) {
          logger.error('[OptimisticUpdate] Error in conflict handler', {
            error: conflictError instanceof Error ? conflictError.message : String(conflictError),
          });
          // Fall back to default rollback on conflict handler error
          if (context?.previousValues) {
            for (const { queryKey, data } of context.previousValues) {
              queryClient.setQueryData(queryKey, data);
            }
          }
        }
      } else {
        // Default error handling: rollback optimistic update
        if (context?.previousValues) {
          for (const { queryKey, data } of context.previousValues) {
            queryClient.setQueryData(queryKey, data);
          }
        }

        // Edge 6.2: Show conflict message to user
        if (isConflict && showConflictMessage) {
          const conflictMessage = getConflictMessage(error);
          toast.error(conflictMessage, {
            duration: 5000,
          });
          logger.warn('[OptimisticUpdate] Conflict detected', {
            error: error instanceof Error ? error.message : String(error),
            variables: typeof variables === 'object' ? variables : undefined,
          });
        }
      }
    },
    onSettled: () => {
      for (const queryKey of queryKeys) {
        queryClient.invalidateQueries({ queryKey });
      }
    },
  };
}
