import { bumpEpoch } from '@/server/cache/epoch.js';
import type { DrizzleClient } from '@/server/db/client.js';
import { insertOutboxRow } from '@/server/db/queries/outbox.js';
import { captureCaught } from '@/server/observability/capture.server.js';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';

export type CacheInvalidateScope =
  | { scope: 'global' }
  | { scope: 'deal'; dealId: string; categorySlug?: string; vendorSlug?: string }
  | { scope: 'category'; categorySlug: string }
  | { scope: 'vendor'; vendorSlug: string };

export const CACHE_INVALIDATE_EVENT = 'cache.invalidate';

// outbox.aggregate_id is a Postgres uuid column. Cache invalidations for
// category/vendor/global scopes have no single aggregate row, so they use
// the nil UUID sentinel — the real discriminant + ids live in payload, which
// is what the T8 dispatcher switches on.
const NIL_UUID = '00000000-0000-0000-0000-000000000000';

function aggregateIdFor(scope: CacheInvalidateScope): string {
  return scope.scope === 'deal' ? scope.dealId : NIL_UUID;
}

/**
 * Bumps catalog epoch INLINE (next read soft-misses + regenerates) and enqueues
 * async prewarm via outbox. Best-effort — never throws.
 */
export async function invalidateCatalog(
  db: DrizzleClient,
  scope: CacheInvalidateScope,
): Promise<void> {
  try {
    await bumpEpoch();
  } catch (err) {
    captureCaught(err, { scope: 'server.cache.invalidate.bump', severity: 'error' });
  }

  try {
    const { id } = await insertOutboxRow(db, {
      aggregateType: 'cache',
      aggregateId: aggregateIdFor(scope),
      eventType: CACHE_INVALIDATE_EVENT,
      payload: { ...scope },
    });
    await enqueueOutbox(id);
  } catch (err) {
    captureCaught(err, { scope: 'server.cache.invalidate.enqueue', severity: 'error' });
  }
}
