/**
 * createOptimisticMutation — standard helper for optimistic mutations.
 *
 * Wraps TanStack Query's `useMutation` with:
 *  - Immediate optimistic update via `onMutate`
 *  - Automatic rollback + Sentry breadcrumb + error toast on failure
 *  - Optional success toast
 *  - Query invalidation on settled (default) or success
 *
 * Usage:
 *   const useAddToCart = createOptimisticMutation({
 *     mutationFn: apiAddToCart,
 *     queryKey: qk.cart(),
 *     optimisticUpdate: (prev, vars) => ({ ...prev, items: [...prev.items, vars] }),
 *     errorToast: () => t('cart.add_failed'),
 *     successToast: () => t('cart.add_success'),
 *   });
 *
 *   // In component:
 *   const { mutate } = useAddToCart();
 */

import {
  useMutation,
  useQueryClient,
  type QueryKey,
  type MutationFunction,
  type QueryClient,
} from '@tanstack/react-query';
import * as Sentry from '@sentry/astro';
import { broadcastMhEvent } from '@/lib/cross-tab-sync';
import { notify } from './toast-bridge';

export interface OptimisticMutationConfig<TData, TVars, TSnapshot> {
  /** The async function that performs the server mutation. */
  mutationFn: MutationFunction<TData, TVars>;
  /** Query key to optimistically update and invalidate. */
  queryKey: QueryKey;
  /** Produce the optimistic snapshot from current cache + variables. */
  optimisticUpdate: (prev: TSnapshot | undefined, vars: TVars) => TSnapshot;
  /** Whether to invalidate on 'settled' (default) or 'success'. */
  invalidateOn?: 'settled' | 'success';
  /** Return an error message string for the toast. Defaults to generic message. */
  errorToast?: (err: unknown, vars: TVars) => string;
  /** Return a success message or null to suppress toast. */
  successToast?: (data: TData, vars: TVars) => string | null;
  /** Run extra success-side effects after the optimistic mutation succeeds. */
  afterSuccess?: (args: {
    data: TData;
    vars: TVars;
    queryClient: QueryClient;
  }) => void | Promise<void>;
  /** Run extra rollback/error-side effects after the mutation fails. */
  afterError?: (args: {
    err: unknown;
    vars: TVars;
    previous: TSnapshot | undefined;
    queryClient: QueryClient;
  }) => void | Promise<void>;
  /** Broadcast a cross-tab mutation event after server success. */
  broadcast?: boolean;
}

/**
 * Returns a custom `useOptimistic()` hook bound to the given config.
 * Call the returned hook inside a React component.
 */
export function createOptimisticMutation<TData, TVars, TSnapshot>(
  cfg: OptimisticMutationConfig<TData, TVars, TSnapshot>,
) {
  return function useOptimistic() {
    const qc = useQueryClient();
    return useMutation<TData, Error, TVars, { prev: TSnapshot | undefined }>({
      mutationFn: cfg.mutationFn,

      onMutate: async (vars) => {
        // Cancel in-flight queries to avoid race conditions
        await qc.cancelQueries({ queryKey: cfg.queryKey });
        const prev = qc.getQueryData<TSnapshot>(cfg.queryKey);
        qc.setQueryData<TSnapshot>(cfg.queryKey, (old) => cfg.optimisticUpdate(old, vars));
        return { prev };
      },

      onError: (err, vars, ctx) => {
        void qc.invalidateQueries({ queryKey: cfg.queryKey });
        // Record breadcrumb in Sentry for debugging
        Sentry.addBreadcrumb({
          category: 'mutation',
          level: 'warning',
          message: 'optimistic rollback',
          data: { queryKey: cfg.queryKey },
        });
        const msg = cfg.errorToast ? cfg.errorToast(err, vars) : 'Action failed — reverted';
        if (msg) notify.error(msg);
        void cfg.afterError?.({ err, vars, previous: ctx?.prev, queryClient: qc });
      },

      onSuccess: (data, vars) => {
        if ((cfg.invalidateOn ?? 'settled') === 'success') {
          void qc.invalidateQueries({ queryKey: cfg.queryKey });
        }
        const msg = cfg.successToast?.(data, vars) ?? null;
        if (msg) notify.success(msg);
        void cfg.afterSuccess?.({ data, vars, queryClient: qc });
        if (cfg.broadcast) {
          broadcastMhEvent({ kind: 'mutation' });
        }
      },

      onSettled: () => {
        if ((cfg.invalidateOn ?? 'settled') === 'settled') {
          void qc.invalidateQueries({ queryKey: cfg.queryKey });
        }
      },
    });
  };
}
