'use server';

import { getDb } from '@pintrush/db';
import { headers } from 'next/headers';
import { signFormToken, UsedNonces, verifyFormToken } from '@/lib/form-token';
import type { FormTokenCheck } from '@/lib/form-token';
import { notifyAdminOfPilotLead } from '@/lib/mailer';
import { isHoneypotTripped, readPilotFormValues, TOKEN_FIELD, validatePilotLead } from '@/lib/pilot';
import type { PilotFieldErrors, PilotFormState, PilotFormValues } from '@/lib/pilot';
import { clientIp, hashIp, ipHashesSince, SlidingWindowLimiter, utcDay } from '@/lib/rate-limit';
import { analyticsSalt, formSecret } from '@/lib/server-env';

const TOKEN_PURPOSE = 'pilot-application';
const HOUR_MS = 60 * 60 * 1000;
/** Spec: at most 3 stored leads per IP hash per hour. */
const MAX_LEADS_PER_HOUR = 3;
/**
 * Ceiling on stored leads per hour across everyone, counted in the database so it holds across
 * restarts and instances. Real pilot interest is a handful a day; this only stops a flood of junk
 * leads (each one e-mails the admin). Only valid submissions with an unused token reach it, so
 * junk requests can't use it up and shut the form for real venues.
 */
const MAX_LEADS_PER_HOUR_GLOBAL = 40;

// Per-process attempt limiter per network (counts invalid attempts too): the first check, so a
// client hammering the form is stopped before any token or database work.
const attemptLimiter = new SlidingWindowLimiter(10, 10 * 60 * 1000);
// Nonces of tokens that already produced a stored lead (single-use tokens).
const usedTokens = new UsedNonces();

/** Called by the form on mount: the page is static, so the signed start time comes from here. */
export async function issuePilotFormToken(): Promise<string> {
  return signFormToken(Date.now(), formSecret(), TOKEN_PURPOSE);
}

function fail(values: PilotFormValues, message: string, extra: { fieldErrors?: PilotFieldErrors; refreshToken?: true } = {}): PilotFormState {
  return { status: 'error', message, fieldErrors: extra.fieldErrors ?? {}, values, ...(extra.refreshToken ? { refreshToken: true } : {}) };
}

const tokenMessages: Record<Exclude<FormTokenCheck, { ok: true }>['reason'], string> = {
  'too-fast': 'That was quick! Give your details a once-over, then send the application again.',
  expired: 'This page was open for a while, so we refreshed the form. Please send your application again.',
  used: 'This application was already sent from this page. Reload the page to send another one.',
  missing: 'We couldn’t verify this form. Please make sure JavaScript is enabled, reload the page and try again.',
  malformed: 'We couldn’t verify this form. Please reload the page and try again.',
  'bad-signature': 'We couldn’t verify this form. Please reload the page and try again.',
};

const BUSY_MESSAGE = 'We’re receiving an unusual number of applications right now. Please try again in an hour, or email us directly.';

export async function submitPilotApplication(_previous: PilotFormState, formData: FormData): Promise<PilotFormState> {
  const now = Date.now();
  const values = readPilotFormValues(formData);

  // Bots get a normal-looking success so they learn nothing; nothing is stored or sent.
  if (isHoneypotTripped(formData)) {
    return { status: 'success', venueName: values.venueName.trim(), email: values.email.trim() };
  }

  const ip = clientIp(await headers());
  const salt = analyticsSalt();
  const ipHash = hashIp(ip, salt, utcDay(now));

  // 1. Per-network attempt limit.
  if (!attemptLimiter.hit(ipHash, now).allowed) {
    return fail(values, 'Too many attempts in a short time. Please wait a few minutes and try again.');
  }

  // 2. Signed, aged, unused token.
  const token = verifyFormToken(formData.get(TOKEN_FIELD), now, formSecret(), TOKEN_PURPOSE, { used: usedTokens });
  if (!token.ok) {
    const refreshToken = token.reason === 'expired' || token.reason === 'used' ? true : undefined;
    return fail(values, tokenMessages[token.reason], refreshToken ? { refreshToken } : {});
  }

  // 3. Field validation (the token stays unspent, so the visitor can fix a field and resend).
  const validation = validatePilotLead(values);
  if (!validation.ok) {
    return fail(values, 'A few details need another look. Please check the highlighted fields.', { fieldErrors: validation.fieldErrors });
  }
  const lead = validation.data;

  // 4. Spend the token before any database work, so two racing submits can't both store a lead.
  if (!usedTokens.claim(token.nonce, token.expiresAt, now)) {
    return fail(values, tokenMessages.used, { refreshToken: true });
  }

  let leadId: string;
  try {
    const db = getDb();
    const since = new Date(now - HOUR_MS);
    // 5. Global ceiling, then 6. the spec's per-network limit, both on stored leads.
    const [lastHour, fromNetwork] = await Promise.all([
      db.pilotLead.count({ where: { createdAt: { gte: since } } }),
      db.pilotLead.count({ where: { ipHash: { in: ipHashesSince(ip, salt, now - HOUR_MS, now) }, createdAt: { gte: since } } }),
    ]);
    if (lastHour >= MAX_LEADS_PER_HOUR_GLOBAL) {
      usedTokens.release(token.nonce);
      console.warn(`[pilot] Global ceiling reached: ${lastHour} leads in the last hour; refusing new applications for now.`);
      return fail(values, BUSY_MESSAGE);
    }
    if (fromNetwork >= MAX_LEADS_PER_HOUR) {
      usedTokens.release(token.nonce);
      return fail(values, 'We’ve already received several applications from your network this hour. Please try again later.');
    }
    // 7. Store.
    const created = await db.pilotLead.create({
      data: {
        venueName: lead.venueName,
        address: lead.address,
        contactName: lead.contactName,
        email: lead.email,
        phone: lead.phone,
        tvCount: lead.tvCount,
        busiestNights: lead.busiestNights,
        currentEntertainment: lead.currentEntertainment,
        message: lead.message,
        ipHash,
      },
      select: { id: true },
    });
    leadId = created.id;
  } catch (error) {
    // Nothing was stored: give the token back so "try again" works from the same page.
    usedTokens.release(token.nonce);
    console.error('[pilot] Could not save pilot application:', error instanceof Error ? error.message : error);
    return fail(values, 'Something went wrong on our side and your application was not saved. Please try again in a minute.');
  }

  // The lead is safely stored; a mail problem must not turn this into an error for the venue.
  await notifyAdminOfPilotLead({ id: leadId, ...lead });

  return { status: 'success', venueName: lead.venueName, email: lead.email };
}
