import "server-only";
import { createHmac, randomBytes } from "node:crypto";
import { isIP } from "node:net";
import { z } from "zod";
import type { ContactProviders } from "./contact-handler";
import type { EnquiryAction } from "./contact-schema";
import type { ServerEnvironment } from "./env";
import { sendMail } from "./mailer";
import { clientDigest, createBucketStore, nextBucket } from "./rate-limit";

function proxiedDigest(request: Request, secret: string) {
  const forwarded =
    request.headers.get("x-forwarded-for")?.split(",")[0].trim() ||
    request.headers.get("x-real-ip")?.trim();
  const ip = forwarded && isIP(forwarded) ? forwarded : "127.0.0.1";
  const canonical =
    isIP(ip) === 6 ? new URL(`http://[${ip}]`).hostname : ip;
  return createHmac("sha256", secret).update(canonical).digest("hex");
}
const mockBuckets = createBucketStore();
const memoryBuckets = createBucketStore();
const memoryDigestSecret = randomBytes(32).toString("hex");
const turnstileSchema = z.object({
  success: z.boolean(),
  hostname: z.string().optional(),
  action: z.string().optional(),
});
const mockDigestSecret = "mock-contact-rate-limit-digest-namespace";
const subjects: Record<EnquiryAction, string> = {
  contact: "Website enquiry",
  "quick-apply": "Quick apply request",
};

export function contactProviders(
  env: ServerEnvironment,
  origin: string,
  action: EnquiryAction = "contact",
): ContactProviders {
  if (env.CONTACT_MODE === "mock")
    return {
      mode: "mock",
      origin,
      clientKey: (request) => {
        try {
          return `${action}-${clientDigest(request, mockDigestSecret, process.env.VERCEL === "1")}`;
        } catch {
          return `mock-${action}-unattributed-bucket`;
        }
      },
      consume: async (key) => {
        const { allowed, retryAfter } = mockBuckets.consume(key, Date.now());
        return { allowed, retryAfter };
      },
      verify: async (token) => token === `mock-${action}-verification`,
      send: async () => {},
    };
  const send: ContactProviders["send"] = async (enquiry, requestId) =>
    sendMail(env, {
      to: env.CONTACT_NOTIFICATION_EMAIL,
      replyTo: enquiry.email,
      subject: `${subjects[action]}: ${enquiry.interest}`,
      text: `Name: ${enquiry.name}\nEmail: ${enquiry.email}\nContact number: ${enquiry.phone}\nInterested in: ${enquiry.interest}\nSubject: ${enquiry.subject}\n\n${enquiry.message}`,
      reference: `${action}.${requestId}`,
    });
  if (env.CONTACT_MODE === "email")
    return {
      mode: "live",
      origin: env.SITE_URL,
      clientKey: (request) =>
        `${action}-${proxiedDigest(request, memoryDigestSecret)}`,
      consume: async (key) => {
        const { allowed, retryAfter } = memoryBuckets.consume(key, Date.now());
        return { allowed, retryAfter };
      },
      verify: async () => true,
      send,
    };
  return {
    mode: "live",
    origin: env.SITE_URL,
    clientKey: (request) =>
      `${action}-${clientDigest(request, env.RATE_LIMIT_HMAC_SECRET, process.env.VERCEL === "1")}`,
    consume: async (key) => {
      const [{ getApps, initializeApp, cert }, { getFirestore, Timestamp }] =
        await Promise.all([
          import("firebase-admin/app"),
          import("firebase-admin/firestore"),
        ]);
      const app =
        getApps().find((app) => app.name === "contact") ??
        initializeApp(
          {
            credential: cert({
              projectId: env.FIREBASE_PROJECT_ID,
              clientEmail: env.FIREBASE_CLIENT_EMAIL,
              privateKey: env.FIREBASE_PRIVATE_KEY,
            }),
          },
          "contact",
        );
      const db = getFirestore(app),
        ref = db.collection("contactRateLimits").doc(key);
      return db.runTransaction(
        async (transaction) => {
          const snapshot = await transaction.get(ref);
          const result = nextBucket(
            snapshot.exists ? snapshot.data() : undefined,
            Date.now(),
          );
          if (result.allowed)
            transaction.set(ref, {
              ...result.bucket,
              expiresAt: Timestamp.fromMillis(result.bucket.resetAt),
            });
          return { allowed: result.allowed, retryAfter: result.retryAfter };
        },
        { maxAttempts: 3 },
      );
    },
    verify: async (token) => {
      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 provider unavailable");
      const result = turnstileSchema.parse(await response.json());
      return (
        result.success &&
        result.hostname === new URL(env.SITE_URL).hostname &&
        result.action === action
      );
    },
    send,
  };
}
