/**
 * The App Attest registration challenge: the Worker issues it, a Durable
 * Object remembers it, and registration consumes it exactly once.
 *
 * The challenge is what binds an attestation to one registration attempt: the
 * app attests its key over the SHA-256 of a value the server chose, so a
 * captured registration cannot be replayed. One Durable Object per challenge
 * value makes "exactly once" atomic: consume is a single-threaded
 * test-and-set, and a spent or expired challenge never passes again.
 */
import type { DurableObjectNamespaceLike, Env } from "./env.ts";

/** A challenge is good for one registration attempt within this window. */
export const CHALLENGE_TTL_MS = 10 * 60 * 1000;

/** Injectable seam: issue a fresh challenge, consume it exactly once. */
export interface ChallengeStore {
  issue(): Promise<string>;
  consume(challenge: string): Promise<boolean>;
}

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

interface StateLike {
  storage: StorageLike;
}

type ChallengeRequest =
  | { action: "issue"; now: number }
  | { action: "consume"; now: number };

/** One instance per challenge value (idFromName). Classic fetch-based DO. */
export class AttestChallenge {
  storage: StorageLike;

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

  async fetch(request: Request): Promise<Response> {
    const { action, now } = (await request.json()) as ChallengeRequest;
    if (action === "issue") {
      await this.storage.put("issuedAt", now);
      return Response.json({ ok: true });
    }
    const issuedAt = (await this.storage.get("issuedAt")) as number | undefined;
    const used = ((await this.storage.get("used")) ?? false) as boolean;
    if (issuedAt == null || used || now - issuedAt > CHALLENGE_TTL_MS) {
      return Response.json({ ok: false });
    }
    await this.storage.put("used", true);
    return Response.json({ ok: true });
  }
}

/** Wrap the ATTEST_CHALLENGES binding, or null when it is not configured. */
export function challengeStoreFromEnv(env: Env): ChallengeStore | null {
  const ns: DurableObjectNamespaceLike | undefined = env.ATTEST_CHALLENGES;
  if (!ns) return null;
  const call = async (challenge: string, action: "issue" | "consume") => {
    const stub = ns.get(ns.idFromName(challenge));
    const res = await stub.fetch("https://challenge/", {
      method: "POST",
      body: JSON.stringify({ action, now: Date.now() }),
    });
    const { ok } = (await res.json()) as { ok: boolean };
    return ok;
  };
  return {
    async issue() {
      const challenge = crypto.randomUUID();
      await call(challenge, "issue");
      return challenge;
    },
    consume: (challenge) => call(challenge, "consume"),
  };
}
