import { createHash, randomInt, timingSafeEqual } from "node:crypto";

import * as db from "./db";

const OTP_LENGTH = 4;
const OTP_TTL_MS = 5 * 60 * 1000;
const RESEND_COOLDOWN_MS = 60 * 1000;
const MAX_ATTEMPTS = 5;

type MemoryChallenge = {
  id: number;
  phone: string;
  codeHash: string;
  attemptCount: number;
  expiresAt: Date;
  createdAt: Date;
  consumedAt?: Date;
};

type SmsTransport = (phone: string, message: string) => Promise<void>;

const challenges = new Map<string, MemoryChallenge>();
let testTransport: SmsTransport | null = null;

export function normalizeJordanPhone(value: string) {
  const digits = value.replace(/\D/g, "");
  if (/^07\d{8}$/.test(digits)) return `+962${digits.slice(1)}`;
  if (/^7\d{8}$/.test(digits)) return `+962${digits}`;
  if (/^9627\d{8}$/.test(digits)) return `+${digits}`;
  if (/^009627\d{8}$/.test(digits)) return `+${digits.slice(2)}`;
  throw new Error("يرجى إدخال رقم هاتف أردني صحيح بصيغة 07 أو 7 أو 00962");
}

function hashOtp(phone: string, code: string) {
  const pepper = process.env.SMSJO_API_TOKEN ?? "etqan-otp-pepper";
  return createHash("sha256").update(`${phone}:${code}:${pepper}`).digest("hex");
}

function equalHash(left: string, right: string) {
  const leftBuffer = Buffer.from(left, "hex");
  const rightBuffer = Buffer.from(right, "hex");
  return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
}

export function resolveSmsJoSendEndpoint(configuredEndpoint: string) {
  return configuredEndpoint
    .replace(/\/$/, "")
    .replace(/\/api\/http$/, "/api/http/sms/send")
    .replace(/\/api\/v3$/, "/api/http/sms/send");
}

export function formatOtpMessage(template: string, code: string) {
  return template.replace(/\{\{code\}\}/g, code);
}

async function deliverSms(phone: string, message: string) {
  if (testTransport) return testTransport(phone, message);
  const provider = await db.getSmsProviderConfig();
  if (!provider || !provider.isActive) throw new Error("إعدادات مزود الرسائل النصية في قاعدة البيانات غير مكتملة أو غير مفعلة");
  const token = process.env[provider.tokenSecretKey];
  if (!token) throw new Error("رمز الوصول إلى مزود الرسائل غير متاح في الإعدادات الآمنة");

  const endpoint = resolveSmsJoSendEndpoint(provider.endpoint);
  const payload: Record<string, string> = {
    api_token: token,
    recipient: phone.replace(/^\+/, ""),
    sender_id: provider.senderId,
    type: "plain",
    message,
  };

  const response = await fetch(endpoint, {
    method: "POST",
    headers: { "Content-Type": "application/json", Accept: "application/json" },
    body: JSON.stringify(payload),
  });
  const body = await response.json().catch(() => null) as { status?: string; message?: string } | null;
  if (!response.ok || body?.status === "error") throw new Error(body?.message || "تعذر إرسال رمز التحقق حالياً. يرجى المحاولة لاحقاً.");
}

export async function requestPhoneOtp(rawPhone: string) {
  const phone = normalizeJordanPhone(rawPhone);
  const previous = challenges.get(phone);
  if (previous && !previous.consumedAt && Date.now() - previous.createdAt.getTime() < RESEND_COOLDOWN_MS) {
    const retryAfterSeconds = Math.ceil((RESEND_COOLDOWN_MS - (Date.now() - previous.createdAt.getTime())) / 1000);
    return { sent: false as const, retryAfterSeconds, expiresInSeconds: Math.ceil((previous.expiresAt.getTime() - Date.now()) / 1000) };
  }

  const code = String(randomInt(10 ** (OTP_LENGTH - 1), 10 ** OTP_LENGTH));
  const createdAt = new Date();
  const expiresAt = new Date(createdAt.getTime() + OTP_TTL_MS);
  const challenge: MemoryChallenge = { id: Date.now(), phone, codeHash: hashOtp(phone, code), attemptCount: 0, createdAt, expiresAt };
  const template = await db.getSmsOtpMessageTemplate();
  if (!template) throw new Error("قالب رسالة التحقق في قاعدة البيانات غير مكتمل");
  await deliverSms(phone, formatOtpMessage(template, code));
  challenges.set(phone, challenge);
  await db.createPhoneOtpChallenge({ phone, codeHash: challenge.codeHash, attemptCount: 0, expiresAt });
  return { sent: true as const, retryAfterSeconds: Math.ceil(RESEND_COOLDOWN_MS / 1000), expiresInSeconds: Math.ceil(OTP_TTL_MS / 1000) };
}

export async function verifyPhoneOtp(rawPhone: string, code: string) {
  const phone = normalizeJordanPhone(rawPhone);
  const challenge = challenges.get(phone);
  if (!challenge || challenge.consumedAt || challenge.expiresAt.getTime() < Date.now()) return { verified: false as const, reason: "expired" as const };
  if (challenge.attemptCount >= MAX_ATTEMPTS) return { verified: false as const, reason: "locked" as const };
  const isValid = /^\d{4}$/.test(code) && equalHash(challenge.codeHash, hashOtp(phone, code));
  if (!isValid) {
    challenge.attemptCount += 1;
    return { verified: false as const, reason: "invalid" as const, attemptsRemaining: Math.max(MAX_ATTEMPTS - challenge.attemptCount, 0) };
  }
  challenge.consumedAt = new Date();
  await db.consumePhoneOtpChallenge(challenge.id);
  return { verified: true as const, phone };
}

export function setSmsTransportForTest(transport: SmsTransport | null) {
  testTransport = transport;
}

export function clearOtpStateForTest() {
  challenges.clear();
  testTransport = null;
}
