/**
 * Public enquiry create (PLANNING.md §9).
 *
 * Order: rate limit -> spam screen -> persist -> notify. Every path that stores
 * a row returns/raises in the frontend's contract shape:
 *   - success / caught honeypot -> { ok: true, id }   (200; the bot learns nothing)
 *   - over the rate limit       -> 429 { ok: false, error } (row kept, spam_reason='rate')
 *   - email failure             -> still { ok: true, id } (the enquiry is stored)
 */
import { AppError } from '../../lib/AppError';
import { env } from '../../env';
import { logger } from '../../lib/logger';
import { hashIp } from '../../lib/hash';
import { createWindowLimiter } from '../../lib/rateLimit';
import { renderTemplate, sendMail } from '../../lib/mailer';
import { enquiryNotificationTemplate } from '../../emails/enquiryNotification';
import type { EnquiryInput } from '../../types/contract';
import { insertEnquiry, markNotified, type NewEnquiryRow } from './enquiry.repository';
import { screen, type SpamReason } from './spam.service';

const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const HOURLY_LIMIT = 5;
const DAILY_LIMIT = 20;

const limiter = createWindowLimiter();

export interface EnquiryCtx {
  ip: string;
  userAgent?: string;
  referer?: string;
}

export interface EnquiryOk {
  ok: true;
  id: string;
}

function buildRow(
  input: EnquiryInput,
  ctx: EnquiryCtx,
  ipHash: string,
  status: 'new' | 'spam',
  spamReason: SpamReason | 'rate' | null,
  spamScore: number,
): NewEnquiryRow {
  return {
    name: input.name,
    phone: input.phone,
    email: input.email,
    message: input.message,
    source: input.source,
    status,
    spam_score: spamScore,
    spam_reason: spamReason,
    ip_hash: ipHash,
    user_agent: ctx.userAgent?.slice(0, 255) ?? null,
    referer: ctx.referer?.slice(0, 255) ?? null,
  };
}

export async function createEnquiry(input: EnquiryInput, ctx: EnquiryCtx): Promise<EnquiryOk> {
  const ipHash = hashIp(ctx.ip);

  const hourly = limiter.hit(`enq:h:${ipHash}`, HOURLY_LIMIT, HOUR);
  const daily = limiter.hit(`enq:d:${ipHash}`, DAILY_LIMIT, DAY);
  if (!hourly.allowed || !daily.allowed) {
    await insertEnquiry(buildRow(input, ctx, ipHash, 'spam', 'rate', 60));
    throw new AppError(429, 'Too many requests. Please try again later.', {
      envelope: 'ok-false',
      expose: true,
    });
  }

  const verdict = await screen(input, ctx.ip);
  if (verdict.status === 'spam') {
    const id = await insertEnquiry(
      buildRow(input, ctx, ipHash, 'spam', verdict.spamReason, verdict.spamScore),
    );
    return { ok: true, id };
  }

  const id = await insertEnquiry(buildRow(input, ctx, ipHash, 'new', null, 0));

  try {
    await notify(id, input);
  } catch (err) {
    // The row is committed; a bounced SMTP connection must not tell the user
    // their message was lost (§9). An unnotified-enquiry count covers the gap.
    logger.error({ err, id }, 'enquiry notification failed');
  }

  return { ok: true, id };
}

async function notify(id: string, input: EnquiryInput): Promise<void> {
  if (!env.ENQUIRY_NOTIFY_TO) {
    logger.warn({ id }, 'ENQUIRY_NOTIFY_TO not set — enquiry notification skipped');
    return;
  }
  const html = renderTemplate(enquiryNotificationTemplate, {
    name: input.name,
    email: input.email,
    phone: input.phone,
    message: input.message,
    source: input.source,
    id,
    receivedAt: new Date().toISOString(),
  });
  const { skipped } = await sendMail({
    to: env.ENQUIRY_NOTIFY_TO,
    subject: `New enquiry via ${input.source}`,
    html,
  });
  if (!skipped) await markNotified(id);
}
