import bcrypt from 'bcryptjs';
import { OAuth2Client } from 'google-auth-library';
import prisma from '../services/prisma.js';
import { signToken } from '../services/token.service.js';
import {
  registerSchema,
  loginSchema,
  googleAuthSchema,
  verifyEmailSchema,
  resendOtpSchema,
  forgotPasswordSchema,
  resetPasswordSchema,
  updateProfileSchema,
  changePasswordSchema,
  changeEmailRequestSchema,
  changeEmailVerifySchema,
} from '../utils/validators.js';
import asyncHandler from '../utils/asyncHandler.js';
import ApiError from '../utils/ApiError.js';
import { env, isProd } from '../config/env.js';
import { createOtp, verifyOtp, otpErrorMessage } from '../services/otp.service.js';
import {
  sendOtpEmail,
  sendWelcomeEmail,
  sendPasswordChangedEmail,
} from '../services/mail.service.js';

const googleClient = new OAuth2Client(env.googleClientId);

// Email sends are best-effort: never fail the request if the mailer hiccups.
function fireAndForget(promise) {
  Promise.resolve(promise).catch((err) =>
    // eslint-disable-next-line no-console
    console.error('[mail] send failed:', err.message)
  );
}

function setAuthCookie(res, token) {
  res.cookie('token', token, {
    httpOnly: true,
    secure: isProd,
    sameSite: isProd ? 'none' : 'lax',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });
}

function publicUser(u) {
  return { id: u.id, name: u.name, email: u.email, role: u.role, emailVerified: u.emailVerified };
}

export const register = asyncHandler(async (req, res) => {
  const data = registerSchema.parse(req.body);
  const email = data.email.toLowerCase();

  const existing = await prisma.user.findUnique({ where: { email } });
  if (existing) {
    // A Google account owns this email — there's no password flow to verify.
    if (existing.googleId) {
      throw ApiError.conflict('This email uses Google sign-in. Continue with Google instead.');
    }
    // Already a fully registered (verified) account.
    if (existing.emailVerified) throw ApiError.conflict('Email already registered');
    // An unverified signup exists: let them retry with fresh details + a new code
    // instead of being permanently blocked by the half-finished account.
    const passwordHash = await bcrypt.hash(data.password, 12);
    await prisma.user.update({
      where: { id: existing.id },
      data: { name: data.name, passwordHash },
    });
    const code = await createOtp(email, 'EMAIL_VERIFY');
    await sendOtpEmail(email, code, 'EMAIL_VERIFY');
    return res.status(200).json({ success: true, requiresVerification: true, email });
  }

  const passwordHash = await bcrypt.hash(data.password, 12);
  await prisma.user.create({
    data: { name: data.name, email, passwordHash },
  });

  // Send the verification code. The account is NOT usable and the user is NOT logged in
  // until they verify the email, so we await the send and surface any failure.
  const code = await createOtp(email, 'EMAIL_VERIFY');
  await sendOtpEmail(email, code, 'EMAIL_VERIFY');

  res.status(201).json({ success: true, requiresVerification: true, email });
});

// Verify the email address with the emailed OTP, then send a welcome email.
export const verifyEmail = asyncHandler(async (req, res) => {
  const { email, code } = verifyEmailSchema.parse(req.body);
  const lower = email.toLowerCase();

  const user = await prisma.user.findUnique({ where: { email: lower } });
  if (!user) throw ApiError.badRequest('Account not found');
  if (user.emailVerified) {
    return res.json({ success: true, alreadyVerified: true, user: publicUser(user) });
  }

  const result = await verifyOtp(lower, 'EMAIL_VERIFY', code);
  if (!result.ok) throw ApiError.badRequest(otpErrorMessage(result.reason));

  const updated = await prisma.user.update({
    where: { id: user.id },
    data: { emailVerified: true },
  });
  fireAndForget(sendWelcomeEmail(updated.email, updated.name));

  // Verification is the moment the account becomes real — log them in now.
  const token = signToken({ sub: updated.id, role: updated.role });
  setAuthCookie(res, token);
  res.json({ success: true, token, user: publicUser(updated) });
});

// Resend an OTP (email verification or password reset). Generic response — never leak
// whether the email exists.
export const resendOtp = asyncHandler(async (req, res) => {
  const { email, purpose } = resendOtpSchema.parse(req.body);
  const lower = email.toLowerCase();
  const user = await prisma.user.findUnique({ where: { email: lower } });

  const shouldSend =
    user &&
    (purpose === 'PASSWORD_RESET' ? Boolean(user.passwordHash) : !user.emailVerified);

  if (shouldSend) {
    const code = await createOtp(lower, purpose);
    await sendOtpEmail(lower, code, purpose);
  }

  res.json({ success: true, message: 'If the email is registered, a new code has been sent.' });
});

// Start a password reset: email a PASSWORD_RESET OTP. Generic response to avoid leaking accounts.
export const forgotPassword = asyncHandler(async (req, res) => {
  const { email } = forgotPasswordSchema.parse(req.body);
  const lower = email.toLowerCase();
  const user = await prisma.user.findUnique({ where: { email: lower } });

  // Only password-based accounts can reset (Google accounts have no password).
  if (user && user.passwordHash) {
    const code = await createOtp(lower, 'PASSWORD_RESET');
    await sendOtpEmail(lower, code, 'PASSWORD_RESET');
  }

  res.json({ success: true, message: 'If that email is registered, a reset code has been sent.' });
});

// Complete a password reset with the OTP + new password.
export const resetPassword = asyncHandler(async (req, res) => {
  const { email, code, password } = resetPasswordSchema.parse(req.body);
  const lower = email.toLowerCase();

  const result = await verifyOtp(lower, 'PASSWORD_RESET', code);
  if (!result.ok) throw ApiError.badRequest(otpErrorMessage(result.reason));

  const user = await prisma.user.findUnique({ where: { email: lower } });
  if (!user) throw ApiError.badRequest('Account not found');

  const passwordHash = await bcrypt.hash(password, 12);
  await prisma.user.update({ where: { id: user.id }, data: { passwordHash } });
  fireAndForget(sendPasswordChangedEmail(lower));

  res.json({ success: true, message: 'Password updated. You can now log in.' });
});

export const login = asyncHandler(async (req, res) => {
  const data = loginSchema.parse(req.body);

  const user = await prisma.user.findUnique({ where: { email: data.email.toLowerCase() } });
  if (!user) throw ApiError.unauthorized('Invalid email or password');

  // Accounts created via Google have no password set.
  if (!user.passwordHash) {
    throw ApiError.unauthorized('This account uses Google sign-in. Continue with Google instead.');
  }

  const ok = await bcrypt.compare(data.password, user.passwordHash);
  if (!ok) throw ApiError.unauthorized('Invalid email or password');

  // A deactivated account cannot log in until a super admin reactivates it.
  // Only an explicit `false` blocks (undefined/null = active by default).
  if (user.isActive === false) {
    throw ApiError.forbidden('Your account has been deactivated. Please contact support.');
  }

  // Email verification is mandatory: an unverified account can't log in. Send a fresh
  // code and tell the client to route the user to the verification step.
  if (!user.emailVerified) {
    fireAndForget(
      createOtp(user.email, 'EMAIL_VERIFY').then((code) =>
        sendOtpEmail(user.email, code, 'EMAIL_VERIFY')
      )
    );
    throw new ApiError(403, 'Please verify your email to continue. We sent you a new code.', {
      needsVerification: true,
      email: user.email,
    });
  }

  const token = signToken({ sub: user.id, role: user.role });
  setAuthCookie(res, token);
  res.json({ success: true, token, user: publicUser(user) });
});

// Sign in / sign up with a Google ID token (credential) from Google Identity Services.
export const googleAuth = asyncHandler(async (req, res) => {
  const { credential } = googleAuthSchema.parse(req.body);

  if (!env.googleClientId) {
    throw ApiError.badRequest('Google sign-in is not configured on the server');
  }

  let payload;
  try {
    const ticket = await googleClient.verifyIdToken({
      idToken: credential,
      audience: env.googleClientId,
    });
    payload = ticket.getPayload();
  } catch {
    throw ApiError.unauthorized('Could not verify Google sign-in');
  }

  if (!payload?.email || !payload.email_verified) {
    throw ApiError.unauthorized('Google account email is not verified');
  }

  const email = payload.email.toLowerCase();
  const googleId = payload.sub;
  const name = payload.name || email.split('@')[0];

  let user = await prisma.user.findUnique({ where: { email } });
  if (user && user.isActive === false) {
    throw ApiError.forbidden('Your account has been deactivated. Please contact support.');
  }
  if (user) {
    // Link the Google identity to an existing local account on first Google login.
    // Google has already verified the email, so mark it verified too.
    if (!user.googleId || !user.emailVerified) {
      user = await prisma.user.update({
        where: { id: user.id },
        data: { googleId, emailVerified: true },
      });
    }
  } else {
    user = await prisma.user.create({ data: { email, name, googleId, emailVerified: true } });
  }

  const token = signToken({ sub: user.id, role: user.role });
  setAuthCookie(res, token);
  res.json({ success: true, token, user: publicUser(user) });
});

// ----- Self-service account management (the logged-in user editing their own account) -----

// Update the current user's display name.
export const updateProfile = asyncHandler(async (req, res) => {
  const { name } = updateProfileSchema.parse(req.body);
  const user = await prisma.user.update({
    where: { id: req.user.id },
    data: { name },
  });
  res.json({ success: true, user: publicUser(user) });
});

// Change password with the current password (distinct from the OTP-based reset flow).
export const changePassword = asyncHandler(async (req, res) => {
  const { currentPassword, newPassword } = changePasswordSchema.parse(req.body);

  const user = await prisma.user.findUnique({ where: { id: req.user.id } });
  if (!user) throw ApiError.unauthorized();

  // Google accounts have no password to verify against.
  if (!user.passwordHash) {
    throw ApiError.badRequest('This account uses Google sign-in and has no password to change.');
  }

  const ok = await bcrypt.compare(currentPassword, user.passwordHash);
  if (!ok) throw ApiError.badRequest('Your current password is incorrect.');

  const passwordHash = await bcrypt.hash(newPassword, 12);
  await prisma.user.update({ where: { id: user.id }, data: { passwordHash } });
  fireAndForget(sendPasswordChangedEmail(user.email));

  res.json({ success: true, message: 'Password updated.' });
});

// Step 1 of an email change: email a verification code to the NEW address.
export const requestEmailChange = asyncHandler(async (req, res) => {
  const { newEmail } = changeEmailRequestSchema.parse(req.body);
  const lower = newEmail.toLowerCase();

  if (lower === req.user.email.toLowerCase()) {
    throw ApiError.badRequest('That is already your email address.');
  }
  const existing = await prisma.user.findUnique({ where: { email: lower } });
  if (existing) throw ApiError.conflict('That email is already in use by another account.');

  const code = await createOtp(lower, 'EMAIL_VERIFY');
  await sendOtpEmail(lower, code, 'EMAIL_VERIFY');

  res.json({ success: true, message: 'We sent a verification code to your new email.' });
});

// Step 2 of an email change: verify the code, then switch the address over.
export const verifyEmailChange = asyncHandler(async (req, res) => {
  const { newEmail, code } = changeEmailVerifySchema.parse(req.body);
  const lower = newEmail.toLowerCase();

  // Guard against the address being taken between request and verify.
  const existing = await prisma.user.findUnique({ where: { email: lower } });
  if (existing && existing.id !== req.user.id) {
    throw ApiError.conflict('That email is already in use by another account.');
  }

  const result = await verifyOtp(lower, 'EMAIL_VERIFY', code);
  if (!result.ok) throw ApiError.badRequest(otpErrorMessage(result.reason));

  const user = await prisma.user.update({
    where: { id: req.user.id },
    data: { email: lower, emailVerified: true },
  });

  res.json({ success: true, user: publicUser(user) });
});

export const me = asyncHandler(async (req, res) => {
  res.json({ success: true, user: publicUser(req.user) });
});

export const logout = asyncHandler(async (_req, res) => {
  res.clearCookie('token');
  res.json({ success: true, message: 'Logged out' });
});
