import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth';
import { type IpGeoInfo, fetchGeoInfo } from '@/lib/geoip';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import type {
  PasswordResetResendInput,
  PasswordResetStartInput,
  PasswordResetVerifyInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';

const OTP_LENGTH = 6;
const OTP_TTL_MINUTES = Number(Bun.env.PASSWORD_RESET_OTP_TTL_MINUTES ?? 10);
const OTP_MAX_ATTEMPTS = Number(Bun.env.PASSWORD_RESET_OTP_MAX_ATTEMPTS ?? 5);
const OTP_RESEND_COOLDOWN_SECONDS = Number(
  Bun.env.PASSWORD_RESET_OTP_RESEND_COOLDOWN_SECONDS ?? 30
);

export type PasswordResetStartResult = {
  message: string;
  requestId: string;
  email: string;
  expiresAt: string;
  cooldownSeconds: number;
};

export type PasswordResetVerifyResult = {
  message: string;
};

export type PasswordResetResendResult = {
  message: string;
  requestId: string;
  email: string;
  expiresAt: string;
  cooldownSeconds: number;
};

export class PasswordResetError extends Error {
  status: 400 | 404 | 409 | 429;

  constructor(message: string, status: 400 | 404 | 409 | 429 = 400) {
    super(message);
    this.name = 'PasswordResetError';
    this.status = status;
  }
}

function nowPlusMinutes(minutes: number): Date {
  const date = new Date();
  date.setMinutes(date.getMinutes() + minutes);
  return date;
}

function normalizeEmail(email: string): string {
  return email.trim().toLowerCase();
}

function createOtpCode(): string {
  return randomInt(0, 10 ** OTP_LENGTH)
    .toString()
    .padStart(OTP_LENGTH, '0');
}

function hashValue(value: string): string {
  return createHash('sha256').update(value).digest('hex');
}

function getRemainingAttempts(otpAttempts: number): number {
  return Math.max(0, OTP_MAX_ATTEMPTS - otpAttempts);
}

function getCooldownSeconds(otpLastSentAt: Date): number {
  const elapsedMs = Date.now() - otpLastSentAt.getTime();
  const remainingMs = OTP_RESEND_COOLDOWN_SECONDS * 1000 - elapsedMs;
  return Math.max(0, Math.ceil(remainingMs / 1000));
}

type UserAgentInfo = {
  browser: string;
  os: string;
  device: string;
};

function parseUserAgent(ua: string | undefined): UserAgentInfo {
  if (!ua) {
    return { browser: 'Desconocido', os: 'Desconocido', device: 'Desconocido' };
  }

  const uaLower = ua.toLowerCase();

  let browser = 'Desconocido';
  if (uaLower.includes('chrome')) browser = 'Chrome';
  else if (uaLower.includes('firefox')) browser = 'Firefox';
  else if (uaLower.includes('safari')) browser = 'Safari';
  else if (uaLower.includes('edge')) browser = 'Edge';
  else if (uaLower.includes('opera')) browser = 'Opera';

  let os = 'Desconocido';
  if (uaLower.includes('windows')) os = 'Windows';
  else if (uaLower.includes('mac')) os = 'macOS';
  else if (uaLower.includes('linux')) os = 'Linux';
  else if (uaLower.includes('android')) os = 'Android';
  else if (uaLower.includes('ios') || uaLower.includes('iphone') || uaLower.includes('ipad'))
    os = 'iOS';

  let device = 'Desktop';
  if (uaLower.includes('mobile') || uaLower.includes('android')) device = 'Móvil';
  else if (uaLower.includes('tablet') || uaLower.includes('ipad')) device = 'Tablet';

  return { browser, os, device };
}

async function sendResetOtpEmail(email: string, otpCode: string) {
  await sendMail({
    to: email,
    subject: 'Código para restablecer tu contraseña',
    text: `Tu código OTP es ${otpCode}. Vence en ${OTP_TTL_MINUTES} minutos.`,
    html: `<p>Tu código OTP es <strong>${otpCode}</strong>.</p><p>Vence en ${OTP_TTL_MINUTES} minutos.</p>`,
  });
}

async function sendPasswordChangedEmail(
  email: string,
  ipInfo: IpGeoInfo | null,
  userAgent: string | undefined
) {
  const deviceInfo = parseUserAgent(userAgent);
  const now = new Date();
  const formattedDate = now.toLocaleString('es-AR', {
    timeZone: 'America/Argentina/Buenos_Aires',
    dateStyle: 'full',
    timeStyle: 'short',
  });

  const location = ipInfo
    ? `${ipInfo.city}${ipInfo.city !== 'Red local' ? `, ${ipInfo.country}` : ''}`
    : 'No disponible';
  const ip = ipInfo?.ip || 'No disponible';

  await sendMail({
    to: email,
    subject: 'Tu contraseña ha sido cambiada',
    text: `Tu contraseña fue cambiada el ${formattedDate}. Si no fuiste vos, contactanos inmediatamente.`,
    html: `
      <h2>Tu contraseña ha sido cambiada</h2>
      <p>Se ha modificado la contraseña de tu cuenta.</p>
      <h3>Detalles del cambio:</h3>
      <ul>
        <li><strong>Fecha y hora:</strong> ${formattedDate}</li>
        <li><strong>Dirección IP:</strong> ${ip}</li>
        <li><strong>Ubicación:</strong> ${location}</li>
        <li><strong>Dispositivo:</strong> ${deviceInfo.device} (${deviceInfo.os} - ${deviceInfo.browser})</li>
      </ul>
      <p>Si no fuiste vos, contactanos inmediatamente.</p>
    `,
  });
}

async function findBetterAuthUserByEmail(email: string): Promise<{ id: string } | null> {
  const user = await db.user.findUnique({
    where: { email },
    select: { id: true },
  });

  if (!user) {
    return null;
  }

  return { id: user.id };
}

export async function startPasswordReset(
  input: PasswordResetStartInput
): Promise<PasswordResetStartResult> {
  const email = normalizeEmail(input.email);

  const existingUser = await findBetterAuthUserByEmail(email);
  if (!existingUser) {
    return {
      message: 'Si el email existe, recibirás un código de verificación.',
      requestId: uuidv7(),
      email,
      expiresAt: new Date().toISOString(),
      cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
    };
  }

  const existingRequest = await db.passwordResetRequest.findFirst({
    where: { email, usedAt: null },
    orderBy: { createdAt: 'desc' },
  });

  let requestId: string;
  let otpCode: string;
  let otpHash: string;
  let otpExpiresAt: Date;
  let cooldownSeconds = 0;

  if (existingRequest) {
    requestId = existingRequest.id;
    otpExpiresAt = new Date(existingRequest.otpExpiresAt);

    if (new Date() > otpExpiresAt) {
      otpCode = createOtpCode();
      otpHash = hashValue(otpCode);
      otpExpiresAt = nowPlusMinutes(OTP_TTL_MINUTES);

      await db.passwordResetRequest.update({
        where: { id: requestId },
        data: {
          otpHash,
          otpExpiresAt,
          otpAttempts: 0,
          otpLastSentAt: new Date(),
          otpResendCount: { increment: 1 },
        },
      });
    } else {
      cooldownSeconds = getCooldownSeconds(new Date(existingRequest.otpLastSentAt));

      if (cooldownSeconds > 0) {
        return {
          message: `Debes esperar ${cooldownSeconds} segundos antes de reenviar.`,
          requestId,
          email,
          expiresAt: otpExpiresAt.toISOString(),
          cooldownSeconds,
        };
      }

      otpCode = createOtpCode();
      otpHash = hashValue(otpCode);
      otpExpiresAt = nowPlusMinutes(OTP_TTL_MINUTES);

      await db.passwordResetRequest.update({
        where: { id: requestId },
        data: {
          otpHash,
          otpExpiresAt,
          otpAttempts: 0,
          otpLastSentAt: new Date(),
          otpResendCount: { increment: 1 },
        },
      });

      await sendResetOtpEmail(email, otpCode);

      return {
        message: 'Código reenviado.',
        requestId,
        email,
        expiresAt: otpExpiresAt.toISOString(),
        cooldownSeconds: 0,
      };
    }
  } else {
    requestId = uuidv7();
    otpCode = createOtpCode();
    otpHash = hashValue(otpCode);
    otpExpiresAt = nowPlusMinutes(OTP_TTL_MINUTES);

    await db.passwordResetRequest.create({
      data: {
        id: requestId,
        email,
        otpHash,
        otpExpiresAt,
        otpLastSentAt: new Date(),
      },
    });
  }

  await sendResetOtpEmail(email, otpCode);

  return {
    message: 'Código enviado successfully.',
    requestId,
    email,
    expiresAt: otpExpiresAt.toISOString(),
    cooldownSeconds: 0,
  };
}

export async function verifyOtpAndResetPassword(
  input: PasswordResetVerifyInput,
  ip: string | undefined,
  userAgent: string | undefined
): Promise<PasswordResetVerifyResult> {
  const { requestId, otp, newPassword } = input;

  const request = await db.passwordResetRequest.findUnique({
    where: { id: requestId },
  });

  if (!request) {
    throw new PasswordResetError('Solicitud inválida.', 404);
  }

  if (request.usedAt) {
    throw new PasswordResetError('Esta solicitud ya fue utilizada.', 400);
  }

  if (new Date() > new Date(request.otpExpiresAt)) {
    throw new PasswordResetError('El código ha vencido.', 400);
  }

  const remainingAttempts = getRemainingAttempts(request.otpAttempts);
  if (remainingAttempts <= 0) {
    throw new PasswordResetError('Superaste la cantidad maxima de intentos.', 429);
  }

  const otpHash = hashValue(otp);
  if (otpHash !== request.otpHash) {
    await db.passwordResetRequest.update({
      where: { id: requestId },
      data: { otpAttempts: { increment: 1 } },
    });

    throw new PasswordResetError('Código incorrecto.', 400);
  }

  const existingUser = await findBetterAuthUserByEmail(request.email);
  if (!existingUser) {
    throw new PasswordResetError('Usuario no encontrado.', 404);
  }

  const resetToken = uuidv7();
  const identifier = `reset-password:${resetToken}`;
  const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);

  await db.verification.create({
    data: {
      id: uuidv7(),
      identifier,
      value: existingUser.id,
      expiresAt,
    },
  });

  await auth.api.resetPassword({
    asResponse: false,
    body: {
      token: resetToken,
      newPassword,
    },
  });

  await db.passwordResetRequest.update({
    where: { id: requestId },
    data: { usedAt: new Date() },
  });

  const geoInfo = await fetchGeoInfo(ip || 'unknown');
  await sendPasswordChangedEmail(request.email, geoInfo, userAgent);

  return {
    message: 'Contraseña restablecida correctamente.',
  };
}

export async function resendPasswordResetOtp(
  input: PasswordResetResendInput
): Promise<PasswordResetResendResult> {
  const { requestId } = input;

  const request = await db.passwordResetRequest.findUnique({
    where: { id: requestId },
  });

  if (!request) {
    throw new PasswordResetError('Solicitud inválida.', 404);
  }

  if (request.usedAt) {
    throw new PasswordResetError('Esta solicitud ya fue utilizada.', 400);
  }

  const cooldownSeconds = getCooldownSeconds(new Date(request.otpLastSentAt));
  if (cooldownSeconds > 0) {
    throw new PasswordResetError(
      `Debes esperar ${cooldownSeconds} segundos antes de reenviar.`,
      429
    );
  }

  const otpCode = createOtpCode();
  const otpHash = hashValue(otpCode);
  const otpExpiresAt = nowPlusMinutes(OTP_TTL_MINUTES);

  await db.passwordResetRequest.update({
    where: { id: requestId },
    data: {
      otpHash,
      otpExpiresAt,
      otpAttempts: 0,
      otpLastSentAt: new Date(),
      otpResendCount: { increment: 1 },
    },
  });

  await sendResetOtpEmail(request.email, otpCode);

  return {
    message: 'Código reenviado.',
    requestId,
    email: request.email,
    expiresAt: otpExpiresAt.toISOString(),
    cooldownSeconds: 0,
  };
}
