import { createHash, randomInt } from "crypto";
import { OtpChallenge } from "@/db/models/OtpChallenge";
import { AppError } from "@/lib/errors";
import { sendMail } from "@/services/email";
import { buildOtpEmailHtml, buildOtpEmailText } from "@/services/emailTemplates";
import { isAuthMysql } from "@/db/activeDatabase";
import {
  bumpOtpAttemptsMysql,
  consumeOtpMysql,
  consumeOpenEmailOtpsMysql,
  createEmailOtpMysql,
  findLatestOpenEmailOtpMysql,
} from "@/db/mysql/otp";

const OTP_TTL_MS = 10 * 60 * 1000;
const MAX_ATTEMPTS = 5;
const RESEND_COOLDOWN_MS = 60 * 1000;
const OTP_MINUTES = Math.round(OTP_TTL_MS / 60000);

function hashCode(code: string) {
  return createHash("sha256").update(code).digest("hex");
}

function generateCode() {
  return String(randomInt(100000, 999999));
}

export async function issueEmailOtp(params: {
  userId: string;
  email: string;
  fullName: string;
}) {
  if (isAuthMysql()) {
    const latest = await findLatestOpenEmailOtpMysql(params.userId);
    if (latest?.createdAt && Date.now() - latest.createdAt.getTime() < RESEND_COOLDOWN_MS) {
      const waitSec = Math.ceil(
        (RESEND_COOLDOWN_MS - (Date.now() - latest.createdAt.getTime())) / 1000,
      );
      throw new AppError(429, `Please wait ${waitSec}s before requesting another code`);
    }

    const now = Date.now();
    const code = generateCode();
    await consumeOpenEmailOtpsMysql(params.userId);
    await createEmailOtpMysql({
      userId: params.userId,
      codeHash: hashCode(code),
      expiresAt: new Date(now + OTP_TTL_MS),
    });

    const mail = await sendMail({
      to: params.email,
      subject: `مرحباً بك في سفر ليبيا — رمز التحقق`,
      text: buildOtpEmailText({
        fullName: params.fullName,
        code,
        minutesValid: OTP_MINUTES,
      }),
      html: buildOtpEmailHtml({
        fullName: params.fullName,
        code,
        minutesValid: OTP_MINUTES,
      }),
    });

    if (!mail.delivered) {
      console.warn(`[otp] email not delivered to ${params.email} (mode=${mail.mode})`);
    }

    return { expiresAt: new Date(now + OTP_TTL_MS), delivered: mail.delivered };
  }

  const latest = await OtpChallenge.findOne({
    userId: params.userId,
    channel: "EMAIL",
    consumedAt: null,
  })
    .sort({ createdAt: -1 })
    .lean();

  if (latest?.createdAt && Date.now() - new Date(latest.createdAt).getTime() < RESEND_COOLDOWN_MS) {
    const waitSec = Math.ceil(
      (RESEND_COOLDOWN_MS - (Date.now() - new Date(latest.createdAt).getTime())) / 1000,
    );
    throw new AppError(429, `Please wait ${waitSec}s before requesting another code`);
  }

  const now = Date.now();
  const code = generateCode();

  await OtpChallenge.updateMany(
    { userId: params.userId, channel: "EMAIL", consumedAt: null },
    { $set: { consumedAt: new Date() } },
  );

  await OtpChallenge.create({
    userId: params.userId,
    channel: "EMAIL",
    codeHash: hashCode(code),
    expiresAt: new Date(now + OTP_TTL_MS),
  });

  const mail = await sendMail({
    to: params.email,
    subject: `مرحباً بك في سفر ليبيا — رمز التحقق`,
    text: buildOtpEmailText({
      fullName: params.fullName,
      code,
      minutesValid: OTP_MINUTES,
    }),
    html: buildOtpEmailHtml({
      fullName: params.fullName,
      code,
      minutesValid: OTP_MINUTES,
    }),
  });

  if (!mail.delivered) {
    console.warn(`[otp] email not delivered to ${params.email} (mode=${mail.mode})`);
  }

  return { expiresAt: new Date(now + OTP_TTL_MS), delivered: mail.delivered };
}

export async function verifyEmailOtp(params: { userId: string; code: string }) {
  const code = params.code.trim();
  if (!/^\d{6}$/.test(code)) throw new AppError(400, "Invalid OTP format");

  if (isAuthMysql()) {
    const challenge = await findLatestOpenEmailOtpMysql(params.userId);
    if (!challenge) throw new AppError(400, "No active email OTP");
    if (challenge.expiresAt < new Date()) throw new AppError(400, "OTP expired");
    if (challenge.attempts >= MAX_ATTEMPTS) throw new AppError(400, "OTP locked — request a new code");

    const attempts = challenge.attempts + 1;
    await bumpOtpAttemptsMysql(challenge.id, attempts);

    if (challenge.codeHash !== hashCode(code)) {
      throw new AppError(400, "Invalid OTP");
    }
    await consumeOtpMysql(challenge.id);
    return;
  }

  const challenge = await OtpChallenge.findOne({
    userId: params.userId,
    channel: "EMAIL",
    consumedAt: null,
  }).sort({ createdAt: -1 });

  if (!challenge) throw new AppError(400, "No active email OTP");
  if (challenge.expiresAt < new Date()) throw new AppError(400, "OTP expired");
  if (challenge.attempts >= MAX_ATTEMPTS) throw new AppError(400, "OTP locked — request a new code");

  challenge.attempts += 1;
  await challenge.save();

  if (challenge.codeHash !== hashCode(code)) {
    throw new AppError(400, "Invalid OTP");
  }

  challenge.consumedAt = new Date();
  await challenge.save();
}
