// Real signature verification for App Store JWS payloads.
//
// Wraps Apple's official `@apple/app-store-server-library` SignedDataVerifier:
//   1. Loads bundled Apple Root CAs (vendored as base64 constants in
//      apple-root-certs.ts so tsc compiles to dist without copying assets)
//   2. Caches a per-(bundleId, environment) verifier instance — Apple SDK keeps
//      a public-key cache inside each verifier, so reusing is much faster than
//      constructing per-call
//   3. Provides verifyTransaction / verifyNotification that throw on bad signatures
//
// Env var `APP_STORE_SKIP_SIGNATURE_VERIFY=true` bypasses the SDK entirely
// (decode-only). Use for unit tests and sandbox debugging — never in production.

// NOTE: @apple/app-store-server-library has top-level side-effects (jsrsasign
// initializes RNG in global scope, see https://github.com/apple/app-store-server-library-node).
// Cloudflare Workers forbid I/O / random / setTimeout in global scope, so we
// **lazy-load** the SDK inside the first call to getVerifier. This keeps the
// worker startup clean and lets non-Apple endpoints (Google Play, entitlements,
// etc.) mount even when Apple SDK is present in the deps.
import type {
  AppStoreServerAPIClient,
  JWSTransactionDecodedPayload,
  ResponseBodyV2DecodedPayload,
  SignedDataVerifier,
  StatusResponse,
} from '@apple/app-store-server-library';

import logger from '../../libs/logger';
import { APPLE_ROOT_CERTS } from './apple-root-certs';

const verifierCache = new Map<string, SignedDataVerifier>();

async function getVerifier(bundleId: string, environment: 'production' | 'sandbox'): Promise<SignedDataVerifier> {
  const key = `${bundleId}:${environment}`;
  const cached = verifierCache.get(key);
  if (cached) return cached;
  // Dynamic import — keeps the SDK out of the worker bundle's global scope.
  const mod = await import('@apple/app-store-server-library');
  const env = environment === 'production' ? mod.Environment.PRODUCTION : mod.Environment.SANDBOX;
  // enableOnlineChecks=false — turning this on does OCSP revocation lookups on
  // every verify, which adds latency + network dependency. Apple's published
  // recommendation is to leave it off unless you specifically need it.
  const verifier = new mod.SignedDataVerifier(APPLE_ROOT_CERTS, false, env, bundleId);
  verifierCache.set(key, verifier);
  return verifier;
}

export function isSignatureVerificationSkipped(): boolean {
  if (process.env.APP_STORE_SKIP_SIGNATURE_VERIFY !== 'true') return false;
  // Production fail-closed: even when the bypass flag is set we refuse to
  // honor it in production, and log loudly so the misconfiguration is
  // visible. The flag exists for unit tests / local sandbox debugging where
  // the synthetic JWS isn't signed by Apple; in production it would silently
  // downgrade a critical trust boundary to decode-only (CWE-347).
  if (process.env.BLOCKLET_MODE === 'production') {
    logger.error(
      'app_store: APP_STORE_SKIP_SIGNATURE_VERIFY=true ignored in production — JWS signature verification stays enabled'
    );
    return false;
  }
  return true;
}

export async function verifySignedTransaction(
  signedTransaction: string,
  bundleId: string,
  environment: 'production' | 'sandbox'
): Promise<JWSTransactionDecodedPayload> {
  if (isSignatureVerificationSkipped()) {
    logger.warn('app_store: signature verification skipped via APP_STORE_SKIP_SIGNATURE_VERIFY');
    return decodeUnsafe<JWSTransactionDecodedPayload>(signedTransaction);
  }
  const verifier = await getVerifier(bundleId, environment);
  return verifier.verifyAndDecodeTransaction(signedTransaction);
}

export async function verifySignedNotification(
  signedPayload: string,
  bundleId: string,
  environment: 'production' | 'sandbox'
): Promise<ResponseBodyV2DecodedPayload> {
  if (isSignatureVerificationSkipped()) {
    logger.warn('app_store: signature verification skipped via APP_STORE_SKIP_SIGNATURE_VERIFY');
    return decodeUnsafe<ResponseBodyV2DecodedPayload>(signedPayload);
  }
  const verifier = await getVerifier(bundleId, environment);
  return verifier.verifyAndDecodeNotification(signedPayload);
}

/** Decode-only fallback for when signature verification is intentionally bypassed. */
function decodeUnsafe<T>(jws: string): T {
  const parts = jws.split('.');
  if (parts.length !== 3) {
    throw new Error('AppStore JWS format invalid (expected 3 segments)');
  }
  try {
    return JSON.parse(Buffer.from(parts[1]!, 'base64url').toString('utf8'));
  } catch (err) {
    throw new Error(`AppStore JWS payload not valid JSON: ${(err as Error).message}`);
  }
}

// Exposed for tests — reset module-level caches between tests.
// eslint-disable-next-line @typescript-eslint/naming-convention
export function __resetVerifierCachesForTests(): void {
  verifierCache.clear();
  apiClientCache.clear();
}

// ============================================================================
// App Store Server API client — for pulling fresh state (subscription status,
// transaction history) when our local cache might be stale (e.g. at renewal,
// or when reconciling after a missed webhook). Requires issuer_id/key_id/.p8
// credentials in PaymentMethod settings; throws clearly when not configured.
// ============================================================================

export type AppStoreApiCredentials = {
  issuerId: string;
  keyId: string;
  privateKeyPem: string;
  bundleId: string;
  environment: 'production' | 'sandbox';
};

const apiClientCache = new Map<string, AppStoreServerAPIClient>();
async function getApiClient(creds: AppStoreApiCredentials): Promise<AppStoreServerAPIClient> {
  const key = `${creds.bundleId}:${creds.environment}:${creds.keyId}`;
  const cached = apiClientCache.get(key);
  if (cached) return cached;
  // Lazy import — same reason as getVerifier (global-scope I/O ban in CF Workers).
  const mod = await import('@apple/app-store-server-library');
  const env = creds.environment === 'production' ? mod.Environment.PRODUCTION : mod.Environment.SANDBOX;
  const client = new mod.AppStoreServerAPIClient(creds.privateKeyPem, creds.keyId, creds.issuerId, creds.bundleId, env);
  apiClientCache.set(key, client);
  return client;
}

/**
 * Fetch all subscription statuses for a given originalTransactionId.
 * Returns Apple's StatusResponse which contains `signedTransactionInfo` /
 * `signedRenewalInfo` JWS strings — verify those with the SignedDataVerifier
 * if you want decoded payloads.
 */
export async function getAllSubscriptionStatuses(
  originalTransactionId: string,
  creds: AppStoreApiCredentials
): Promise<StatusResponse> {
  const client = await getApiClient(creds);
  return client.getAllSubscriptionStatuses(originalTransactionId);
}
