/**
 * @beignet/core/payments
 *
 * Provider-neutral payments primitives for Beignet applications.
 */

import {
  createProvider,
  createProviderInstrumentation,
} from "../providers/index.js";

/**
 * Value or promise of that value.
 */
export type MaybePromise<T> = T | Promise<T>;

/**
 * String metadata attached to provider-owned payment objects.
 */
export type PaymentMetadata = Record<string, string>;

/**
 * Checkout modes understood by Beignet's provider-neutral payment port.
 */
export type PaymentCheckoutMode = "payment" | "subscription";

/**
 * Hosted checkout line item.
 */
export interface PaymentCheckoutLineItem {
  /**
   * Provider price identifier.
   */
  priceId: string;
  /**
   * Quantity for this price. Defaults to the provider's default behavior.
   */
  quantity?: number;
}

/**
 * Input for creating a hosted checkout session.
 */
export interface CreateCheckoutSessionInput {
  /**
   * One-time payment or subscription checkout.
   */
  mode: PaymentCheckoutMode;
  /**
   * Line items to include in checkout.
   */
  lineItems: readonly PaymentCheckoutLineItem[];
  /**
   * URL the provider redirects to after successful checkout.
   */
  successUrl: string;
  /**
   * URL the provider redirects to when checkout is canceled.
   */
  cancelUrl: string;
  /**
   * Existing provider customer ID, when known.
   */
  customerId?: string;
  /**
   * App-owned reference copied into the provider session.
   */
  clientReferenceId?: string;
  /**
   * Provider metadata.
   */
  metadata?: PaymentMetadata;
  /**
   * Provider idempotency key for this external call.
   */
  idempotencyKey?: string;
}

/**
 * Hosted checkout session returned by a payment provider.
 */
export interface CheckoutSession {
  /**
   * Provider session ID.
   */
  id: string;
  /**
   * Provider name.
   */
  provider: string;
  /**
   * Checkout mode.
   */
  mode: PaymentCheckoutMode;
  /**
   * Hosted checkout URL, when the provider returns one.
   */
  url?: string;
  /**
   * Client secret, when the provider supports embedded checkout.
   */
  clientSecret?: string;
  /**
   * Provider customer ID, when known.
   */
  customerId?: string;
  /**
   * Provider status, when known.
   */
  status?: string;
  /**
   * Provider metadata.
   */
  metadata?: PaymentMetadata;
  /**
   * Raw provider response for app-owned escape hatches.
   */
  raw?: unknown;
}

/**
 * Input for creating a billing portal session.
 */
export interface CreateBillingPortalSessionInput {
  /**
   * Provider customer ID.
   */
  customerId: string;
  /**
   * URL the provider redirects to after leaving the portal.
   */
  returnUrl: string;
  /**
   * Provider idempotency key for this external call.
   */
  idempotencyKey?: string;
}

/**
 * Billing portal session returned by a payment provider.
 */
export interface BillingPortalSession {
  /**
   * Provider session ID.
   */
  id: string;
  /**
   * Provider name.
   */
  provider: string;
  /**
   * Hosted portal URL.
   */
  url: string;
  /**
   * Provider customer ID.
   */
  customerId: string;
  /**
   * Raw provider response for app-owned escape hatches.
   */
  raw?: unknown;
}

/**
 * Refund reasons common to hosted payment providers.
 */
export type PaymentRefundReason =
  | "duplicate"
  | "fraudulent"
  | "requested_by_customer";

/**
 * Input for creating a refund.
 */
export interface CreateRefundInput {
  /**
   * Provider payment ID, such as a payment intent ID.
   */
  paymentId: string;
  /**
   * Amount in the provider's smallest currency unit. Omit for a full refund.
   */
  amount?: number;
  /**
   * Reason for the refund.
   */
  reason?: PaymentRefundReason;
  /**
   * Provider metadata.
   */
  metadata?: PaymentMetadata;
  /**
   * Provider idempotency key for this external call.
   */
  idempotencyKey?: string;
}

/**
 * Refund returned by a payment provider.
 */
export interface Refund {
  /**
   * Provider refund ID.
   */
  id: string;
  /**
   * Provider name.
   */
  provider: string;
  /**
   * Provider payment ID that was refunded.
   */
  paymentId: string;
  /**
   * Refunded amount in the provider's smallest currency unit, when known.
   */
  amount?: number;
  /**
   * Currency code, when known.
   */
  currency?: string;
  /**
   * Provider refund status, when known.
   */
  status?: string;
  /**
   * Provider metadata.
   */
  metadata?: PaymentMetadata;
  /**
   * Raw provider response for app-owned escape hatches.
   */
  raw?: unknown;
}

/**
 * Raw webhook payload accepted by payment providers.
 */
export type PaymentWebhookRawBody = string | Uint8Array | ArrayBuffer;

/**
 * Input for verifying and parsing a provider webhook.
 */
export interface VerifyPaymentWebhookInput {
  /**
   * Raw request body. Do not pass a parsed JSON body.
   */
  rawBody: PaymentWebhookRawBody;
  /**
   * Provider signature header value.
   */
  signature: string;
}

/**
 * Normalized provider webhook event.
 */
export interface PaymentWebhookEvent {
  /**
   * Provider event ID.
   */
  id: string;
  /**
   * Provider event type, such as "checkout.session.completed".
   */
  type: string;
  /**
   * Provider name.
   */
  provider: string;
  /**
   * Event creation time, when known.
   */
  createdAt?: Date;
  /**
   * Whether the event came from live mode, when the provider exposes it.
   */
  livemode?: boolean;
  /**
   * Provider event data object.
   */
  data: unknown;
  /**
   * Raw provider event for app-owned escape hatches.
   */
  raw?: unknown;
}

/**
 * App-facing payments port.
 *
 * Implement this with hosted payment providers such as Stripe. Application
 * billing logic should depend on this interface instead of provider SDKs.
 */
export interface PaymentsPort {
  /**
   * Create a hosted checkout session.
   */
  createCheckoutSession(
    input: CreateCheckoutSessionInput,
  ): Promise<CheckoutSession>;
  /**
   * Create a hosted billing portal session.
   */
  createBillingPortalSession(
    input: CreateBillingPortalSessionInput,
  ): Promise<BillingPortalSession>;
  /**
   * Create a refund.
   */
  createRefund(input: CreateRefundInput): Promise<Refund>;
  /**
   * Verify and parse a provider webhook.
   */
  verifyWebhook(input: VerifyPaymentWebhookInput): Promise<PaymentWebhookEvent>;
}

/**
 * Error thrown by payment helpers and provider adapters.
 */
export class PaymentProviderError extends Error {
  /**
   * Provider name when known.
   */
  readonly provider?: string;
  /**
   * Operation that failed.
   */
  readonly operation: string;
  /**
   * Provider error code when known.
   */
  readonly code?: string;
  /**
   * Original provider error when available.
   */
  readonly cause?: unknown;

  constructor(args: {
    provider?: string;
    operation: string;
    message: string;
    code?: string;
    cause?: unknown;
  }) {
    super(args.message);
    this.name = "PaymentProviderError";
    this.provider = args.provider;
    this.operation = args.operation;
    this.code = args.code;
    this.cause = args.cause;
  }
}

/**
 * Captured checkout session created by the memory payments adapter.
 */
export type MemoryCheckoutSession = CheckoutSession & {
  input: CreateCheckoutSessionInput;
  createdAt: Date;
};

/**
 * Captured billing portal session created by the memory payments adapter.
 */
export type MemoryBillingPortalSession = BillingPortalSession & {
  input: CreateBillingPortalSessionInput;
  createdAt: Date;
};

/**
 * Captured refund created by the memory payments adapter.
 */
export type MemoryRefund = Refund & {
  input: CreateRefundInput;
  createdAt: Date;
};

/**
 * In-memory payments port for tests and local examples.
 */
export interface MemoryPaymentsPort extends PaymentsPort {
  /**
   * Captured checkout sessions.
   */
  readonly checkoutSessions: readonly MemoryCheckoutSession[];
  /**
   * Captured billing portal sessions.
   */
  readonly billingPortalSessions: readonly MemoryBillingPortalSession[];
  /**
   * Captured refunds.
   */
  readonly refunds: readonly MemoryRefund[];
  /**
   * Webhook events that were verified by the memory adapter.
   */
  readonly webhookEvents: readonly PaymentWebhookEvent[];
  /**
   * Queue a webhook event to be returned by the next `verifyWebhook(...)` call.
   */
  queueWebhookEvent(event: PaymentWebhookEvent): void;
  /**
   * Clear captured state.
   */
  clear(): void;
}

/**
 * Options for `createMemoryPayments(...)`.
 */
export interface CreateMemoryPaymentsOptions {
  /**
   * Clock used for captured payment objects.
   */
  now?: () => Date;
  /**
   * ID factory used for captured payment objects.
   */
  id?: (prefix: string) => string;
  /**
   * Observer called after a checkout session is created.
   */
  onCheckoutSessionCreated?: (
    session: MemoryCheckoutSession,
  ) => MaybePromise<void>;
  /**
   * Observer called after a billing portal session is created.
   */
  onBillingPortalSessionCreated?: (
    session: MemoryBillingPortalSession,
  ) => MaybePromise<void>;
  /**
   * Observer called after a refund is created.
   */
  onRefundCreated?: (refund: MemoryRefund) => MaybePromise<void>;
  /**
   * Observer called after a webhook event is verified.
   */
  onWebhookVerified?: (event: PaymentWebhookEvent) => MaybePromise<void>;
}

function defaultMemoryId(prefix: string): string {
  return `${prefix}_${crypto.randomUUID()}`;
}

function cloneMetadata(
  metadata: PaymentMetadata | undefined,
): PaymentMetadata | undefined {
  return metadata ? { ...metadata } : undefined;
}

function cloneCheckoutInput(
  input: CreateCheckoutSessionInput,
): CreateCheckoutSessionInput {
  return {
    ...input,
    lineItems: input.lineItems.map((item) => ({ ...item })),
    metadata: cloneMetadata(input.metadata),
  };
}

function cloneRefundInput(input: CreateRefundInput): CreateRefundInput {
  return {
    ...input,
    metadata: cloneMetadata(input.metadata),
  };
}

/**
 * Create an in-memory payments port for tests, local development, and
 * examples.
 *
 * The memory adapter does not contact a payment provider or validate webhook
 * signatures. Queue webhook events explicitly with `queueWebhookEvent(...)`.
 */
export function createMemoryPayments(
  options: CreateMemoryPaymentsOptions = {},
): MemoryPaymentsPort {
  const checkoutSessions: MemoryCheckoutSession[] = [];
  const billingPortalSessions: MemoryBillingPortalSession[] = [];
  const refunds: MemoryRefund[] = [];
  const webhookEvents: PaymentWebhookEvent[] = [];
  const queuedWebhookEvents: PaymentWebhookEvent[] = [];
  const now = options.now ?? (() => new Date());
  const id = options.id ?? defaultMemoryId;

  return {
    get checkoutSessions() {
      return checkoutSessions;
    },

    get billingPortalSessions() {
      return billingPortalSessions;
    },

    get refunds() {
      return refunds;
    },

    get webhookEvents() {
      return webhookEvents;
    },

    async createCheckoutSession(input) {
      const session: MemoryCheckoutSession = {
        id: id("checkout"),
        provider: "memory",
        mode: input.mode,
        url: `https://payments.example.test/checkout/${id("url")}`,
        customerId: input.customerId,
        metadata: cloneMetadata(input.metadata),
        input: cloneCheckoutInput(input),
        createdAt: now(),
      };

      checkoutSessions.push(session);
      await options.onCheckoutSessionCreated?.(session);

      return session;
    },

    async createBillingPortalSession(input) {
      const session: MemoryBillingPortalSession = {
        id: id("portal"),
        provider: "memory",
        url: `https://payments.example.test/portal/${id("url")}`,
        customerId: input.customerId,
        input: { ...input },
        createdAt: now(),
      };

      billingPortalSessions.push(session);
      await options.onBillingPortalSessionCreated?.(session);

      return session;
    },

    async createRefund(input) {
      const refund: MemoryRefund = {
        id: id("refund"),
        provider: "memory",
        paymentId: input.paymentId,
        amount: input.amount,
        status: "succeeded",
        metadata: cloneMetadata(input.metadata),
        input: cloneRefundInput(input),
        createdAt: now(),
      };

      refunds.push(refund);
      await options.onRefundCreated?.(refund);

      return refund;
    },

    async verifyWebhook() {
      const event = queuedWebhookEvents.shift();
      if (!event) {
        throw new PaymentProviderError({
          provider: "memory",
          operation: "payments.verifyWebhook",
          message:
            "No memory payment webhook event is queued. Call queueWebhookEvent(...) before verifyWebhook(...).",
        });
      }

      webhookEvents.push(event);
      await options.onWebhookVerified?.(event);

      return event;
    },

    queueWebhookEvent(event) {
      queuedWebhookEvents.push(event);
    },

    clear() {
      checkoutSessions.length = 0;
      billingPortalSessions.length = 0;
      refunds.length = 0;
      webhookEvents.length = 0;
      queuedWebhookEvents.length = 0;
    },
  };
}

/**
 * Options for the memory payments provider.
 */
export interface MemoryPaymentsProviderOptions
  extends CreateMemoryPaymentsOptions {
  /**
   * Provider name. Defaults to "memory-payments".
   */
  name?: string;
}

/**
 * Ports contributed by the memory payments provider.
 */
export interface MemoryPaymentsProviderPorts {
  /**
   * Beignet payments port.
   */
  payments: PaymentsPort;
}

/**
 * Create a provider that contributes an in-memory payments port.
 */
export function createMemoryPaymentsProvider(
  options: MemoryPaymentsProviderOptions = {},
) {
  const {
    name = "memory-payments",
    onCheckoutSessionCreated,
    onBillingPortalSessionCreated,
    onRefundCreated,
    onWebhookVerified,
    ...paymentsOptions
  } = options;

  return createProvider({
    name,
    metadata: {
      ports: ["payments"],
      watchers: ["payments"],
    },
    setup({ ports }) {
      const instrumentation = createProviderInstrumentation(ports, {
        providerName: name,
        watcher: "payments",
      });
      const payments: PaymentsPort = createMemoryPayments({
        ...paymentsOptions,
        async onCheckoutSessionCreated(session) {
          instrumentation.custom({
            name: "payments.checkout.created",
            label: "Checkout session created",
            summary: session.id,
            details: {
              id: session.id,
              mode: session.mode,
              customerId: session.customerId,
            },
          });
          await onCheckoutSessionCreated?.(session);
        },
        async onBillingPortalSessionCreated(session) {
          instrumentation.custom({
            name: "payments.portal.created",
            label: "Billing portal session created",
            summary: session.id,
            details: {
              id: session.id,
              customerId: session.customerId,
            },
          });
          await onBillingPortalSessionCreated?.(session);
        },
        async onRefundCreated(refund) {
          instrumentation.custom({
            name: "payments.refund.created",
            label: "Refund created",
            summary: refund.id,
            details: {
              id: refund.id,
              paymentId: refund.paymentId,
              amount: refund.amount,
              status: refund.status,
            },
          });
          await onRefundCreated?.(refund);
        },
        async onWebhookVerified(event) {
          instrumentation.custom({
            name: "payments.webhook.verified",
            label: "Payment webhook verified",
            summary: event.type,
            details: {
              id: event.id,
              eventType: event.type,
              livemode: event.livemode,
            },
          });
          await onWebhookVerified?.(event);
        },
      });

      return {
        ports: {
          payments,
        } satisfies MemoryPaymentsProviderPorts,
      };
    },
  });
}
