/**
 * Vendor Deal workflows - CRUD with locked-field enforcement (FDS §3.6, §5.5).
 */

import { eq, sql, and, inArray } from 'drizzle-orm';
import { bytesToHex } from '@/lib/encoding.js';
import { firstZodIssue } from '@/lib/validation/zod.js';
import type { DealType } from '@/lib/deal-types/index.js';
import type { DrizzleClient, TxDrizzleClient } from '../db/client.js';
import { deals, adminActions, dealSkus, vendors, order, orderLine } from '../db/schema.js';
import { refreshDealCache } from '@/server/domain/variants/cache.js';
import { getDefaultSku } from '@/server/domain/variants/read.js';
import * as dealQueries from '../db/queries/deals.js';
import * as vendorQueries from '../db/queries/vendors.js';
import { createDealBodySchema, updateDealBodySchema } from '../schemas/vendor.js';
import type { CreateDealBody, UpdateDealBody } from '../schemas/vendor.js';
import { getSystemConfigCached } from '../db/queries/system-config.js';
import { createDraft } from './vendor-deal-draft.js';
import { getDraftById } from '../db/queries/deal-drafts.js';
import { runIdempotentDealDuplicate } from './deal-duplicate-idempotency.js';
import { decide } from '@/server/domain/vendor-deal/machine.js';
import { applyEffects } from '@/server/domain/vendor-deal/apply-effects.js';
import { createDealWithSkus } from '@/server/domain/vendor-deal/create-deal.js';
import type { DealState } from '@/server/domain/vendor-deal/events.js';
import type { DoClient } from '@/server/services/types.js';
import { writeImageSet } from '@/server/domain/vendor-deal-draft/apply-effects.js';
import { getPlatformImageLimits } from '@/server/services/image-limits.js';
import { invalidateCatalog } from '@/server/cache/invalidate.js';
import { captureCaught } from '@/server/observability/capture.server.js';

// ─── Typed errors ─────────────────────────────────────────────────────────────

export class LockedFieldsError extends Error {
  readonly code = 'LOCKED_FIELDS' as const;
  readonly fields: string[];

  constructor(fields: string[]) {
    super(
      `Cannot change locked field(s) [${fields.join(', ')}] after first sale. Contact Multideal admin.`,
    );
    this.name = 'LockedFieldsError';
    this.fields = fields;
  }
}

export class DealNotFoundError extends Error {
  readonly code = 'DEAL_NOT_FOUND' as const;
  constructor(dealId: string) {
    super(`Deal ${dealId} not found`);
    this.name = 'DealNotFoundError';
  }
}

export class UnauthorizedError extends Error {
  readonly code = 'UNAUTHORIZED' as const;
  constructor(message = 'Unauthorized') {
    super(message);
    this.name = 'UnauthorizedError';
  }
}

export class InvalidStateError extends Error {
  readonly code = 'INVALID_STATE' as const;
  constructor(message: string) {
    super(message);
    this.name = 'InvalidStateError';
  }
}

export class DuplicateContentError extends Error {
  readonly code = 'DUPLICATE_CONTENT' as const;
  constructor(reason: string) {
    super(reason);
    this.name = 'DuplicateContentError';
  }
}

export class DiscountTooLowError extends Error {
  readonly code = 'DISCOUNT_TOO_LOW' as const;
  readonly minPercent: number;
  constructor(minPercent: number) {
    super(`Deal discount must be at least ${minPercent}%.`);
    this.name = 'DiscountTooLowError';
    this.minPercent = minPercent;
  }
}

export class VendorNotActiveError extends Error {
  readonly code = 'VENDOR_NOT_ACTIVE' as const;
  constructor() {
    super('Vendor account is not active. Only ACTIVE vendors can publish deals.');
    this.name = 'VendorNotActiveError';
  }
}

export class DraftCapExceededError extends Error {
  readonly code = 'DRAFT_CAP_EXCEEDED' as const;
  readonly cap: number;
  constructor(cap: number) {
    super(`Draft cap of ${cap} reached. Become ACTIVE to create more deals.`);
    this.name = 'DraftCapExceededError';
    this.cap = cap;
  }
}

// ─── Deps interface ───────────────────────────────────────────────────────────

export interface VendorDealDeps {
  db: TxDrizzleClient;
  doClient: DoClient;
  env: { DATABASE_URL: string };
  waitUntil?: (p: Promise<unknown>) => void;
}

// ─── Content hash ─────────────────────────────────────────────────────────────

/**
 * Compute a deterministic SHA-256 content hash for deduplication.
 * Input: vendorId + normalized title + description + discountPercent
 */
export async function computeContentHash(
  vendorId: string,
  title: string,
  description: string,
  discountPercent: number,
): Promise<string> {
  const raw = `${vendorId}|${title.trim().toLowerCase()}|${description.trim().toLowerCase()}|${discountPercent}`;
  const encoded = new TextEncoder().encode(raw);
  const hashBuffer = await crypto.subtle.digest('SHA-256', encoded);
  return bytesToHex(hashBuffer);
}

// ─── Locked fields (mirrors deals.ts query helper) ────────────────────────────

/** Map from UpdateDealBody keys to locked deal column names (FDS §3.6).
 * Note: originalPrice / discountedPrice / discountPercent / quantityTotal now live in deal_skus.
 * SKU-level field locking is handled separately in the variant update flow.
 */
const LOCKED_FIELD_MAP: Record<string, string> = {
  pickupAddress: 'pickupAddress',
  pickupStart: 'pickupStart',
  pickupEnd: 'pickupEnd',
  title: 'title',
  description: 'description',
};

/** States that allow a vendor to publish (transition a deal to ACTIVE). */
const PUBLISHABLE_VENDOR_STATES = ['ACTIVE', 'VETERAN'] as const;
type PublishableVendorState = (typeof PUBLISHABLE_VENDOR_STATES)[number];

function isVendorPublishable(state: string): state is PublishableVendorState {
  return (PUBLISHABLE_VENDOR_STATES as readonly string[]).includes(state);
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

async function assertDealBelongsToVendor(db: DrizzleClient, dealId: string, vendorId: string) {
  const deal = await dealQueries.findById(db, dealId);
  if (!deal) throw new DealNotFoundError(dealId);
  if (deal.vendorId !== vendorId)
    throw new UnauthorizedError('Deal does not belong to this vendor');
  return deal;
}

// ─── createDeal helpers ───────────────────────────────────────────────────────

type CreateDealData = {
  dealType: DealType;
  title: string;
  description: string;
  categoryId: string | undefined;
  contentHash: string;
  windowStart?: string;
  windowEnd?: string;
  pickupStart?: string | null;
  pickupEnd?: string | null;
  pickupAddress?: string;
  specialInstructions?: string | null;
  isVoucher?: boolean;
  maxPerUser?: number | null;
  originalPrice: string;
  discountPercent: number;
  discountedPrice: string;
  quantityTotal: number;
  imageSet: Parameters<typeof writeImageSet>[2];
};

/**
 * Validate image set caps, persist deal + SKU row, and return the deal.
 * Extracted from createDeal to keep function under 100 lines.
 */
async function insertDealTiers(deps: VendorDealDeps, vendorId: string, data: CreateDealData) {
  const { imageSet } = data;
  const limits = await getPlatformImageLimits(deps.env);
  if (imageSet.deal.length > limits.maxImagesPerDeal) {
    throw new Error(
      `deal image count ${imageSet.deal.length} exceeds platform cap ${limits.maxImagesPerDeal}`,
    );
  }
  for (const set of imageSet.skus ?? []) {
    if (set.images.length > limits.maxImagesPerSku) {
      throw new Error(
        `sku ${set.optionValueCodes.join('|')} image count ${set.images.length} exceeds platform cap ${limits.maxImagesPerSku}`,
      );
    }
  }

  const { dealId: createdDealId } = await createDealWithSkus(deps.db, {
    dealRow: {
      vendorId,
      dealType: data.dealType,
      title: data.title,
      description: data.description,
      categoryId: data.categoryId,
      contentHash: data.contentHash,
      windowStart: data.windowStart ? new Date(data.windowStart) : undefined,
      windowEnd: data.windowEnd ? new Date(data.windowEnd) : undefined,
      pickupStart: data.pickupStart,
      pickupEnd: data.pickupEnd,
      pickupAddress: data.pickupAddress ?? '',
      specialInstructions: data.specialInstructions,
      isVoucher: data.isVoucher ?? false,
      maxPerUser: data.maxPerUser ?? null,
    },
    mode: 'single',
    single: {
      originalPrice: data.originalPrice,
      discountPercent: data.discountPercent,
      discountedPrice: data.discountedPrice,
      quantityTotal: data.quantityTotal,
    },
  });
  const deal = await dealQueries.findById(deps.db, createdDealId);
  if (!deal) throw new Error(`Deal ${createdDealId} not found after create`);
  return deal;
}

/**
 * Run the state-machine decider, apply effects, and write the image set.
 * Extracted from createDeal to keep function under 100 lines.
 */
async function enqueueDealCreatedEvent(
  deps: VendorDealDeps,
  deal: NonNullable<Awaited<ReturnType<typeof dealQueries.findById>>>,
  vendorId: string,
  isVeteran: boolean,
  contentHash: string,
  llmInputs: {
    title: string;
    description: string;
    categoryId: string | null;
    dealType: string;
    originalPrice: string;
    discountedPrice: string;
    discountPercent: number;
    primaryImageKey: string | null;
  },
  imageSet: Parameters<typeof writeImageSet>[2],
) {
  const decideResult = decide(
    { state: 'DRAFT' },
    {
      kind: 'create_requested',
      dealId: deal.id,
      vendorId,
      vendorTier: isVeteran ? 'VETERAN' : 'NEW',
      contentHash,
      windowEnd: deal.windowEnd ?? null,
      llmInputs,
      at: new Date(),
    },
  );

  if (!decideResult.ok) {
    throw new InvalidStateError(decideResult.message);
  }

  // Write images before applyEffects: the DB constraint deal_incomplete_for_active
  // requires at least one image when transitioning to ACTIVE (VETERAN path).
  await writeImageSet(
    deps.db,
    deal.id,
    imageSet,
    (isVeteran ? 'APPROVED' : 'PENDING') as 'PENDING' | 'APPROVED' | 'REJECTED',
  );

  const updated = await applyEffects(
    { db: deps.db, waitUntil: (promise) => deps.waitUntil?.(promise) },
    decideResult.effects,
  );

  return updated!;
}

// ─── computeAndPromoteVeteran ─────────────────────────────────────────────────

const VETERAN_REQUIRED_DEALS = 10;
const VETERAN_MAX_CHARGEBACK_RATE = 0.02;

/**
 * Lazily evaluates veteran eligibility at deal-submission time.
 * Already-VETERAN vendors return true immediately (single column read).
 * NEW vendors are checked against criteria; if met, tier is updated to VETERAN
 * and true is returned so the current deal also benefits immediately.
 */
async function computeAndPromoteVeteran(
  db: DrizzleClient,
  vendorId: string,
  currentTier: string,
): Promise<boolean> {
  if (currentTier === 'VETERAN') return true;

  const [dealCountRow] = await db
    .select({ cnt: sql<number>`COUNT(*)` })
    .from(deals)
    .where(
      and(
        eq(deals.vendorId, vendorId),
        inArray(deals.dealState, ['ACTIVE', 'SOLD_OUT', 'EXPIRED']),
        sql`EXISTS (SELECT 1 FROM deal_skus ds WHERE ds.deal_id = ${deals.id} AND ds.quantity_sold >= 1)`,
      ),
    );

  if (Number(dealCountRow?.cnt ?? 0) < VETERAN_REQUIRED_DEALS) return false;

  const [stats] = await db
    .select({
      total: sql<number>`COUNT(*) FILTER (WHERE ${order.status} = 'completed')`,
      refunded: sql<number>`COUNT(*) FILTER (WHERE ${order.status} IN ('refunded', 'partially_refunded'))`,
    })
    .from(orderLine)
    .innerJoin(order, eq(order.id, orderLine.orderId))
    .where(eq(orderLine.vendorId, vendorId));

  const totalCompleted = Number(stats?.total ?? 0);
  if (totalCompleted === 0) return false;

  const chargebackRate = Number(stats?.refunded ?? 0) / totalCompleted;
  if (chargebackRate >= VETERAN_MAX_CHARGEBACK_RATE) return false;

  await db
    .update(vendors)
    .set({ tier: 'VETERAN', accountState: 'VETERAN' })
    .where(eq(vendors.id, vendorId));
  return true;
}

// ─── createDeal ───────────────────────────────────────────────────────────────

/**
 * Creates a deal for a vendor.
 * - Validates input via Zod (discount 1–99%)
 * - Computes content hash and checks for duplicates
 * - VETERAN vendors: deal goes ACTIVE immediately
 * - Non-veteran: UNDER_REVIEW (LLM async) → on LLM pass: ACTIVE; on LLM flag: PENDING_APPROVAL
 */
export async function createDeal(deps: VendorDealDeps, vendorId: string, input: CreateDealBody) {
  const parsed = createDealBodySchema.safeParse(input);
  if (!parsed.success) {
    throw new Error(`Validation error: ${firstZodIssue(parsed.error)}`);
  }

  const vendor = await vendorQueries.findById(deps.db, vendorId);
  if (!vendor) throw new UnauthorizedError('Vendor not found');

  // ── Vendor state machine gate ────────────────────────────────────────────
  // Only ACTIVE / VETERAN vendors may create live deals via this workflow.
  // Non-active vendors must use the draft workflow (createDraft) instead.
  if (!isVendorPublishable(vendor.accountState)) {
    throw new VendorNotActiveError();
  }

  const contentHash = await computeContentHash(
    vendorId,
    parsed.data.title,
    parsed.data.description ?? '',
    parsed.data.discountPercent,
  );

  // ── Dedup check ──────────────────────────────────────────────────────────
  const existing = await deps.db
    .select({ id: deals.id, dealState: deals.dealState, createdAt: deals.createdAt })
    .from(deals)
    .where(and(eq(deals.vendorId, vendorId), eq(deals.contentHash, contentHash)));

  if (existing.length > 0) {
    const inFlight = existing.filter((d) =>
      ['UNDER_REVIEW', 'PENDING_APPROVAL', 'ACTIVE', 'PAUSED'].includes(d.dealState),
    );
    if (inFlight.length > 0) {
      throw new DuplicateContentError(
        'An identical deal is already active or in the approval pipeline.',
      );
    }

    // Reject re-submission of a recently-rejected deal (within 7 days)
    const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
    const recentlyRejected = existing.filter(
      (d) =>
        d.dealState === 'REJECTED' && d.createdAt != null && new Date(d.createdAt) > sevenDaysAgo,
    );
    if (recentlyRejected.length > 0) {
      throw new DuplicateContentError(
        'This deal was rejected recently. Please wait 7 days before resubmitting identical content.',
      );
    }
    // All previous same-hash deals are EXPIRED or SOLD_OUT - allow re-run
  }

  // ── Optional admin-configured minimum discount (default: none) ───────────
  const minDiscountStr = await getSystemConfigCached(deps.db, 'deal_min_discount_percent');
  const minDiscount = parseInt(minDiscountStr, 10) || 0;
  if (parsed.data.discountPercent < minDiscount) {
    throw new DiscountTooLowError(minDiscount);
  }

  const isVeteran = await computeAndPromoteVeteran(deps.db, vendorId, vendor.tier);

  const imageSet = parsed.data.imageSet;

  const deal = await insertDealTiers(deps, vendorId, {
    dealType: parsed.data.dealType,
    title: parsed.data.title,
    description: parsed.data.description ?? '',
    categoryId: parsed.data.categoryId || undefined,
    contentHash,
    windowStart: parsed.data.windowStart,
    windowEnd: parsed.data.windowEnd,
    pickupStart: parsed.data.pickupStart,
    pickupEnd: parsed.data.pickupEnd,
    pickupAddress: parsed.data.pickupAddress,
    specialInstructions: parsed.data.specialInstructions,
    isVoucher: parsed.data.isVoucher,
    maxPerUser: parsed.data.maxPerUser,
    originalPrice: parsed.data.originalPrice,
    discountPercent: parsed.data.discountPercent,
    discountedPrice: parsed.data.discountedPrice,
    quantityTotal: parsed.data.quantityTotal,
    imageSet,
  });

  const primaryImageKey = imageSet.deal.find((i) => i.isPrimary)?.r2Key ?? null;

  // Run decider + apply effects + write image set (VETERAN→ACTIVE, NEW→UNDER_REVIEW)
  return enqueueDealCreatedEvent(
    deps,
    deal,
    vendorId,
    isVeteran,
    contentHash,
    {
      title: parsed.data.title,
      description: parsed.data.description ?? '',
      categoryId: parsed.data.categoryId ?? null,
      dealType: parsed.data.dealType,
      originalPrice: parsed.data.originalPrice,
      discountedPrice: parsed.data.discountedPrice,
      discountPercent: parsed.data.discountPercent,
      primaryImageKey,
    },
    imageSet,
  );
}

// ─── updateDeal ───────────────────────────────────────────────────────────────

/**
 * Updates a deal with locked-field enforcement.
 * Throws LockedFieldsError if trying to change locked fields after first sale.
 */
export async function updateDeal(
  deps: VendorDealDeps,
  vendorId: string,
  dealId: string,
  patch: UpdateDealBody,
) {
  const parsed = updateDealBodySchema.safeParse(patch);
  if (!parsed.success) {
    throw new Error(`Validation error: ${firstZodIssue(parsed.error)}`);
  }

  await assertDealBelongsToVendor(deps.db, dealId, vendorId);

  // Check locked fields if any SKU has sold at least 1 unit
  const [hasSale] = await deps.db
    .select({ v: sql<number>`1` })
    .from(dealSkus)
    .where(and(eq(dealSkus.dealId, dealId), sql`${dealSkus.quantitySold} > 0`))
    .limit(1);
  if (hasSale) {
    const lockedAttempted: string[] = [];
    for (const [patchKey, dealKey] of Object.entries(LOCKED_FIELD_MAP)) {
      if (parsed.data[patchKey as keyof typeof parsed.data] !== undefined) {
        lockedAttempted.push(dealKey);
      }
    }
    if (lockedAttempted.length > 0) {
      throw new LockedFieldsError(lockedAttempted);
    }
  }

  // originalPrice / discountPercent / discountedPrice / quantityTotal now live in deal_skus.
  // If patch includes those fields, they must be applied to the default SKU separately
  // via the variant-update API. The deals UPDATE only touches deal-level fields.
  const [updated] = await deps.db
    .update(deals)
    .set({
      ...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),
      ...(parsed.data.description !== undefined ? { description: parsed.data.description } : {}),
      ...(parsed.data.categoryId !== undefined ? { categoryId: parsed.data.categoryId } : {}),
      ...(parsed.data.windowStart !== undefined
        ? { windowStart: new Date(parsed.data.windowStart) }
        : {}),
      ...(parsed.data.windowEnd !== undefined
        ? { windowEnd: new Date(parsed.data.windowEnd) }
        : {}),
      ...(parsed.data.pickupStart !== undefined ? { pickupStart: parsed.data.pickupStart } : {}),
      ...(parsed.data.pickupEnd !== undefined ? { pickupEnd: parsed.data.pickupEnd } : {}),
      ...(parsed.data.pickupAddress !== undefined
        ? { pickupAddress: parsed.data.pickupAddress }
        : {}),
      ...(parsed.data.specialInstructions !== undefined
        ? { specialInstructions: parsed.data.specialInstructions }
        : {}),
      ...(parsed.data.isVoucher !== undefined ? { isVoucher: parsed.data.isVoucher } : {}),
      ...(parsed.data.maxPerUser !== undefined ? { maxPerUser: parsed.data.maxPerUser } : {}),
    })
    .where(eq(deals.id, dealId))
    .returning();

  // Image set replacement — when client sends `imageSet`, treat it as
  // the authoritative new image set. writeImageSet deletes existing rows
  // and re-inserts deal-level + per-SKU images + visual-axis flag.
  const newImageSet = parsed.data.imageSet;
  if (newImageSet !== undefined) {
    const vendor = await vendorQueries.findById(deps.db, vendorId);
    const isVeteran = vendor?.tier === 'VETERAN';

    // Cap check before writes.
    const limits = await getPlatformImageLimits(deps.env);
    if (newImageSet.deal.length > limits.maxImagesPerDeal) {
      throw new Error(
        `deal image count ${newImageSet.deal.length} exceeds platform cap ${limits.maxImagesPerDeal}`,
      );
    }
    for (const set of newImageSet.skus ?? []) {
      if (set.images.length > limits.maxImagesPerSku) {
        throw new Error(
          `sku ${set.optionValueCodes.join('|')} image count ${set.images.length} exceeds platform cap ${limits.maxImagesPerSku}`,
        );
      }
    }

    await writeImageSet(
      deps.db,
      dealId,
      newImageSet,
      (isVeteran ? 'APPROVED' : 'PENDING') as 'PENDING' | 'APPROVED' | 'REJECTED',
    );
  }

  await invalidateCatalog(deps.db, { scope: 'deal', dealId });
  return updated!;
}

// ─── addQuantity ──────────────────────────────────────────────────────────────

/**
 * Increases quantity_total for an active deal (FDS §5.5).
 * No re-approval required. Creates an audit row.
 */
export async function addQuantity(
  deps: VendorDealDeps,
  vendorId: string,
  dealId: string,
  delta: number,
) {
  if (!Number.isInteger(delta) || delta < 1) {
    throw new Error('delta must be a positive integer');
  }

  const deal = await assertDealBelongsToVendor(deps.db, dealId, vendorId);

  if (deal.dealState !== 'ACTIVE') {
    throw new InvalidStateError(`Cannot add quantity to a deal in state ${deal.dealState}`);
  }

  // Update the default SKU's quantity_total (post-contract: price/qty live in deal_skus).
  const [updatedSku] = await deps.db
    .update(dealSkus)
    .set({ quantityTotal: sql`${dealSkus.quantityTotal} + ${delta}` })
    .where(and(eq(dealSkus.dealId, dealId), eq(dealSkus.optionIdsHash, 'default')))
    .returning({ quantityTotal: dealSkus.quantityTotal });

  // Refresh denormalised deal-level cache (stock_remaining, min/max price).
  await refreshDealCache(deps.db, dealId);
  await invalidateCatalog(deps.db, { scope: 'deal', dealId });

  // Audit log
  await deps.db.insert(adminActions).values({
    targetType: 'DEAL',
    targetId: dealId,
    action: 'ADD_QUANTITY',
    note: `Vendor added ${delta} units. New SKU total: ${updatedSku?.quantityTotal ?? 'unknown'}`,
  });

  return deal;
}

// ─── pauseDeal ────────────────────────────────────────────────────────────────

export async function pauseDeal(deps: VendorDealDeps, vendorId: string, dealId: string) {
  const deal = await assertDealBelongsToVendor(deps.db, dealId, vendorId);

  const result = decide(
    { state: deal.dealState as DealState },
    { kind: 'pause_requested', dealId, at: new Date() },
  );
  if (!result.ok) {
    throw new InvalidStateError(result.message);
  }

  const updated = await applyEffects(
    { db: deps.db, waitUntil: (promise) => deps.waitUntil?.(promise) },
    result.effects,
  );
  return updated!;
}

// ─── resumeDeal ───────────────────────────────────────────────────────────────

export async function resumeDeal(deps: VendorDealDeps, vendorId: string, dealId: string) {
  const deal = await assertDealBelongsToVendor(deps.db, dealId, vendorId);

  const result = decide(
    { state: deal.dealState as DealState },
    { kind: 'resume_requested', dealId, at: new Date() },
  );
  if (!result.ok) {
    throw new InvalidStateError(result.message);
  }

  const updated = await applyEffects(
    { db: deps.db, waitUntil: (promise) => deps.waitUntil?.(promise) },
    result.effects,
  );
  return updated!;
}

// ─── archiveDeal ──────────────────────────────────────────────────────────────

/**
 * Archives an ACTIVE or PAUSED deal — terminal soft-removal.
 * Hides the deal from default listings without deleting historical data
 * (purchases, redemptions, reviews remain intact for accounting/audit).
 * Cannot be reversed via the standard UI; admins may un-archive via DB if needed.
 */
export async function archiveDeal(deps: VendorDealDeps, vendorId: string, dealId: string) {
  const deal = await assertDealBelongsToVendor(deps.db, dealId, vendorId);

  const result = decide(
    { state: deal.dealState as DealState },
    { kind: 'archive_requested', dealId, at: new Date() },
  );
  if (!result.ok) {
    throw new InvalidStateError(result.message);
  }

  const updated = await applyEffects(
    { db: deps.db, waitUntil: (promise) => deps.waitUntil?.(promise) },
    result.effects,
  );

  try {
    await deps.doClient.disarmDealAlarm(dealId);
  } catch (err) {
    captureCaught(err, { scope: 'disarmDealAlarm.archive', extra: { dealId } });
  }

  return updated!;
}

// ─── duplicateDeal ────────────────────────────────────────────────────────────

/**
 * Duplicates an expired/sold-out deal as a new draft (via the draft workflow).
 * Copies: title, description, category, price, discount, instructions.
 * Resets: quantity to 10, window fields cleared (vendor sets fresh).
 * Draft cap is enforced by createDraft for non-ACTIVE/VETERAN vendors.
 * FDS §5.5 expired-row duplication.
 */
export async function duplicateDeal(
  deps: VendorDealDeps,
  vendorId: string,
  dealId: string,
  idempotencyKey: string,
) {
  return runIdempotentDealDuplicate(
    deps.db as TxDrizzleClient,
    { vendorId, sourceDealId: dealId, idempotencyKey },
    getDraftById,
    async (tx) => {
      const source = await assertDealBelongsToVendor(tx, dealId, vendorId);

      if (source.dealState !== 'EXPIRED' && source.dealState !== 'SOLD_OUT') {
        throw new InvalidStateError(
          `Can only duplicate EXPIRED or SOLD_OUT deals. Current state: ${source.dealState}`,
        );
      }

      // Price now lives on the default SKU, not the deal row.
      const sourceSku = await getDefaultSku(tx, dealId);
      if (!sourceSku) throw new InvalidStateError('Deal has no default SKU to duplicate');

      // Clone source fields into a draft payload; window fields intentionally omitted.
      const payload: Record<string, unknown> = {
        dealType: source.dealType,
        title: source.title,
        ...(source.description != null ? { description: source.description } : {}),
        ...(source.categoryId != null ? { categoryId: source.categoryId } : {}),
        originalPrice: sourceSku.originalPrice,
        discountPercent: sourceSku.discountPercent,
        discountedPrice: sourceSku.discountedPrice,
        quantityTotal: 10, // reset to default
        ...(source.pickupAddress != null ? { pickupAddress: source.pickupAddress } : {}),
        ...(source.specialInstructions != null
          ? { specialInstructions: source.specialInstructions }
          : {}),
      };

      return createDraft(tx, deps.doClient, { vendorId, payload }, deps.env);
    },
  );
}

// ─── submitForApproval ────────────────────────────────────────────────────────

/**
 * Transitions REJECTED → UNDER_REVIEW (re-submission after admin rejection).
 * DRAFT deals must go through the draft workflow (submitDraft) instead.
 */
export async function submitForApproval(deps: VendorDealDeps, vendorId: string, dealId: string) {
  const deal = await assertDealBelongsToVendor(deps.db, dealId, vendorId);

  const vendor = await vendorQueries.findById(deps.db, vendorId);
  if (!vendor) throw new UnauthorizedError('Vendor not found');

  // Only ACTIVE / VETERAN vendors may re-submit.
  if (!isVendorPublishable(vendor.accountState)) {
    throw new VendorNotActiveError();
  }

  const result = decide(
    { state: deal.dealState as DealState },
    { kind: 'submit_requested', dealId, at: new Date() },
  );
  if (!result.ok) {
    throw new InvalidStateError(result.message);
  }

  const updated = await applyEffects(
    { db: deps.db, waitUntil: (promise) => deps.waitUntil?.(promise) },
    result.effects,
  );
  return updated!;
}
