import fs from "node:fs";
import path from "node:path";
import {
  Environment,
  SignedDataVerifier,
  type JWSTransactionDecodedPayload,
  type ResponseBodyV2DecodedPayload,
} from "@apple/app-store-server-library";
import { appleSignIn } from "@/lib/auth/config";
import type { PlanId } from "./pricing";

/**
 * App Store Server Notifications V2 (`/api/webhooks/apple`) — the iOS
 * counterpart to `/api/webhooks/stripe`. This module does the two things a
 * Stripe webhook gets almost for free but Apple does not: (1) verify the
 * notification was genuinely signed by Apple, which for Apple means walking
 * a full x5c certificate chain up to Apple's own root CA — NOT hand-rolled
 * here, see the long comment on the "why a dependency" decision in
 * app/ios/README.md and PROJECT context; this uses Apple's own official
 * `@apple/app-store-server-library` (MIT, published by Apple Inc.) — and
 * (2) map a verified transaction back to one of this app's own users.
 */

/**
 * Apple Root CA - G3, DER-encoded, downloaded directly from
 * https://www.apple.com/certificateauthority/AppleRootCA-G3.cer (Apple's own
 * domain, over HTTPS) — this is the standard root every App Store Server
 * Library integration ships with; Apple's own sample projects include the
 * identical file. Self-signed, CN=Apple Root CA - G3, valid until 2039.
 */
const ROOT_CERT_PATH = path.join(process.cwd(), "src/lib/billing/certs/AppleRootCA-G3.cer");

let cachedRootCert: Buffer | null = null;
function rootCertificate(): Buffer {
  if (!cachedRootCert) {
    cachedRootCert = fs.readFileSync(ROOT_CERT_PATH);
  }
  return cachedRootCert;
}

/**
 * Maps this app's StoreKit product identifiers (see the iOS app's
 * `PurchaseService.swift` → `PurchaseProductID`) to the backend's own plan
 * vocabulary (`users.plan`). Keep these two files in sync manually — they
 * live in different repos (this one, and the iOS app under `app/ios/`)
 * with no shared build step to enforce it automatically.
 */
export const APPLE_PRODUCT_TO_PLAN: Record<string, PlanId> = {
  "app.scan-receipts.ios.pro.weekly": "weekly",
  "app.scan-receipts.ios.pro.annual": "annual",
  "app.scan-receipts.ios.pro.lifetime": "lifetime",
};

/**
 * The app's numeric App Store ID (shown in App Store Connect once the app
 * exists there) — required for Apple's PRODUCTION-environment identity
 * check, not needed for Sandbox. Until this app has a real App Store
 * Connect listing, this stays unset on purpose: PRODUCTION verification
 * will then correctly fail closed (see `verifyNotification` in the
 * library — an undefined `appAppleId` never matches a real one) rather
 * than silently accepting production notifications this deployment can't
 * actually have received yet. Sandbox notifications (all of them, during
 * development/TestFlight) are unaffected by this and verify normally.
 */
function configuredAppAppleId(): number | undefined {
  const raw = process.env.APPLE_APP_APPLE_ID;
  return raw ? Number(raw) : undefined;
}

let sandboxVerifier: SignedDataVerifier | null = null;
let productionVerifier: SignedDataVerifier | null = null;

function verifierFor(environment: Environment): SignedDataVerifier {
  const cache = environment === Environment.SANDBOX ? sandboxVerifier : productionVerifier;
  if (cache) return cache;

  const verifier = new SignedDataVerifier(
    [rootCertificate()],
    true, // enableOnlineChecks — revocation checking + real expiration dates
    environment,
    appleSignIn.bundleId,
    configuredAppAppleId()
  );

  if (environment === Environment.SANDBOX) sandboxVerifier = verifier;
  else productionVerifier = verifier;
  return verifier;
}

export interface VerifiedAppleNotification {
  payload: ResponseBodyV2DecodedPayload;
  environment: Environment;
}

/**
 * Verifies and decodes a `signedPayload` from an incoming
 * `POST /api/webhooks/apple` call. Apple's notification itself declares
 * which environment (Sandbox/Production) it's from, but the verifier has to
 * be told which one to check IN ADVANCE (it refuses a mismatch) — since a
 * single webhook URL receives both (Sandbox during any testing, Production
 * once live), this tries Sandbox first, falling back to Production. Only
 * throws once BOTH have failed to verify, i.e. the payload is not
 * legitimately from Apple at all (or Production genuinely isn't configured
 * yet — see `configuredAppAppleId`).
 */
export async function verifyAppleNotification(
  signedPayload: string
): Promise<VerifiedAppleNotification> {
  try {
    const payload = await verifierFor(Environment.SANDBOX).verifyAndDecodeNotification(signedPayload);
    return { payload, environment: Environment.SANDBOX };
  } catch {
    const payload = await verifierFor(Environment.PRODUCTION).verifyAndDecodeNotification(signedPayload);
    return { payload, environment: Environment.PRODUCTION };
  }
}

/** Verifies+decodes the nested `data.signedTransactionInfo` JWS. */
export async function verifyAppleTransaction(
  signedTransactionInfo: string,
  environment: Environment
): Promise<JWSTransactionDecodedPayload> {
  return verifierFor(environment).verifyAndDecodeTransaction(signedTransactionInfo);
}

/**
 * Reconstructs this app's own user id from a StoreKit `appAccountToken`.
 *
 * The iOS app sets `appAccountToken` at purchase time to a UUID built from
 * the signed-in user's own id (`"usr_" + 32 hex chars` — exactly a UUID with
 * the dashes stripped, since that's literally how `newId("usr")` mints it,
 * see `src/lib/auth/tokens.ts`) by re-inserting the dashes (see the iOS
 * app's `Models/User.swift` → `appleAccountToken`). Reversing that here is
 * pure string manipulation — no lookup table, no extra stored mapping,
 * nothing that could drift out of sync.
 */
export function userIdFromAppAccountToken(token: string | undefined | null): string | null {
  if (!token) return null;
  const hex = token.replace(/-/g, "").toLowerCase();
  if (!/^[0-9a-f]{32}$/.test(hex)) return null;
  return `usr_${hex}`;
}
