import "server-only";
import { createReadStream } from "node:fs";
import { readFile, unlink } from "node:fs/promises";
import { resolve } from "node:path";
import { z } from "zod";
import { getApps, initializeApp, cert } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";
import { getStorage } from "firebase-admin/storage";
import { getAuth } from "firebase-admin/auth";
import { getServerEnvironment } from "./env";
import {
  applicationServiceEnabled,
  getApplicationConfig,
} from "./application-config";
import { ApplicationError } from "./application-record";
import {
  firestoreApplicationRepository,
  mockApplicationRepository,
} from "./application-repository";
import {
  createUploadTarget,
  type UploadTicket,
  type UploadStorage,
  uploadObjectPath,
} from "./application-upload";
import { firebaseUploadStorage } from "./firebase-upload-storage";
import { mockStorageUrl } from "./mock-storage-protocol";
import { contactProviders } from "./contact-providers";
import { sendMail } from "./mailer";
import { applicationMaintenance } from "./application-maintenance";
export function applicationProviders() {
  const config = getApplicationConfig();
  if (!applicationServiceEnabled(config))
    throw new ApplicationError(
      404,
      "Please use our existing application service.",
    );
  const env = getServerEnvironment();
  const mock = config.APPLICATION_MODE === "mock";
  const app =
    !mock && env.CONTACT_MODE === "live"
      ? (getApps().find((app) => app.name === "applications") ??
        initializeApp(
          {
            credential: cert({
              projectId: env.FIREBASE_PROJECT_ID,
              clientEmail: env.FIREBASE_CLIENT_EMAIL,
              privateKey: env.FIREBASE_PRIVATE_KEY,
            }),
            storageBucket: config.FIREBASE_STORAGE_BUCKET,
          },
          "applications",
        ))
      : null;
  const bucket = app ? getStorage(app).bucket() : null;
  const storage: UploadStorage = bucket
    ? firebaseUploadStorage(bucket)
    : {
        signUpload: async () => {
          throw new Error("Use the isolated mock signer");
        },
        read: async (path, signal) => {
          if (!/^applications\/uploads\/[a-f0-9-]{36}$/.test(path))
            throw new Error("Invalid mock object");
          const file = resolve(
            ".cache/application-mock/objects",
            path.split("/").at(-1)!,
          );
          const metadata = z
            .object({
              size: z.string().transform(Number),
              contentType: z.string(),
              generation: z.string(),
            })
            .parse(JSON.parse(await readFile(`${file}.json`, "utf8")));
          return { ...metadata, body: createReadStream(file, { signal }) };
        },
      };
  const limiter = contactProviders(env, env.SITE_URL);
  return {
    config,
    mock,
    origin: env.SITE_URL,
    repo: app
      ? firestoreApplicationRepository(getFirestore(app))
      : mockApplicationRepository(),
    storage,
    auth: app ? getAuth(app) : null,
    maintenance: applicationMaintenance(app ? getFirestore(app) : null),
    async limit(request: Request) {
      if (
        !mock &&
        !(await limiter.consume(`application-${limiter.clientKey(request)}`))
          .allowed
      )
        throw new ApplicationError(
          429,
          "Too many attempts. Please wait a few minutes before retrying.",
        );
    },
    async verify(token: string) {
      if (mock) return token === "mock-application-verification";
      if (env.CONTACT_MODE !== "live") return false;
      const response = await fetch(
        "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        {
          method: "POST",
          body: new URLSearchParams({
            secret: env.TURNSTILE_SECRET_KEY,
            response: token,
          }),
          signal: AbortSignal.timeout(5000),
        },
      );
      if (!response.ok) throw new Error("Verification unavailable");
      const result = z
        .object({
          success: z.boolean(),
          hostname: z.string().optional(),
          action: z.string().optional(),
        })
        .parse(await response.json());
      return (
        result.success &&
        result.action === "application" &&
        result.hostname === new URL(env.SITE_URL).hostname
      );
    },
    async target(ticket: UploadTicket, notAfter = Number.POSITIVE_INFINITY) {
      if (!mock)
        return createUploadTarget(
          ticket,
          storage,
          Date.now(),
          undefined,
          notAfter,
        );
      const headers = {
        "Content-Type": ticket.document.type,
        "x-goog-content-length-range": `${ticket.document.size},${ticket.document.size}`,
        "x-goog-if-generation-match": "0",
      };
      return {
        url: mockStorageUrl(
          uploadObjectPath(ticket),
          "PUT",
          Math.min(Date.now() + 600000, ticket.expiresAt, notAfter),
          headers,
        ),
        headers,
      };
    },
    async notify(id: string, reference: string) {
      if (mock) return;
      if (env.CONTACT_MODE === "mock")
        throw new Error("Notification unavailable");
      await sendMail(env, {
        to: config.APPLICATION_NOTIFICATION_EMAIL,
        subject: `Application ${reference}`,
        text: `Reference: ${reference}\nReview: ${env.SITE_URL}/staff/${id}`,
        reference: `application.${id}`,
      });
    },
    async download(path: string, generation: string, type: string) {
      if (mock) return mockStorageUrl(path, "GET", Date.now() + 60000);
      if (!bucket || !Number.isSafeInteger(Number(generation)))
        throw new Error("Document unavailable");
      const file = bucket.file(path, { generation });
      await file.getMetadata();
      const [url] = await file.getSignedUrl({
        version: "v4",
        action: "read",
        expires: Date.now() + 60000,
        responseDisposition: 'attachment; filename="document"',
        responseType: type,
      });
      return url;
    },
    async remove(path: string, generation: string | null) {
      if (!/^applications\/uploads\/[a-f0-9-]{36}$/.test(path))
        throw new Error("Invalid object");
      if (bucket) {
        await bucket
          .file(path, generation ? { generation } : undefined)
          .delete({ ignoreNotFound: true });
        return;
      }
      const file = resolve(
        ".cache/application-mock/objects",
        path.split("/").at(-1)!,
      );
      for (const target of [file, `${file}.json`])
        await unlink(target).catch((error: NodeJS.ErrnoException) => {
          if (error.code !== "ENOENT")
            throw new Error("Fixture deletion failed");
        });
    },
  };
}
export type ApplicationProviders = Omit<
  ReturnType<typeof applicationProviders>,
  "auth"
> & {
  auth: Pick<
    ReturnType<typeof getAuth>,
    | "verifyIdToken"
    | "verifySessionCookie"
    | "createSessionCookie"
    | "revokeRefreshTokens"
  > | null;
};
