import { createSign, createPrivateKey, type KeyObject } from 'node:crypto';

// The APNs authorization token: an ES256 JWT signed with the .p8 auth key.
//
// Two details here are the difference between working and a 403 with no useful message:
//
// 1. The signature must be raw r||s (64 bytes), NOT DER. Node signs ECDSA as DER by default, which is
//    a valid signature that Apple rejects — `dsaEncoding: 'ieee-p1363'` is what produces the JOSE form.
// 2. Apple rejects a token minted more than once every 20 minutes (TooManyProviderTokenUpdates) and any
//    token older than 60 minutes (ExpiredProviderToken). So it is cached and refreshed on a schedule that
//    sits between those two walls, not per request.

const REFRESH_AFTER_MS = 40 * 60 * 1000; // comfortably inside 60, comfortably outside 20

export type ApnsAuthConfig = {
  /** Contents of the .p8 file — PEM, PKCS#8, EC P-256. */
  keyP8: string;
  /** The 10-character Key ID shown beside the key in the Apple Developer portal. */
  keyId: string;
  /** The 10-character Team ID. */
  teamId: string;
};

const b64url = (input: object | Buffer): string =>
  (Buffer.isBuffer(input) ? input : Buffer.from(JSON.stringify(input))).toString('base64url');

export class ApnsAuthToken {
  private key: KeyObject;
  private cached: { token: string; mintedAt: number } | null = null;

  constructor(private config: ApnsAuthConfig) {
    // Parsed once, at construction, so a malformed .p8 fails at startup rather than on the first push.
    // `\n` is unescaped because the key usually arrives through an env var, where real newlines do not survive.
    this.key = createPrivateKey({ key: config.keyP8.replace(/\\n/g, '\n'), format: 'pem' });
  }

  /** A valid provider token, minted only when the cached one is old enough to warrant it. */
  get(): string {
    const now = Date.now();
    if (this.cached && now - this.cached.mintedAt < REFRESH_AFTER_MS) return this.cached.token;

    const header = b64url({ alg: 'ES256', kid: this.config.keyId });
    const claims = b64url({ iss: this.config.teamId, iat: Math.floor(now / 1000) });
    const signingInput = `${header}.${claims}`;

    const signer = createSign('SHA256');
    signer.update(signingInput);
    const signature = signer.sign({ key: this.key, dsaEncoding: 'ieee-p1363' });

    const token = `${signingInput}.${b64url(signature)}`;
    this.cached = { token, mintedAt: now };
    return token;
  }
}
