/**
 * useCart — unified cart hook.
 *
 * Branches by auth state:
 * - Anon  → localStorage (`md_cart_v2`), cross-tab sync, zustand store for badge.
 * - Logged-in → react-query `['cart']` against GET /api/cart; mutations
 *   against POST/PATCH/DELETE /api/cart/items with optimistic updates + rollback.
 *
 * Exposed API:
 *   { items, count, subtotal, isLoading, add, updateQty, remove, clear }
 *
 * Error codes emitted via `onError` callback (not translated strings):
 *   'OUT_OF_STOCK' | 'MAX_REACHED' | 'DEAL_NOT_FOUND' | 'NETWORK'
 *
 * NOTE: `items` for logged-in users is hydrated from the server snapshot and
 * includes rich fields (title, vendorName, unitPrice, imageId, maxQty, dealSkuId, skuLabel).
 * `items` for anon users contain {dealSkuId, qty, addedAt} plus optional snapshot
 * fields (title, imageUrl) stored in localStorage at add-time.
 */

'use client';

import { useEffect, useRef, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getCsrfToken } from '@/lib/csrf';
import { createOptimisticMutation } from '@/lib/query/optimistic';
import { qk } from '@/lib/query/keys';
import { captureCaught } from '@/lib/observability';
import {
  useAnonCart,
  readLocalCart,
  writeLocalCart,
  clearLocalCart,
  bootstrapAnonCart,
  type AnonCartItem,
} from './anonCartStore';

// ─── Error codes ──────────────────────────────────────────────────────────────

export type CartErrorCode = 'OUT_OF_STOCK' | 'MAX_REACHED' | 'DEAL_NOT_FOUND' | 'NETWORK';

// ─── Cart item shape ──────────────────────────────────────────────────────────

/** Minimal fields present for both anon and logged-in items. */
export interface CartItem {
  dealSkuId: string;
  /** Present for logged-in items only (resolved from SKU row). */
  dealId?: string;
  qty: number;
  addedAt: string; // ISO-8601
  /** Snapshot fields — present for logged-in; undefined for anon */
  title?: string;
  vendorName?: string;
  /** Unit price in agorot (integer). Derived from discountedPrice * 100. */
  unitPrice?: number;
  imageId?: string;
  /** Effective max qty this user may add (min of maxPerUser, remainingStock). */
  maxQty?: number;
  /** Locale-aware SKU variant label e.g. "L · Red". Null for deals without variants. */
  skuLabel?: string | null;
  /** Qty-tier definitions for this SKU. Empty array when no tiers apply. */
  qtyTiers?: { minQty: number; discountPercent: number }[];
}

// ─── Anon-only add options ────────────────────────────────────────────────────

export interface AnonAddOptions {
  /** If provided, qty is clamped to min(maxPerUser, stock) client-side. */
  maxPerUser?: number | null;
  stock?: number;
  /** Snapshot fields stored in localStorage so drawer can render without a server round-trip. */
  title?: string;
  imageUrl?: string;
}

// ─── Hook return type ─────────────────────────────────────────────────────────

export interface UseCartReturn {
  /** Cart line items. Anon items lack snapshot fields. */
  items: CartItem[];
  /** Sum of all item quantities. Raw number; display cap ("9+") is done in UI. */
  count: number;
  /** Subtotal in agorot (integer). Logged-in: derived from server; anon: 0 (no price snapshot). */
  subtotal: number;
  /** True while the server cart is loading or a mutation is in-flight. */
  isLoading: boolean;
  /** True when the underlying session or server-cart query has errored. */
  isError: boolean;
  /** First non-null query error, if any (session or server cart). */
  error: Error | null;
  /**
   * Add `qty` units of `dealSkuId` to the cart.
   * Anon: optional `opts` allows client-side clamping before accepting the qty.
   */
  add: (dealSkuId: string, qty: number, opts?: AnonAddOptions) => void;
  /** Update qty for a cart line. Removes the line if qty <= 0. */
  updateQty: (dealSkuId: string, qty: number) => void;
  /** Remove a cart line. */
  remove: (dealSkuId: string) => void;
  /** Clear all cart lines. */
  clear: () => void;
  /** Active undoable cart mutation, if any. */
  pendingUndo: PendingUndoState | null;
  /** Restore the pending cart mutation and cancel its commit. */
  undoPendingChange: () => void;
  /** Commit the pending cart mutation immediately. */
  dismissPendingUndo: () => void;
  /** Commit the shared pending cart mutation and wait for its server write. */
  flushPendingUndo: () => Promise<void>;
}

export interface PendingUndoState {
  id: string;
  dealSkuId: string;
  itemLabel: string;
  previousQty: number;
  nextQty: number;
  duration: number;
}

// ─── Session detection ────────────────────────────────────────────────────────

interface SessionData {
  ok: boolean;
  user?: { id: string };
}

async function fetchSession(): Promise<SessionData> {
  const res = await fetch('/api/auth/session');
  if (!res.ok) return { ok: false };
  return res.json() as Promise<SessionData>;
}

// ─── API helpers ──────────────────────────────────────────────────────────────

interface ServerCartResponse {
  ok: boolean;
  items?: ServerCartItem[];
  removed?: { dealId: string; reason: string }[];
  /** agorot integer from real API; ILS string from unit-test mocks */
  subtotal?: string | number;
  error?: string;
  code?: string;
}

interface ServerCartItem {
  dealSkuId?: string | null;
  dealId?: string;
  qty: number;
  addedAt: string | Date;
  dealTitle?: string;
  vendorName?: string;
  /** agorot integer from real API; ILS decimal string from unit-test mocks */
  discountedPrice?: string | number;
  maxPerUser?: number | null;
  remainingStock?: number;
  imageUrl?: string | null;
  skuLabel?: string | null;
  /** Qty-tier definitions for this SKU. Empty array when no tiers apply. */
  qtyTiers?: { minQty: number; discountPercent: number }[];
}

/**
 * Convert a discountedPrice field to agorot integer.
 * Real API sends integer agorot; unit-test mocks send ILS decimal strings.
 */
function toAgorot(value: string | number | undefined): number | undefined {
  if (value === undefined) return undefined;
  if (typeof value === 'number') return Math.round(value); // already agorot
  const v = parseFloat(value);
  return Number.isFinite(v) ? Math.round(v * 100) : undefined; // ILS string → agorot
}

function normalizeServerItems(raw: ServerCartItem[]): CartItem[] {
  return raw.map((item) => {
    const unitPrice = toAgorot(item.discountedPrice);
    const maxQty =
      item.maxPerUser !== undefined && item.remainingStock !== undefined
        ? item.maxPerUser !== null
          ? Math.min(item.maxPerUser, item.remainingStock)
          : item.remainingStock
        : undefined;
    // dealSkuId must be present on server items; fall back gracefully if absent (legacy data)
    const dealSkuId = item.dealSkuId ?? item.dealId ?? '';
    return {
      dealSkuId,
      dealId: item.dealId,
      qty: item.qty,
      addedAt: item.addedAt instanceof Date ? item.addedAt.toISOString() : (item.addedAt as string),
      title: item.dealTitle,
      vendorName: item.vendorName,
      unitPrice,
      maxQty,
      imageId: item.imageUrl ?? undefined,
      skuLabel: item.skuLabel ?? null,
      qtyTiers: item.qtyTiers ?? [],
    };
  });
}

function normalizeServerSubtotal(subtotal: string | number | undefined): number {
  if (subtotal === undefined) return 0;
  if (typeof subtotal === 'number') return Math.round(subtotal); // already agorot from real API
  const v = parseFloat(subtotal);
  return Number.isFinite(v) ? Math.round(v * 100) : 0; // ILS string from unit-test mocks
}

function calculateCartSubtotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + (item.unitPrice ?? 0) * item.qty, 0);
}

async function apiFetchCart(): Promise<{ items: CartItem[]; subtotal: number }> {
  const res = await fetch('/api/cart');
  const data = (await res.json()) as ServerCartResponse;
  if (!data.ok) throw new Error(data.error ?? 'Failed to load cart');
  return {
    items: normalizeServerItems(data.items ?? []),
    subtotal: normalizeServerSubtotal(data.subtotal),
  };
}

async function apiAddItem(
  dealSkuId: string,
  qty: number,
): Promise<{ items: CartItem[]; subtotal: number }> {
  const res = await fetch('/api/cart/items', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-csrf-token': getCsrfToken(),
    },
    body: JSON.stringify({ dealSkuId, qty }),
  });
  const data = (await res.json()) as ServerCartResponse;
  if (!data.ok) throw Object.assign(new Error(data.error ?? 'Add failed'), { code: data.code });
  return {
    items: normalizeServerItems(data.items ?? []),
    subtotal: normalizeServerSubtotal(data.subtotal),
  };
}

async function apiUpdateQty(
  dealSkuId: string,
  qty: number,
): Promise<{ items: CartItem[]; subtotal: number }> {
  const res = await fetch(`/api/cart/items/${dealSkuId}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
      'x-csrf-token': getCsrfToken(),
    },
    body: JSON.stringify({ qty }),
  });
  const data = (await res.json()) as ServerCartResponse;
  if (!data.ok) throw Object.assign(new Error(data.error ?? 'Update failed'), { code: data.code });
  return {
    items: normalizeServerItems(data.items ?? []),
    subtotal: normalizeServerSubtotal(data.subtotal),
  };
}

async function apiRemoveItem(dealSkuId: string): Promise<{ items: CartItem[]; subtotal: number }> {
  const res = await fetch(`/api/cart/items/${dealSkuId}`, {
    method: 'DELETE',
    headers: { 'x-csrf-token': getCsrfToken() },
  });
  const data = (await res.json()) as ServerCartResponse;
  if (!data.ok) throw Object.assign(new Error(data.error ?? 'Remove failed'), { code: data.code });
  return {
    items: normalizeServerItems(data.items ?? []),
    subtotal: normalizeServerSubtotal(data.subtotal),
  };
}

// ─── Error code extraction ────────────────────────────────────────────────────

function toCartErrorCode(err: unknown): CartErrorCode {
  const code = (err as { code?: string } | undefined)?.code;
  if (code === 'OUT_OF_STOCK') return 'OUT_OF_STOCK';
  if (code === 'MAX_REACHED') return 'MAX_REACHED';
  if (code === 'DEAL_NOT_FOUND') return 'DEAL_NOT_FOUND';
  return 'NETWORK';
}

// ─── Hook options ─────────────────────────────────────────────────────────────

export interface UseCartOptions {
  /** Called on any mutation or data error with a semantic code. */
  onError?: (code: CartErrorCode) => void;
}

const UNDO_WINDOW_MS = 5000;

type UndoableMutation = {
  id: string;
  owner: symbol;
  undo: PendingUndoState;
  applyOptimistic: () => void;
  restore: () => void;
  commit: () => Promise<void>;
  invalidate?: () => Promise<void>;
};

type PendingUndoableMutation = Omit<UndoableMutation, 'owner'>;

const sharedUndoState = {
  pendingMutation: null as UndoableMutation | null,
  pendingTimer: null as number | null,
  mutationGeneration: 0,
  pendingGeneration: 0,
  lineWriteQueues: new Map<string, Promise<void>>(),
  unsettledLineWrites: new Set<Promise<void>>(),
  reconciliationLines: new Set<string>(),
  listeners: new Set<() => void>(),
  activeHookInstances: 0,
};

function getSharedPendingUndo(): PendingUndoState | null {
  return sharedUndoState.pendingMutation?.undo ?? null;
}

function notifySharedPendingUndo(): void {
  sharedUndoState.listeners.forEach((listener) => listener());
}

function subscribeToSharedPendingUndo(listener: () => void): () => void {
  sharedUndoState.listeners.add(listener);
  return () => sharedUndoState.listeners.delete(listener);
}

function resetSharedUndoStateWhenUnused(): void {
  if (
    sharedUndoState.activeHookInstances !== 0 ||
    sharedUndoState.pendingMutation ||
    sharedUndoState.unsettledLineWrites.size !== 0
  ) {
    return;
  }
  sharedUndoState.lineWriteQueues.clear();
  sharedUndoState.reconciliationLines.clear();
  sharedUndoState.mutationGeneration = 0;
  sharedUndoState.pendingGeneration = 0;
}

function useCartMutation<TData, TVars, TSnapshot>(
  config: Parameters<typeof createOptimisticMutation<TData, TVars, TSnapshot>>[0],
) {
  const useMutationHook = createOptimisticMutation<TData, TVars, TSnapshot>(config);
  return useMutationHook();
}

// ─── useCart ──────────────────────────────────────────────────────────────────

/**
 * Unified cart hook. Branches by auth state detected via /api/auth/session.
 *
 * @example
 * ```tsx
 * const { items, count, add } = useCart({ onError: (code) => toast(t(code)) });
 * ```
 */
export function useCart(options: UseCartOptions = {}): UseCartReturn {
  const { onError } = options;
  const onErrorRef = useRef(onError);
  const ownerRef = useRef(Symbol('cart-undo-owner'));
  const [pendingUndo, setPendingUndo] = useState<PendingUndoState | null>(getSharedPendingUndo);
  const commitPendingMutationRef = useRef<() => Promise<void>>(() => Promise.resolve());
  useEffect(() => {
    onErrorRef.current = onError;
  });

  useEffect(() => subscribeToSharedPendingUndo(() => setPendingUndo(getSharedPendingUndo())), []);

  const queryClient = useQueryClient();

  const enqueueServerWrite = <T>(dealSkuId: string, write: () => Promise<T>): Promise<T> => {
    const previous = sharedUndoState.lineWriteQueues.get(dealSkuId) ?? Promise.resolve();
    const result = previous.then(write);
    const terminalWrite = result.then(
      () => undefined,
      () => undefined,
    );
    sharedUndoState.lineWriteQueues.set(dealSkuId, terminalWrite);
    sharedUndoState.unsettledLineWrites.add(terminalWrite);
    void terminalWrite.then(() => {
      sharedUndoState.unsettledLineWrites.delete(terminalWrite);
      resetSharedUndoStateWhenUnused();
    });
    return result;
  };

  const reconcileLineWhenSettled = (dealSkuId: string) => {
    const terminalWrite = sharedUndoState.lineWriteQueues.get(dealSkuId);
    if (!terminalWrite) return;
    void terminalWrite.then(() => {
      if (sharedUndoState.lineWriteQueues.get(dealSkuId) !== terminalWrite) {
        reconcileLineWhenSettled(dealSkuId);
        return;
      }
      if (sharedUndoState.pendingMutation?.undo.dealSkuId === dealSkuId) return;
      if (sharedUndoState.reconciliationLines.delete(dealSkuId)) {
        void queryClient.invalidateQueries({ queryKey: qk.cart() });
      }
    });
  };

  const clearPendingTimer = () => {
    if (sharedUndoState.pendingTimer !== null) {
      window.clearTimeout(sharedUndoState.pendingTimer);
      sharedUndoState.pendingTimer = null;
    }
  };

  const clearPendingUndo = () => {
    clearPendingTimer();
    sharedUndoState.pendingMutation = null;
    notifySharedPendingUndo();
  };

  const commitPendingMutation = (id?: string, rethrow = false): Promise<void> => {
    const pending = sharedUndoState.pendingMutation;
    if (!pending) return Promise.resolve();
    if (id !== undefined && pending.id !== id) return Promise.resolve();
    const generation = sharedUndoState.pendingGeneration;
    clearPendingTimer();
    sharedUndoState.pendingMutation = null;
    notifySharedPendingUndo();

    const dealSkuId = pending.undo.dealSkuId;
    const commitPromise = pending
      .commit()
      .then(async () => {
        if (generation === sharedUndoState.mutationGeneration) {
          if (sharedUndoState.reconciliationLines.delete(dealSkuId)) {
            await queryClient.invalidateQueries({ queryKey: qk.cart() });
          } else {
            await pending.invalidate?.();
          }
        }
      })
      .catch((err) => {
        captureCaught(err, {
          scope: 'features.cart.useCart.commitPendingMutation',
          severity: 'warning',
        });
        sharedUndoState.reconciliationLines.add(dealSkuId);
        if (rethrow) throw err;
      })
      .finally(() => {
        reconcileLineWhenSettled(dealSkuId);
      });
    return commitPromise;
  };

  const supersedePendingMutation = () => {
    sharedUndoState.mutationGeneration += 1;
    if (sharedUndoState.pendingMutation) {
      void commitPendingMutation();
    }
  };

  useEffect(() => {
    commitPendingMutationRef.current = commitPendingMutation;
  });

  const schedulePendingMutation = (mutation: PendingUndoableMutation) => {
    supersedePendingMutation();
    void queryClient.cancelQueries({ queryKey: qk.cart() });
    mutation.applyOptimistic();
    sharedUndoState.pendingMutation = { ...mutation, owner: ownerRef.current };
    sharedUndoState.pendingGeneration = sharedUndoState.mutationGeneration;
    notifySharedPendingUndo();
    clearPendingTimer();
    sharedUndoState.pendingTimer = window.setTimeout(() => {
      void commitPendingMutation(mutation.id);
    }, mutation.undo.duration);
  };

  useEffect(
    () => () => {
      if (sharedUndoState.pendingMutation?.owner === ownerRef.current) {
        void commitPendingMutationRef.current();
      }
      resetSharedUndoStateWhenUnused();
    },
    [],
  );

  useEffect(() => {
    sharedUndoState.activeHookInstances += 1;
    return () => {
      sharedUndoState.activeHookInstances -= 1;
      resetSharedUndoStateWhenUnused();
    };
  }, []);

  // ── Session query (always runs; decides anon vs logged-in branch) ──────────
  const sessionQuery = useQuery({
    queryKey: ['session'],
    queryFn: fetchSession,
    staleTime: 5 * 60_000,
    retry: false,
  });

  const isLoggedIn = sessionQuery.data?.ok === true && sessionQuery.data.user !== undefined;
  const sessionPending = sessionQuery.isPending;

  // ── Anon store bootstrap ───────────────────────────────────────────────────
  useEffect(() => {
    bootstrapAnonCart();
  }, []);

  // ── Anon state ─────────────────────────────────────────────────────────────
  const anonItems = useAnonCart((s) => s.items);

  // ── Server cart query (only when logged-in) ────────────────────────────────
  const serverCartQuery = useQuery({
    queryKey: ['cart'],
    queryFn: apiFetchCart,
    enabled: isLoggedIn,
    staleTime: 30_000,
  });

  // ── Optimistic mutations (logged-in) ──────────────────────────────────────

  type CartSnapshot = { items: CartItem[]; subtotal: number };

  // Factories are created inline so onErrorRef is captured in errorToast callbacks.
  // createOptimisticMutation returns a hook; we call it here (unconditional, top-level of useCart).

  // Wrap API fns to fire onError callback before re-throwing.
  async function addItemWithCallback(vars: {
    dealSkuId: string;
    qty: number;
  }): Promise<CartSnapshot> {
    try {
      return await apiAddItem(vars.dealSkuId, vars.qty);
    } catch (err) {
      onErrorRef.current?.(toCartErrorCode(err));
      throw err;
    }
  }
  async function updateQtyWithCallback(vars: {
    dealSkuId: string;
    qty: number;
  }): Promise<CartSnapshot> {
    try {
      return await apiUpdateQty(vars.dealSkuId, vars.qty);
    } catch (err) {
      onErrorRef.current?.(toCartErrorCode(err));
      throw err;
    }
  }
  async function removeItemWithCallback(vars: { dealSkuId: string }): Promise<CartSnapshot> {
    try {
      return await apiRemoveItem(vars.dealSkuId);
    } catch (err) {
      onErrorRef.current?.(toCartErrorCode(err));
      throw err;
    }
  }

  const addMutation = useCartMutation<
    CartSnapshot,
    { dealSkuId: string; qty: number },
    CartSnapshot
    // ref read happens inside async mutationFn at mutation time (event handler), not during render.
  >({
    mutationFn: (vars) => enqueueServerWrite(vars.dealSkuId, () => addItemWithCallback(vars)),
    queryKey: qk.cart(),
    optimisticUpdate: (prev, { dealSkuId, qty }) => {
      if (!prev) return { items: [], subtotal: 0 };
      const existing = prev.items.find((i) => i.dealSkuId === dealSkuId);
      const newQty = existing ? existing.qty + qty : qty;
      const optimistic: CartItem = existing
        ? { ...existing, qty: newQty }
        : { dealSkuId, qty: newQty, addedAt: new Date().toISOString() };
      return {
        items: existing
          ? prev.items.map((i) => (i.dealSkuId === dealSkuId ? optimistic : i))
          : [...prev.items, optimistic],
        subtotal: prev.subtotal,
      };
    },
  });

  const updateQtyMutation = useCartMutation<
    CartSnapshot,
    { dealSkuId: string; qty: number },
    CartSnapshot
    // ref read happens inside async mutationFn at mutation time (event handler), not during render.
  >({
    mutationFn: (vars) => enqueueServerWrite(vars.dealSkuId, () => updateQtyWithCallback(vars)),
    queryKey: qk.cart(),
    optimisticUpdate: (prev, { dealSkuId, qty }) => {
      if (!prev) return { items: [], subtotal: 0 };
      return {
        items: prev.items.map((i) => (i.dealSkuId === dealSkuId ? { ...i, qty } : i)),
        subtotal: prev.subtotal,
      };
    },
  });

  const removeMutation = useCartMutation<
    CartSnapshot,
    { dealSkuId: string },
    CartSnapshot
    // ref read happens inside async mutationFn at mutation time (event handler), not during render.
  >({
    mutationFn: (vars) => enqueueServerWrite(vars.dealSkuId, () => removeItemWithCallback(vars)),
    queryKey: qk.cart(),
    optimisticUpdate: (prev, { dealSkuId }) => {
      if (!prev) return { items: [], subtotal: 0 };
      return {
        items: prev.items.filter((i) => i.dealSkuId !== dealSkuId),
        subtotal: prev.subtotal,
      };
    },
  });

  // ── Anon actions ───────────────────────────────────────────────────────────

  function anonAdd(dealSkuId: string, qty: number, opts?: AnonAddOptions): void {
    const current = readLocalCart();
    const existing = current.find((i) => i.dealSkuId === dealSkuId);
    const existingQty = existing?.qty ?? 0;
    const requested = existingQty + qty;

    let clamped = requested;
    if (opts !== undefined) {
      const { maxPerUser = null, stock } = opts;
      if (stock !== undefined) {
        const cap = maxPerUser !== null ? Math.min(maxPerUser, stock) : stock;
        clamped = Math.max(0, Math.min(requested, cap));
        if (clamped === existingQty) {
          if (stock <= 0) {
            onErrorRef.current?.('OUT_OF_STOCK');
          } else {
            onErrorRef.current?.('MAX_REACHED');
          }
          return;
        }
      }
    }

    const now = new Date().toISOString();
    const snapshot = { title: opts?.title, imageUrl: opts?.imageUrl };
    const updated: AnonCartItem[] = existing
      ? current.map((i) => (i.dealSkuId === dealSkuId ? { ...i, qty: clamped, ...snapshot } : i))
      : [...current, { dealSkuId, qty: clamped, addedAt: now, ...snapshot }];
    supersedePendingMutation();
    writeLocalCart(updated);
  }

  function anonUpdateQty(dealSkuId: string, qty: number): void {
    const current = readLocalCart();
    if (qty <= 0) {
      writeLocalCart(current.filter((i) => i.dealSkuId !== dealSkuId));
      return;
    }
    writeLocalCart(current.map((i) => (i.dealSkuId === dealSkuId ? { ...i, qty } : i)));
  }

  function queueAnonMutation(
    dealSkuId: string,
    nextItems: AnonCartItem[],
    previousItems: AnonCartItem[],
  ): void {
    const previousLine = previousItems.find((item) => item.dealSkuId === dealSkuId);
    if (!previousLine) return;
    const nextLine = nextItems.find((item) => item.dealSkuId === dealSkuId);
    const itemLabel = previousLine.title ?? dealSkuId.slice(0, 8) + '…';
    const id = `undo-${Date.now()}-${dealSkuId}`;
    schedulePendingMutation({
      id,
      undo: {
        id,
        dealSkuId,
        itemLabel,
        previousQty: previousLine.qty,
        nextQty: nextLine?.qty ?? 0,
        duration: UNDO_WINDOW_MS,
      },
      applyOptimistic: () => writeLocalCart(nextItems),
      restore: () => writeLocalCart(previousItems),
      commit: async () => {},
    });
  }

  function queueServerMutation(
    dealSkuId: string,
    previousSnapshot: CartSnapshot,
    nextSnapshot: CartSnapshot,
    commit: () => Promise<unknown>,
  ): void {
    const previousLine = previousSnapshot.items.find((item) => item.dealSkuId === dealSkuId);
    if (!previousLine) return;
    const nextLine = nextSnapshot.items.find((item) => item.dealSkuId === dealSkuId);
    const itemLabel = previousLine.title ?? previousLine.dealSkuId.slice(0, 8) + '…';
    const id = `undo-${Date.now()}-${dealSkuId}`;
    schedulePendingMutation({
      id,
      undo: {
        id,
        dealSkuId,
        itemLabel,
        previousQty: previousLine.qty,
        nextQty: nextLine?.qty ?? 0,
        duration: UNDO_WINDOW_MS,
      },
      applyOptimistic: () => queryClient.setQueryData(qk.cart(), nextSnapshot),
      restore: () => queryClient.setQueryData(qk.cart(), previousSnapshot),
      commit: async () => {
        await enqueueServerWrite(dealSkuId, commit);
      },
      invalidate: () => queryClient.invalidateQueries({ queryKey: qk.cart() }),
    });
  }

  // ── Derive exposed values ──────────────────────────────────────────────────

  let items: CartItem[];
  let subtotal: number;
  let isLoading: boolean;
  let isError: boolean;
  let error: Error | null;

  if (!isLoggedIn) {
    // Anon branch (also used while session is still pending to avoid flicker)
    items = anonItems.map(
      (i): CartItem => ({
        dealSkuId: i.dealSkuId,
        qty: i.qty,
        addedAt: i.addedAt,
        title: i.title,
        imageId: i.imageUrl,
        qtyTiers: [],
      }),
    );
    subtotal = 0;
    isLoading = sessionPending;
    isError = sessionQuery.isError;
    error = (sessionQuery.error as Error | null) ?? null;
  } else {
    // Logged-in branch
    items = serverCartQuery.data?.items ?? [];
    subtotal = serverCartQuery.data?.subtotal ?? 0;
    isLoading =
      serverCartQuery.isLoading ||
      addMutation.isPending ||
      updateQtyMutation.isPending ||
      removeMutation.isPending;
    isError = sessionQuery.isError || serverCartQuery.isError;
    error = (sessionQuery.error as Error | null) ?? (serverCartQuery.error as Error | null) ?? null;
  }

  const count = items.reduce((sum, i) => sum + i.qty, 0);

  // ── Actions ────────────────────────────────────────────────────────────────

  const add = (dealSkuId: string, qty: number, opts?: AnonAddOptions): void => {
    if (!isLoggedIn) {
      anonAdd(dealSkuId, qty, opts);
    } else {
      supersedePendingMutation();
      addMutation.mutate({ dealSkuId, qty });
    }
  };

  const updateQty = (dealSkuId: string, qty: number): void => {
    if (!isLoggedIn) {
      const current = readLocalCart();
      const currentLine = current.find((item) => item.dealSkuId === dealSkuId);
      if (!currentLine) return;
      if (qty >= currentLine.qty) {
        supersedePendingMutation();
        anonUpdateQty(dealSkuId, qty);
        return;
      }
      const nextItems =
        qty <= 0
          ? current.filter((item) => item.dealSkuId !== dealSkuId)
          : current.map((item) => (item.dealSkuId === dealSkuId ? { ...item, qty } : item));
      queueAnonMutation(dealSkuId, nextItems, current);
    } else {
      const currentSnapshot = (queryClient.getQueryData(qk.cart()) as CartSnapshot | undefined) ?? {
        items,
        subtotal,
      };
      const currentLine = currentSnapshot.items.find((item) => item.dealSkuId === dealSkuId);
      if (!currentLine) return;
      if (qty >= currentLine.qty) {
        supersedePendingMutation();
        updateQtyMutation.mutate({ dealSkuId, qty });
        return;
      }
      const optimisticItems =
        qty <= 0
          ? currentSnapshot.items.filter((item) => item.dealSkuId !== dealSkuId)
          : currentSnapshot.items.map((item) =>
              item.dealSkuId === dealSkuId ? { ...item, qty } : item,
            );
      const nextSnapshot: CartSnapshot = {
        items: optimisticItems,
        subtotal: calculateCartSubtotal(optimisticItems),
      };
      queueServerMutation(
        dealSkuId,
        currentSnapshot,
        nextSnapshot,
        qty <= 0
          ? async () => {
              return removeItemWithCallback({ dealSkuId });
            }
          : async () => {
              return updateQtyWithCallback({ dealSkuId, qty });
            },
      );
    }
  };

  const remove = (dealSkuId: string): void => {
    if (!isLoggedIn) {
      const current = readLocalCart();
      const nextItems = current.filter((item) => item.dealSkuId !== dealSkuId);
      queueAnonMutation(dealSkuId, nextItems, current);
    } else {
      const currentSnapshot = (queryClient.getQueryData(qk.cart()) as CartSnapshot | undefined) ?? {
        items,
        subtotal,
      };
      const optimisticItems = currentSnapshot.items.filter((item) => item.dealSkuId !== dealSkuId);
      const nextSnapshot: CartSnapshot = {
        items: optimisticItems,
        subtotal: calculateCartSubtotal(optimisticItems),
      };
      queueServerMutation(dealSkuId, currentSnapshot, nextSnapshot, async () => {
        return removeItemWithCallback({ dealSkuId });
      });
    }
  };

  const clear = (): void => {
    supersedePendingMutation();
    if (!isLoggedIn) {
      clearLocalCart();
    } else {
      // Optimistic clear
      queryClient.setQueryData(qk.cart(), { items: [], subtotal: 0 });
      void queryClient.invalidateQueries({ queryKey: qk.cart() });
    }
  };

  const undoPendingChange = () => {
    const pending = sharedUndoState.pendingMutation;
    if (!pending) return;
    const dealSkuId = pending.undo.dealSkuId;
    pending.restore();
    clearPendingUndo();
    sharedUndoState.reconciliationLines.add(dealSkuId);
    reconcileLineWhenSettled(dealSkuId);
  };

  const dismissPendingUndo = () => {
    void commitPendingMutation();
  };

  const flushPendingUndo = async () => {
    await commitPendingMutation(undefined, true);
    await Promise.all(sharedUndoState.lineWriteQueues.values());
  };

  return {
    items,
    count,
    subtotal,
    isLoading,
    isError,
    error,
    add,
    updateQty,
    remove,
    clear,
    pendingUndo,
    undoPendingChange,
    dismissPendingUndo,
    flushPendingUndo,
  };
}
