import { createHmac, randomInt } from "node:crypto";
import { and, eq, isNull, sql } from "drizzle-orm";
import { db } from "@/db";
import { phoneVerificationChallenges, userPhones } from "@/db/schema";
import { sendBotSmsPin } 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 PhoneVerificationError extends Error {
  status: number;
  constructor(message: string, status = 400) { super(message); this.status = status; }
}

/** Normalizes Russian phone input to 7XXXXXXXXXX; null when unparseable. */
export function normalizePhone(input: string) {
  const digits = input.replace(/\D/g, "");
  if (digits.length === 11 && digits.startsWith("8")) return `7${digits.slice(1)}`;
  if (digits.length === 11 && digits.startsWith("7")) return digits;
  if (digits.length === 10 && digits.startsWith("9")) return `7${digits}`;
  return null;
}

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

/**
 * Creates a single-use SMS challenge (§10.2): 60 s cooldown, at most 3 sends
 * per hour per number, 6-digit cryptographic PIN stored only as HMAC.
 */
export async function requestPhoneChallenge(params: { userId: number; phone: string; ip?: string }) {
  if (!db) throw new PhoneVerificationError("База данных недоступна", 503);
  const now = new Date();
  const [recent] = await db.select({ createdAt: phoneVerificationChallenges.createdAt }).from(phoneVerificationChallenges).where(eq(phoneVerificationChallenges.phone, params.phone)).orderBy(sql`${phoneVerificationChallenges.createdAt} DESC`).limit(1);
  if (recent && now.getTime() - recent.createdAt.getTime() < SEND_COOLDOWN_MS) throw new PhoneVerificationError("Код уже отправлялся — повторите через минуту", 429);
  const [{ count }] = await db.select({ count: sql<number>`count(*)::int` }).from(phoneVerificationChallenges).where(and(eq(phoneVerificationChallenges.phone, params.phone), sql`${phoneVerificationChallenges.createdAt} > now() - interval '1 hour'`));
  if (count >= MAX_SENDS_PER_HOUR) throw new PhoneVerificationError("Слишком много отправок за час. Попробуйте позже.", 429);
  // Supersede any open challenge: only one is ever valid per number.
  await db.update(phoneVerificationChallenges).set({ consumedAt: now }).where(and(eq(phoneVerificationChallenges.phone, params.phone), isNull(phoneVerificationChallenges.consumedAt)));
  const pin = String(randomInt(0, 1_000_000)).padStart(6, "0");
  const expiresAt = new Date(now.getTime() + PIN_TTL_MS);
  await db.insert(phoneVerificationChallenges).values({ phone: params.phone, pinHash: hmac(params.phone, pin), userId: params.userId, ip: params.ip?.slice(0, 64) ?? null, expiresAt });
  try {
    await sendBotSmsPin(`+${params.phone}`, pin);
  } catch (error) {
    const status = (error as { status?: unknown }).status;
    // Close the challenge so the cooldown clock still protects the number.
    if (status !== 429) await db.update(phoneVerificationChallenges).set({ consumedAt: new Date() }).where(and(eq(phoneVerificationChallenges.phone, params.phone), isNull(phoneVerificationChallenges.consumedAt))).catch(() => undefined);
    throw new PhoneVerificationError(status === 400 ? "Проверьте номер телефона." : "Не удалось отправить SMS. Попробуйте позже.", status === 400 ? 400 : 502);
  }
}

/** Verifies the PIN once, marks the phone verified for the user (§10.2). */
export async function verifyPhoneChallenge(params: { userId: number; phone: string; pin: string }) {
  if (!db) throw new PhoneVerificationError("База данных недоступна", 503);
  const now = new Date();
  const [challenge] = await db.select().from(phoneVerificationChallenges).where(and(eq(phoneVerificationChallenges.phone, params.phone), isNull(phoneVerificationChallenges.consumedAt))).limit(1);
  if (!challenge || challenge.expiresAt < now) throw new PhoneVerificationError("Код истёк — запросите новый.");
  if (challenge.attempts >= MAX_VERIFY_ATTEMPTS) {
    await db.update(phoneVerificationChallenges).set({ consumedAt: now }).where(eq(phoneVerificationChallenges.id, challenge.id));
    throw new PhoneVerificationError("Слишком много попыток — запросите новый код.");
  }
  if (hmac(params.phone, params.pin) !== challenge.pinHash) {
    const attempts = challenge.attempts + 1;
    await db.update(phoneVerificationChallenges).set({ attempts, consumedAt: attempts >= MAX_VERIFY_ATTEMPTS ? now : null }).where(eq(phoneVerificationChallenges.id, challenge.id));
    throw new PhoneVerificationError("Неверный код.");
  }
  await db.update(phoneVerificationChallenges).set({ consumedAt: now }).where(eq(phoneVerificationChallenges.id, challenge.id));
  const [existingByPhone] = await db.select().from(userPhones).where(eq(userPhones.phone, params.phone)).limit(1);
  if (existingByPhone && existingByPhone.userId !== params.userId) throw new PhoneVerificationError("Не удалось подтвердить номер.");
  const [existingByUser] = await db.select().from(userPhones).where(eq(userPhones.userId, params.userId)).limit(1);
  if (existingByUser) await db.update(userPhones).set({ phone: params.phone, verifiedAt: now, updatedAt: now }).where(eq(userPhones.id, existingByUser.id));
  else await db.insert(userPhones).values({ userId: params.userId, phone: params.phone, verifiedAt: now });
}

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

/** Attaches a phone already verified by a trusted identity provider. */
export async function attachVerifiedPhone(userId: number, input: string) {
  if (!db) return false;
  const phone = normalizePhone(input);
  if (!phone) return false;
  const [existingByPhone] = await db.select().from(userPhones).where(eq(userPhones.phone, phone)).limit(1);
  if (existingByPhone && existingByPhone.userId !== userId) return false;
  const now = new Date();
  const [existingByUser] = await db.select().from(userPhones).where(eq(userPhones.userId, userId)).limit(1);
  if (existingByUser) await db.update(userPhones).set({ phone, verifiedAt: now, updatedAt: now }).where(eq(userPhones.id, existingByUser.id));
  else await db.insert(userPhones).values({ userId, phone, verifiedAt: now });
  return true;
}
