import "server-only";
import type { z } from "zod";
import { contactRequestSchema, type Enquiry } from "./contact-schema";
export type ContactProviders = {
  mode: "mock" | "live";
  origin: string;
  clientKey(request: Request): string;
  consume(key: string): Promise<{ allowed: boolean; retryAfter: number }>;
  verify(token: string): Promise<boolean>;
  send(enquiry: Enquiry, requestId: string): Promise<void>;
};
type Submission = {
  website: string;
  turnstileToken: string;
  requestId: string;
};
export type EnquiryMessages = {
  method: string;
  unreadable: string;
  invalid: string;
  received: string;
  limited: string;
  expired: string;
  mock: string;
  sent: string;
  failed: string;
};
export type EnquiryKind<T extends Submission> = {
  schema: z.ZodType<T>;
  toEnquiry(data: T): Enquiry;
  messages: EnquiryMessages;
};
function sameSiteOrigin(origin: string | null, allowed: string) {
  if (!origin) return false;
  if (origin === allowed) return true;
  try {
    const submitted = new URL(origin);
    const expected = new URL(allowed);
    if (submitted.protocol !== expected.protocol) return false;
    const host = (value: string) => value.replace(/^www\./, "");
    return (
      host(submitted.hostname) === host(expected.hostname) &&
      submitted.port === expected.port
    );
  } catch {
    return false;
  }
}
const MAX_BYTES = 16_384;
async function within<T>(operation: Promise<T>, milliseconds: number) {
  let timer: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      operation,
      new Promise<never>((_, reject) => {
        timer = setTimeout(
          () => reject(new Error("Provider timeout")),
          milliseconds,
        );
      }),
    ]);
  } finally {
    clearTimeout(timer);
  }
}
const reply = (
  status: number,
  message: string,
  extra: Record<string, boolean> = {},
  headers: Record<string, string> = {},
) =>
  Response.json(
    { ok: status === 200, message, ...extra },
    { status, headers: { "Cache-Control": "no-store", ...headers } },
  );

async function readBody(request: Request) {
  if (Number(request.headers.get("content-length")) > MAX_BYTES)
    throw new RangeError();
  if (!request.body) return null;
  const reader = request.body.getReader();
  let timedOut = false;
  const timer = setTimeout(() => {
    timedOut = true;
    void reader.cancel().catch(() => {});
  }, 5000);
  const decoder = new TextDecoder("utf-8", { fatal: true });
  let size = 0,
    body = "";
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      size += value.byteLength;
      if (size > MAX_BYTES) {
        await reader.cancel();
        throw new RangeError();
      }
      body += decoder.decode(value, { stream: true });
    }
    if (timedOut) throw new Error("Request body timeout");
    body += decoder.decode();
    return JSON.parse(body) as unknown;
  } finally {
    clearTimeout(timer);
    reader.releaseLock();
  }
}

export async function handleEnquiry<T extends Submission>(
  request: Request,
  providers: ContactProviders,
  kind: EnquiryKind<T>,
) {
  const { messages } = kind;
  if (request.method !== "POST")
    return reply(405, messages.method, {}, { Allow: "POST" });
  if (!sameSiteOrigin(request.headers.get("origin"), providers.origin))
    return reply(403, "Please submit the form from this website.");
  if (
    request.headers.get("content-type")?.split(";")[0].trim().toLowerCase() !==
    "application/json"
  )
    return reply(415, "Unsupported request format.");
  let raw: unknown;
  try {
    raw = await readBody(request);
  } catch (error) {
    return reply(error instanceof RangeError ? 413 : 400, messages.unreadable);
  }
  const result = kind.schema.safeParse(raw);
  if (!result.success) return reply(400, messages.invalid);
  if (result.data.website) return reply(200, messages.received);
  try {
    const limit = await within(
      providers.consume(providers.clientKey(request)),
      5000,
    );
    if (!limit.allowed)
      return reply(
        429,
        messages.limited,
        {},
        { "Retry-After": String(limit.retryAfter) },
      );
    if (!(await providers.verify(result.data.turnstileToken)))
      return reply(400, messages.expired);
    await providers.send(kind.toEnquiry(result.data), result.data.requestId);
    return providers.mode === "mock"
      ? reply(200, messages.mock, { mock: true })
      : reply(200, messages.sent);
  } catch {
    return reply(503, messages.failed);
  }
}

export const contactKind: EnquiryKind<z.output<typeof contactRequestSchema>> = {
  schema: contactRequestSchema,
  toEnquiry: ({ name, email, phone, subject, interest, message }) => ({
    name,
    email,
    phone,
    subject,
    interest,
    message,
  }),
  messages: {
    method: "Use the contact form to send an enquiry.",
    unreadable:
      "The enquiry could not be read. Please check its length and try again.",
    invalid: "Please check the form fields and complete the security check.",
    received: "Thank you for your enquiry.",
    limited: "Too many enquiries. Please wait a few minutes or call 6336 4144.",
    expired: "The security check expired or failed. Please complete it again.",
    mock: "Preview complete. No enquiry was sent.",
    sent: "Thank you. Your enquiry has been sent to MaxCredit.",
    failed:
      "We could not confirm your enquiry was sent. Please retry or call 6336 4144.",
  },
};
export const handleContact = (request: Request, providers: ContactProviders) =>
  handleEnquiry(request, providers, contactKind);
