/**
 * The entitlement decision: a pure function, no crypto and no network. Given a
 * decoded StoreKit 2 transaction and the gate config, decide whether the caller
 * is entitled. Keeping it pure makes it testable in isolation.
 */
import type { JWSTransaction } from "./transaction.ts";

export interface GateConfig {
  allowedBundleIds: Set<string>;
  allowedProductIds: Set<string> | null; // null = any product
  now: number; // ms since epoch
}

export type GateResult =
  | { ok: true }
  | { ok: false; status: 401 | 403; reason: string };

/** Parse a comma-separated env var into a trimmed, non-empty Set. */
export function parseList(value: string | undefined): Set<string> {
  return new Set(
    (value ?? "")
      .split(",")
      .map((s) => s.trim())
      .filter((s) => s.length > 0),
  );
}

export function evaluateEntitlement(
  txn: JWSTransaction,
  cfg: GateConfig,
): GateResult {
  if (!txn.bundleId || !cfg.allowedBundleIds.has(txn.bundleId)) {
    return { ok: false, status: 403, reason: "bundle_id_not_allowed" };
  }
  if (
    cfg.allowedProductIds &&
    (!txn.productId || !cfg.allowedProductIds.has(txn.productId))
  ) {
    return { ok: false, status: 403, reason: "product_id_not_allowed" };
  }
  // A sandbox transaction is signed by the same chain as production and costs
  // nothing to mint, so it must never clear the gate.
  if (txn.environment !== "Production") {
    return { ok: false, status: 403, reason: "environment_not_production" };
  }
  if (txn.revocationDate != null) {
    return { ok: false, status: 403, reason: "entitlement_revoked" };
  }
  // Trial and paid are the same check: a subscription is valid while its
  // expiresDate is in the future. Non-subscription products omit expiresDate.
  if (txn.expiresDate != null && txn.expiresDate <= cfg.now) {
    return { ok: false, status: 403, reason: "entitlement_expired" };
  }
  return { ok: true };
}
