import Stripe from 'stripe'
import { InvalidAmountError, RefundFailedError, WebhookVerificationError } from './errors.js'
import {
  idempotencyKey,
  type ChargeRequest,
  type ChargeResult,
  type PaymentProvider,
  type ProviderEvent,
  type ProviderFactory,
  type RefundRequest,
  type RefundReconciliationRequest,
  type RefundReconciliationResult,
  type RefundResult,
} from './index.js'

export type StripeCreds = {
  secretKey: string
  webhookSecret: string
  stripe?: Stripe
  apiVersion?: string
  /** Host authority for a requested Connect destination + fee tuple. */
  authorizeConnectSplit?: (
    chargeKey: string,
    extras: StripeChargeExtras,
  ) => boolean | Promise<boolean>
  /** Host-owned authoritative lookup for every refund target. */
  resolvePaymentIntentId?: (chargeKey: string) => string | Promise<string>
}

/** Stripe refund calls need the original PaymentIntent id (pi_…). */
export type StripeRefundRequest = RefundRequest & {
  paymentIntentId?: string
}

/**
 * Structured, charge-time-only Stripe Connect inputs. These MONEY-ROUTING
 * fields ride a TYPED request extension — NEVER `metadata` (spec floor #6 /
 * request-extension pattern). `metadata` is a caller-supplied
 * `Record<string,string>`; reading `transfer_data.destination` from it let any
 * caller who could set a metadata key REDIRECT the payout to an arbitrary
 * Connect account (a funds-redirect authorization hole). The adapter now reads
 * the destination/fee ONLY from this typed field and IGNORES the metadata path.
 */
export type StripeChargeExtras = {
  /** Connect account that receives the transfer (acct_…). Routes money → typed, never metadata. */
  connectDestination?: string
  /** Platform application fee in minor units, retained on the platform. */
  applicationFeeAmount?: number
}

export type StripeChargeRequest = ChargeRequest & {
  stripe?: StripeChargeExtras
}

const DEFAULT_API_VERSION = '2026-04-22.dahlia'

/**
 * Resolve the Connect split from the TYPED `req.stripe` extension only. Fails
 * closed: an absent/blank `connectDestination` yields NO `transfer_data` (a
 * normal non-split charge), never a metadata-sourced destination. A present
 * destination must be a non-empty string (a typed-but-empty value is rejected
 * rather than silently dropped onto an undefined transfer).
 */
function parseConnectSplit(extras: StripeChargeExtras | undefined): {
  applicationFeeAmount?: number
  transferData?: { destination: string }
} {
  if (!extras) return {}

  let transferData: { destination: string } | undefined
  if (extras.connectDestination !== undefined) {
    if (typeof extras.connectDestination !== 'string' || extras.connectDestination.trim() === '') {
      throw new Error('Stripe charge stripe.connectDestination must be a non-empty Connect account id')
    }
    transferData = { destination: extras.connectDestination }
  }

  // Fail LOUD on a malformed fee — never silently drop it. A dropped fee = the
  // platform commission silently becomes zero and the FULL amount transfers to
  // the vendor (the same silent-money-loss class as the destination footgun).
  // Reject invalid fees before any provider request. Absent = no fee (valid).
  let applicationFeeAmount: number | undefined
  if (extras.applicationFeeAmount !== undefined) {
    const fee = extras.applicationFeeAmount
    if (typeof fee !== 'number' || !Number.isInteger(fee) || fee < 0) {
      throw new Error(
        `Stripe charge stripe.applicationFeeAmount must be a non-negative integer minor-units value, got ${fee}`,
      )
    }
    applicationFeeAmount = fee
  }

  return { applicationFeeAmount, transferData }
}

export async function classifyStripeEvent(
  event: Stripe.Event,
  client: Stripe,
): Promise<ProviderEvent> {
  const base = { eventId: event.id }

  if (event.type === 'payment_intent.succeeded') {
    const pi = event.data.object as Stripe.PaymentIntent
    const chargeKey = pi.metadata?.chargeKey
    if (!chargeKey) {
      return { ...base, kind: 'other', raw: event }
    }
    return {
      ...base,
      kind: 'settlement',
      chargeKey,
      providerRef: pi.id,
      amount: pi.amount_received,
      currency: pi.currency.toUpperCase(),
    }
  }

  if (event.type === 'refund.created' || event.type === 'refund.updated') {
    const refund = event.data.object as Stripe.Refund
    if (refund.status !== 'succeeded') {
      return { ...base, kind: 'other', raw: event }
    }

    const paymentIntentId =
      typeof refund.payment_intent === 'string'
        ? refund.payment_intent
        : refund.payment_intent?.id

    if (!paymentIntentId) {
      return { ...base, kind: 'other', raw: event }
    }

    const pi = await client.paymentIntents.retrieve(paymentIntentId)
    const chargeKey = pi.metadata?.chargeKey
    if (!chargeKey) {
      return { ...base, kind: 'other', raw: event }
    }

    return {
      ...base,
      kind: 'refund',
      refundKey: idempotencyKey(['refund', chargeKey, refund.id]),
      chargeKey,
      providerChargeId: paymentIntentId,
      providerRef: refund.id,
      amount: refund.amount ?? 0,
      currency: refund.currency?.toUpperCase() ?? pi.currency.toUpperCase(),
    }
  }

  return { ...base, kind: 'other', raw: event }
}

export const stripe: ProviderFactory = (creds: unknown): PaymentProvider => {
  const {
    secretKey,
    webhookSecret,
    stripe: injected,
    apiVersion = DEFAULT_API_VERSION,
    authorizeConnectSplit,
    resolvePaymentIntentId,
  } = creds as StripeCreds

  const client =
    injected ??
    new Stripe(secretKey, {
      apiVersion: apiVersion as never,
    })

  return {
    provider: 'stripe',
    emitsInvoiceOnCharge: false,

    async charge(req: ChargeRequest): Promise<ChargeResult> {
      // Connect split rides the TYPED req.stripe extension ONLY — never metadata
      // (which is caller-supplied scalar round-trip data; a metadata-sourced
      // destination is a funds-redirect footgun). Fails closed: no typed
      // destination → a normal non-split charge.
      const stripeExtras = (req as StripeChargeRequest).stripe
      const { applicationFeeAmount, transferData } = parseConnectSplit(stripeExtras)
      // Fee > amount would transfer a NEGATIVE net to the vendor; fail fast
      // locally instead of relying on Stripe's server-side rejection.
      if (applicationFeeAmount !== undefined && applicationFeeAmount > req.amount) {
        throw new InvalidAmountError(
          `Stripe charge stripe.applicationFeeAmount ${applicationFeeAmount} exceeds the charge amount ${req.amount}`,
          { value: applicationFeeAmount, field: 'stripe.applicationFeeAmount' },
        )
      }
      if (
        stripeExtras !== undefined &&
        (!authorizeConnectSplit || !(await authorizeConnectSplit(req.chargeKey, stripeExtras)))
      ) {
        throw new Error('Stripe charge Connect split is not authorized for this chargeKey')
      }
      const currency = req.currency.toLowerCase()

      const params: Stripe.PaymentIntentCreateParams = {
        amount: req.amount,
        currency,
        // chargeKey merges LAST: caller metadata must never shadow the key the
        // webhook uses to attribute the settlement (misattribution = wrong ledger
        // post + wrong-order dispatch).
        metadata: {
          ...(req.metadata ?? {}),
          chargeKey: req.chargeKey,
        },
        ...(applicationFeeAmount !== undefined
          ? { application_fee_amount: applicationFeeAmount }
          : {}),
        ...(transferData ? { transfer_data: transferData } : {}),
        automatic_payment_methods: { enabled: true },
      }

      const pi = await client.paymentIntents.create(params, {
        idempotencyKey: req.chargeKey,
      })

      if (pi.status === 'succeeded') {
        return {
          kind: 'settled',
          chargeKey: req.chargeKey,
          providerRef: pi.id,
          amount: pi.amount_received,
          currency: pi.currency.toUpperCase(),
        }
      }

      if (!pi.client_secret) {
        throw new Error('Stripe PaymentIntent missing client_secret for client confirmation')
      }

      return {
        kind: 'requires_client_action',
        chargeKey: req.chargeKey,
        providerRef: pi.id,
        clientSecret: pi.client_secret,
      }
    },

    async refund(req: RefundRequest): Promise<RefundResult> {
      const stripeReq = req as StripeRefundRequest
      if (!resolvePaymentIntentId) {
        throw new Error('Stripe refund requires resolvePaymentIntentId authority in creds')
      }
      const paymentIntent = await resolvePaymentIntentId(req.chargeKey)
      if (!paymentIntent) {
        throw new Error('Stripe refund resolvePaymentIntentId returned no authoritative target')
      }
      if (stripeReq.paymentIntentId && stripeReq.paymentIntentId !== paymentIntent) {
        throw new Error('Stripe refund paymentIntentId does not match the authoritative charge target')
      }

      const refund = await client.refunds.create(
        {
          payment_intent: paymentIntent,
          amount: req.amount,
          reverse_transfer: true,
          refund_application_fee: true,
        },
        { idempotencyKey: req.refundKey },
      )

      // Mirror the webhook path (posts only on 'succeeded'): a 'refunded' result
      // drives a reversing ledger post, so a failed/canceled refund must throw —
      // it moved no money, and posting would understate received funds.
      if (refund.status === 'succeeded') {
        const currency = refund.currency?.toUpperCase()
        if (!currency) {
          throw new Error('Stripe refund response missing currency')
        }
        return {
          kind: 'refunded',
          refundKey: idempotencyKey(['refund', req.chargeKey, refund.id]),
          chargeKey: req.chargeKey,
          providerRef: refund.id,
          amount: refund.amount ?? req.amount,
          currency,
        }
      }
      if (refund.status === 'pending' || refund.status === 'requires_action') {
        return { kind: 'pending' }
      }
      throw new RefundFailedError({
        chargeKey: req.chargeKey,
        providerRef: refund.id,
        status: refund.status ?? 'unknown',
      })
    },

    async reconcileRefund(
      req: RefundReconciliationRequest,
    ): Promise<RefundReconciliationResult> {
      if (req.amountMinor <= 0n || req.amountMinor > BigInt(Number.MAX_SAFE_INTEGER)) {
        return { kind: 'definite_failure', code: 'INVALID_AMOUNT' }
      }
      try {
        const refund = await client.refunds.create(
          {
            payment_intent: req.providerChargeId,
            amount: Number(req.amountMinor),
          },
          { idempotencyKey: req.refundKey },
        )
        if (refund.status === 'succeeded') {
          return {
            kind: 'confirmed',
            providerRefundId: refund.id,
            amountMinor: BigInt(refund.amount),
            currency: refund.currency.toUpperCase(),
          }
        }
        if (refund.status === 'failed' || refund.status === 'canceled') {
          return { kind: 'definite_failure', code: refund.status.toUpperCase() }
        }
        return { kind: 'pending_or_unknown' }
      } catch {
        return { kind: 'pending_or_unknown' }
      }
    },

    async parseWebhook(raw: string, headers: Headers): Promise<ProviderEvent> {
      const sig = headers.get('stripe-signature')
      if (!sig) {
        throw new WebhookVerificationError('missing stripe-signature header')
      }

      let event: Stripe.Event
      try {
        event = client.webhooks.constructEvent(raw, sig, webhookSecret)
      } catch (err) {
        throw new WebhookVerificationError(
          err instanceof Error ? err.message : 'webhook verification failed',
        )
      }

      return classifyStripeEvent(event, client)
    },
  }
}
