import "server-only";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import {
  applicationRecordSchema,
  type ApplicationRecord,
} from "./application-record";
import { recentPage, type RecentQuery } from "./staff-search";
import type { Firestore } from "firebase-admin/firestore";
export interface ApplicationRepository {
  get(id: string): Promise<ApplicationRecord | null>;
  change<T>(
    id: string,
    update: (current: ApplicationRecord | null) => {
      record: ApplicationRecord | null;
      result: T;
    },
  ): Promise<T>;
  list(after?: string, limit?: number): Promise<ApplicationRecord[]>;
  recent(query: RecentQuery): Promise<ApplicationRecord[]>;
  ids(after?: string, limit?: number): Promise<string[]>;
  pendingNotifications(limit: number): Promise<string[]>;
}
const validId = (id: string) => z.uuid().parse(id).toLowerCase();
export function firestoreApplicationRepository(
  db: Firestore,
): ApplicationRepository {
  const collection = db.collection("applications");
  return {
    async get(id) {
      const doc = await collection.doc(validId(id)).get();
      return doc.exists ? applicationRecordSchema.parse(doc.data()) : null;
    },
    async change(id, update) {
      return db.runTransaction(
        async (tx) => {
          const ref = collection.doc(validId(id)),
            snapshot = await tx.get(ref);
          const { record, result } = update(
            snapshot.exists
              ? applicationRecordSchema.parse(snapshot.data())
              : null,
          );
          if (record) tx.set(ref, applicationRecordSchema.parse(record));
          else tx.delete(ref);
          return result;
        },
        { maxAttempts: 5 },
      );
    },
    async list(after, limit = 50) {
      let query = collection.orderBy("__name__").limit(limit);
      if (after) query = query.startAfter(validId(after));
      return (await query.get()).docs.map((doc) =>
        applicationRecordSchema.parse(doc.data()),
      );
    },
    async recent({ order, after, limit, from, before }) {
      const direction = order === "newest" ? "desc" : "asc";
      let query = collection
        .orderBy("createdAt", direction)
        .orderBy("__name__", direction)
        .limit(limit);
      if (from) query = query.where("createdAt", ">=", from);
      if (before) query = query.where("createdAt", "<", before);
      if (after) query = query.startAfter(after.createdAt, validId(after.id));
      return (await query.get()).docs.map((doc) =>
        applicationRecordSchema.parse(doc.data()),
      );
    },
    async ids(after, limit = 50) {
      let query = collection.orderBy("__name__").select().limit(limit);
      if (after) query = query.startAfter(validId(after));
      return (await query.get()).docs.map((doc) => doc.id);
    },
    async pendingNotifications(limit) {
      return (
        await collection
          .where("notification.state", "==", "pending")
          .select()
          .limit(limit)
          .get()
      ).docs.map((doc) => doc.id);
    },
  };
}
const state = globalThis as typeof globalThis & {
  applicationMockQueue?: Promise<unknown>;
};
const mockPath = resolve(".cache/application-mock/records.json");
async function serialized<T>(run: () => Promise<T>): Promise<T> {
  const pending = (state.applicationMockQueue ?? Promise.resolve()).then(
    run,
    run,
  );
  state.applicationMockQueue = pending.catch(() => {});
  return pending;
}
async function readRecords() {
  try {
    return z
      .record(z.uuid(), z.unknown())
      .parse(JSON.parse(await readFile(mockPath, "utf8")));
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
    throw new Error("Mock application store unavailable");
  }
}
export function mockApplicationRepository(): ApplicationRepository {
  return {
    get: (id) =>
      serialized(async () => {
        const record = (await readRecords())[validId(id)];
        return record ? applicationRecordSchema.parse(record) : null;
      }),
    change: (id, update) =>
      serialized(async () => {
        id = validId(id);
        const records = await readRecords();
        const { record, result } = update(
          records[id] ? applicationRecordSchema.parse(records[id]) : null,
        );
        if (record) records[id] = applicationRecordSchema.parse(record);
        else delete records[id];
        await mkdir(resolve(".cache/application-mock"), { recursive: true });
        const temporary = `${mockPath}.${randomUUID()}.tmp`;
        await writeFile(temporary, JSON.stringify(records), { mode: 0o600 });
        await rename(temporary, mockPath);
        return result;
      }),
    list: (after, limit = 50) =>
      serialized(async () =>
        Object.entries(await readRecords())
          .filter(([id]) => !after || id > validId(after))
          .sort(([a], [b]) => a.localeCompare(b))
          .slice(0, limit)
          .map(([, record]) => applicationRecordSchema.parse(record)),
      ),
    recent: (query) =>
      serialized(async () =>
        recentPage(
          Object.values(await readRecords()).map((record) =>
            applicationRecordSchema.parse(record),
          ),
          query,
        ),
      ),
    ids: (after, limit = 50) =>
      serialized(async () =>
        Object.keys(await readRecords())
          .filter((id) => !after || id > validId(after))
          .sort()
          .slice(0, limit),
      ),
    pendingNotifications: (limit) =>
      serialized(async () =>
        Object.entries(await readRecords())
          .filter(
            ([, record]) =>
              applicationRecordSchema.safeParse(record).data?.notification
                .state === "pending",
          )
          .slice(0, limit)
          .map(([id]) => id),
      ),
  };
}
