/**
 * The server-side revocation record: what Apple has told us since the JWS in
 * the caller's hand was minted.
 *
 * The entitlement gate reads revocationDate from the token the client sends,
 * so on its own it only catches a refund the client honestly reports. This
 * record closes that: the notifications webhook writes refunds and
 * revocations here as Apple reports them, and the worker checks it on every
 * request. One Durable Object per originalTransactionId keeps the record
 * strongly consistent everywhere with great distributed performance.
 *
 * The notification-to-action mapping is a pure function, kept apart from the
 * store so it tests in isolation.
 */
import type { DurableObjectNamespaceLike, Env } from "./env.ts";

/** Injectable seam over the REVOCATIONS binding. */
export interface RevocationStore {
  isRevoked(originalTransactionId: string): Promise<boolean>;
  revoke(originalTransactionId: string, reason: string): Promise<void>;
  unrevoke(originalTransactionId: string): Promise<void>;
}

interface StorageLike {
  get(key: string): Promise<unknown>;
  put(key: string, value: unknown): Promise<void>;
}

interface StateLike {
  storage: StorageLike;
}

type RevocationRequest =
  | { action: "get" }
  | { action: "revoke"; reason: string }
  | { action: "unrevoke" };

/**
 * One instance per originalTransactionId (idFromName). Classic fetch-based
 * Durable Object.
 */
export class RevocationRecord {
  storage: StorageLike;

  constructor(state: StateLike) {
    this.storage = state.storage;
  }

  async fetch(request: Request): Promise<Response> {
    const body = (await request.json()) as RevocationRequest;
    if (body.action === "revoke") {
      await this.storage.put("revoked", { reason: body.reason });
      return Response.json({ revoked: true });
    }
    if (body.action === "unrevoke") {
      await this.storage.put("revoked", null);
      return Response.json({ revoked: false });
    }
    const revoked = ((await this.storage.get("revoked")) ?? null) != null;
    return Response.json({ revoked });
  }
}

/** Wrap the REVOCATIONS binding, or null when it is not configured. */
export function revocationStoreFromEnv(env: Env): RevocationStore | null {
  const ns: DurableObjectNamespaceLike | undefined = env.REVOCATIONS;
  if (!ns) return null;
  const call = async (id: string, body: RevocationRequest) => {
    const stub = ns.get(ns.idFromName(id));
    const res = await stub.fetch("https://revocation/", {
      method: "POST",
      body: JSON.stringify(body),
    });
    const { revoked } = (await res.json()) as { revoked: boolean };
    return revoked;
  };
  return {
    isRevoked: (id) => call(id, { action: "get" }),
    async revoke(id, reason) {
      await call(id, { action: "revoke", reason });
    },
    async unrevoke(id) {
      await call(id, { action: "unrevoke" });
    },
  };
}

export type RevocationAction =
  | { kind: "revoke"; originalTransactionId: string; reason: string }
  | { kind: "unrevoke"; originalTransactionId: string }
  | null;

/**
 * Map a verified notification to a revocation action. REFUND and REVOKE
 * (family sharing loss) revoke; REFUND_REVERSED restores; every other type
 * (renewals, subscribes, test notifications) is none of this record's
 * business and maps to null.
 */
export function revocationFromNotification(
  notificationType: string | undefined,
  txn: { originalTransactionId?: string },
): RevocationAction {
  const id = txn.originalTransactionId;
  if (!id) return null;
  if (notificationType === "REFUND" || notificationType === "REVOKE") {
    return { kind: "revoke", originalTransactionId: id, reason: notificationType };
  }
  if (notificationType === "REFUND_REVERSED") {
    return { kind: "unrevoke", originalTransactionId: id };
  }
  return null;
}
