/**
 * Pure, side-effect-free decision logic for Stripe webhook processing.
 *
 * The route layer (`src/app/api/webhooks/stripe/route.ts`) owns all I/O —
 * signature verification, Stripe API calls, database writes — and delegates
 * every *decision* that decides whether a license may be granted to these
 * helpers. Because they are pure, they are unit-testable without Stripe
 * secrets, a database, or network access (see `tests/e2e/webhook_verification.test.ts`).
 *
 * Security invariant: a license is only ever granted when every gate here
 * passes. A single failing gate acknowledges the event (`received: true`) but
 * must NOT create or upgrade anything.
 */

import { getPlanConfig, type PlanId } from "./pricing";

/**
 * Event types this application acts on. Anything else is signature-verified,
 * acknowledged and ignored. `invoice.payment_succeeded` renews an existing
 * subscription license, so it is deliberately part of the allowlist.
 */
export const ALLOWED_EVENT_TYPES: ReadonlySet<string> = new Set([
  "checkout.session.completed",
  "checkout.session.expired",
  "invoice.payment_failed",
  "invoice.payment_succeeded",
  "customer.subscription.updated",
  "customer.subscription.deleted",
]);

/** Minimal structural view of a Stripe Checkout.Session used by the policy. */
export interface CheckoutSessionLike {
  id?: string;
  mode?: string | null;
  payment_status?: string | null;
  amount_total?: number | null;
}

/** Minimal structural view of a Stripe Subscription used by the policy. */
export interface SubscriptionLike {
  status?: string | null;
  current_period_end?: number | null;
}

/** True when `eventType` is a type the application is allowed to act on. */
export function isAllowedEventType(eventType: string): boolean {
  return ALLOWED_EVENT_TYPES.has(eventType);
}

/**
 * True only when the payment behind a checkout session is confirmed:
 *
 *  - one-time payments (`mode: "payment"`): `payment_status === "paid"`
 *  - subscriptions (`mode: "subscription"`): the resolved subscription must
 *    exist and be `active` or `trialing` (trial checkouts legitimately have
 *    `payment_status: "no_payment_required"`)
 *  - anything else (missing/unknown mode): `false` — never grant on
 *    ambiguous data
 */
export function isPaymentConfirmed(
  session: CheckoutSessionLike,
  subscription: SubscriptionLike | null | undefined
): boolean {
  if (session.mode === "payment") {
    return session.payment_status === "paid";
  }
  if (session.mode === "subscription") {
    return (
      subscription != null &&
      (subscription.status === "active" || subscription.status === "trialing")
    );
  }
  return false;
}

/**
 * Cross-checks the amount actually paid (minor units) against the shared
 * price catalog (`getPlanConfig(plan).unitAmountMinor`). Enforced only for
 * one-time payments: a subscription session legitimately carries
 * `amount_total === 0` while it is in the trial, so subscription amounts are
 * intentionally not cross-checked here — status gating covers them.
 *
 * Returns `false` (→ do not grant) when the amount is missing or differs.
 */
export function verifyPaidAmount(
  session: CheckoutSessionLike,
  plan: PlanId | string | null | undefined
): boolean {
  const config = getPlanConfig(plan);
  if (config.mode !== "payment") return true;
  if (session.amount_total == null) return false;
  return session.amount_total === config.unitAmountMinor;
}

/**
 * License expiry derived from the subscription's `current_period_end`
 * (unix seconds) — never from `Date.now()`, so a renewal can never shorten
 * an already-paid window. `null` when no period end is known.
 */
export function subscriptionExpiresAt(
  subscription: SubscriptionLike | null | undefined
): Date | null {
  if (subscription == null || subscription.current_period_end == null) {
    return null;
  }
  return new Date(subscription.current_period_end * 1000);
}

/**
 * The plan taken from checkout metadata must agree with the session mode
 * Stripe actually created (a `payment` session ⇔ one-time plan, a
 * `subscription` session ⇔ recurring plan). A mismatch means the metadata
 * does not describe the real transaction — refuse to grant.
 */
export function sessionModeMatchesPlan(
  sessionMode: string | null | undefined,
  plan: PlanId | string | null | undefined
): boolean {
  const config = getPlanConfig(plan);
  return config.mode === sessionMode;
}
