import { createHmac, randomInt } from "node:crypto";
import { and, eq, isNull, sql } from "drizzle-orm";
import { db } from "@/db";
import { emailVerificationChallenges, userEmails } from "@/db/schema";
import { sendBotEmail } from "@/lib/bot-api";

const PIN_TTL_MS = 10 * 60 * 1000;
const SEND_COOLDOWN_MS = 60 * 1000;
const MAX_SENDS_PER_HOUR = 3;
const MAX_VERIFY_ATTEMPTS = 5;

export class EmailVerificationError extends Error {
  status: number;
  constructor(message: string, status = 400) { super(message); this.status = status; }
}

/** Lowercases and shape-checks the address; null when invalid. */
export function normalizeEmail(input: string) {
  const email = input.trim().toLowerCase();
  return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email) && email.length <= 254 ? email : null;
}

function hmac(email: string, pin: string) {
  return createHmac("sha256", process.env.AUTH_SECRET || "kiln-dev-secret").update(`email:${email}:${pin}`).digest("hex");
}

/**
 * Single-use email challenge (§10.2 analogy): 60 s cooldown, at most 3 sends
 * per hour per address, 6-digit PIN stored only as HMAC; the bot is a plain
 * transport (contract §2а).
 */
export async function requestEmailChallenge(params: { email: string; userId?: number; ip?: string }) {
  if (!db) throw new EmailVerificationError("База данных недоступна", 503);
  const now = new Date();
  const [recent] = await db.select({ createdAt: emailVerificationChallenges.createdAt }).from(emailVerificationChallenges).where(eq(emailVerificationChallenges.email, params.email)).orderBy(sql`${emailVerificationChallenges.createdAt} DESC`).limit(1);
  if (recent && now.getTime() - recent.createdAt.getTime() < SEND_COOLDOWN_MS) throw new EmailVerificationError("Код уже отправлялся — повторите через минуту", 429);
  const [{ count }] = await db.select({ count: sql<number>`count(*)::int` }).from(emailVerificationChallenges).where(and(eq(emailVerificationChallenges.email, params.email), sql`${emailVerificationChallenges.createdAt} > now() - interval '1 hour'`));
  if (count >= MAX_SENDS_PER_HOUR) throw new EmailVerificationError("Слишком много отправок за час. Попробуйте позже.", 429);
  await db.update(emailVerificationChallenges).set({ consumedAt: now }).where(and(eq(emailVerificationChallenges.email, params.email), isNull(emailVerificationChallenges.consumedAt)));
  const pin = String(randomInt(0, 1_000_000)).padStart(6, "0");
  const expiresAt = new Date(now.getTime() + PIN_TTL_MS);
  await db.insert(emailVerificationChallenges).values({ email: params.email, pinHash: hmac(params.email, pin), userId: params.userId ?? null, ip: params.ip?.slice(0, 64) ?? null, expiresAt });
  try {
    await sendBotEmail(params.email, "Код подтверждения — ресторан КИЛН", `Ваш код для входа на сайт КИЛН: ${pin}\n\nКод действует 10 минут и запрашивался с сайта kiln.spb.ru.\nЕсли вы не запрашивали вход, просто игнорируйте это письмо.`);
  } catch (error) {
    const status = (error as { status?: unknown }).status;
    if (status !== 429) await db.update(emailVerificationChallenges).set({ consumedAt: new Date() }).where(and(eq(emailVerificationChallenges.email, params.email), isNull(emailVerificationChallenges.consumedAt))).catch(() => undefined);
    throw new EmailVerificationError(status === 400 ? "Проверьте адрес почты." : "Не удалось отправить письмо. Попробуйте позже.", status === 400 ? 400 : 502);
  }
}

/** Verifies the PIN once; account binding is up to the caller. */
export async function verifyEmailChallenge(params: { email: string; pin: string }) {
  if (!db) throw new EmailVerificationError("База данных недоступна", 503);
  const now = new Date();
  const [challenge] = await db.select().from(emailVerificationChallenges).where(and(eq(emailVerificationChallenges.email, params.email), isNull(emailVerificationChallenges.consumedAt))).limit(1);
  if (!challenge || challenge.expiresAt < now) throw new EmailVerificationError("Код истёк — запросите новый.");
  if (challenge.attempts >= MAX_VERIFY_ATTEMPTS) {
    await db.update(emailVerificationChallenges).set({ consumedAt: now }).where(eq(emailVerificationChallenges.id, challenge.id));
    throw new EmailVerificationError("Слишком много попыток — запросите новый код.");
  }
  if (hmac(params.email, params.pin) !== challenge.pinHash) {
    const attempts = challenge.attempts + 1;
    await db.update(emailVerificationChallenges).set({ attempts, consumedAt: attempts >= MAX_VERIFY_ATTEMPTS ? now : null }).where(eq(emailVerificationChallenges.id, challenge.id));
    throw new EmailVerificationError("Неверный код.");
  }
  await db.update(emailVerificationChallenges).set({ consumedAt: now }).where(eq(emailVerificationChallenges.id, challenge.id));
}

export async function upsertUserEmail(userId: number, email: string) {
  if (!db) throw new EmailVerificationError("База данных недоступна", 503);
  const now = new Date();
  const [existingByEmail] = await db.select().from(userEmails).where(eq(userEmails.email, email)).limit(1);
  if (existingByEmail && existingByEmail.userId !== userId) throw new EmailVerificationError("Не удалось подтвердить почту.");
  const [existingByUser] = await db.select().from(userEmails).where(eq(userEmails.userId, userId)).limit(1);
  if (existingByUser) await db.update(userEmails).set({ email, verifiedAt: now, updatedAt: now }).where(eq(userEmails.id, existingByUser.id));
  else await db.insert(userEmails).values({ userId, email, verifiedAt: now });
  await db.execute(sql`UPDATE auth_users SET email = ${email}, updated_at = now() WHERE id = ${userId}`).catch(() => undefined);
}

export async function getVerifiedEmail(userId: number) {
  if (!db) return null;
  const [row] = await db.select().from(userEmails).where(and(eq(userEmails.userId, userId), sql`${userEmails.verifiedAt} IS NOT NULL`)).limit(1);
  return row?.email ?? null;
}
