import { createDbService } from '@/server/services/db.js';
import type Stripe from 'stripe';
import { bytesToHex } from '@/lib/encoding.js';
import type { MultidealEnv } from '../env.js';
import type {
  PaymentProvider,
  EnsureCustomerInput,
  ChargeInput,
  ChargeOutcome,
  ChargeOk,
  PaymentFailure,
  FinalizeInput,
  FinalizeOutcome,
  CheckInput,
  CardTokenOutcome,
  HoldInput,
  HoldOutcome,
  CaptureInput,
  ReleaseInput,
  RefundInput,
  RefundOutcome,
  ReconcileInput,
  ReconcileOutcome,
  OnboardInput,
  OnboardOutcome,
  OnboardingSessionInput,
  OnboardingSessionOutcome,
  ClientConfig,
} from './provider.js';
import { PaymentErrorCode } from './provider.js';
import { getStripe } from './stripe/client.js';
import { idempotency } from './stripe/idempotency.js';
import { mapStripeError, mapStripePaymentIntentFailure } from './stripe/error-map.js';
import { captureCaught } from '@/server/observability/capture.server';
import * as purchaseQueries from '@/server/db/queries/purchases.js';
import {
  cancelEnqueuedPayoutRelease,
  claimPayoutReleaseForRefund,
  revertPayoutRefundClaim,
  stampPayoutRefund,
} from '@/server/db/queries/payment-writes.js';
import * as userQueries from '@/server/db/queries/users.js';
import * as vendorQueries from '@/server/db/queries/vendors.js';
import {
  vendors,
  vendorPayoutReleases,
  order,
  orderLine,
  paymentExpectedContracts,
} from '@/server/db/schema.js';
import { eq } from 'drizzle-orm';
import { finalizePurchase } from './finalize.js';
import { computeStripeAmounts } from './stripe-promo-math.js';
import { buildSkuLabel, buildStripeDescription } from './stripe/line-items.js';
import { getDealWithSkus } from '@/server/domain/variants/read.js';
import { insertExpectedPaymentContract } from '@/server/db/queries/payment-finalize.js';
import { formatAgorotPlain } from '@/lib/money.js';

/**
 * HMAC-SHA-256 of a Stripe card fingerprint, keyed by PII_KEY.
 * Produces a hex digest safe to store — attacker knowing the raw fingerprint
 * cannot reproduce the hash without PII_KEY.
 */
async function hmacCardFingerprint(raw: string, piiKey: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(piiKey),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(raw));
  return bytesToHex(sig);
}

export class StripePaymentProvider implements PaymentProvider {
  private readonly stripe: Stripe;
  constructor(private readonly env: MultidealEnv) {
    this.stripe = getStripe(env);
  }

  async ensureCustomerId(input: EnsureCustomerInput): Promise<string> {
    const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });
    const existing = await userQueries.getUserStripeCustomerId(db, input.userId);
    if (existing) return existing;

    const validEmail = input.buyer.email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.buyer.email);
    const customer = await this.stripe.customers.create(
      {
        ...(validEmail ? { email: input.buyer.email } : {}),
        ...(input.buyer.name !== 'Customer' ? { name: input.buyer.name } : {}),
        metadata: { userId: input.userId },
      },
      { idempotencyKey: idempotency.customer(input.userId) },
    );
    await userQueries.setUserStripeCustomerId(db, input.userId, customer.id);
    return customer.id;
  }

  async charge(input: ChargeInput): Promise<ChargeOutcome> {
    const feeRaw = Number(this.env.PLATFORM_FEE_PCT);
    const platformFeePct = Number.isFinite(feeRaw) ? feeRaw : 10;
    const { amount: chargeAmount, applicationFeeAmount: platformAgorot } = computeStripeAmounts(
      input.totalAgorot,
      input.promoClaim,
      platformFeePct,
    );
    const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });

    // For off-session callers (paymentMethodId present), resolve user's stripeCustomerId.
    let customerId = input.customerId;
    if (input.paymentMethodId && !customerId) {
      const purchase = await purchaseQueries.findById(db, input.purchaseId);
      const userId = purchase?.userId;
      if (userId) customerId = (await userQueries.getUserStripeCustomerId(db, userId)) ?? undefined;
    }

    const createParams: Stripe.PaymentIntentCreateParams = {
      amount: chargeAmount,
      currency: 'ils',
      capture_method: 'automatic',
      application_fee_amount: platformAgorot,
      transfer_data: { destination: input.vendor.providerAccountId },
      metadata: {
        purchaseId: input.purchaseId,
        vendorId: input.vendor.vendorId,
        dealTitle: input.vendor.dealTitle,
        buyerName: input.buyer.name,
      },
      ...(customerId ? { customer: customerId } : {}),
      ...(() => {
        const isValidEmail = (s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
        return input.buyer.email && isValidEmail(input.buyer.email)
          ? { receipt_email: input.buyer.email }
          : {};
      })(),
    };

    // Off-session saved-card charge: confirm immediately
    if (input.paymentMethodId) {
      createParams.payment_method = input.paymentMethodId;
      createParams.confirm = true;
      createParams.off_session = true;
    } else {
      // Interactive: enable PM attach + 3DS via client
      createParams.setup_future_usage = 'off_session';
      createParams.automatic_payment_methods = { enabled: true };
    }

    let pi: Stripe.PaymentIntent;
    try {
      pi = await this.stripe.paymentIntents.create(createParams, {
        idempotencyKey: idempotency.purchaseIntent(input.purchaseId),
      });
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }

    // Off-session: confirm done in create() — finalize sync
    if (input.paymentMethodId) {
      if (pi.status === 'succeeded') return finalizePurchase(this.env, pi);
      return mapStripePaymentIntentFailure(pi);
    }

    // Interactive: Stripe may replay the idempotent create after client-side
    // confirmation, so interpret every status instead of assuming a fresh PI.
    switch (pi.status) {
      case 'succeeded':
        return finalizePurchase(this.env, pi);
      case 'requires_payment_method':
      case 'requires_confirmation':
      case 'requires_action':
        if (typeof pi.client_secret !== 'string' || pi.client_secret.length === 0) {
          return {
            ok: false,
            code: PaymentErrorCode.PROVIDER_ERROR,
            message: 'Payment intent has no client secret',
          };
        }
        return {
          ok: true,
          status: 'requires_client_confirmation',
          providerPaymentId: pi.id,
          clientSecret: pi.client_secret,
        };
      case 'canceled':
        return mapStripePaymentIntentFailure(pi);
      case 'processing':
      case 'requires_capture':
        return {
          ok: false,
          code: PaymentErrorCode.PROVIDER_ERROR,
          message: `Payment intent is ${pi.status}`,
        };
    }
  }

  async finalize(input: FinalizeInput): Promise<FinalizeOutcome> {
    try {
      const pi = await this.stripe.paymentIntents.retrieve(input.providerPaymentId);
      if (pi.status === 'succeeded') return finalizePurchase(this.env, pi);
      if (pi.status === 'canceled' || pi.status === 'requires_payment_method') {
        return mapStripePaymentIntentFailure(pi);
      }
      return {
        ok: false,
        code: PaymentErrorCode.PROVIDER_ERROR,
        message: `PI still ${pi.status}`,
      };
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async checkCard(input: CheckInput): Promise<CardTokenOutcome> {
    try {
      const pm = await this.stripe.paymentMethods.retrieve(input.paymentMethodId);
      if (pm.type !== 'card' || !pm.card) {
        return {
          ok: false,
          code: PaymentErrorCode.TOKEN_INVALID,
          message: 'Not a card PM',
        };
      }
      let cardFingerprint: string | undefined;
      if (pm.card.fingerprint && this.env.PII_KEY) {
        try {
          cardFingerprint = await hmacCardFingerprint(pm.card.fingerprint, this.env.PII_KEY);
        } catch (fpErr) {
          captureCaught(fpErr, {
            scope: 'payments.stripe.checkCard.fingerprint',
            severity: 'warning',
          });
        }
      }
      return {
        ok: true,
        providerCardToken: pm.id,
        expirationMonth: pm.card.exp_month,
        expirationYear: pm.card.exp_year,
        brand: pm.card.brand,
        last4: pm.card.last4,
        cardFingerprint,
      };
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async createSetupIntent(
    userId: string,
  ): Promise<
    { ok: true; clientSecret: string } | { ok: true; clientSecret: null } | PaymentFailure
  > {
    const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });

    // Get or create Stripe Customer so the PM attaches and is chargeable off-session
    let customerId = (await userQueries.getUserStripeCustomerId(db, userId)) ?? undefined;
    if (!customerId) {
      try {
        const customer = await this.stripe.customers.create({
          metadata: { userId },
        });
        customerId = customer.id;
        await userQueries.setUserStripeCustomerId(db, userId, customerId);
      } catch (e) {
        return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
      }
    }

    // Create SetupIntent attached to customer for off-session use
    try {
      const si = await this.stripe.setupIntents.create({
        customer: customerId,
        usage: 'off_session',
        automatic_payment_methods: { enabled: true },
      });
      return { ok: true as const, clientSecret: si.client_secret! };
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async placeHold(input: HoldInput): Promise<HoldOutcome> {
    const { amount: holdAmount, applicationFeeAmount: computedFee } = computeStripeAmounts(
      input.totalAgorot,
      undefined,
      Number(this.env.PLATFORM_FEE_PCT) || 10,
    );
    const platformAgorot =
      input.applicationFeeAgorot != null ? input.applicationFeeAgorot : computedFee;

    if (input.purchaseId) {
      const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });
      const [purchaseOrder] = await db
        .select({ orderId: order.id })
        .from(orderLine)
        .innerJoin(order, eq(order.id, orderLine.orderId))
        .where(eq(orderLine.id, input.purchaseId))
        .limit(1);
      if (!purchaseOrder) {
        return {
          ok: false,
          code: PaymentErrorCode.PROVIDER_ERROR,
          message: `Purchase not found: ${input.purchaseId}`,
        };
      }
      const expected = {
        purchaseId: input.purchaseId,
        orderId: purchaseOrder.orderId,
        amountAgorot: holdAmount,
        currency: 'ils',
        customerId: input.customerId ?? null,
        destinationAccountId: input.vendor.providerAccountId,
        applicationFeeAgorot: platformAgorot,
        effectKey: `payment-contract:${input.purchaseId}`,
      };
      const [inserted] = await insertExpectedPaymentContract(db, expected);
      if (!inserted) {
        const [existing] = await db
          .select()
          .from(paymentExpectedContracts)
          .where(eq(paymentExpectedContracts.purchaseId, input.purchaseId))
          .limit(1);
        if (
          !existing ||
          existing.orderId !== expected.orderId ||
          existing.amountAgorot !== expected.amountAgorot ||
          existing.currency !== expected.currency ||
          existing.customerId !== expected.customerId ||
          existing.destinationAccountId !== expected.destinationAccountId ||
          existing.applicationFeeAgorot !== expected.applicationFeeAgorot
        ) {
          return {
            ok: false,
            code: PaymentErrorCode.PAYMENT_CONTRACT_MISMATCH,
            message: `Payment contract mismatch for ${input.purchaseId}`,
          };
        }
      }
    }

    // Resolve SKU label when deal+SKU identity is provided.
    let skuLabel: string | null = null;
    if (input.dealId && input.dealSkuId) {
      try {
        const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });
        const dealWithSkus = await getDealWithSkus(db, input.dealId);
        skuLabel = buildSkuLabel(dealWithSkus, input.dealSkuId);
      } catch (labelErr) {
        // Non-fatal: label enrichment failure must never block a payment hold.
        captureCaught(labelErr, {
          scope: 'payments.stripe.placeHold.skuLabel',
          severity: 'warning',
        });
      }
    }

    const description = buildStripeDescription(input.vendor.dealTitle, skuLabel);

    try {
      const pi = await this.stripe.paymentIntents.create(
        {
          amount: holdAmount,
          currency: 'ils',
          capture_method: 'manual',
          payment_method: input.providerCardToken,
          ...(input.customerId ? { customer: input.customerId } : {}),
          confirm: true,
          off_session: true,
          application_fee_amount: platformAgorot,
          transfer_data: { destination: input.vendor.providerAccountId },
          description,
          metadata: {
            reservationId: input.reservationId,
            ...(input.promoReservationId ? { promoReservationId: input.promoReservationId } : {}),
            vendorId: input.vendor.vendorId,
            ...(input.purchaseId ? { purchaseId: input.purchaseId } : {}),
            ...(input.checkoutKind ? { checkoutKind: input.checkoutKind } : {}),
            ...(input.dealId ? { dealId: input.dealId } : {}),
            ...(input.dealSkuId ? { dealSkuId: input.dealSkuId } : {}),
            ...(skuLabel ? { skuLabel } : {}),
          },
        },
        { idempotencyKey: idempotency.hold(input.reservationId) },
      );

      if (pi.status !== 'requires_capture') {
        return mapStripePaymentIntentFailure(pi);
      }
      // Stripe defaults manual-capture window to 7 days
      const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
      return { ok: true, providerHoldId: pi.id, expiresAt };
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async captureHold(input: CaptureInput): Promise<ChargeOk | PaymentFailure> {
    let pi: Stripe.PaymentIntent;
    try {
      const held = await this.stripe.paymentIntents.retrieve(input.providerHoldId);
      if (input.totalAgorot > held.amount) {
        return {
          ok: false,
          code: PaymentErrorCode.PROVIDER_ERROR,
          message: `Capture amount ${input.totalAgorot} exceeds hold ${held.amount} agorot`,
        };
      }

      // Partial capture: scale the application fee proportionally — Stripe
      // keeps the hold-time fee otherwise, over-debiting the vendor.
      const heldFee = held.application_fee_amount ?? 0;
      const scaledFee =
        input.totalAgorot < held.amount && heldFee > 0
          ? Math.floor((heldFee * input.totalAgorot) / held.amount)
          : null;

      pi = await this.stripe.paymentIntents.capture(
        input.providerHoldId,
        {
          amount_to_capture: input.totalAgorot,
          ...(scaledFee != null ? { application_fee_amount: scaledFee } : {}),
        },
        { idempotencyKey: idempotency.captureHold(input.providerHoldId) },
      );

      if (pi.status !== 'succeeded') return mapStripePaymentIntentFailure(pi);
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }

    const platformAgorot = pi.application_fee_amount ?? 0;
    return {
      ok: true,
      status: 'succeeded',
      providerPaymentId: pi.id,
      vendorAgorot: input.totalAgorot - platformAgorot,
      platformAgorot,
      vendorTaxDocId: null,
      platformTaxDocId: null,
      vendorTaxDocPdfUrl: null,
      platformTaxDocPdfUrl: null,
    };
  }

  async releaseHold(input: ReleaseInput): Promise<void> {
    try {
      await this.stripe.paymentIntents.cancel(
        input.providerHoldId,
        {
          cancellation_reason: 'requested_by_customer',
        },
        { idempotencyKey: idempotency.releaseHold(input.providerHoldId) },
      );
    } catch (e) {
      // Idempotent void — if already canceled, swallow
      const err = e as InstanceType<typeof Stripe.errors.StripeError>;
      if (err.code !== 'payment_intent_unexpected_state') throw e;
    }
  }

  async reconcileHold(input: { providerHoldId: string }): Promise<{
    status: 'succeeded' | 'cancellable' | 'processing';
    providerPaymentId?: string;
  }> {
    const pi = await this.stripe.paymentIntents.retrieve(input.providerHoldId);
    if (pi.status === 'succeeded') return { status: 'succeeded', providerPaymentId: pi.id };
    if (pi.status === 'requires_capture') return { status: 'cancellable' };
    if (pi.status === 'canceled') return { status: 'cancellable' };
    if (pi.status === 'processing' || pi.status === 'requires_action')
      return { status: 'processing' };
    throw new Error(`unexpected hold state ${pi.status}`);
  }

  async refund(input: RefundInput): Promise<RefundOutcome> {
    const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });
    const [orderRow] = await db
      .select({ chargeRef: order.chargeRef, lineTotal: orderLine.lineTotal })
      .from(orderLine)
      .innerJoin(order, eq(order.id, orderLine.orderId))
      .where(eq(orderLine.id, input.purchaseId))
      .limit(1);
    if (
      orderRow?.chargeRef &&
      input.providerPaymentId &&
      input.providerPaymentId !== orderRow.chargeRef
    ) {
      return {
        ok: false,
        code: PaymentErrorCode.PAYMENT_CONTRACT_MISMATCH,
        message: 'Payment reference does not match purchase',
      };
    }
    const p = orderRow
      ? {
          providerPaymentId: input.providerPaymentId ?? orderRow.chargeRef,
          amountPaid: formatAgorotPlain(Number(orderRow.lineTotal)),
        }
      : null;
    if (!p?.providerPaymentId) {
      return {
        ok: false,
        code: PaymentErrorCode.PROVIDER_ERROR,
        message: 'Purchase has no provider_payment_id',
      };
    }

    const refundAmountAgorot = input.amountAgorot ?? Math.round(parseFloat(p.amountPaid) * 100);
    let paymentIntent: Stripe.PaymentIntent;
    try {
      paymentIntent = await this.stripe.paymentIntents.retrieve(p.providerPaymentId);
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
    const paidAgorot = paymentIntent.amount_received ?? paymentIntent.amount;
    if (
      paymentIntent.id !== p.providerPaymentId ||
      paymentIntent.metadata.purchaseId !== input.purchaseId ||
      paymentIntent.currency.toLowerCase() !== 'ils' ||
      paidAgorot < refundAmountAgorot
    ) {
      return {
        ok: false,
        code: PaymentErrorCode.PAYMENT_CONTRACT_MISMATCH,
        message: 'Payment intent does not match purchase refund contract',
      };
    }

    // ── vendor_payout_releases branching (T12) ──────────────────────────────
    const releaseRows = await db
      .select()
      .from(vendorPayoutReleases)
      .where(eq(vendorPayoutReleases.orderLineId, input.purchaseId))
      .limit(1);
    const release = releaseRows[0] ?? null;
    let claimFromStatus = release?.status;

    if (!release) {
      // Legacy purchase pre-T7 backfill — no release row. Proceed with legacy path.
      captureCaught(new Error(`refund_no_release_row purchase=${input.purchaseId}`), {
        scope: 'payments.stripe.refund.no_release_row',
        severity: 'warning',
      });
    } else {
      // Branch on release status
      let effectiveStatus = release.status;

      if (effectiveStatus === 'refunded' || effectiveStatus === 'cancelled') {
        return {
          ok: false,
          code: PaymentErrorCode.PROVIDER_ERROR,
          message: 'already_refunded',
        };
      }

      if (effectiveStatus === 'enqueued') {
        // Atomically cancel via Drizzle builder; .returning() confirms row was ours
        const cancelled = await cancelEnqueuedPayoutRelease(db, release.id);

        if (cancelled.length === 0) {
          // Race: consumer claimed the row — treat as releasing, fall through to poll
          effectiveStatus = 'releasing';
        } else {
          // Row is now 'cancelled' in DB; claim block must match current status, not stale release.status
          claimFromStatus = 'cancelled';
        }
        // If cancelled.length > 0: proceed to Stripe refund below (fall through)
      }

      if (effectiveStatus === 'releasing') {
        // A payout sweep is actively transferring funds to the vendor.
        // CF Workers must not spin-wait (up to 20 sequential Neon RTTs burns CPU budget).
        // Return retry_later immediately — the caller (outbox retry handler or admin UI)
        // will re-invoke once the sweep settles (status transitions out of 'releasing').
        captureCaught(new Error(`refund_releasing_in_flight purchase=${input.purchaseId}`), {
          scope: 'payments.stripe.refund.releasing_in_flight',
          severity: 'warning',
        });
        return {
          ok: false,
          code: PaymentErrorCode.PROVIDER_ERROR,
          message: 'retry_later:release_in_flight',
        };
      }
      // status='held' or 'released' (or enqueued→cancelled above): fall through
    }
    // ─────────────────────────────────────────────────────────────────────────

    if (!input.idempotencyKey) {
      return {
        ok: false,
        code: PaymentErrorCode.PAYMENT_CONTRACT_MISMATCH,
        message: 'refund event idempotency key required',
      };
    }
    const stripeIdempotencyKey = input.idempotencyKey;
    const hasVendorTransfer = release?.transferId != null;

    // Atomically claim the release row before calling Stripe to prevent concurrent
    // refunds racing on the same purchase. We set status='refunded' here and revert
    // if the Stripe call fails. If another request already claimed it, we return
    // already_refunded. Legacy purchases with no release row skip this guard.
    if (release && claimFromStatus) {
      const claimed = await claimPayoutReleaseForRefund(db, release.id, claimFromStatus);
      if (claimed.length === 0) {
        return {
          ok: false,
          code: PaymentErrorCode.PROVIDER_ERROR,
          message: 'already_refunded',
        };
      }
    }

    try {
      const refund = await this.stripe.refunds.create(
        {
          payment_intent: p.providerPaymentId,
          ...(input.amountAgorot != null ? { amount: input.amountAgorot } : {}),
          ...(hasVendorTransfer
            ? {
                reverse_transfer: true,
                refund_application_fee: true,
              }
            : {}),
          reason: 'requested_by_customer',
          metadata: { purchaseId: input.purchaseId },
        },
        { idempotencyKey: stripeIdempotencyKey },
      );

      await purchaseQueries.setPurchaseRefund(db, input.purchaseId, {
        refundId: refund.id,
        amount: refund.amount,
      });

      // Stamp the Stripe refund ID now that we have it (row already set to 'refunded')
      if (release) {
        await stampPayoutRefund(db, release.id, refund.id);
      }

      return {
        ok: true,
        purchaseId: input.purchaseId,
        refundedAgorot: refund.amount,
        providerRefundId: refund.id,
      };
    } catch (e) {
      // Revert the optimistic claim so the operator can retry.
      if (release) {
        await revertPayoutRefundClaim(db, release.id, release.status).catch(
          (revertErr: unknown) => {
            captureCaught(revertErr as Error, {
              scope: 'payments.stripe.refund.revert_claim',
              severity: 'error',
            });
          },
        );
      }
      captureCaught(e as Error, {
        scope: 'payments.stripe.refund.failed',
        severity: 'error',
      });
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async onboardVendor(input: OnboardInput): Promise<OnboardOutcome> {
    const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });

    // Reuse existing account if vendor already has one
    const [existing] = await db
      .select({ stripeAccountId: vendors.stripeAccountId })
      .from(vendors)
      .where(eq(vendors.id, input.vendorId))
      .limit(1);

    let accountId = existing?.stripeAccountId;
    try {
      if (!accountId) {
        const acct = await this.stripe.accounts.create(
          {
            type: 'express',
            country: 'IL',
            email: input.contactEmail,
            business_profile: {
              name: input.businessName,
              mcc: '5812',
            },
            capabilities: {
              // IL connected accounts cannot be card acquirers — the US platform is
              // merchant-of-record and acquires the card. Vendor only RECEIVES funds via
              // destination-charge transfers, so it needs `transfers` only.
              // Stripe requires the `recipient` service agreement for IL transfers-only
              // accounts (verified: `full` agreement create returns
              // "A `recipient` service agreement is required for accounts in IL").
              transfers: { requested: true },
            },
            tos_acceptance: { service_agreement: 'recipient' },
            settings: {
              payouts: {
                schedule: { interval: 'weekly', weekly_anchor: 'monday' },
              },
            },
          },
          { idempotencyKey: idempotency.account(input.vendorId) },
        );
        accountId = acct.id;
        await this.stripe.accounts.update(accountId, {
          settings: { payouts: { schedule: { interval: 'manual' } } },
        });
        // setVendorStripeAccount(db, vendorId, stripeAccountId) — positional args
        await vendorQueries.setVendorStripeAccount(db, input.vendorId, accountId);
      }

      const link = await this.stripe.accountLinks.create(
        {
          account: accountId,
          refresh_url: `${this.env.PUBLIC_SITE_URL}/vendor/onboarding/refresh`,
          return_url: `${this.env.PUBLIC_SITE_URL}/vendor/onboarding/done`,
          type: 'account_onboarding',
        },
        { idempotencyKey: idempotency.accountLink(input.vendorId) },
      );

      return {
        ok: true,
        state: 'account_created',
        providerAccountId: accountId,
        hostedOnboardingUrl: `${link.url}?locale=he`,
        chargesEnabled: false,
      };
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async createOnboardingSession(input: OnboardingSessionInput): Promise<OnboardingSessionOutcome> {
    const db = createDbService({ DATABASE_URL: this.env.DATABASE_URL });
    const [existing] = await db
      .select({ stripeAccountId: vendors.stripeAccountId })
      .from(vendors)
      .where(eq(vendors.id, input.vendorId))
      .limit(1);

    let accountId = existing?.stripeAccountId;
    try {
      if (!accountId) {
        const acct = await this.stripe.accounts.create(
          {
            type: 'express',
            country: 'IL',
            email: input.contactEmail,
            business_profile: { name: input.businessName, mcc: '5812' },
            capabilities: { transfers: { requested: true } },
            tos_acceptance: { service_agreement: 'recipient' },
            settings: {
              payouts: {
                schedule: { interval: 'weekly', weekly_anchor: 'monday' },
              },
            },
          },
          { idempotencyKey: idempotency.account(input.vendorId) },
        );
        accountId = acct.id;
        await this.stripe.accounts.update(accountId, {
          settings: { payouts: { schedule: { interval: 'manual' } } },
        });
        await vendorQueries.setVendorStripeAccount(db, input.vendorId, accountId);
      }

      const session = await this.stripe.accountSessions.create(
        {
          account: accountId,
          components: { account_onboarding: { enabled: true } },
        },
        { idempotencyKey: idempotency.accountSession(input.vendorId) },
      );

      return {
        ok: true,
        clientSecret: session.client_secret,
        stripeAccountId: accountId,
      };
    } catch (e) {
      return mapStripeError(e as InstanceType<typeof Stripe.errors.StripeError>);
    }
  }

  async reconcile(_input: ReconcileInput): Promise<ReconcileOutcome> {
    const { reconcilePendingPurchases } = await import('./stripe/reconcile.js');
    return reconcilePendingPurchases(this.env);
  }

  getClientConfig(): ClientConfig {
    const publishableKey = this.env.STRIPE_PUBLISHABLE_KEY;
    if (!publishableKey) throw new Error('STRIPE_PUBLISHABLE_KEY not set');
    return { provider: 'stripe', publishableKey };
  }
}
