/**
 * Case workflow — single entry point per mutation.
 *
 * Every state transition runs through the pure support-case decider and writes
 * support_state_transitions in the same Drizzle transaction. Side effects
 * (email, push, provider refund execution) go through the outbox.
 */

import { eq } from 'drizzle-orm';
import type { DrizzleClient, TxDrizzleClient } from '@/server/db/client.js';
import * as casesQ from '@/server/db/queries/support-cases.js';
import * as offersQ from '@/server/db/queries/case-offers.js';
import * as resolutionsQ from '@/server/db/queries/case-resolutions.js';
import * as messagesQ from '@/server/db/queries/support-messages.js';
import { getSystemConfig } from '@/server/db/queries/system-config.js';
import { insertOutboxRow } from '@/server/db/queries/outbox.js';
import { applyEffects } from '@/server/domain/support-case/apply-effects.js';
import type { SupportCaseEffect } from '@/server/domain/support-case/effects.js';
import { decide } from '@/server/domain/support-case/machine.js';
import { enqueueCaseEmail } from '../email/enqueue-case-email.js';
import type { CaseStatus } from '../state-machines.js';
import { shouldAutoExecute } from '../policy.js';
import * as purchaseQueries from '@/server/db/queries/purchases.js';
import {
  assertRefundablePurchase,
  assertRefundAmountWithinPaid,
  RefundError,
} from '@/server/workflows/refund.js';
import { caseOffers } from '@/server/db/schema.js';
import type { MultidealEnv } from '@/server/env.js';
import type { CaseCategory } from '../state-machines.js';
import type { CaseOfferOutcome } from '../policy.js';
import { armSupportCaseAlarm } from '@/server/do-client.js';
import { captureCaught } from '@/server/observability/capture.server';
import { asCaseId } from '@/server/platform-seams/ids.js';
import type { CaseTransitionContext } from '@/server/domain/support-case/events.js';

// ─── Error ────────────────────────────────────────────────────────────────────

export class CaseWorkflowError extends Error {
  readonly code: string;
  constructor(code: string, message: string) {
    super(message);
    this.name = 'CaseWorkflowError';
    this.code = code;
  }
}

// ─── Deps ─────────────────────────────────────────────────────────────────────

export interface CaseWorkflowDeps {
  db: TxDrizzleClient;
  messageEncryptionKey: string;
  env: MultidealEnv;
}

// ─── Customer ask accessor (stored in metadata) ───────────────────────────────

export interface CustomerAsk {
  outcome: CaseOfferOutcome;
  amountCents: number | null;
}

export function getCustomerAsk(metadata: Record<string, unknown>): CustomerAsk | null {
  const ask = metadata['customerAsk'];
  if (!ask || typeof ask !== 'object') return null;
  const a = ask as Record<string, unknown>;
  if (typeof a['outcome'] !== 'string') return null;
  return {
    outcome: a['outcome'] as CaseOfferOutcome,
    amountCents: typeof a['amountCents'] === 'number' ? a['amountCents'] : null,
  };
}

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

function transitionEffects(
  result: ReturnType<typeof decide>,
  patch?: { resolvedAt?: Date | null; reopenCount?: number },
): SupportCaseEffect[] {
  if (!result.ok) {
    throw new CaseWorkflowError('ILLEGAL_TRANSITION', result.message);
  }
  return result.effects.map((effect) =>
    effect.kind === 'set-case-status'
      ? {
          ...effect,
          ...(patch?.resolvedAt !== undefined ? { resolvedAt: patch.resolvedAt } : {}),
          ...(patch?.reopenCount !== undefined ? { reopenCount: patch.reopenCount } : {}),
        }
      : effect,
  );
}

function decideTransition(
  state: CaseStatus,
  input: {
    caseId: string;
    to: CaseStatus;
    actorType: 'customer' | 'vendor' | 'human_agent' | 'system' | 'ai';
    actorId: string | null;
    reason?: string;
    at?: Date;
    ctx?: CaseTransitionContext;
  },
  patch?: { resolvedAt?: Date | null; reopenCount?: number },
): { nextState: CaseStatus; effects: SupportCaseEffect[] } {
  const result = decide(
    { state },
    {
      kind: 'case_transition_requested',
      caseId: input.caseId,
      to: input.to,
      actorType: input.actorType,
      actorId: input.actorId,
      reason: input.reason,
      at: input.at,
      ctx: input.ctx,
    },
  );
  if (!result.ok) {
    throw new CaseWorkflowError('ILLEGAL_TRANSITION', result.message);
  }
  return { nextState: result.nextState, effects: transitionEffects(result, patch) };
}

// ─── openCase / postVendorOffer helpers ───────────────────────────────────────

/**
 * Arm DO alarms after a case is opened (outside transaction, best-effort).
 * Extracted from openCase to keep function under 100 lines.
 */
async function sendCaseOpenedNotifications(
  env: MultidealEnv,
  db: DrizzleClient,
  caseId: string,
  now: Date,
  escapeUsed: boolean,
  vendorWindowExpiresAt: Date | null,
): Promise<void> {
  if (!escapeUsed && vendorWindowExpiresAt) {
    const halfPoint = new Date(
      now.getTime() + (vendorWindowExpiresAt.getTime() - now.getTime()) / 2,
    );
    await armSupportCaseAlarm(env, caseId, 'vendor_window_50pct', halfPoint).catch((err) => {
      captureCaught(err, { scope: 'server.support.workflows.case', severity: 'info' });
    });
    await armSupportCaseAlarm(env, caseId, 'vendor_window_expires_at', vendorWindowExpiresAt).catch(
      (err) => {
        captureCaught(err, { scope: 'server.support.workflows.case', severity: 'info' });
      },
    );
  } else {
    const slaHoursStr = await getSystemConfig(db, 'support_sla_human_hours');
    const slaHuman = new Date(now.getTime() + parseInt(slaHoursStr, 10) * 60 * 60 * 1000);
    await armSupportCaseAlarm(env, caseId, 'sla_human_due_at', slaHuman).catch((err) => {
      captureCaught(err, { scope: 'server.support.workflows.case', severity: 'info' });
    });
  }
}

/**
 * Arm autoclose alarm after vendor offer is auto-executed (outside transaction, best-effort).
 * Extracted from postVendorOffer to keep function under 100 lines.
 */
async function sendVendorOfferNotifications(
  env: MultidealEnv,
  db: DrizzleClient,
  caseId: string,
  autoExec: boolean,
): Promise<void> {
  if (!autoExec) return;
  const autocloseDaysStr = await getSystemConfig(db, 'support_autoclose_days');
  const autocloseAt = new Date(Date.now() + parseInt(autocloseDaysStr, 10) * 24 * 60 * 60 * 1000);
  await armSupportCaseAlarm(env, caseId, 'autoclose_at', autocloseAt).catch((err) => {
    captureCaught(err, { scope: 'server.support.workflows.case', severity: 'info' });
  });
}

// ─── openCase ─────────────────────────────────────────────────────────────────

export interface OpenCaseInput {
  orderLineId: string;
  customerId: string;
  vendorId: string;
  category: CaseCategory;
  customerAskOutcome: CaseOfferOutcome;
  customerAskAmountCents: number | null;
  description: string;
  escapeRequested: boolean;
}

export async function openCase(
  deps: CaseWorkflowDeps,
  input: OpenCaseInput,
): Promise<{ caseId: string; status: CaseStatus }> {
  const { db, env } = deps;

  // Server-side trust: force escapeUsed for fraud/safety categories
  const escapeUsed =
    input.escapeRequested || input.category === 'fraud' || input.category === 'safety';

  // Get vendor window config
  const windowHoursStr = await getSystemConfig(db, 'support_vendor_window_hours');
  const windowHours = parseInt(windowHoursStr, 10);
  const now = new Date();
  const vendorWindowExpiresAt = escapeUsed
    ? null
    : new Date(now.getTime() + windowHours * 60 * 60 * 1000);

  const metadata: Record<string, unknown> = {
    customerAsk: {
      outcome: input.customerAskOutcome,
      amountCents: input.customerAskAmountCents,
    },
  };

  let caseRow: Awaited<ReturnType<typeof casesQ.insert>>;
  try {
    caseRow = await db.transaction(async (tx) => {
      // Create the case
      const newCase = await casesQ.insert(tx as unknown as DrizzleClient, {
        orderLineId: input.orderLineId,
        customerId: input.customerId,
        vendorId: input.vendorId,
        category: input.category,
        status: 'opened',
        escapeUsed,
        vendorWindowHours: windowHours,
        vendorWindowExpiresAt,
        metadata,
      });

      let currentState: CaseStatus = 'opened';
      const initialDecision = decideTransition(currentState, {
        caseId: newCase.id,
        to: escapeUsed ? 'escalated' : 'vendor_review',
        actorType: 'system',
        actorId: null,
        reason: escapeUsed ? 'escape_route' : 'normal_flow',
        ctx: { category: input.category, escapeUsed },
      });
      await applyEffects({ db: tx as unknown as DrizzleClient }, initialDecision.effects);
      currentState = initialDecision.nextState;

      // Write initial message (description)
      await messagesQ.insertEncrypted(
        tx as unknown as DrizzleClient,
        {
          parentType: 'case',
          parentId: newCase.id,
          authorType: 'customer',
          authorId: input.customerId,
          visibility: 'public',
        },
        input.description,
        deps.messageEncryptionKey,
      );

      // Enqueue notification outbox rows
      await enqueueCaseEmail(deps.env, tx as unknown as DrizzleClient, {
        caseId: newCase.id,
        templateKey: 'support.case_opened',
        recipients: ['customer', 'vendor'],
        params: {
          caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${newCase.id}`,
        },
      });

      // Enqueue AI dispatch for non-escaped cases (agent assesses once vendor window opens)
      if (!escapeUsed) {
        await insertOutboxRow(tx as unknown as DrizzleClient, {
          aggregateType: 'case',
          aggregateId: newCase.id,
          eventType: 'support.ai.dispatch',
          payload: { parentType: 'case', parentId: newCase.id, trigger: 'opened' },
        });
      }

      // If escape — also escalate to human_review immediately (phase 4 policy)
      if (escapeUsed) {
        const humanDecision = decideTransition(currentState, {
          caseId: newCase.id,
          to: 'human_review',
          actorType: 'system',
          actorId: null,
          reason: 'phase4_no_ai',
        });
        await applyEffects({ db: tx as unknown as DrizzleClient }, humanDecision.effects);

        let _siteEmails: string[];
        try {
          const _raw = await getSystemConfig(deps.db, 'support_notification_emails');
          _siteEmails = JSON.parse(_raw) as string[];
        } catch (err) {
          captureCaught(err, { scope: 'support.workflows.case.escalate', severity: 'warning' });
          _siteEmails = [deps.env.RESEND_FROM_EMAIL];
        }
        await enqueueCaseEmail(deps.env, tx as unknown as DrizzleClient, {
          caseId: newCase.id,
          templateKey: 'support.escalated_to_human',
          recipients: ['customer', 'vendor'],
          siteEmails: _siteEmails.filter(Boolean),
          params: {
            caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${newCase.id}`,
          },
        });
      }

      return newCase;
    });
  } catch (err) {
    // Partial unique index `tx_cases_purchase_open_uniq` blocks concurrent open cases
    // for the same purchase. Map pg 23505 to a domain DUPLICATE_OPEN error.
    const e = err as {
      code?: string;
      constraint?: string;
      cause?: { code?: string; constraint?: string };
    };
    const code = e?.code ?? e?.cause?.code;
    const constraint = e?.constraint ?? e?.cause?.constraint;
    if (code === '23505' && constraint === 'tx_cases_purchase_open_uniq') {
      throw new CaseWorkflowError(
        'DUPLICATE_OPEN',
        'An open case already exists for this purchase',
      );
    }
    throw err;
  }

  // Arm DO alarms (outside tx — best effort; DO state is idempotent)
  await sendCaseOpenedNotifications(env, db, caseRow.id, now, escapeUsed, vendorWindowExpiresAt);

  const finalStatus = escapeUsed ? 'human_review' : 'vendor_review';
  return { caseId: caseRow.id, status: finalStatus as CaseStatus };
}

// ─── postCaseMessage ──────────────────────────────────────────────────────────

export async function postCaseMessage(
  deps: CaseWorkflowDeps,
  args: {
    caseId: string;
    authorType: 'customer' | 'vendor' | 'human_agent' | 'system' | 'admin';
    authorId: string | null;
    text: string;
    visibility: 'public' | 'vendor_internal' | 'site_internal';
  },
): Promise<{ messageId: string }> {
  const caseRow = await casesQ.findById(deps.db, args.caseId);
  if (!caseRow) throw new CaseWorkflowError('CASE_NOT_FOUND', `Case ${args.caseId} not found`);
  const gate = decide(
    { state: caseRow.status as CaseStatus },
    { kind: 'case_message_posted', caseId: args.caseId },
  );
  if (!gate.ok) {
    throw new CaseWorkflowError('CASE_CLOSED', gate.message);
  }

  const msg = await messagesQ.insertEncrypted(
    deps.db,
    {
      parentType: 'case',
      parentId: args.caseId,
      authorType: args.authorType,
      authorId: args.authorId,
      visibility: args.visibility,
    },
    args.text,
    deps.messageEncryptionKey,
  );

  return { messageId: msg.id };
}

// ─── postVendorOffer ──────────────────────────────────────────────────────────

export async function postVendorOffer(
  deps: CaseWorkflowDeps,
  args: {
    caseId: string;
    vendorUserId: string;
    outcome: CaseOfferOutcome;
    amountCents: number | null;
    reason: string;
  },
): Promise<{ offerId: string; autoExecuted: boolean }> {
  const { db, env } = deps;

  const caseRow = await casesQ.findById(db, args.caseId);
  if (!caseRow) throw new CaseWorkflowError('CASE_NOT_FOUND', `Case ${args.caseId} not found`);

  if (caseRow.status !== 'vendor_review' && caseRow.status !== 'vendor_offered') {
    throw new CaseWorkflowError('FORBIDDEN_STATUS', `Case is in status ${caseRow.status}`);
  }

  // Check auto-execute
  const customerAsk = getCustomerAsk(caseRow.metadata);
  const autoExec = customerAsk
    ? shouldAutoExecute(customerAsk, { outcome: args.outcome, amountCents: args.amountCents })
    : false;

  let offerId: string;
  let pendingMonetaryRefund: PendingCaseMonetaryRefund | null = null;

  await db.transaction(async (tx) => {
    // Supersede any previous pending offers
    const existing = await offersQ.listByCase(tx as unknown as DrizzleClient, args.caseId);
    for (const o of existing.filter((o) => o.status === 'pending')) {
      await offersQ.markSuperseded(tx as unknown as DrizzleClient, o.id);
    }

    const offer = await offersQ.insert(tx as unknown as DrizzleClient, {
      caseId: asCaseId(args.caseId),
      offeredBy: 'vendor',
      outcome: args.outcome,
      amountCents: args.amountCents,
      reason: args.reason,
      status: autoExec ? 'auto_executed' : 'pending',
    });
    offerId = offer.id;

    if (autoExec) {
      // Mark offer auto_executed + resolve case
      await (tx as unknown as DrizzleClient)
        .update(caseOffers)
        .set({ status: 'auto_executed', decidedAt: new Date() })
        .where(eq(caseOffers.id, offer.id));

      const resolvedAt = new Date();
      const resolutionDecision = decideTransition(
        caseRow.status as CaseStatus,
        {
          caseId: args.caseId,
          to: 'resolved',
          actorType: 'system',
          actorId: null,
          reason: 'offer_auto_executed',
          ctx: { autoExecuted: true },
        },
        { resolvedAt },
      );
      await applyEffects({ db: tx as unknown as DrizzleClient }, resolutionDecision.effects);

      // Execute resolution in same tx (claim only for monetary refunds; Stripe runs after commit)
      await _executeResolutionInTx(tx as unknown as DrizzleClient, args.caseId, {
        outcome: args.outcome,
        amountCents: args.amountCents,
        executedBy: 'vendor_auto',
        purchaseId: caseRow.orderLineId,
      });

      if (
        isMonetaryCaseResolution(args.outcome, args.amountCents) &&
        !(await resolutionsQ.findByCase(tx as unknown as DrizzleClient, args.caseId))
      ) {
        pendingMonetaryRefund = {
          caseId: args.caseId,
          purchaseId: caseRow.orderLineId,
          amountCents: args.amountCents,
          outcome: args.outcome,
          executedBy: 'vendor_auto',
        };
      }

      if (!pendingMonetaryRefund) {
        await enqueueCaseEmail(deps.env, tx as unknown as DrizzleClient, {
          caseId: args.caseId,
          templateKey: 'support.case_resolved',
          recipients: ['customer', 'vendor'],
          params: {
            caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${args.caseId}`,
            resolution: args.reason ?? '',
          },
        });
      }
    } else {
      // Transition to vendor_offered
      if (caseRow.status !== 'vendor_offered') {
        const offeredDecision = decideTransition(caseRow.status as CaseStatus, {
          caseId: args.caseId,
          to: 'vendor_offered',
          actorType: 'vendor',
          actorId: args.vendorUserId,
        });
        await applyEffects({ db: tx as unknown as DrizzleClient }, offeredDecision.effects);
      }

      await enqueueCaseEmail(deps.env, tx as unknown as DrizzleClient, {
        caseId: args.caseId,
        templateKey: 'support.vendor_offered',
        recipients: ['customer'],
        params: {
          caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${args.caseId}`,
          offerSummary: args.reason,
        },
      });
    }
  });

  if (pendingMonetaryRefund) {
    await finalizeCaseMonetaryResolution(deps, pendingMonetaryRefund, {
      templateKey: 'support.case_resolved',
      recipients: ['customer', 'vendor'],
      params: {
        caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${args.caseId}`,
        resolution: args.reason ?? '',
      },
    });
  }

  // Arm autoclose alarm if auto-executed (outside tx, best-effort)
  await sendVendorOfferNotifications(env, db, args.caseId, autoExec);

  return { offerId: offerId!, autoExecuted: autoExec };
}

// ─── decideOffer ─────────────────────────────────────────────────────────────

export async function decideOffer(
  deps: CaseWorkflowDeps,
  args: {
    caseId: string;
    customerId: string;
    offerId: string;
    decision: 'accept' | 'reject';
  },
): Promise<{ status: CaseStatus }> {
  const { db, env } = deps;

  const caseRow = await casesQ.findById(db, args.caseId);
  if (!caseRow) throw new CaseWorkflowError('CASE_NOT_FOUND', `Case ${args.caseId} not found`);
  if (caseRow.status !== 'vendor_offered') {
    throw new CaseWorkflowError('FORBIDDEN_STATUS', `Case is in status ${caseRow.status}`);
  }

  let finalStatus: CaseStatus = args.decision === 'accept' ? 'resolved' : 'human_review';
  let pendingMonetaryRefund: PendingCaseMonetaryRefund | null = null;

  await db.transaction(async (tx) => {
    if (args.decision === 'accept') {
      const accepted = await offersQ.tryAccept(tx as unknown as DrizzleClient, args.offerId);
      if (!accepted) {
        throw new CaseWorkflowError('OFFER_ALREADY_DECIDED', 'Offer already decided');
      }

      const resolvedAt = new Date();
      const acceptDecision = decideTransition(
        'vendor_offered',
        {
          caseId: args.caseId,
          to: 'resolved',
          actorType: 'customer',
          actorId: args.customerId,
          reason: 'customer_accept',
          ctx: { autoExecuted: true },
        },
        { resolvedAt },
      );
      await applyEffects({ db: tx as unknown as DrizzleClient }, acceptDecision.effects);

      await _executeResolutionInTx(tx as unknown as DrizzleClient, args.caseId, {
        outcome: accepted.outcome,
        amountCents: accepted.amountCents,
        executedBy: 'customer_accept',
        purchaseId: caseRow.orderLineId,
      });

      if (
        isMonetaryCaseResolution(accepted.outcome, accepted.amountCents) &&
        !(await resolutionsQ.findByCase(tx as unknown as DrizzleClient, args.caseId))
      ) {
        pendingMonetaryRefund = {
          caseId: args.caseId,
          purchaseId: caseRow.orderLineId,
          amountCents: accepted.amountCents,
          outcome: accepted.outcome,
          executedBy: 'customer_accept',
        };
      }

      if (!pendingMonetaryRefund) {
        await enqueueCaseEmail(deps.env, tx as unknown as DrizzleClient, {
          caseId: args.caseId,
          templateKey: 'support.customer_decided',
          recipients: ['vendor'],
          params: {
            caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${args.caseId}`,
            decision: 'accept',
          },
        });
      }
    } else {
      await offersQ.markRejected(tx as unknown as DrizzleClient, args.offerId);

      const escalatedDecision = decideTransition('vendor_offered', {
        caseId: args.caseId,
        to: 'escalated',
        actorType: 'customer',
        actorId: args.customerId,
        reason: 'customer_reject',
      });
      await applyEffects({ db: tx as unknown as DrizzleClient }, escalatedDecision.effects);
      const aiDecision = decideTransition(escalatedDecision.nextState, {
        caseId: args.caseId,
        to: 'ai_handling',
        actorType: 'system',
        actorId: null,
        reason: 'phase5_ai_triage',
      });
      await applyEffects({ db: tx as unknown as DrizzleClient }, aiDecision.effects);

      // Enqueue AI dispatch — agent will escalate to human_review if needed
      await insertOutboxRow(tx as unknown as DrizzleClient, {
        aggregateType: 'case',
        aggregateId: args.caseId,
        eventType: 'support.ai.dispatch',
        payload: { parentType: 'case', parentId: args.caseId, trigger: 'escalated' },
      });

      finalStatus = 'ai_handling';
    }
  });

  if (pendingMonetaryRefund) {
    await finalizeCaseMonetaryResolution(deps, pendingMonetaryRefund, {
      templateKey: 'support.customer_decided',
      recipients: ['vendor'],
      params: {
        caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${args.caseId}`,
        decision: 'accept',
      },
    });
  }

  if (finalStatus! === 'resolved') {
    const autocloseDaysStr = await getSystemConfig(db, 'support_autoclose_days');
    const autocloseAt = new Date(Date.now() + parseInt(autocloseDaysStr, 10) * 24 * 60 * 60 * 1000);
    await armSupportCaseAlarm(env, args.caseId, 'autoclose_at', autocloseAt).catch((err) => {
      captureCaught(err, { scope: 'server.support.workflows.case', severity: 'info' });
    });
  }

  return { status: finalStatus! };
}

function isMonetaryCaseResolution(
  outcome: CaseOfferOutcome,
  amountCents: number | null,
): amountCents is number {
  return (
    (outcome === 'refund_full' || outcome === 'refund_partial') &&
    amountCents !== null &&
    amountCents > 0
  );
}

interface PendingCaseMonetaryRefund {
  caseId: string;
  purchaseId: string;
  amountCents: number;
  outcome: CaseOfferOutcome;
  executedBy: 'vendor_auto' | 'customer_accept' | 'ai_auto' | 'human';
}

async function assertCaseMonetaryRefundEligible(
  db: DrizzleClient,
  purchaseId: string,
  amountCents: number,
): Promise<void> {
  const purchase = await purchaseQueries.findById(db, purchaseId);
  if (!purchase) {
    throw new CaseWorkflowError('PURCHASE_NOT_FOUND', `Purchase ${purchaseId} not found`);
  }

  try {
    assertRefundablePurchase(purchase);
    assertRefundAmountWithinPaid(purchase, amountCents);
  } catch (err) {
    if (err instanceof RefundError) {
      throw new CaseWorkflowError(err.code, err.message);
    }
    throw err;
  }
}

async function claimCaseRefundIntent(
  db: DrizzleClient,
  input: purchaseQueries.ClaimRefundIntentInput,
  inTransaction: boolean,
): Promise<void> {
  try {
    if (inTransaction) {
      await purchaseQueries.claimRefundIntentInTx(db as TxDrizzleClient, input);
    } else {
      await purchaseQueries.claimRefundIntent(db, input);
    }
  } catch (err) {
    if (err instanceof purchaseQueries.AlreadyRefundedError) {
      throw new CaseWorkflowError('INVALID_STATE', 'Purchase is not refundable');
    }
    throw err;
  }
}

async function claimCaseMonetaryRefundInTx(
  db: DrizzleClient,
  purchaseId: string,
  amountCents: number,
): Promise<void> {
  await assertCaseMonetaryRefundEligible(db, purchaseId, amountCents);
  await claimCaseRefundIntent(
    db,
    { purchaseId, refundAmountAgorot: amountCents, refundType: 'full', initiatedBy: 'system' },
    true,
  );
}

async function finalizeCaseMonetaryResolution(
  deps: CaseWorkflowDeps,
  pending: PendingCaseMonetaryRefund,
  email: {
    templateKey: 'support.case_resolved' | 'support.customer_decided';
    recipients: Array<'customer' | 'vendor'>;
    params: Record<string, string>;
  },
): Promise<void> {
  const existing = await resolutionsQ.findByCase(deps.db, pending.caseId);
  if (existing) return;
  const decision = decide(
    { state: 'resolved' },
    {
      kind: 'resolution_requested',
      caseId: pending.caseId,
      purchaseId: pending.purchaseId,
      outcome: pending.outcome,
      amountCents: pending.amountCents,
      executedBy: pending.executedBy,
      email,
    },
  );
  if (!decision.ok) {
    throw new CaseWorkflowError('INVALID_STATE', decision.message);
  }
  await applyEffects({ db: deps.db }, decision.effects);
}

// ─── executeResolution ────────────────────────────────────────────────────────

export async function executeResolution(
  deps: CaseWorkflowDeps,
  args: {
    caseId: string;
    outcome: CaseOfferOutcome;
    amountCents: number | null;
    executedBy: 'vendor_auto' | 'customer_accept' | 'ai_auto' | 'human';
  },
): Promise<
  | { resolutionId: string; providerRefundId: string | null; queued?: false }
  | { resolutionId: null; providerRefundId: null; queued: true }
> {
  // Check idempotency: if resolution already exists, return it
  const existing = await resolutionsQ.findByCase(deps.db, args.caseId);
  if (existing) {
    return { resolutionId: existing.id, providerRefundId: existing.providerRefundId };
  }

  const caseRow = await casesQ.findById(deps.db, args.caseId);
  if (!caseRow) throw new CaseWorkflowError('CASE_NOT_FOUND', `Case ${args.caseId} not found`);

  const providerRefundId: string | null = null;

  // Only refund monetary outcomes
  if (
    isMonetaryCaseResolution(args.outcome, args.amountCents) &&
    args.amountCents !== null &&
    args.amountCents > 0
  ) {
    await assertCaseMonetaryRefundEligible(deps.db, caseRow.orderLineId, args.amountCents);
    await claimCaseRefundIntent(
      deps.db,
      {
        purchaseId: caseRow.orderLineId,
        refundAmountAgorot: args.amountCents,
        refundType: 'full',
        initiatedBy: 'case_resolution',
      },
      false,
    );
    const decision = decide(
      { state: caseRow.status as CaseStatus },
      {
        kind: 'resolution_requested',
        caseId: args.caseId,
        purchaseId: caseRow.orderLineId,
        outcome: args.outcome,
        amountCents: args.amountCents,
        executedBy: args.executedBy,
      },
    );
    if (!decision.ok) {
      throw new CaseWorkflowError('INVALID_STATE', decision.message);
    }
    await applyEffects({ db: deps.db }, decision.effects);
    return { resolutionId: null, providerRefundId: null, queued: true };
  }

  const resolution = await resolutionsQ.insert(deps.db, {
    caseId: asCaseId(args.caseId),
    outcome: args.outcome,
    amountCents: args.amountCents,
    executedBy: args.executedBy,
    providerRefundId,
  });

  return { resolutionId: resolution.id, providerRefundId };
}

// ─── reopenCase ──────────────────────────────────────────────────────────────

export async function reopenCase(
  deps: CaseWorkflowDeps,
  args: {
    caseId: string;
    customerId: string;
    reason: string;
  },
): Promise<{ status: CaseStatus }> {
  const { db } = deps;

  const caseRow = await casesQ.findById(db, args.caseId);
  if (!caseRow) throw new CaseWorkflowError('CASE_NOT_FOUND', `Case ${args.caseId} not found`);
  if (caseRow.status !== 'closed') {
    throw new CaseWorkflowError('FORBIDDEN_STATUS', 'Only closed cases can be reopened');
  }

  const reopenLimitStr = await getSystemConfig(db, 'support_reopen_limit');
  const reopenLimit = parseInt(reopenLimitStr, 10);
  if (caseRow.reopenCount >= reopenLimit) {
    throw new CaseWorkflowError('OVER_LIMIT', 'Reopen limit exceeded');
  }

  await db.transaction(async (tx) => {
    const reopenedDecision = decideTransition(
      'closed',
      {
        caseId: args.caseId,
        to: 'reopened',
        actorType: 'customer',
        actorId: args.customerId,
        reason: args.reason,
      },
      { reopenCount: caseRow.reopenCount + 1 },
    );
    await applyEffects({ db: tx as unknown as DrizzleClient }, reopenedDecision.effects);
    const escalatedDecision = decideTransition(reopenedDecision.nextState, {
      caseId: args.caseId,
      to: 'escalated',
      actorType: 'system',
      actorId: null,
      reason: 'phase4_reopen_escalate',
    });
    await applyEffects({ db: tx as unknown as DrizzleClient }, escalatedDecision.effects);
    const humanDecision = decideTransition(escalatedDecision.nextState, {
      caseId: args.caseId,
      to: 'human_review',
      actorType: 'system',
      actorId: null,
      reason: 'phase4_no_ai',
    });
    await applyEffects({ db: tx as unknown as DrizzleClient }, humanDecision.effects);

    await enqueueCaseEmail(deps.env, tx as unknown as DrizzleClient, {
      caseId: args.caseId,
      templateKey: 'support.reopened',
      recipients: ['customer', 'vendor'],
      params: {
        caseUrl: `${deps.env.PUBLIC_SITE_URL}/support/cases/${args.caseId}`,
      },
    });
  });

  return { status: 'human_review' };
}

// ─── Internal: executeResolution inside a transaction ────────────────────────

async function _executeResolutionInTx(
  db: DrizzleClient,
  caseId: string,
  args: {
    outcome: CaseOfferOutcome;
    amountCents: number | null;
    executedBy: 'vendor_auto' | 'customer_accept' | 'ai_auto' | 'human';
    purchaseId: string;
  },
): Promise<void> {
  // Idempotency: check existing (should not exist at this point, but guard)
  const existing = await resolutionsQ.findByCase(db, caseId);
  if (existing) return;

  if (isMonetaryCaseResolution(args.outcome, args.amountCents)) {
    await claimCaseMonetaryRefundInTx(db, args.purchaseId, args.amountCents);
    return;
  }

  await resolutionsQ.insert(db, {
    caseId: asCaseId(caseId),
    outcome: args.outcome,
    amountCents: args.amountCents,
    executedBy: args.executedBy,
    providerRefundId: null,
  });
}
