import "server-only";
import { randomUUID } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { z } from "zod";
import type { Firestore } from "firebase-admin/firestore";
const controlSchema = z
  .object({
    cursor: z.uuid().nullable(),
    token: z.uuid().nullable(),
    leaseUntil: z.number().int().nonnegative(),
  })
  .strict();
type Control = z.infer<typeof controlSchema>;
export interface ApplicationMaintenance {
  claim(now: number): Promise<{ token: string; cursor: string | null } | null>;
  finish(token: string, cursor: string | null): Promise<void>;
}
const empty: Control = { cursor: null, token: null, leaseUntil: 0 };
const state = globalThis as typeof globalThis & {
  mockMaintenanceQueue?: Promise<unknown>;
};
export function applicationMaintenance(
  db: Firestore | null,
  mockDirectory = ".cache/application-mock",
): ApplicationMaintenance {
  async function change<T>(
    update: (current: Control) => { control: Control; result: T },
  ): Promise<T> {
    if (db)
      return db.runTransaction(async (tx) => {
        const ref = db.collection("applicationMaintenance").doc("scheduled"),
          snapshot = await tx.get(ref);
        const { control, result } = update(
          snapshot.exists ? controlSchema.parse(snapshot.data()) : { ...empty },
        );
        tx.set(ref, controlSchema.parse(control));
        return result;
      });
    const run = async () => {
      const path = resolve(mockDirectory, "maintenance.json");
      let current: Control;
      try {
        current = controlSchema.parse(JSON.parse(await readFile(path, "utf8")));
      } catch (error) {
        if ((error as NodeJS.ErrnoException).code !== "ENOENT")
          throw new Error("Maintenance unavailable");
        current = { ...empty };
      }
      const { control, result } = update(current);
      await mkdir(resolve(mockDirectory), { recursive: true });
      const temporary = `${path}.${randomUUID()}.tmp`;
      await writeFile(temporary, JSON.stringify(controlSchema.parse(control)), {
        mode: 0o600,
      });
      await rename(temporary, path);
      return result;
    };
    const pending = (state.mockMaintenanceQueue ?? Promise.resolve()).then(
      run,
      run,
    );
    state.mockMaintenanceQueue = pending.catch(() => {});
    return pending;
  }
  return {
    claim: (now) =>
      change((current) => {
        if (current.leaseUntil > now) return { control: current, result: null };
        const token = randomUUID();
        return {
          control: { ...current, token, leaseUntil: now + 120000 },
          result: { token, cursor: current.cursor },
        };
      }),
    finish: (token, cursor) =>
      change((current) => ({
        control:
          current.token === token
            ? { cursor, token: null, leaseUntil: 0 }
            : current,
        result: undefined,
      })),
  };
}
