import bcrypt from "bcryptjs";
import { and, desc, eq, gt, isNull } from "drizzle-orm";
import type { Response } from "express";
import type {
  AuthenticatedRequest,
  AuthTokenResponse,
  OtpRequestResponse,
  RefreshTokenResponse,
  PublicCustomer,
  PublicStaffUser,
} from "../@types/auth.js";
import { db } from "../db/index.js";
import { authOtps, customers, staffUsers } from "../db/schema.js";
import {
  toPublicCustomer,
  toPublicStaffUser,
} from "../services/authSerializers.js";
import {
  generateOtpCode,
  hashOtpCode,
  OTP_EXPIRES_IN_MINUTES,
  otpExpiryDate,
} from "../services/authSecurity.js";
import { sendOtpEmail } from "../services/mailer.js";
import {
  createAuthSession,
  findValidRefreshSession,
  revokeAllActorSessions,
  revokeRefreshSession,
  rotateRefreshSession,
} from "../services/sessionService.js";
import { sendError, sendSuccess } from "../utils/apiResponse.js";
import type {
  ChangePasswordInput,
  CustomerRequestOtpInput,
  CustomerUpdateProfileInput,
  CustomerVerifyOtpInput,
  LogoutInput,
  RefreshTokenInput,
  StaffLoginInput,
  StaffRegisterInput,
  StaffRequestOtpInput,
  StaffVerifyOtpInput,
} from "../@types/authRequests.js";

const MAX_OTP_ATTEMPTS = 5;

const getRequestIp = (req: AuthenticatedRequest) =>
  req.ip || req.socket.remoteAddress || undefined;
const getRequestUserAgent = (req: AuthenticatedRequest) =>
  req.headers["user-agent"]?.slice(0, 512);

const createAndSendOtp = async ({
  actorType,
  email,
  purpose,
}: {
  actorType: "customer" | "staff";
  email: string;
  purpose: "signup" | "login";
}) => {
  const code = generateOtpCode();

  await db.insert(authOtps).values({
    actorType,
    email,
    codeHash: hashOtpCode(code),
    purpose,
    expiresAt: otpExpiryDate(),
  });

  const delivery = await sendOtpEmail({
    to: email,
    code,
    purpose: actorType === "customer" ? "Customer login" : "Staff verification",
    expiresInMinutes: OTP_EXPIRES_IN_MINUTES,
  });

  return { delivery, code };
};

const consumeValidOtp = async ({
  actorType,
  email,
  code,
  purpose,
}: {
  actorType: "customer" | "staff";
  email: string;
  code: string;
  purpose: "signup" | "login";
}) => {
  const [otp] = await db
    .select()
    .from(authOtps)
    .where(
      and(
        eq(authOtps.actorType, actorType),
        eq(authOtps.email, email),
        eq(authOtps.purpose, purpose),
        isNull(authOtps.consumedAt),
        gt(authOtps.expiresAt, new Date()),
      ),
    )
    .orderBy(desc(authOtps.createdAt))
    .limit(1);

  if (!otp) return false;

  if (otp.attempts >= MAX_OTP_ATTEMPTS) {
    await db
      .update(authOtps)
      .set({ consumedAt: new Date() })
      .where(eq(authOtps.id, otp.id));
    return false;
  }

  if (otp.codeHash !== hashOtpCode(code)) {
    await db
      .update(authOtps)
      .set({
        attempts: otp.attempts + 1,
        ...(otp.attempts + 1 >= MAX_OTP_ATTEMPTS
          ? { consumedAt: new Date() }
          : {}),
      })
      .where(eq(authOtps.id, otp.id));
    return false;
  }

  await db
    .update(authOtps)
    .set({ consumedAt: new Date() })
    .where(eq(authOtps.id, otp.id));
  return true;
};

const sendOtpRequestResponse = (
  res: Response,
  email: string,
  delivery: "email" | "console",
  code?: string,
) => {
  return sendSuccess<OtpRequestResponse>(
    res,
    200,
    {
      email,
      expiresInMinutes: OTP_EXPIRES_IN_MINUTES,
      delivery,
      ...(code ? { code } : {}),
    },
    delivery === "console"
      ? "OTP generated. Check the server console in development."
      : "OTP sent to email.",
  );
};

export const requestCustomerOtp = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  const body = req.validatedBody as CustomerRequestOtpInput;
  const { delivery, code } = await createAndSendOtp({
    actorType: "customer",
    email: body.email,
    purpose: "login",
  });
  return sendOtpRequestResponse(res, body.email, delivery, code);
};

export const verifyCustomerOtp = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  const body = req.validatedBody as CustomerVerifyOtpInput;
  const isValid = await consumeValidOtp({
    actorType: "customer",
    email: body.email,
    code: body.code,
    purpose: "login",
  });

  if (!isValid) {
    return sendError(res, 401, "INVALID_OTP", "Invalid or expired OTP.");
  }

  const [existingCustomer] = await db
    .select()
    .from(customers)
    .where(eq(customers.email, body.email))
    .limit(1);
  const now = new Date();

  if (existingCustomer) {
    await db
      .update(customers)
      .set({
        ...(body.name ? { name: body.name } : {}),
        ...(body.phoneNumber ? { phoneNumber: body.phoneNumber } : {}),
        emailVerifiedAt: existingCustomer.emailVerifiedAt ?? now,
        lastLoginAt: now,
      })
      .where(eq(customers.id, existingCustomer.id));
  } else {
    await db.insert(customers).values({
      email: body.email,
      name: body.name,
      phoneNumber: body.phoneNumber,
      emailVerifiedAt: now,
      lastLoginAt: now,
    });
  }

  const [customer] = await db
    .select()
    .from(customers)
    .where(eq(customers.email, body.email))
    .limit(1);
  if (!customer)
    return sendError(
      res,
      500,
      "CUSTOMER_AUTH_FAILED",
      "Could not authenticate customer.",
    );

  const publicCustomer = toPublicCustomer(customer);
  const session = await createAuthSession({
    actorType: "customer",
    actorId: publicCustomer.id,
    userAgent: getRequestUserAgent(req),
    ipAddress: getRequestIp(req),
  });

  return sendSuccess<AuthTokenResponse<PublicCustomer>>(
    res,
    200,
    { ...session, user: publicCustomer },
    "Customer authenticated.",
  );
};

export const registerStaff = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  const body = req.validatedBody as StaffRegisterInput;
  const [firstStaff] = await db
    .select({ id: staffUsers.id })
    .from(staffUsers)
    .limit(1);
  const isBootstrapRegistration = !firstStaff;

  if (!isBootstrapRegistration) {
    if (req.auth?.actorType !== "staff" || req.auth.staff.role !== "admin") {
      return sendError(
        res,
        403,
        "FORBIDDEN",
        "Only an admin can create staff accounts.",
      );
    }
  }

  const passwordHash = await bcrypt.hash(body.password, 12);

  try {
    await db.insert(staffUsers).values({
      name: body.name,
      email: body.email,
      passwordHash,
      role: isBootstrapRegistration ? "admin" : body.role,
      isActive: true,
    });
  } catch {
    return sendError(
      res,
      409,
      "STAFF_EXISTS",
      "A staff account with this email already exists.",
    );
  }

  const [staff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.email, body.email))
    .limit(1);
  if (!staff)
    return sendError(
      res,
      500,
      "STAFF_REGISTRATION_FAILED",
      "Could not create staff account.",
    );

  const { delivery, code } = await createAndSendOtp({
    actorType: "staff",
    email: body.email,
    purpose: "signup",
  });

  return sendSuccess(
    res,
    201,
    {
      user: toPublicStaffUser(staff),
      otp: {
        email: body.email,
        expiresInMinutes: OTP_EXPIRES_IN_MINUTES,
        delivery,
        code,
      },
    },
    isBootstrapRegistration
      ? "First admin account created. Verify OTP before using the account."
      : "Staff account created. OTP sent for verification.",
  );
};

export const loginStaff = async (req: AuthenticatedRequest, res: Response) => {
  const body = req.validatedBody as StaffLoginInput;
  const [staff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.email, body.email))
    .limit(1);

  const isPasswordValid = staff
    ? await bcrypt.compare(body.password, staff.passwordHash)
    : false;

  if (!staff || !staff.isActive || !isPasswordValid) {
    return sendError(
      res,
      401,
      "INVALID_CREDENTIALS",
      "Invalid email or password.",
    );
  }

  const { delivery, code } = await createAndSendOtp({
    actorType: "staff",
    email: body.email,
    purpose: staff.emailVerifiedAt ? "login" : "signup",
  });
  return sendOtpRequestResponse(res, body.email, delivery, code);
};

export const requestStaffOtp = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  const body = req.validatedBody as StaffRequestOtpInput;
  const [staff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.email, body.email))
    .limit(1);

  if (!staff || !staff.isActive) {
    return sendSuccess<OtpRequestResponse>(
      res,
      200,
      {
        email: body.email,
        expiresInMinutes: OTP_EXPIRES_IN_MINUTES,
        delivery: "email",
      },
      "If the staff account exists, an OTP will be sent.",
    );
  }

  const { delivery, code } = await createAndSendOtp({
    actorType: "staff",
    email: body.email,
    purpose: staff.emailVerifiedAt ? "login" : "signup",
  });
  return sendOtpRequestResponse(res, body.email, delivery, code);
};

export const verifyStaffOtp = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  const body = req.validatedBody as StaffVerifyOtpInput;
  const [staff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.email, body.email))
    .limit(1);

  if (!staff || !staff.isActive) {
    return sendError(res, 401, "INVALID_OTP", "Invalid or expired OTP.");
  }

  const purpose = staff.emailVerifiedAt ? "login" : "signup";
  const isValid = await consumeValidOtp({
    actorType: "staff",
    email: body.email,
    code: body.code,
    purpose,
  });

  if (!isValid) {
    return sendError(res, 401, "INVALID_OTP", "Invalid or expired OTP.");
  }

  const now = new Date();
  await db
    .update(staffUsers)
    .set({ emailVerifiedAt: staff.emailVerifiedAt ?? now, lastLoginAt: now })
    .where(eq(staffUsers.id, staff.id));

  const [updatedStaff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.id, staff.id))
    .limit(1);
  if (!updatedStaff)
    return sendError(
      res,
      500,
      "STAFF_AUTH_FAILED",
      "Could not authenticate staff account.",
    );

  const publicStaff = toPublicStaffUser(updatedStaff);
  const session = await createAuthSession({
    actorType: "staff",
    actorId: publicStaff.id,
    role: publicStaff.role,
    userAgent: getRequestUserAgent(req),
    ipAddress: getRequestIp(req),
  });

  return sendSuccess<AuthTokenResponse<PublicStaffUser>>(
    res,
    200,
    { ...session, user: publicStaff },
    "Staff authenticated.",
  );
};

export const refreshAuthToken = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  const body = req.validatedBody as RefreshTokenInput;
  const session = await findValidRefreshSession(body.refreshToken);

  if (!session) {
    return sendError(
      res,
      401,
      "INVALID_REFRESH_TOKEN",
      "Invalid or expired refresh token.",
    );
  }

  if (session.actorType === "customer") {
    const [customer] = await db
      .select()
      .from(customers)
      .where(eq(customers.id, session.actorId))
      .limit(1);

    if (!customer) {
      await revokeRefreshSession(body.refreshToken);
      return sendError(
        res,
        401,
        "INVALID_REFRESH_TOKEN",
        "Invalid or expired refresh token.",
      );
    }

    const rotated = await rotateRefreshSession({
      oldRefreshToken: body.refreshToken,
      actorType: "customer",
      actorId: customer.id,
      userAgent: getRequestUserAgent(req),
      ipAddress: getRequestIp(req),
    });

    return sendSuccess<RefreshTokenResponse>(
      res,
      200,
      rotated,
      "Token refreshed.",
    );
  }

  const [staff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.id, session.actorId))
    .limit(1);

  if (!staff || !staff.isActive) {
    await revokeRefreshSession(body.refreshToken);
    return sendError(
      res,
      401,
      "INVALID_REFRESH_TOKEN",
      "Invalid or expired refresh token.",
    );
  }

  const rotated = await rotateRefreshSession({
    oldRefreshToken: body.refreshToken,
    actorType: "staff",
    actorId: staff.id,
    role: staff.role,
    userAgent: getRequestUserAgent(req),
    ipAddress: getRequestIp(req),
  });

  return sendSuccess<RefreshTokenResponse>(
    res,
    200,
    rotated,
    "Token refreshed.",
  );
};

export const logout = async (req: AuthenticatedRequest, res: Response) => {
  const body = req.validatedBody as LogoutInput;
  await revokeRefreshSession(body.refreshToken);
  return sendSuccess(res, 200, null, "Logged out.");
};

export const logoutAll = async (req: AuthenticatedRequest, res: Response) => {
  if (!req.auth) {
    return sendError(res, 401, "UNAUTHENTICATED", "Authentication required.");
  }

  if (req.auth.actorType === "customer") {
    await revokeAllActorSessions("customer", req.auth.customer.id);
  } else {
    await revokeAllActorSessions("staff", req.auth.staff.id);
  }

  return sendSuccess(res, 200, null, "Logged out from all devices.");
};

export const changePassword = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  if (!req.auth || req.auth.actorType !== "staff") {
    return sendError(res, 401, "UNAUTHENTICATED", "Staff authentication required.");
  }

  const body = req.validatedBody as ChangePasswordInput;
  const [staff] = await db
    .select()
    .from(staffUsers)
    .where(eq(staffUsers.id, req.auth.staff.id))
    .limit(1);

  if (!staff) {
    return sendError(res, 404, "STAFF_NOT_FOUND", "Staff account not found.");
  }

  const isCurrentPasswordValid = await bcrypt.compare(body.currentPassword, staff.passwordHash);
  if (!isCurrentPasswordValid) {
    return sendError(res, 400, "INVALID_CURRENT_PASSWORD", "Current password is incorrect.");
  }

  const newPasswordHash = await bcrypt.hash(body.newPassword, 12);
  await db
    .update(staffUsers)
    .set({ passwordHash: newPasswordHash })
    .where(eq(staffUsers.id, staff.id));

  return sendSuccess(res, 200, null, "Password updated successfully.");
};

export const updateCustomerProfile = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  if (!req.auth || req.auth.actorType !== "customer") {
    return sendError(
      res,
      401,
      "UNAUTHENTICATED",
      "Customer authentication required.",
    );
  }

  const body = req.validatedBody as CustomerUpdateProfileInput;
  const customerId = req.auth.customer.id;

  if (!body.name && !body.phoneNumber) {
    return sendError(
      res,
      400,
      "NO_FIELDS",
      "At least one field (name or phoneNumber) must be provided.",
    );
  }

  await db
    .update(customers)
    .set({
      ...(body.name !== undefined ? { name: body.name } : {}),
      ...(body.phoneNumber !== undefined
        ? { phoneNumber: body.phoneNumber }
        : {}),
    })
    .where(eq(customers.id, customerId));

  const [updated] = await db
    .select()
    .from(customers)
    .where(eq(customers.id, customerId))
    .limit(1);

  if (!updated) {
    return sendError(res, 404, "NOT_FOUND", "Customer not found.");
  }

  return sendSuccess<PublicCustomer>(
    res,
    200,
    toPublicCustomer(updated),
    "Profile updated.",
  );
};

export const getCurrentUser = async (
  req: AuthenticatedRequest,
  res: Response,
) => {
  if (!req.auth)
    return sendError(res, 401, "UNAUTHENTICATED", "Authentication required.");
  return sendSuccess(res, 200, req.auth);
};
