/**
 * Vendor Deal Draft workflows.
 *
 * Decider/executor split:
 *   - domain/vendor-deal-draft/machine.ts       pure validation + effect emission
 *   - domain/vendor-deal-draft/apply-effects.ts DB writes + atomic submit tx
 *   - this file                                 loads context, runs decider, applies effects
 *
 * Manages the full lifecycle of deal drafts:
 *   createDraft → updateDraftPayload → toggleStar → deleteDraftWorkflow → submitDraft
 *
 * submitDraft is atomic: createDeal + relinkImages + enqueueOutbox + deleteDraft
 * all run inside a single Drizzle transaction. OUTBOX_QUEUE.send must be called
 * by the caller (API route) after the transaction commits, per outbox pattern (§8).
 */

import type { DrizzleClient, TxDrizzleClient } from '../db/client.js';
import { acquireCapLock, countDraftsByVendor, getDraftById } from '../db/queries/deal-drafts.js';
import * as vendorQueries from '../db/queries/vendors.js';
import { decide } from '@/server/domain/vendor-deal-draft/machine.js';
import {
  applyEffects,
  applyTransactionalEffects,
} from '@/server/domain/vendor-deal-draft/apply-effects.js';
import type { DoClient } from '@/server/services/types.js';

type EnvLike = { DATABASE_URL: string };

// ─── Error classes ────────────────────────────────────────────────────────────

export class EmptyDraftError extends Error {
  readonly code = 'EMPTY_DRAFT' as const;
  constructor() {
    super('Draft payload is fully empty — nothing to save.');
    this.name = 'EmptyDraftError';
  }
}

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

export class DraftNotFoundError extends Error {
  readonly code = 'DRAFT_NOT_FOUND' as const;
  constructor(id?: string) {
    super(id ? `Draft ${id} not found.` : 'Draft not found.');
    this.name = 'DraftNotFoundError';
  }
}

export class UnauthorizedDraftError extends Error {
  readonly code = 'UNAUTHORIZED' as const;
  constructor() {
    super('You do not have access to this draft.');
    this.name = 'UnauthorizedDraftError';
  }
}

export class PaymentNotReadyError extends Error {
  readonly code = 'PAYMENT_NOT_READY' as const;
  constructor() {
    super('Payment setup not complete — connect Stripe before publishing.');
    this.name = 'PaymentNotReadyError';
  }
}

export class InvalidDraftPayloadError extends Error {
  readonly code = 'INVALID_PAYLOAD' as const;
  readonly fieldErrors: Record<string, string[]>;
  constructor(fieldErrors: Record<string, string[]>) {
    super('Draft payload is not a valid publishable deal.');
    this.name = 'InvalidDraftPayloadError';
    this.fieldErrors = fieldErrors;
  }
}

// ─── Internal helpers ─────────────────────────────────────────────────────────

async function ownedOr404(db: DrizzleClient, id: string, vendorId: string) {
  const d = await getDraftById(db, id);
  if (!d) throw new DraftNotFoundError(id);
  if (d.vendorId !== vendorId) throw new UnauthorizedDraftError();
  return d;
}

function mapDeciderError(result: {
  ok: false;
  error: string;
  message: string;
  fieldErrors?: Record<string, string[]>;
  cap?: number;
}): never {
  switch (result.error) {
    case 'EMPTY_DRAFT':
      throw new EmptyDraftError();
    case 'DRAFT_CAP_EXCEEDED':
      throw new DraftCapExceededError(result.cap ?? 3);
    case 'INVALID_PAYLOAD':
      throw new InvalidDraftPayloadError(result.fieldErrors ?? {});
    default:
      throw new Error(`Unknown decider error: ${result.error} — ${result.message}`);
  }
}

// ─── Workflow functions ───────────────────────────────────────────────────────

/**
 * Create a new draft for a vendor.
 *
 * Validates that the payload is not fully empty, then enforces the draft cap
 * for non-ACTIVE vendors using an advisory lock to prevent race conditions.
 */
export async function createDraft(
  db: TxDrizzleClient,
  doClient: DoClient,
  input: { vendorId: string; payload: unknown },
  env: EnvLike,
) {
  return db.transaction(async (tx) => {
    await acquireCapLock(tx, input.vendorId);

    const vendor = await vendorQueries.findById(tx, input.vendorId);
    if (!vendor) throw new UnauthorizedDraftError();

    const capExempt = vendor.accountState === 'ACTIVE' || vendor.accountState === 'VETERAN';
    const currentDraftCount = capExempt ? 0 : await countDraftsByVendor(tx, input.vendorId);

    const result = decide({
      kind: 'create_requested',
      vendorId: input.vendorId,
      payload: input.payload,
      capExempt,
      currentDraftCount,
      at: new Date(),
    });
    if (!result.ok) mapDeciderError(result);

    const applied = await applyEffects({ db: tx, doClient, env }, result.effects);
    if (!applied.insertedDraft) throw new Error('insert-draft effect did not produce a row');
    return applied.insertedDraft;
  });
}

/**
 * Replace the payload of an existing draft.
 *
 * If the incoming payload is fully empty the draft is auto-deleted and
 * `{ deleted: true }` is returned — callers should treat this as success.
 */
export async function updateDraftPayload(
  db: DrizzleClient,
  doClient: DoClient,
  input: { id: string; vendorId: string; payload: unknown },
  env: EnvLike,
): Promise<{ deleted: true } | { draft: Awaited<ReturnType<typeof getDraftById>> }> {
  await ownedOr404(db, input.id, input.vendorId);

  const result = decide({
    kind: 'update_requested',
    draftId: input.id,
    vendorId: input.vendorId,
    payload: input.payload,
    at: new Date(),
  });
  if (!result.ok) mapDeciderError(result);

  const applied = await applyEffects({ db, doClient, env }, result.effects);
  if (applied.deleted) return { deleted: true };
  if (!applied.updatedDraft) throw new Error('update-draft effect did not produce a row');
  return { draft: applied.updatedDraft };
}

/**
 * Toggle the starred flag on a draft.
 */
export async function toggleStar(
  db: DrizzleClient,
  doClient: DoClient,
  input: { id: string; vendorId: string },
  env: EnvLike,
) {
  const d = await ownedOr404(db, input.id, input.vendorId);
  const result = decide({
    kind: 'star_toggle',
    draftId: input.id,
    vendorId: input.vendorId,
    currentlyStarred: d.isStarred,
    at: new Date(),
  });
  if (!result.ok) mapDeciderError(result);
  const applied = await applyEffects({ db, doClient, env }, result.effects);
  return applied.starredDraft;
}

/**
 * Delete a draft explicitly (vendor action).
 */
export async function deleteDraftWorkflow(
  db: DrizzleClient,
  doClient: DoClient,
  input: { id: string; vendorId: string },
  env: EnvLike,
) {
  await ownedOr404(db, input.id, input.vendorId);
  const result = decide({
    kind: 'delete_requested',
    draftId: input.id,
    vendorId: input.vendorId,
    at: new Date(),
  });
  if (!result.ok) mapDeciderError(result);
  await applyEffects({ db, doClient, env }, result.effects);
}

/**
 * Submit a draft as a publishable deal.
 *
 * Atomically (single transaction):
 *   1. Validates the full createDealBodySchema against the draft payload.
 *   2. Inserts a deal row in UNDER_REVIEW state (isVoucher included).
 *   3. Inserts deal_images rows from imageSet (deal-level + per-SKU + visual-axis flag).
 *   4. Assigns tagIds via setDealTags.
 *   5. Inserts an outbox event so the queue consumer triggers LLM moderation.
 *   6. Deletes the draft.
 *
 * Returns `{ dealId, outboxId }`. Caller must call `env.OUTBOX_QUEUE.send({ outboxId })`
 * after this function returns to trigger the queue consumer (outbox pattern §8).
 */
export async function submitDraft(
  db: TxDrizzleClient,
  doClient: DoClient,
  input: { id: string; vendorId: string },
  env: EnvLike,
): Promise<{ dealId: string; outboxId: string }> {
  const d = await ownedOr404(db, input.id, input.vendorId);

  const vendor = await vendorQueries.findById(db, input.vendorId);
  if (!vendor || !vendor.stripeChargesEnabled) {
    throw new PaymentNotReadyError();
  }

  // Normalize legacy autosave field names before strict validation.
  // Older payloads used form field names that differ from the server schema.
  const normalized: Record<string, unknown> =
    typeof d.payload === 'object' && d.payload !== null
      ? { ...(d.payload as Record<string, unknown>) }
      : {};
  if ('quantity' in normalized && !('quantityTotal' in normalized)) {
    normalized.quantityTotal = normalized.quantity;
    delete normalized.quantity;
  }
  if (!normalized.discountedPrice && normalized.originalPrice && normalized.discountPercent) {
    const orig = parseFloat(String(normalized.originalPrice));
    const pct = Number(normalized.discountPercent);
    if (!isNaN(orig) && !isNaN(pct)) {
      normalized.discountedPrice = (orig * (1 - pct / 100)).toFixed(2);
    }
  }

  const result = decide({
    kind: 'submit_requested',
    draftId: input.id,
    vendorId: input.vendorId,
    normalizedPayload: normalized,
    at: new Date(),
  });
  if (!result.ok) mapDeciderError(result);

  const applied = await applyTransactionalEffects({ db, doClient, env }, result.effects);
  if (!applied.dealId || !applied.outboxId) {
    throw new Error('atomic-submit did not produce expected outputs');
  }
  return { dealId: applied.dealId, outboxId: applied.outboxId };
}
