import { createHash, randomInt } from 'node:crypto';
import { auth } from '@/lib/auth';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import type {
  OnboardingCompleteInput,
  OnboardingResendOtpInput,
  OnboardingStartInput,
  OnboardingVerifyOtpInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';

const OTP_LENGTH = 6;
const OTP_TTL_MINUTES = Number(Bun.env.ONBOARDING_OTP_TTL_MINUTES ?? 10);
const OTP_MAX_ATTEMPTS = Number(Bun.env.ONBOARDING_OTP_MAX_ATTEMPTS ?? 5);
const OTP_RESEND_COOLDOWN_SECONDS = Number(Bun.env.ONBOARDING_OTP_RESEND_COOLDOWN_SECONDS ?? 30);

type VerifyOtpResult = {
  message: string;
  verified: boolean;
  requestId: string;
  email: string | null;
  expiresAt: string | null;
  remainingAttempts: number;
  cooldownSeconds: number;
};

type ResendOtpResult = {
  message: string;
  requestId: string;
  email: string;
  expiresAt: string;
  cooldownSeconds: number;
  remainingAttempts: number;
};

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

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

  constructor(message: string, status: 400 | 404 | 409 | 429 = 400) {
    super(message);
    this.name = 'OnboardingError';
    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));
}

function slugify(value: string): string {
  return value
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-');
}

async function buildUniqueComplexSlug(complexName: string): Promise<string> {
  const base = slugify(complexName);
  const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;

  let candidate = fallback;
  let index = 1;

  while (true) {
    const existing = await db.complex.findFirst({
      where: { complexSlug: candidate },
      select: { id: true },
    });

    if (!existing) {
      return candidate;
    }

    index += 1;
    candidate = `${fallback}-${index}`;
  }
}

async function sendOtpEmail(email: string, otpCode: string) {
  await sendMail({
    to: email,
    subject: 'Codigo OTP para validar tu email',
    text: `Tu codigo OTP es ${otpCode}. Vence en ${OTP_TTL_MINUTES} minutos.`,
    html: `<p>Tu codigo OTP es <strong>${otpCode}</strong>.</p><p>Vence en ${OTP_TTL_MINUTES} minutos.</p>`,
  });
}

export async function startOnboarding(input: OnboardingStartInput): Promise<StartOnboardingResult> {
  const email = normalizeEmail(input.email);
  const otpCode = createOtpCode();
  const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
  const now = new Date();

  const request = await db.onboardingRequest.create({
    data: {
      id: uuidv7(),
      fullName: input.fullName.trim(),
      email,
      otpHash: hashValue(otpCode),
      otpExpiresAt: expiresAt,
      otpAttempts: 0,
      otpLastSentAt: now,
      otpResendCount: 0,
    },
    select: {
      id: true,
      email: true,
      otpExpiresAt: true,
    },
  });

  await sendOtpEmail(email, otpCode);

  return {
    message: 'Te enviamos un codigo OTP para validar tu direccion.',
    requestId: request.id,
    email: request.email,
    expiresAt: request.otpExpiresAt.toISOString(),
    cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
  };
}

export async function verifyOnboardingOtp(
  input: OnboardingVerifyOtpInput
): Promise<VerifyOtpResult> {
  const request = await db.onboardingRequest.findUnique({
    where: { id: input.requestId },
  });

  if (!request) {
    return {
      message: 'Solicitud de onboarding invalida o expirada.',
      verified: false,
      requestId: input.requestId,
      email: null,
      expiresAt: null,
      remainingAttempts: 0,
      cooldownSeconds: 0,
    };
  }

  if (request.completedAt) {
    return {
      message: 'Este onboarding ya fue completado.',
      verified: true,
      requestId: request.id,
      email: request.email,
      expiresAt: request.otpExpiresAt.toISOString(),
      remainingAttempts: getRemainingAttempts(request.otpAttempts),
      cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
    };
  }

  if (request.otpExpiresAt < new Date()) {
    return {
      message: 'El codigo OTP vencio. Solicita un nuevo codigo.',
      verified: false,
      requestId: request.id,
      email: request.email,
      expiresAt: request.otpExpiresAt.toISOString(),
      remainingAttempts: getRemainingAttempts(request.otpAttempts),
      cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
    };
  }

  if (request.otpAttempts >= OTP_MAX_ATTEMPTS) {
    return {
      message: 'Se agotaron los intentos de OTP. Solicita un nuevo codigo.',
      verified: false,
      requestId: request.id,
      email: request.email,
      expiresAt: request.otpExpiresAt.toISOString(),
      remainingAttempts: 0,
      cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
    };
  }

  const otpMatches = hashValue(input.otp) === request.otpHash;

  if (!otpMatches) {
    const updated = await db.onboardingRequest.update({
      where: { id: request.id },
      data: {
        otpAttempts: {
          increment: 1,
        },
      },
      select: {
        id: true,
        email: true,
        otpAttempts: true,
        otpExpiresAt: true,
        otpLastSentAt: true,
      },
    });

    const remainingAttempts = getRemainingAttempts(updated.otpAttempts);

    return {
      message:
        remainingAttempts > 0
          ? 'Codigo OTP invalido.'
          : 'Se agotaron los intentos de OTP. Solicita un nuevo codigo.',
      verified: false,
      requestId: updated.id,
      email: updated.email,
      expiresAt: updated.otpExpiresAt.toISOString(),
      remainingAttempts,
      cooldownSeconds: getCooldownSeconds(updated.otpLastSentAt),
    };
  }

  if (!request.emailVerifiedAt) {
    await db.onboardingRequest.update({
      where: { id: request.id },
      data: { emailVerifiedAt: new Date() },
    });
  }

  return {
    message: 'Email verificado correctamente.',
    verified: true,
    requestId: request.id,
    email: request.email,
    expiresAt: request.otpExpiresAt.toISOString(),
    remainingAttempts: getRemainingAttempts(request.otpAttempts),
    cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
  };
}

export async function resendOnboardingOtp(
  input: OnboardingResendOtpInput
): Promise<ResendOtpResult> {
  const request = await db.onboardingRequest.findUnique({
    where: { id: input.requestId },
  });

  if (!request) {
    throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 404);
  }

  if (request.completedAt) {
    throw new OnboardingError('Este onboarding ya fue completado.', 400);
  }

  if (request.emailVerifiedAt) {
    throw new OnboardingError('El email ya fue verificado.', 400);
  }

  const cooldownSeconds = getCooldownSeconds(request.otpLastSentAt);
  if (cooldownSeconds > 0) {
    throw new OnboardingError(`Debes esperar ${cooldownSeconds}s antes de reenviar el OTP.`, 429);
  }

  const otpCode = createOtpCode();
  const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
  const now = new Date();

  const updated = await db.onboardingRequest.update({
    where: { id: request.id },
    data: {
      otpHash: hashValue(otpCode),
      otpExpiresAt: expiresAt,
      otpAttempts: 0,
      otpLastSentAt: now,
      otpResendCount: {
        increment: 1,
      },
    },
    select: {
      id: true,
      email: true,
      otpExpiresAt: true,
      otpAttempts: true,
    },
  });

  await sendOtpEmail(updated.email, otpCode);

  return {
    message: 'Te enviamos un nuevo codigo OTP.',
    requestId: updated.id,
    email: updated.email,
    expiresAt: updated.otpExpiresAt.toISOString(),
    cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
    remainingAttempts: getRemainingAttempts(updated.otpAttempts),
  };
}

export async function completeOnboarding(input: OnboardingCompleteInput) {
  const onboardingRequest = await db.onboardingRequest.findUnique({
    where: { id: input.onboardingRequestId },
  });

  if (!onboardingRequest) {
    throw new OnboardingError('Solicitud de onboarding inválida o expirada.', 400);
  }

  if (onboardingRequest.otpExpiresAt < new Date()) {
    throw new OnboardingError('La sesión de onboarding expiró. Solicita un nuevo OTP.', 400);
  }

  if (!onboardingRequest.emailVerifiedAt) {
    throw new OnboardingError('Debes verificar el email antes de continuar.', 400);
  }

  if (onboardingRequest.completedAt) {
    throw new OnboardingError('Este onboarding ya fue completado.', 400);
  }

  const plan = await db.plan.findUnique({
    where: { code: input.planCode },
    select: { code: true },
  });

  if (!plan) {
    throw new OnboardingError('El plan seleccionado no existe.', 400);
  }

  const { user } = await auth.api.signUpEmail({
    asResponse: false,
    body: {
      email: onboardingRequest.email,
      password: input.password,
      name: onboardingRequest.fullName,
    },
  });

  const complexSlug = await buildUniqueComplexSlug(input.complexName);

  const result = await db.$transaction(async (tx) => {
    await tx.user.update({
      where: { id: user.id },
      data: { emailVerified: true },
    });

    const complex = await tx.complex.create({
      data: {
        id: uuidv7(),
        complexName: input.complexName.trim(),
        physicalAddress: input.physicalAddress.trim(),
        city: input.city?.trim() || null,
        state: input.state?.trim() || null,
        country: input.country?.trim() || null,
        complexSlug,
        adminEmail: onboardingRequest.email,
        planCode: input.planCode,
      },
    });

    await tx.complexUser.create({
      data: {
        complexId: complex.id,
        userId: user.id,
        role: 'ADMIN',
      },
    });

    await tx.onboardingRequest.update({
      where: { id: onboardingRequest.id },
      data: {
        completedAt: new Date(),
      },
    });

    return {
      userId: user.id,
      complexId: complex.id,
      complexSlug: complex.complexSlug,
    };
  });

  return {
    ...result,
  };
}
