/**
 * Refund workflows (FDS §6.11).
 *
 * Refund rules are delegated to the statutory refund policy.
 */

import { createHash, randomUUID } from 'node:crypto';
import { and, eq, like, sql } from 'drizzle-orm';
import type { DealType } from '@/lib/deal-types';
import { REFUNDABLE_PAYMENT_STATUSES } from '@/lib/enums/payment-status';
import type { DrizzleClient, TxDrizzleClient } from '../db/client.js';
import { purchaseMessages, vendors } from '../db/schema.js';
import * as purchaseQueries from '../db/queries/purchases.js';
import * as vendorQueries from '../db/queries/vendors.js';
import * as dealQueries from '../db/queries/deals.js';
import { insertOutboxRow } from '../db/queries/outbox.js';
import {
  claimCommand,
  completeCommand,
  failCommand,
  reclaimCommand,
} from '@/server/domain/commands/index.js';
import { selectCommandByKey } from '@/server/domain/commands/shared.js';
import { decide } from '@/server/domain/refund/machine.js';
import { applyEffects } from '@/server/domain/refund/apply-effects.js';
import type { CommandRecord } from '@/server/domain/commands/types.js';
import type { RefundPaymentState } from '@/server/domain/refund/events.js';
import type { PaymentProvider } from '@/server/payments/provider.js';
import { isBusinessDay } from '@/server/calendar/il-business-days.js';
import { statutoryRefund } from '@/server/domain/refund/policy.js';
import {
  isDealCategoryStatutoryExempt,
  isDealCategoryStatutoryExemptById,
} from '@/server/domain/refund/category-exemptions.js';

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

export interface RefundDeps {
  db: TxDrizzleClient;
}

export interface ExecuteRefundDeps extends RefundDeps {
  payments: () => Promise<PaymentProvider>;
}

interface ExecuteRefundResult {
  purchaseId: string;
  refundId: string;
  amount: string;
  outboxId: string;
  replayed?: true;
}

// ─── Errors ───────────────────────────────────────────────────────────────────

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

function toRefundError(err: unknown): RefundError {
  if (err instanceof RefundError) return err;
  if (err instanceof Error) return new RefundError('REFUND_FAILED', err.message, true);
  return new RefundError('REFUND_FAILED', 'Refund failed', true);
}

function toRefundCommandKey(input: {
  purchaseId: string;
  amount: string;
  refundType: 'full' | 'partial' | 'admin';
  initiatedBy: string;
  refundEventId: string;
}): string {
  const eventHash = createHash('sha256').update(input.refundEventId).digest('hex');
  return `refund.execute:${input.purchaseId}:${eventHash}`;
}

function toRefundPayloadHash(input: {
  purchaseId: string;
  amount: string;
  refundType: 'full' | 'partial' | 'admin';
  initiatedBy: string;
  refundEventId: string;
}): string {
  return createHash('sha256').update(JSON.stringify(input)).digest('hex');
}

function toRefundProviderKey(refundEventId: string): string {
  const eventHash = createHash('sha256').update(refundEventId).digest('hex');
  return `refund-event:${eventHash}`;
}

function toCancellationRequestPayloadHash(purchaseId: string): string {
  return createHash('sha256').update(JSON.stringify({ purchaseId })).digest('hex');
}

function isRetryableProviderFailure(
  code: string | null | undefined,
  message: string | null | undefined,
): boolean {
  return (
    code === 'RATE_LIMITED' || code === 'UNKNOWN' || (message?.startsWith('retry_later:') ?? false)
  );
}

function replayRefundResult(record: CommandRecord): ExecuteRefundResult | null {
  if (record.status === 'COMPLETED') {
    const result = record.resultPayload as ExecuteRefundResult | null;
    if (
      result &&
      typeof result.purchaseId === 'string' &&
      typeof result.refundId === 'string' &&
      typeof result.amount === 'string' &&
      typeof result.outboxId === 'string'
    ) {
      return { ...result, replayed: true };
    }
  }

  if (record.status === 'FAILED') {
    throw new RefundError(
      record.failureCode ?? 'REFUND_FAILED',
      record.failureMessage ?? 'Refund failed',
    );
  }

  return null;
}

type PurchaseRow = NonNullable<Awaited<ReturnType<typeof purchaseQueries.findById>>>;

/**
 * Payment statuses (order.status, lowercase T7 order model) that allow
 * initiating a new refund. Matches commerce-orders REFUNDABLE_STATUSES.
 */
export function amountPaidAgorot(purchase: Pick<PurchaseRow, 'amountPaid'>): number {
  return Math.round(parseFloat(purchase.amountPaid) * 100);
}

export function parseRefundAmountAgorot(amount: string): number {
  if (!/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(amount)) {
    throw new RefundError('INVALID_AMOUNT', 'Refund amount must be a canonical ILS decimal');
  }
  const [whole = '', fraction = ''] = amount.split('.');
  const agorot = BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0'));
  if (agorot <= 0n || agorot > BigInt(Number.MAX_SAFE_INTEGER)) {
    throw new RefundError('INVALID_AMOUNT', 'Refund amount is outside the supported range');
  }
  return Number(agorot);
}

/**
 * Advisory sync estimate of agorot already refunded, derived from order status.
 * The authoritative over-refund guard is claimRefundIntent's atomic SUM over
 * refund_intent rows — this pre-check only produces friendlier errors.
 */
export function alreadyRefundedAgorot(
  purchase: Pick<PurchaseRow, 'paymentStatus' | 'amountPaid'>,
): number {
  if (purchase.paymentStatus === 'refunded') return amountPaidAgorot(purchase);
  return 0;
}

export function remainingRefundableAgorot(
  purchase: Pick<PurchaseRow, 'paymentStatus' | 'amountPaid'>,
): number {
  return Math.max(0, amountPaidAgorot(purchase) - alreadyRefundedAgorot(purchase));
}

export function assertRefundablePurchase(
  purchase: Pick<PurchaseRow, 'paymentStatus' | 'redemptionStatus'>,
): void {
  if (
    !REFUNDABLE_PAYMENT_STATUSES.includes(
      purchase.paymentStatus as (typeof REFUNDABLE_PAYMENT_STATUSES)[number],
    )
  ) {
    throw new RefundError(
      'INVALID_STATE',
      `Purchase payment status ${purchase.paymentStatus} is not refundable`,
    );
  }
  if (purchase.redemptionStatus === 'REDEEMED') {
    throw new RefundError('ALREADY_REDEEMED', 'Cannot refund a redeemed purchase');
  }
}

export function assertRefundAmountWithinPaid(
  purchase: Pick<PurchaseRow, 'paymentStatus' | 'amountPaid'>,
  amountAgorot: number,
): void {
  const remaining = remainingRefundableAgorot(purchase);
  if (amountAgorot > remaining) {
    throw new RefundError(
      'REFUND_EXCEEDS_PAID',
      `Refund amount ${amountAgorot} agorot exceeds remaining refundable balance ${remaining} agorot`,
    );
  }
}

async function assertRefundAmountWithinLineBalance(
  db: DrizzleClient,
  purchaseId: string,
  purchase: Pick<PurchaseRow, 'amountPaid'>,
  amountAgorot: number,
): Promise<void> {
  const alreadyRefunded = await purchaseQueries.refundedAgorotForLine(db, purchaseId);
  const remaining = Math.max(0, amountPaidAgorot(purchase) - alreadyRefunded);
  if (amountAgorot > remaining) {
    throw new RefundError(
      'REFUND_EXCEEDS_PAID',
      `Refund amount ${amountAgorot} agorot exceeds remaining refundable balance ${remaining} agorot`,
    );
  }
}

export async function settlePurchaseRefund(
  deps: { db: DrizzleClient },
  input: {
    purchase: PurchaseRow;
    refundAmountAgorot: number;
    refundAmountIls: string;
    providerRefundId: string;
    refundReasonText: string;
    refundType?: 'full' | 'partial' | 'admin';
  },
): Promise<{ outboxId: string | null }> {
  const originalAgorot = amountPaidAgorot(input.purchase);
  const refundFraction = originalAgorot > 0 ? input.refundAmountAgorot / originalAgorot : 1;
  const now = new Date();

  const decideResult = decide(
    { state: input.purchase.paymentStatus as RefundPaymentState },
    {
      kind: 'refund_executed',
      purchaseId: input.purchase.id,
      userId: input.purchase.userId ?? null,
      dealId: input.purchase.dealId,
      refundAmount: input.refundAmountIls,
      refundReasonText: input.refundReasonText,
      at: now,
      stripeRefundId: input.providerRefundId,
      refundFraction,
      refundAmountAgorot: input.refundAmountAgorot,
      refundType: input.refundType,
    },
  );

  if (!decideResult.ok) {
    throw new RefundError('INVALID_STATE', decideResult.message);
  }

  const applied = await applyEffects({ db: deps.db }, decideResult.effects);
  return { outboxId: applied.outboxId };
}

// ─── Business-day helpers (Israeli calendar: Sun-Fri, holiday-aware) ──────────

/**
 * Counts business days between two dates (inclusive of start, exclusive of end).
 * Uses the canonical IL holiday-aware isBusinessDay from server/calendar/il-business-days.
 */
export function businessDaysBetween(start: Date, end: Date): number {
  if (end <= start) return 0;

  let count = 0;
  // Truncate to UTC midnight so holiday key lookup ('YYYY-MM-DD') is stable
  const cursor = new Date(
    Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()),
  );
  const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()));

  while (cursor < endDay) {
    if (isBusinessDay(cursor)) {
      count++;
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }

  return count;
}

export function assertProviderRefundOutcome(
  outcome: {
    ok: boolean;
    refundedAgorot?: number;
    providerRefundId?: string;
  },
  expectedAgorot: number,
): asserts outcome is {
  ok: true;
  refundedAgorot: number;
  providerRefundId: string;
} {
  if (
    outcome.ok !== true ||
    !Number.isSafeInteger(outcome.refundedAgorot) ||
    outcome.refundedAgorot !== expectedAgorot ||
    typeof outcome.providerRefundId !== 'string' ||
    outcome.providerRefundId.length === 0
  ) {
    throw new RefundError(
      'PROVIDER_REFUND_MISMATCH',
      'Provider refund response did not match the claimed refund',
    );
  }
}

// ─── calculateRefundAmount ────────────────────────────────────────────────────

/**
 * Pure function - calculates refund amount per deal type and days since purchase.
 *
 * Returns:
 *   { customerRefund, platformShare, vendorShare }
 *
 * where customerRefund is what goes back to the customer.
 */
export function calculateRefundAmount(
  dealType: DealType,
  daysSincePurchase: number,
  amountPaid: number,
  categorySlug: string | null = null,
): { customerRefund: number; platformShare: number; vendorShare: number } {
  const cancelledAt = new Date(0);
  const result = statutoryRefund({
    dealType,
    amountPaidAgorot: Math.round(amountPaid * 100),
    purchasedAt: new Date(-daysSincePurchase * 86_400_000),
    deliveredAt: null,
    serviceDate: null,
    cancelledAt,
    reason: 'consumer_remorse',
    exempt: isDealCategoryStatutoryExempt(categorySlug),
    redeemedAt: null,
  });

  return {
    customerRefund: result.refundAgorot / 100,
    platformShare: result.platformFeeAgorot / 100,
    vendorShare: result.vendorFeeAgorot / 100,
  };
}

// ─── requestCancellation ─────────────────────────────────────────────────────

/**
 * User-initiated cancellation request.
 *
 * - COUPON/GROUP within the statutory window: creates purchase_message for vendor negotiation
 */
export async function requestCancellation(
  deps: RefundDeps,
  { purchaseId, userId, reason }: { purchaseId: string; userId: string; reason: string },
) {
  const purchase = await purchaseQueries.findById(deps.db, purchaseId);
  if (!purchase) throw new RefundError('PURCHASE_NOT_FOUND', `Purchase ${purchaseId} not found`);

  if (purchase.userId !== userId) {
    throw new RefundError('UNAUTHORIZED', 'This purchase does not belong to you');
  }

  if (purchase.redemptionStatus === 'REDEEMED') {
    throw new RefundError('ALREADY_REDEEMED', 'Cannot cancel a redeemed purchase');
  }

  if (purchase.redemptionStatus === 'CANCELLED') {
    throw new RefundError('ALREADY_CANCELLED', 'Purchase is already cancelled');
  }

  const deal = await dealQueries.findById(deps.db, purchase.dealId);
  if (!deal) throw new RefundError('DEAL_NOT_FOUND', 'Deal not found');

  if (deal.isPhysical) {
    throw new RefundError(
      'USE_RETURNS_FLOW',
      'Physical-deal purchases must use the returns endpoint (POST /api/returns).',
    );
  }

  const cancelledAt = new Date();
  const exempt = await isDealCategoryStatutoryExemptById(deps.db, deal.categoryId);
  const statutory = statutoryRefund({
    dealType: deal.dealType,
    amountPaidAgorot: Math.round(parseFloat(purchase.amountPaid) * 100),
    purchasedAt: purchase.createdAt,
    deliveredAt: null,
    serviceDate: null,
    cancelledAt,
    reason: 'consumer_remorse',
    exempt,
    redeemedAt: null,
  });
  const daysSince = businessDaysBetween(purchase.createdAt, cancelledAt);

  if (!statutory.eligible) {
    return {
      eligible: false,
      reason: statutory.rule,
      dealType: deal.dealType,
      daysSince,
    };
  }

  const [existingRequest] = await deps.db
    .select({ id: purchaseMessages.id })
    .from(purchaseMessages)
    .where(
      and(
        eq(purchaseMessages.orderLineId, purchaseId),
        eq(purchaseMessages.senderType, 'USER'),
        eq(purchaseMessages.senderId, userId),
        like(purchaseMessages.body, '[CANCELLATION REQUEST]%'),
      ),
    )
    .limit(1);
  if (existingRequest) {
    throw new RefundError(
      'ALREADY_REQUESTED',
      'A cancellation request for this purchase is already pending',
    );
  }

  const commandKey = `refund.request:${purchaseId}`;
  const payloadHash = toCancellationRequestPayloadHash(purchaseId);
  let claimedCommand = await claimCommand(deps.db, {
    commandKey,
    commandType: 'refund.request',
    aggregateType: 'refund',
    aggregateId: purchaseId,
    payloadHash,
    payload: { purchaseId },
    claimedBy: `refund.request:${randomUUID()}`,
  });
  if (!claimedCommand.fresh) {
    const recovery = await reclaimCommand(deps.db, {
      commandId: claimedCommand.record.id,
      claimedBy: `refund.request:${randomUUID()}`,
      expectedCommandKey: commandKey,
      expectedCommandType: 'refund.request',
      expectedAggregateType: 'refund',
      expectedAggregateId: purchaseId,
      expectedPayloadHash: payloadHash,
    });
    if (!recovery.reclaimed) {
      throw new RefundError(
        'ALREADY_REQUESTED',
        'A cancellation request for this purchase is already pending',
      );
    }
    claimedCommand = { fresh: false, record: recovery.record };
  }

  try {
    await deps.db.transaction(async (tx) => {
      await tx.insert(purchaseMessages).values({
        orderLineId: purchaseId,
        senderType: 'USER',
        senderId: userId,
        body: `[CANCELLATION REQUEST] ${reason}`,
      });

      await completeCommand(tx, {
        commandId: claimedCommand.record.id,
        expectedClaimedBy: claimedCommand.record.claimedBy,
        expectedClaimGeneration: claimedCommand.record.claimGeneration,
        completedAt: new Date(),
        resultPayload: { purchaseId },
        outbox: {
          eventType: 'refund.request_completed',
          payload: { purchaseId },
        },
      });
    });
  } catch (err) {
    const refundError = toRefundError(err);
    await failCommand(deps.db, {
      commandId: claimedCommand.record.id,
      expectedClaimedBy: claimedCommand.record.claimedBy,
      expectedClaimGeneration: claimedCommand.record.claimGeneration,
      failedAt: new Date(),
      failureCode: refundError.code,
      failureMessage: refundError.message,
      outbox: {
        eventType: 'refund.request_failed',
        payload: { purchaseId, code: refundError.code },
      },
    });
    throw refundError;
  }

  const refundCalc = {
    customerRefund: statutory.refundAgorot / 100,
    platformShare: statutory.platformFeeAgorot / 100,
    vendorShare: statutory.vendorFeeAgorot / 100,
  };

  return {
    eligible: true,
    requiresVendorApproval: true,
    dealType: deal.dealType,
    daysSince,
    refundBreakdown: refundCalc,
    message: 'Cancellation request sent to vendor',
  };
}

// ─── executeRefund ────────────────────────────────────────────────────────────

/**
 * Executes a refund via the payments provider.
 * Updates purchase.payment_status and redemption_status.
 * Sends refund confirmation email.
 */
export async function executeRefund(
  deps: ExecuteRefundDeps,
  {
    purchaseId,
    amount,
    refundType = 'full',
    initiatedBy = 'user_cancel',
    refundEventId,
  }: {
    purchaseId: string;
    amount: string;
    refundType?: 'full' | 'partial' | 'admin';
    initiatedBy?: string;
    /** Stable identity of the business event; retries MUST reuse it. */
    refundEventId: string;
  },
): Promise<ExecuteRefundResult> {
  const commandInput = {
    purchaseId,
    amount,
    refundType,
    initiatedBy,
    refundEventId,
  } as const;
  const purchase = await purchaseQueries.findById(deps.db, purchaseId);
  if (!purchase) throw new RefundError('PURCHASE_NOT_FOUND', `Purchase ${purchaseId} not found`);

  const amountAgorot = parseRefundAmountAgorot(amount);
  const commandKey = toRefundCommandKey(commandInput);
  const payloadHash = toRefundPayloadHash(commandInput);
  const providerKey = toRefundProviderKey(refundEventId);
  const existingCommand = await selectCommandByKey(deps.db, commandKey);
  let claimedCommand;
  if (existingCommand) {
    const replay = replayRefundResult(existingCommand);
    if (replay) return replay;
    const recovery = await reclaimCommand(deps.db, {
      commandId: existingCommand.id,
      claimedBy: `refund.execute:${randomUUID()}`,
      expectedCommandKey: commandKey,
      expectedCommandType: 'refund.execute',
      expectedAggregateType: 'refund',
      expectedAggregateId: purchaseId,
      expectedPayloadHash: payloadHash,
    });
    if (!recovery.reclaimed) {
      const recoveredReplay = replayRefundResult(recovery.record);
      if (recoveredReplay) return recoveredReplay;
      throw new RefundError('REFUND_IN_PROGRESS', 'Refund already in progress');
    }
    claimedCommand = recovery;
  }

  if (!claimedCommand) {
    assertRefundablePurchase(purchase);
    await assertRefundAmountWithinLineBalance(deps.db, purchaseId, purchase, amountAgorot);

    claimedCommand = await claimCommand(deps.db, {
      commandKey,
      commandType: 'refund.execute',
      aggregateType: 'refund',
      aggregateId: purchaseId,
      payloadHash,
      payload: { ...commandInput, amountAgorot },
      claimedBy: `refund.execute:${randomUUID()}`,
    });
    if (!claimedCommand.fresh) {
      const replay = replayRefundResult(claimedCommand.record);
      if (replay) return replay;
      throw new RefundError('REFUND_IN_PROGRESS', 'Refund already in progress');
    }
  }

  let providerSucceeded = false;
  let terminalProviderFailure = false;
  try {
    const claim = await purchaseQueries.claimRefundIntent(deps.db, {
      purchaseId,
      refundAmountAgorot: amountAgorot,
      refundType,
      initiatedBy,
      refundKey: providerKey,
    });
    const refundResult = await (
      await deps.payments()
    ).refund({
      purchaseId,
      amountAgorot,
      idempotencyKey: providerKey,
    });

    if (!refundResult.ok) {
      await purchaseQueries.releaseRefundClaim(deps.db, purchaseId);
      await insertOutboxRow(deps.db, {
        aggregateType: 'refund',
        aggregateId: purchaseId,
        eventType: 'refund.intent.failed',
        payload: {
          purchaseId,
          amountAgorot,
          refundIntentId: claim.intentId,
          providerCode: refundResult.code ?? null,
          providerMessage: refundResult.message ?? null,
          idempotencyKey: `refund-failed:${purchaseId}:${providerKey}`,
        },
      });
      terminalProviderFailure = !isRetryableProviderFailure(
        refundResult.code,
        refundResult.message,
      );
      throw new RefundError(
        'REFUND_FAILED',
        refundResult.message ?? 'Refund failed',
        !terminalProviderFailure,
      );
    }
    providerSucceeded = true;

    assertProviderRefundOutcome(refundResult, amountAgorot);

    // Decide from the pre-refund snapshot: the provider already settled the
    // intent (setPurchaseRefund), so the refreshed order status is refunded/
    // partially_refunded — a valid *result*, not a valid *source* state.
    const applied = await settlePurchaseRefund(deps, {
      purchase,
      refundAmountAgorot: refundResult.refundedAgorot,
      refundAmountIls: amount,
      providerRefundId: refundResult.providerRefundId,
      refundReasonText: `Refund of ${amount} ILS`,
      refundType,
    });

    const result = {
      purchaseId,
      refundId: refundResult.providerRefundId,
      amount,
      outboxId: applied.outboxId ?? '',
    } satisfies ExecuteRefundResult;
    await completeCommand(deps.db, {
      commandId: claimedCommand.record.id,
      expectedClaimedBy: claimedCommand.record.claimedBy,
      expectedClaimGeneration: claimedCommand.record.claimGeneration,
      completedAt: new Date(),
      resultPayload: result,
      outbox: {
        eventType: 'refund.command_completed',
        payload: { purchaseId },
      },
    });

    return result;
  } catch (err) {
    const refundError =
      err instanceof purchaseQueries.AlreadyRefundedError
        ? new RefundError(
            'INVALID_STATE',
            'Purchase is not refundable or a refund is already in progress',
          )
        : toRefundError(err);
    // Retryable/unknown failures leave the command CLAIMED so the next attempt
    // re-executes via lease-expiry reclaim; only deterministic rejections are
    // recorded as terminal FAILED (replayed as failure for this refundEventId).
    const deterministicFailure =
      err instanceof purchaseQueries.AlreadyRefundedError || terminalProviderFailure;
    if (!providerSucceeded && deterministicFailure) {
      await failCommand(deps.db, {
        commandId: claimedCommand.record.id,
        expectedClaimedBy: claimedCommand.record.claimedBy,
        expectedClaimGeneration: claimedCommand.record.claimGeneration,
        failedAt: new Date(),
        failureCode: refundError.code,
        failureMessage: refundError.message,
        outbox: {
          eventType: 'refund.command_failed',
          payload: { purchaseId, code: refundError.code },
        },
      });
    }
    throw refundError;
  }
}

// ─── flagVoucherComplaint ─────────────────────────────────────────────────────

/**
 * Increments unresolved_voucher_complaints on the vendor.
 * Sets first_voucher_complaint_at if this is the first complaint.
 */
export async function flagVoucherComplaint(
  deps: RefundDeps,
  { purchaseId, reason }: { purchaseId: string; reason: string },
) {
  const purchase = await purchaseQueries.findById(deps.db, purchaseId);
  if (!purchase) throw new RefundError('PURCHASE_NOT_FOUND', `Purchase ${purchaseId} not found`);

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

  const [updated] = await deps.db
    .update(vendors)
    .set({
      unresolvedVoucherComplaints: sql`${vendors.unresolvedVoucherComplaints} + 1`,
      firstVoucherComplaintAt: sql`COALESCE(${vendors.firstVoucherComplaintAt}, CURRENT_TIMESTAMP)`,
    })
    .where(eq(vendors.id, vendor.id))
    .returning({ totalComplaints: vendors.unresolvedVoucherComplaints });
  const totalComplaints = updated?.totalComplaints ?? vendor.unresolvedVoucherComplaints + 1;
  const isFirst = totalComplaints === 1;

  // Log a purchase message for audit trail
  if (purchase.userId) {
    await deps.db.insert(purchaseMessages).values({
      orderLineId: purchaseId,
      senderType: 'USER',
      senderId: purchase.userId!,
      body: `[VOUCHER COMPLAINT] ${reason}`,
    });
  }

  return {
    vendorId: vendor.id,
    totalComplaints,
    isFirst,
  };
}
