// apps/web/src/server/payments/provider.ts (rewritten)

import type { PromoClaim } from '@/server/promo/types.js';

export enum PaymentErrorCode {
  CARD_DECLINED = 'CARD_DECLINED',
  INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
  TOKEN_EXPIRED = 'TOKEN_EXPIRED',
  TOKEN_INVALID = 'TOKEN_INVALID',
  DUPLICATE = 'DUPLICATE',
  HOLD_NOT_SUPPORTED = 'HOLD_NOT_SUPPORTED',
  HOLD_EXPIRED = 'HOLD_EXPIRED',
  TERMINAL_INACTIVE = 'TERMINAL_INACTIVE',
  AUTH_REQUIRED = 'AUTH_REQUIRED',
  RATE_LIMITED = 'RATE_LIMITED',
  PROVIDER_ERROR = 'PROVIDER_ERROR',
  PAYMENT_CONTRACT_MISMATCH = 'PAYMENT_CONTRACT_MISMATCH',
  PAYMENT_CANCELED = 'PAYMENT_CANCELED',
  UNKNOWN = 'UNKNOWN',
}

export interface VendorAccount {
  vendorId: string;
  providerAccountId: string;
  dealTitle: string;
}

export interface Buyer {
  name: string;
  email?: string;
  phone?: string;
}

export interface ChargeInput {
  purchaseId: string;
  customerId?: string;
  totalAgorot: number;
  vendor: VendorAccount;
  buyer: Buyer;
  /**
   * STRIPE — OFF-SESSION saved-card charge: present (pm_xxx) → server confirms immediately, returns ChargeOk sync.
   * STRIPE — INTERACTIVE checkout: omitted → server creates PaymentIntent without PM;
   *   client confirms via PaymentElement; server returns ChargeRequiresClient with clientSecret.
   * MOCK — always returns ChargeOk sync regardless of presence. Scenario read from the
   *   mockScenario cookie inside MockProvider, not from this field.
   */
  paymentMethodId?: string;
  /** Optional promo claim — discount math applied per spec §5.4. */
  promoClaim?: PromoClaim;
}

/** Two-phase result for interactive checkout: client must confirm in browser. */
export interface ChargeRequiresClient {
  ok: true;
  status: 'requires_client_confirmation';
  providerPaymentId: string;
  clientSecret: string;
}

/** Settled charge — fields populated by webhook (Stripe interactive) or sync (mock + off-session). */
export interface ChargeOk {
  ok: true;
  status: 'succeeded';
  providerPaymentId: string;
  vendorAgorot: number;
  platformAgorot: number;
  vendorTaxDocId: string | null;
  platformTaxDocId: string | null;
  vendorTaxDocPdfUrl: string | null;
  platformTaxDocPdfUrl: string | null;
}

export interface PaymentFailure {
  ok: false;
  code: PaymentErrorCode;
  message: string;
}
export type ChargeOutcome = ChargeOk | ChargeRequiresClient | PaymentFailure;

export interface EnsureCustomerInput {
  userId: string;
  buyer: Buyer;
}

/**
 * Finalize a PaymentIntent into a ChargeOk. Called from webhook handler when
 * payment_intent.succeeded arrives, OR from redirect-poll fallback. Idempotent —
 * if already finalized, returns the prior ChargeOk. Fires InvoiceProvider here.
 */
export interface FinalizeInput {
  providerPaymentId: string;
}
export type FinalizeOutcome = ChargeOk | PaymentFailure;

export interface CheckInput {
  paymentMethodId: string;
}
export interface CardTokenOk {
  ok: true;
  providerCardToken: string;
  expirationMonth: number;
  expirationYear: number;
  brand: string;
  last4: string;
  /** HMAC-SHA-256 of Stripe card fingerprint, keyed by PII_KEY. Undefined if unavailable. */
  cardFingerprint?: string;
}
export type CardTokenOutcome = CardTokenOk | PaymentFailure;

export interface HoldInput {
  promoReservationId?: string;
  reservationId: string;
  /**
   * orderLine id (first line of the order) — stamped into PI metadata so
   * finalizePurchase can resolve the order before chargeRef exists.
   * Required for cart holds; group-deal holds pass it at capture time instead.
   */
  purchaseId?: string;
  /** Origin of the hold — 'cart' orders restore SKU stock on payment failure. */
  checkoutKind?: 'cart' | 'group';
  providerCardToken: string;
  /** Stripe customer ID — required for off-session charges so Stripe can
   * verify the mandate attached to the payment method. */
  customerId?: string;
  totalAgorot: number;
  vendor: VendorAccount;
  /**
   * Explicit platform fee override (agorot). When provided, used as
   * `application_fee_amount` directly instead of computing 10% of totalAgorot.
   * Required when a promo discount shifts the fee (vendor-funded = keep original
   * fee on original total; platform-funded = fee minus discount).
   */
  applicationFeeAgorot?: number;
  /**
   * Deal ID — used by the Stripe provider to look up variant axes and build a
   * SKU label for the PaymentIntent `description` and `metadata`.
   * Optional: when absent the PI description falls back to `vendor.dealTitle`.
   */
  dealId?: string;
  /**
   * SKU selected by the customer (UUID).  Paired with `dealId` to resolve the
   * human-readable variant label (e.g. "L · Red") via getDealWithSkus.
   * Null for scalar (no-variant) deals.
   */
  dealSkuId?: string | null;
}
export interface HoldOk {
  ok: true;
  providerHoldId: string;
  expiresAt: string;
}
export type HoldOutcome = HoldOk | PaymentFailure;

export interface CaptureInput {
  reservationId: string;
  purchaseId: string;
  providerHoldId: string;
  totalAgorot: number;
}

export interface ReleaseInput {
  reservationId: string;
  providerHoldId: string;
}

export interface RefundInput {
  purchaseId: string;
  providerPaymentId?: string;
  amountAgorot?: number;
  /** Stable business-event identity. Providers MUST deduplicate retries by this key. */
  idempotencyKey: string;
}
export interface RefundOk {
  ok: true;
  purchaseId: string;
  refundedAgorot: number;
  providerRefundId: string;
}
export type RefundOutcome = RefundOk | PaymentFailure;

export interface ReconcileInput {
  mode: 'mock' | 'stripe';
}
export interface ReconcileOutcome {
  checked: number;
  resolved: number;
  failed: number;
}

export type OnboardingState = 'not_started' | 'account_created' | 'kyc_pending' | 'charges_enabled';

export interface OnboardInput {
  vendorId: string;
  businessName: string;
  contactEmail: string;
  contactPhone?: string;
}
export interface OnboardOk {
  ok: true;
  state: OnboardingState;
  providerAccountId: string;
  hostedOnboardingUrl: string;
  chargesEnabled: boolean;
}
export type OnboardOutcome = OnboardOk | PaymentFailure;

export interface OnboardingSessionInput {
  vendorId: string;
  businessName: string;
  contactEmail: string;
}
export interface OnboardingSessionOk {
  ok: true;
  clientSecret: string;
  stripeAccountId: string;
}
export type OnboardingSessionOutcome = OnboardingSessionOk | PaymentFailure;

export type ClientConfig = { provider: 'stripe'; publishableKey: string } | { provider: 'mock' };

export interface PaymentProvider {
  ensureCustomerId(input: EnsureCustomerInput): Promise<string | null>;

  /**
   * Initiates a charge.
   * - With paymentMethodId (off-session, backend workflows, mock): returns
   *   ChargeOk synchronously after finalize.
   * - Without paymentMethodId (interactive checkout): returns
   *   ChargeRequiresClient with clientSecret; finalize occurs via webhook.
   */
  charge(input: ChargeInput): Promise<ChargeOutcome>;

  /** Finalize a PaymentIntent (idempotent). Called from webhook + redirect-poll fallback. Fires InvoiceProvider. */
  finalize(input: FinalizeInput): Promise<FinalizeOutcome>;

  checkCard(input: CheckInput): Promise<CardTokenOutcome>;
  /**
   * Creates a Stripe SetupIntent (or mock equivalent) for saving a new card.
   * Stripe: gets-or-creates a Stripe Customer for the user, then creates a
   * SetupIntent with usage='off_session' attached to that customer.
   * Mock: returns clientSecret=null (no SI needed for mock flow).
   */
  createSetupIntent(
    userId: string,
  ): Promise<
    { ok: true; clientSecret: string } | { ok: true; clientSecret: null } | PaymentFailure
  >;
  placeHold(input: HoldInput): Promise<HoldOutcome>;
  /** captureHold finalizes synchronously — manual-capture PI has PM attached at placeHold. */
  captureHold(input: CaptureInput): Promise<ChargeOk | PaymentFailure>;
  releaseHold(input: ReleaseInput): Promise<void>;
  refund(input: RefundInput): Promise<RefundOutcome>;
  reconcile(input: ReconcileInput): Promise<ReconcileOutcome>;
  onboardVendor(input: OnboardInput): Promise<OnboardOutcome>;
  createOnboardingSession(input: OnboardingSessionInput): Promise<OnboardingSessionOutcome>;
  getClientConfig(): ClientConfig;
}
