/** Email OTP issuance + verification (hashed at rest). */
import bcrypt from 'bcryptjs';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { enqueueEmail } from './outbox.service';

export type OtpPurpose = 'signup_verify' | 'password_reset' | 'email_change';

const OTP_TTL_MIN = 10;
const RESEND_COOLDOWN_SEC = 60;
const MAX_ATTEMPTS = 6;

function generateCode(): string {
  // 6-digit code (cryptographically strong)
  const buf = require('crypto').randomBytes(4);
  const n = buf.readUInt32BE(0) % 1_000_000;
  return n.toString().padStart(6, '0');
}

export async function issueOtp(email: string, purpose: OtpPurpose, vars: Record<string, unknown> = {}): Promise<{ alreadySentRecently: boolean }> {
  const normalized = email.toLowerCase().trim();

  // Resend cooldown
  const { rows: recent } = await db.execute(sql`
    SELECT id, created_at FROM email_otps
    WHERE email = ${normalized} AND purpose = ${purpose} AND consumed_at IS NULL
    ORDER BY created_at DESC LIMIT 1
  `);
  if (recent[0]) {
    const last = new Date((recent[0] as { created_at: string }).created_at);
    if ((Date.now() - last.getTime()) / 1000 < RESEND_COOLDOWN_SEC) {
      return { alreadySentRecently: true };
    }
  }

  const code = generateCode();
  const codeHash = await bcrypt.hash(code, 10);

  // Invalidate older OTPs for the same email/purpose
  await db.execute(sql`UPDATE email_otps SET consumed_at = NOW() WHERE email = ${normalized} AND purpose = ${purpose} AND consumed_at IS NULL`);

  await db.execute(sql`
    INSERT INTO email_otps (email, code_hash, purpose, expires_at)
    VALUES (${normalized}, ${codeHash}, ${purpose}, NOW() + (${OTP_TTL_MIN} || ' minutes')::interval)
  `);

  await enqueueEmail({
    to: normalized,
    templateKey: 'signup_otp',
    channel: 'immediate',
    kind: 'otp',
    vars: { code, ...vars },
  });

  return { alreadySentRecently: false };
}

export async function verifyOtp(email: string, code: string, purpose: OtpPurpose): Promise<boolean> {
  const normalized = email.toLowerCase().trim();
  const { rows } = await db.execute(sql`
    SELECT id, code_hash, expires_at, attempts FROM email_otps
    WHERE email = ${normalized} AND purpose = ${purpose} AND consumed_at IS NULL
    ORDER BY created_at DESC LIMIT 1
  `);
  const row = rows[0] as { id: string; code_hash: string; expires_at: string; attempts: number } | undefined;
  if (!row) return false;
  if (new Date(row.expires_at).getTime() < Date.now()) return false;
  if (row.attempts >= MAX_ATTEMPTS) return false;

  const ok = await bcrypt.compare(code, row.code_hash);
  if (!ok) {
    await db.execute(sql`UPDATE email_otps SET attempts = attempts + 1 WHERE id = ${row.id}`);
    return false;
  }
  await db.execute(sql`UPDATE email_otps SET consumed_at = NOW() WHERE id = ${row.id}`);
  return true;
}
