/**
 * Refund pure decider.
 *
 * PURITY CONTRACT (enforced by _purity-check.sh refund).
 *
 * Transition table:
 *
 *   paid | fulfilled | completed | unfulfillable | partially_refunded | refunded
 *     + refund_executed → refunded (full) / partially_refunded (partial)
 *     effects: [set-refunded, enqueue-outbox(refund.executed_email), referral-clawback]
 *
 *   pending | charging | failed (money never captured) + refund_executed → rejected INVALID_STATE
 *
 * The actual Stripe refund API call stays in the workflow orchestrator
 * (non-idempotent external I/O). The decider runs post-call when the
 * Stripe side reported success.
 */

import type { RefundEvent, RefundPaymentState } from './events.js';
import type { RefundEffect } from './effects.js';

export type RefundState = RefundPaymentState;

export interface RefundContext {
  state: RefundState;
}

export type DecideResult =
  | { ok: true; nextState: RefundState; effects: RefundEffect[] }
  | { ok: false; error: 'INVALID_STATE'; from: RefundState; message: string };

export function decide(ctx: RefundContext, event: RefundEvent): DecideResult {
  switch (event.kind) {
    case 'refund_executed': {
      // Money was already captured in all these states. 'refunded' is included
      // because the payment provider settles the refund intent (order status
      // recompute) BEFORE the decider runs — the decider must still emit its
      // effects (email, voucher cancel, clawback) after the money moved.
      // Double-refund protection lives upstream in the atomic claim
      // (claimRefundIntent over-refund guard), not here.
      const REFUND_EXECUTABLE: readonly RefundPaymentState[] = [
        'paid',
        'fulfilled',
        'completed',
        'unfulfillable',
        'partially_refunded',
        'refunded',
      ];
      if (!REFUND_EXECUTABLE.includes(ctx.state)) {
        return {
          ok: false,
          error: 'INVALID_STATE',
          from: ctx.state,
          message: `refund_executed only valid from ${REFUND_EXECUTABLE.join('/')}, got ${ctx.state}`,
        };
      }

      // Full-line refund cancels the voucher; a partial refund keeps it live.
      // A 'full'-intent refund also cancels it even when the refunded fraction
      // is < 1 (statutory cancellation fee retained) — the buyer must never
      // keep a redeemable voucher after a cancellation refund.
      const fullRefund = event.refundFraction >= 1;
      const cancelVoucher = fullRefund || event.refundType === 'full';

      const effects: RefundEffect[] = [
        {
          kind: 'set-refunded',
          purchaseId: event.purchaseId,
          cancelledAt: event.at,
          cancellationReason: event.refundReasonText,
          cancelVoucher,
        },
        {
          kind: 'enqueue-outbox',
          aggregateType: 'refund',
          aggregateId: event.purchaseId,
          eventType: 'refund.executed_email',
          dedupeKey: `refund.executed_email:${event.stripeRefundId}`,
          payload: {
            purchaseId: event.purchaseId,
            userId: event.userId,
            dealId: event.dealId,
            refundAmount: event.refundAmount,
            refundDateIso: event.at.toISOString(),
          },
        },
        {
          kind: 'referral-clawback',
          purchaseId: event.purchaseId,
          refundEventId: event.stripeRefundId,
          refundFraction: event.refundFraction,
          refundAmountAgorot: event.refundAmountAgorot,
        },
      ];

      return {
        ok: true,
        nextState: fullRefund ? 'refunded' : 'partially_refunded',
        effects,
      };
    }

    default: {
      const _exhaustive: never = event.kind;
      void _exhaustive;
      return {
        ok: false,
        error: 'INVALID_STATE',
        from: ctx.state,
        message: 'Unhandled event kind',
      };
    }
  }
}
