import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import crypto from "crypto";
import speakeasy from "speakeasy";
import Admin from "../models/admin";
import User from "../models/users";
import { sendPasswordResetEmail } from "../utils/emailService";
import logger from "../utils/logger";

const ALLOWED_DOMAIN = "@eglogics.com";

// ─── Token Helpers ────────────────────────────────────────────────────────────

const generateAccessToken = (id: string, tokenVersion: number): string =>
  jwt.sign({ id, tokenVersion }, process.env.JWT_SECRET!, {
    expiresIn: (process.env.JWT_EXPIRES_IN || "15m") as string,
  } as jwt.SignOptions);

const generateRefreshToken = (id: string): string =>
  jwt.sign({ id }, process.env.REFRESH_TOKEN_SECRET!, {
    expiresIn: (process.env.REFRESH_TOKEN_EXPIRES_IN || "7d") as string,
  } as jwt.SignOptions);

const generateAdmin2FAChallengeToken = (id: string, email: string): string =>
  jwt.sign({ id, email, purpose: "2fa-login" }, process.env.JWT_SECRET!, {
    expiresIn: "10m",
  } as jwt.SignOptions);

const hashToken = (token: string): string =>
  crypto.createHash("sha256").update(token).digest("hex");

// ─── Helpers ──────────────────────────────────────────────────────────────────

const assertEglogicsEmail = (email: string): void => {
  if (!email.endsWith(ALLOWED_DOMAIN)) {
    const err = Object.assign(
      new Error("Only @eglogics.com email addresses are allowed"),
      { statusCode: 400 },
    );
    throw err;
  }
};

const authErr = (msg: string, code = 401): Error & { statusCode: number } =>
  Object.assign(new Error(msg), { statusCode: code });

const validatePassword = (password: string): void => {
  if (password.length < 8)
    throw authErr("Password must be at least 8 characters long", 400);
  if (!/[A-Z]/.test(password))
    throw authErr("Password must include at least 1 uppercase letter", 400);
  if (!/[0-9]/.test(password))
    throw authErr("Password must include at least 1 number", 400);
  if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
    throw authErr("Password must include at least 1 special character", 400);
};

// ─── Admin Auth ───────────────────────────────────────────────────────────────

export const registerAdmin = async (
  name: string,
  email: string,
  password: string,
) => {
  assertEglogicsEmail(email);
  validatePassword(password);
  const existing = await Admin.findOne({ email });
  if (existing) throw authErr("Admin already exists", 400);
  const hashed = await bcrypt.hash(password, 10);
  const admin = await Admin.create({ name, email, password: hashed });
  return {
    id: admin._id,
    name: admin.name,
    email: admin.email,
    role: admin.role,
  };
};

export const loginAdmin = async (email: string, password: string, ip?: string) => {
  const admin = await Admin.findOne({ email }).select("+password");
  if (!admin) {
    logger.warn("Admin login failed: invalid credentials", { email, ip, reason: "not_found" });
    throw authErr("Invalid credentials");
  }
  const isMatch = await bcrypt.compare(password, admin.password!);
  if (!isMatch) {
    logger.warn("Admin login failed: invalid credentials", { email, ip, reason: "wrong_password" });
    throw authErr("Invalid credentials");
  }

  if (admin.twoFactorEnabled) {
    logger.info("Admin login: 2FA challenge issued", { adminId: admin._id.toString(), email, ip });
    return {
      requires_2fa: true as const,
      challenge_token: generateAdmin2FAChallengeToken(
        admin._id.toString(),
        admin.email,
      ),
    };
  }

  const accessToken = generateAccessToken(
    admin._id.toString(),
    admin.tokenVersion,
  );
  const refreshToken = generateRefreshToken(admin._id.toString());
  admin.refreshToken = hashToken(refreshToken);
  await admin.save();
  logger.info("Admin login successful", { adminId: admin._id.toString(), email, ip });
  return {
    accessToken,
    refreshToken,
    admin: {
      id: admin._id,
      name: admin.name,
      email: admin.email,
      role: admin.role,
    },
  };
};

interface Admin2FAChallengePayload {
  id: string;
  email: string;
  purpose: "2fa-login";
}

export const verifyOtpLogin = async (challengeToken: string, code: string) => {
  let decoded: Admin2FAChallengePayload;

  try {
    decoded = jwt.verify(
      challengeToken,
      process.env.JWT_SECRET!,
    ) as Admin2FAChallengePayload;
  } catch (error) {
    const e = error as Error;
    if (e.name === "TokenExpiredError") {
      throw authErr("Challenge token expired", 400);
    }
    throw authErr("Invalid challenge token", 400);
  }

  if (decoded.purpose !== "2fa-login") {
    throw authErr("Invalid challenge token purpose", 400);
  }

  const admin = await Admin.findById(decoded.id);
  if (!admin) throw authErr("Admin not found", 404);
  if (admin.email !== decoded.email) {
    throw authErr("Invalid challenge token", 400);
  }
  if (!admin.twoFactorEnabled) {
    throw authErr("2FA is disabled for this admin", 400);
  }
  if (!admin.twoFactorSecret) {
    throw authErr("2FA secret not found", 400);
  }

  const verified = speakeasy.totp.verify({
    secret: admin.twoFactorSecret,
    encoding: "base32",
    token: code,
    window: 1,
  });

  if (!verified) {
    throw authErr("Invalid verification code", 400);
  }

  const accessToken = generateAccessToken(
    admin._id.toString(),
    admin.tokenVersion,
  );
  const refreshToken = generateRefreshToken(admin._id.toString());
  admin.refreshToken = hashToken(refreshToken);
  await admin.save();

  return {
    accessToken,
    refreshToken,
    admin: {
      id: admin._id,
      name: admin.name,
      email: admin.email,
      role: admin.role,
    },
  };
};

export const refreshAdminToken = async (refreshToken: string) => {
  if (!refreshToken) throw authErr("No refresh token provided");
  let decoded: { id: string };
  try {
    decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET!) as {
      id: string;
    };
  } catch {
    throw authErr("Invalid or expired refresh token");
  }
  const admin = await Admin.findById(decoded.id).select("+refreshToken");
  if (!admin || admin.refreshToken !== hashToken(refreshToken))
    throw authErr("Invalid refresh token");
  const newAccessToken = generateAccessToken(
    admin._id.toString(),
    admin.tokenVersion,
  );
  const newRefreshToken = generateRefreshToken(admin._id.toString());
  admin.refreshToken = hashToken(newRefreshToken);
  await admin.save();
  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
};

export const logoutAdmin = async (refreshToken: string): Promise<void> => {
  if (!refreshToken) return;
  let id: string | undefined;
  try {
    const decoded = jwt.verify(
      refreshToken,
      process.env.REFRESH_TOKEN_SECRET!,
    ) as { id: string };
    id = decoded.id;
  } catch {
    // Token expired but we can still decode to get the id
    const decoded = jwt.decode(refreshToken) as { id?: string } | null;
    id = decoded?.id;
  }
  if (id)
    await Admin.findByIdAndUpdate(id, {
      refreshToken: null,
      $inc: { tokenVersion: 1 },
    });
};

// ─── User Auth ────────────────────────────────────────────────────────────────

export const registerUser = async (
  name: string,
  email: string,
  password: string,
) => {
  assertEglogicsEmail(email);
  validatePassword(password);
  const existing = await User.findOne({ email });
  if (existing) throw authErr("User already exists", 400);
  const hashed = await bcrypt.hash(password, 10);
  const user = await User.create({ name, email, password: hashed });
  return { id: user._id, name: user.name, email: user.email, role: user.role };
};

export const loginUser = async (email: string, password: string, ip?: string) => {
  const user = await User.findOne({ email }).select("+password");
  if (!user) {
    logger.warn("User login failed: invalid credentials", { email, ip, reason: "not_found" });
    throw authErr("Invalid credentials");
  }
  if (!user.password) {
    logger.warn("User login failed: password not set", { email, ip, reason: "no_password" });
    throw authErr("Please set your password using the link sent to your email before logging in");
  }
  const isMatch = await bcrypt.compare(password, user.password!);
  if (!isMatch) {
    logger.warn("User login failed: invalid credentials", { email, ip, reason: "wrong_password" });
    throw authErr("Invalid credentials");
  }
  const accessToken = generateAccessToken(
    user._id.toString(),
    user.tokenVersion,
  );
  const refreshToken = generateRefreshToken(user._id.toString());
  user.refreshToken = hashToken(refreshToken);
  await user.save();
  logger.info("User login successful", { userId: user._id.toString(), email, ip });
  return {
    accessToken,
    refreshToken,
    user: { id: user._id, name: user.name, email: user.email, role: user.role, employee_id: user.employee_id, avatar: user.avatar },
  };
};

export const refreshUserToken = async (refreshToken: string) => {
  if (!refreshToken) throw authErr("No refresh token provided");
  let decoded: { id: string };
  try {
    decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET!) as {
      id: string;
    };
  } catch {
    throw authErr("Invalid or expired refresh token");
  }
  const user = await User.findById(decoded.id).select("+refreshToken");
  if (!user || user.refreshToken !== hashToken(refreshToken))
    throw authErr("Invalid refresh token");
  const newAccessToken = generateAccessToken(
    user._id.toString(),
    user.tokenVersion,
  );
  const newRefreshToken = generateRefreshToken(user._id.toString());
  user.refreshToken = hashToken(newRefreshToken);
  await user.save();
  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
};

export const logoutUser = async (refreshToken: string): Promise<void> => {
  if (!refreshToken) return;
  let id: string | undefined;
  try {
    const decoded = jwt.verify(
      refreshToken,
      process.env.REFRESH_TOKEN_SECRET!,
    ) as { id: string };
    id = decoded.id;
  } catch {
    // Token expired but we can still decode to get the id
    const decoded = jwt.decode(refreshToken) as { id?: string } | null;
    id = decoded?.id;
  }
  if (id)
    await User.findByIdAndUpdate(id, {
      refreshToken: null,
      $inc: { tokenVersion: 1 },
    });
};

// ─── Forgot / Reset Password ──────────────────────────────────────────────────

export const forgotPasswordUser = async (email: string): Promise<void> => {
  const user = await User.findOne({ email });
  // Always return silently — do not reveal whether email is registered
  if (!user) return;

  const rawToken = crypto.randomBytes(32).toString("hex");
  user.resetToken = hashToken(rawToken);
  user.resetTokenExpiry = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
  await user.save();

  const resetLink = `${process.env.USER_URL}/reset-password?token=${rawToken}`;
  try {
    await sendPasswordResetEmail(email, resetLink);
  } catch (error) {
    console.error("Failed to send user reset password email:", error);
  }
};

export const resetPasswordUser = async (
  rawToken: string,
  newPassword: string,
): Promise<void> => {
  const hashed = hashToken(rawToken);
  const user = await User.findOne({
    resetToken: hashed,
    resetTokenExpiry: { $gt: new Date() },
  }).select("+resetToken +resetTokenExpiry");
  if (!user) throw authErr("Invalid or expired reset token", 400);

  user.password = await bcrypt.hash(newPassword, 10);
  user.resetToken = null;
  user.resetTokenExpiry = null;
  await user.save();
};

export const forgotPasswordAdmin = async (email: string): Promise<void> => {
  const admin = await Admin.findOne({ email });
  if (!admin) return;

  const rawToken = crypto.randomBytes(32).toString("hex");
  admin.resetToken = hashToken(rawToken);
  admin.resetTokenExpiry = new Date(Date.now() + 15 * 60 * 1000);
  await admin.save();

  const resetLink = `${process.env.ADMIN_URL}/reset-password?token=${rawToken}`;
  try {
    await sendPasswordResetEmail(email, resetLink);
  } catch (error) {
    console.error("Failed to send admin reset password email:", error);
  }
};

export const resetPasswordAdmin = async (
  rawToken: string,
  newPassword: string,
): Promise<void> => {
  const hashed = hashToken(rawToken);
  const admin = await Admin.findOne({
    resetToken: hashed,
    resetTokenExpiry: { $gt: new Date() },
  }).select("+resetToken +resetTokenExpiry");
  if (!admin) throw authErr("Invalid or expired reset token", 400);

  admin.password = await bcrypt.hash(newPassword, 10);
  admin.resetToken = null;
  admin.resetTokenExpiry = null;
  await admin.save();
};
