import { Request, Response } from "express";
import UserModel from "../models/UserModel";
import AuthConfig from "../config/authConfig";
import { compare, hash } from "bcrypt";
import ResponseUtil from "../utils/Response/responseUtils";
import {
  autoLoginSchema,
  changePasswordSchema,
  changePasswordUserSchema,
  createProfileSchema,
  forgotPasswordSchema,
  loginSchema,
  logoutSchema,
  otpSendSchema,
  otpVerifySchema,
  signupSchema,
  socialLoginSchema,
  upsertProfileSchema,
  type DeviceInput,
} from "../validators/authValidators";
import { compareSync } from "bcrypt";
import { generateToken } from "../utils/Token";
import { randomInt } from "crypto";
import { sendEmail } from "../utils/SendEmail";
import {
  emailTemplateGeneric,
  otpEmailSubject,
} from "../utils/SendEmail/templates";
import { OtpModel } from "../models/OtpModel";
import { AUTH_CONSTANTS, VEHICLE_CONSTANTS } from "../constants/messages";
import { STATUS_CODES } from "../constants/statusCodes";
import { CustomRequest } from "../interfaces/auth";
import { IUser } from "../interfaces/models/userInterface";
import { DEVICETYPE, ROLE } from "../constants/enums";
import DeviceModel from "../models/DevicesModel";
import helper from "../helper";
import { startSession, Types } from "mongoose";
import {
  isOtpPasswordResetEmailReason,
  isOtpRegistrationEmailReason,
  OTP_SEND_REASON,
  resolveOtpPurpose,
} from "../constants/otp";
import { ERROR_CONSTANTS } from "../constants/errorConstants";
import { ensureStripeCustomerAtRegistration } from "../services/stripeCustomerService";
import GasStationModel from "../models/GasStationModel";
import { assertOtpValid, saveOtpForUser } from "../services/otpService";
import {
  stripeFlagsFromGasStation,
  trySyncGasStationStripeStatus,
} from "../services/gasStationStripeSync";
import {
  trySyncWalletUserStripeStatus,
  walletStripeFlagsFromUser,
} from "../services/walletStripeSync";
import { buildImageUrl } from "../utils/imageUrl";
import {
  uploadBufferToR2,
  uploadMulterFileToR2,
} from "../services/r2StorageService";
import fs from "fs";
import path from "path";

async function registerDevice(userId: unknown, device: DeviceInput) {
  return DeviceModel.findOneAndUpdate(
    { userId, deviceToken: device.device_token },
    {
      userId,
      deviceToken: device.device_token,
      deviceType: device.device_type,
      status: true,
    },
    { upsert: true, new: true },
  );
}

function otpSentMessage(reason: string): string {
  if (isOtpRegistrationEmailReason(reason)) {
    return AUTH_CONSTANTS.OTP_SENT_REGISTRATION;
  }
  if (isOtpPasswordResetEmailReason(reason)) {
    return AUTH_CONSTANTS.OTP_SENT_PASSWORD_RESET;
  }
  return AUTH_CONSTANTS.OTP_SENT;
}

async function resolveUserIdFromOtpRequest(email?: string, userId?: string) {
  if (userId && Types.ObjectId.isValid(userId)) {
    return new Types.ObjectId(userId);
  }
  if (email) {
    const user = await UserModel.findOne({ email });
    if (user) return user._id as Types.ObjectId;
  }
  return null;
}

export const signup = async (req: Request, res: Response) => {
  try {
    const { email, password, device } = await signupSchema.parseAsync(req.body);
    const userExist = await UserModel.findOne({
      email: email,
    });
    if (userExist) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        AUTH_CONSTANTS.USER_ALREADY_EXISTS,
      );
    }
    const hashPassword = await hash(password, String(AuthConfig.SALT));
    const user = await UserModel.create({
      email: email,
      password: hashPassword,
    });
    const otp = randomInt(100000, 999999);
    await saveOtpForUser(user._id, otp, OTP_SEND_REASON.REGISTRATION);
    await ensureStripeCustomerAtRegistration(String(user._id));
    const userOut = await UserModel.findById(user._id)
      .select("-password")
      .lean();
    const reason = OTP_SEND_REASON.REGISTRATION;
    const template = emailTemplateGeneric(otp, reason);
    await sendEmail(email, otpEmailSubject(reason), template);
    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {
        user: userOut ?? { email: user.email, _id: user._id },
        ...(device ? { device_registered: true } : {}),
      },
      AUTH_CONSTANTS.OTP_SENT_REGISTRATION,
    );
  } catch (error: any) {
    ResponseUtil.handleError(res, error);
  }
};

export const login = async (req: Request, res: Response) => {
  try {
    const { email, password, device } = await loginSchema.parseAsync(req.body);
    const user = await UserModel.findOne({ email });

    if (!user) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const hashpass = compareSync(password, String(user.password));

    if (!hashpass) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        AUTH_CONSTANTS.PASSWORD_MISMATCH,
      );
    }

    await ensureStripeCustomerAtRegistration(String(user._id));
    await trySyncGasStationStripeStatus(String(user._id));
    await trySyncWalletUserStripeStatus(String(user._id));

    const userOut = await UserModel.findById(user._id)
      .select("-password")
      .lean();
    if (!userOut) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    if (userOut.image) {
      (userOut as { image?: string | null }).image = buildImageUrl(
        userOut.image,
      );
    }

    const gasStation = await GasStationModel.findOne({
      userId: user._id,
    }).lean();
    const gasStationStripe = gasStation
      ? stripeFlagsFromGasStation(gasStation)
      : null;
    const walletStripe = walletStripeFlagsFromUser(userOut);

    const token = generateToken({
      email: email,
      id: String(user._id),
      role: userOut.userType,
    });

    const stripeOverlay = gasStationStripe ?? walletStripe;

    const userWithToken = {
      ...userOut,
      token,
      isStripeConnected: stripeOverlay.isStripeConnected,
      stripeStatus: stripeOverlay.stripeStatus,
      stripeConnected: stripeOverlay.stripeConnected,
      stripeChargesEnabled: stripeOverlay.stripeChargesEnabled,
      stripeDetailsSubmitted: stripeOverlay.stripeDetailsSubmitted,
    };

    if (!userOut.isVerified) {
      const otp = randomInt(100000, 999999);
      const reason = OTP_SEND_REASON.REGISTRATION;
      await saveOtpForUser(user._id, otp, reason);
      const template = emailTemplateGeneric(otp, reason);
      await sendEmail(email, otpEmailSubject(reason), template);

      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        AUTH_CONSTANTS.NOT_VERIFIED,
        userWithToken,
      );
    }

    if (device) {
      await registerDevice(user._id, device);
    }

    if (!userOut.isProfileCompleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        AUTH_CONSTANTS.INCOMPLETE_PROFILE,
        userWithToken,
      );
    }

    if (!userOut.isCarsRegistered) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.SUCCESS,
        VEHICLE_CONSTANTS.VEHICLE_NOT_FOUND,
        userWithToken,
      );
    }
    console.log(res);

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      userWithToken,
      AUTH_CONSTANTS.LOGGED_IN,
    );
  } catch (err) {
    ResponseUtil.handleError(res, err);
  }
};

export const upsertProfile = async (req: CustomRequest, res: Response) => {
  try {
    const validatedData = upsertProfileSchema.parse(req.body);
    const userId = req.userId;

    if (validatedData.device && userId) {
      await registerDevice(new Types.ObjectId(userId), validatedData.device);
    }

    let profileImageUrl: string | undefined;
    if (req.file) {
      profileImageUrl = (await uploadMulterFileToR2(req.file, "profiles")).url;
    }

    const updateData: Partial<IUser> = {
      ...(validatedData.fullName && { fullName: validatedData.fullName }),
      ...(validatedData.dob && { dob: validatedData.dob }),
      ...(validatedData.address && { address: validatedData.address }),
      ...(validatedData.phoneNumber && { phone: validatedData.phoneNumber }),
      ...(validatedData.location?.coordinates && {
        location: {
          type: "Point",
          coordinates: validatedData.location.coordinates,
          ...(validatedData.location.address !== undefined && {
            address: validatedData.location.address,
          }),
          ...(validatedData.location.label !== undefined && {
            label: validatedData.location.label,
          }),
        },
      }),
      ...(profileImageUrl && { image: profileImageUrl }),
      isProfileCompleted: true,
    };

    const updatedUser = await UserModel.findByIdAndUpdate(userId, updateData, {
      new: true,
      runValidators: true,
    }).select("-password -__v");

    if (!updatedUser) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        ERROR_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const token = generateToken({
      email: updatedUser.email,
      id: String(updatedUser._id),
      role: updatedUser.userType,
    });

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      validatedData.type ? { updatedUser, token } : updateData,
      validatedData.type == "create"
        ? AUTH_CONSTANTS.PROFILE_CREATED_SUCCESSFULLY
        : AUTH_CONSTANTS.PROFILE_UPDATED_SUCCESSFULLY,
    );
  } catch (err: any) {
    ResponseUtil.handleError(res, err);
  }
};

/**
 * GET /api/v1/user/viewUserProfile
 */
export const viewUserProfile = async (req: CustomRequest, res: Response) => {
  try {
    const userId = req.userId;
    if (!userId) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.UNAUTHORIZED,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    await trySyncGasStationStripeStatus(String(userId));
    await trySyncWalletUserStripeStatus(String(userId));

    const user = await UserModel.findById(userId)
      .select("-password -__v")
      .lean();

    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const gasStation = await GasStationModel.findOne({ userId }).lean();
    const stripeOverlay = gasStation
      ? stripeFlagsFromGasStation(gasStation)
      : walletStripeFlagsFromUser(user);

    const profile = {
      ...user,
      ...stripeOverlay,
    };

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      profile,
      AUTH_CONSTANTS.USER_DATA_FETCHED,
    );
  } catch (err: any) {
    ResponseUtil.handleError(res, err);
  }
};

/** POST /auth/send-otp — send verification code (reason optional, default registration). */
export const sendOtp = async (req: Request, res: Response) => {
  try {
    const { email, userId, reason } = await otpSendSchema.parseAsync(req.body);
    const uid = await resolveUserIdFromOtpRequest(email, userId);
    if (!uid) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const user = await UserModel.findById(uid);
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const code = randomInt(100000, 999999);
    await saveOtpForUser(uid, code, reason);
    const template = emailTemplateGeneric(code, reason);
    await sendEmail(user.email, otpEmailSubject(reason), template);
    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { userId: String(uid) },
      otpSentMessage(reason),
    );
  } catch (err) {
    ResponseUtil.handleError(res, err);
  }
};

/**
 * POST /auth/verify-otp — body: { userId, otp } only.
 * Flow comes from reason stored when OTP was sent (signup / login / send-otp / forgot-password).
 */
export const verifyOtp = async (req: Request, res: Response) => {
  try {
    const { userId, otp } = await otpVerifySchema.parseAsync(req.body);
    const uid = new Types.ObjectId(userId);

    const check = await assertOtpValid(uid, otp);
    if (!check.ok) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        check.message,
      );
    }

    const user = await UserModel.findById(uid);
    if (!user || user.isDeleted) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const purpose = resolveOtpPurpose(check.otpRes.reason, user.isVerified);

    if (isOtpPasswordResetEmailReason(purpose)) {
      const token = generateToken({
        email: user.email,
        id: userId,
        role: user.userType,
      });
      return ResponseUtil.successResponse(
        res,
        STATUS_CODES.SUCCESS,
        { userId, otpVerified: true, token },
        AUTH_CONSTANTS.OTP_CONFIRMED_PASSWORD_RESET,
      );
    }

    await UserModel.findByIdAndUpdate(uid, { isVerified: true });
    await OtpModel.deleteMany({ userId: uid });

    const token = generateToken({
      email: user.email,
      id: userId,
      role: user.userType,
    });

    await ensureStripeCustomerAtRegistration(userId);
    const userOut = await UserModel.findById(uid).select("-password").lean();
    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { token, user: userOut },
      AUTH_CONSTANTS.OTP_CONFIRMED,
    );
  } catch (err) {
    ResponseUtil.handleError(res, err);
  }
};

export const socialLogin = async (req: Request, res: Response) => {
  try {
    const { role, accessToken, provider, device } = socialLoginSchema.parse(
      req.body,
    );

    let profile;
    switch (provider) {
      case "google":
        profile = await helper.GeneralHelper.verifyGoogleToken(
          accessToken,
          device.device_type,
        );
        break;
      case "apple":
        profile = await helper.GeneralHelper.verifyAppleToken(accessToken);
        break;
      default:
        return ResponseUtil.errorResponse(
          res,
          STATUS_CODES.BAD_REQUEST,
          ERROR_CONSTANTS.UNSUPPORTED_PROVIDER,
        );
    }

    const { email, name, picture } = profile;

    if (!email) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        ERROR_CONSTANTS.EMAIL_NOT_FOUND_IN_SOCIAL_PROFILE,
      );
    }

    let user = await UserModel.findOne({ email });

    if (user?.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        AUTH_CONSTANTS.ACCOUNT_BANNED_ERROR,
      );
    }

    let savedImagePath: string | null = null;
    if (picture) {
      const localName = await helper.GeneralHelper.downloadAndSaveImage(
        picture,
        "./public/uploads",
      );
      if (localName) {
        try {
          const localPath = path.join("./public/uploads", localName);
          const buffer = fs.readFileSync(localPath);
          savedImagePath = (
            await uploadBufferToR2({
              buffer,
              contentType: "image/jpeg",
              folder: "profiles",
              filename: localName,
            })
          ).url;
        } catch (e) {
          console.error("R2 upload of social profile image failed:", e);
          savedImagePath = localName;
        }
      }
    }

    if (!user) {
      user = await UserModel.create({
        email,
        fullName: name,
        isVerified: true,
        userType: role,
        socialType: provider,
        image: savedImagePath,
      });
    } else if (user.userType !== role) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        `${ERROR_CONSTANTS.LOGIN_FAILURE_WITH_ROLE} ${user.userType}`,
      );
    }

    await registerDevice(user._id, device);

    await ensureStripeCustomerAtRegistration(String(user._id));
    const userOut = await UserModel.findById(user._id)
      .select("-password")
      .lean();

    const token = generateToken({
      id: String(user._id),
      email,
      role: String(user.userType),
    });

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { user: userOut ?? user, token },
      AUTH_CONSTANTS.LOGGED_IN,
    );
  } catch (err) {
    return ResponseUtil.handleError(res, err);
  }
};

export const deleteAccount = async (req: CustomRequest, res: Response) => {
  const session = await startSession();
  session.startTransaction();
  try {
    const userId = req.userId;

    const emailSplit = req.email?.split("@") ?? [];
    const user = await UserModel.findByIdAndUpdate(
      userId,
      {
        isDeleted: true,
        email: `deleted_user_${userId}@${emailSplit[1] || "unknown.com"}`,
        deletedEmail: req.email,
      },
      { new: true },
    );

    if (!user) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        ERROR_CONSTANTS.USER_NOT_FOUND,
      );
    }

    await helper.GeneralHelper.handleDeleteUser(userId as string);

    const deviceTokens = await DeviceModel.find({ userId: userId });
    if (deviceTokens.length > 0) {
      await helper.GeneralHelper.unlinkUserDevices(userId as string);
    }
    await session.commitTransaction();
    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {},
      AUTH_CONSTANTS.ACCOUNT_DELETED,
    );
  } catch (error) {
    await session.abortTransaction();
    return ResponseUtil.handleError(res, error);
  } finally {
    await session.endSession();
  }
};

export const forgotPassword = async (req: Request, res: Response) => {
  try {
    const { email } = await forgotPasswordSchema.parseAsync(req.body);

    const user = await UserModel.findOne({ email });

    if (!user) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        ERROR_CONSTANTS.USER_NOT_FOUND,
      );
    }
    if (user?.isBanned) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        ERROR_CONSTANTS.BANNED_ACCOUNT_CONTACT_SUPPORT,
      );
    }

    const otp = randomInt(100000, 999999);
    const reason = OTP_SEND_REASON.FORGOT_PASSWORD;
    await saveOtpForUser(user._id, otp, reason);
    const template = emailTemplateGeneric(otp, reason);
    await sendEmail(email, otpEmailSubject(reason), template);

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { userId: String(user._id) },
      AUTH_CONSTANTS.OTP_SENT_PASSWORD_RESET,
    );
  } catch (error) {
    ResponseUtil.handleError(res, error);
  }
};

/** POST /api/v1/user/changePassword — body: { password, confirm_password } + Bearer JWT. */
export const changePasswordUser = async (req: CustomRequest, res: Response) => {
  try {
    const { password } = changePasswordUserSchema.parse(req.body);
    const uid = req.userId;

    if (!uid) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.UNAUTHORIZED,
        "Authorization Bearer token is required",
      );
    }

    const user = await UserModel.findOne({
      _id: uid,
      isDeleted: { $ne: true },
    });
    if (!user) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        ERROR_CONSTANTS.USER_NOT_FOUND,
      );
    }

    user.password = await hash(password, Number(AuthConfig.SALT));
    if (!user.isVerified) {
      user.isVerified = true;
    }
    await user.save();
    await OtpModel.deleteMany({ userId: new Types.ObjectId(uid) });

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {},
      AUTH_CONSTANTS.PASSWORD_CHANGED,
    );
  } catch (error: unknown) {
    return ResponseUtil.handleError(res, error);
  }
};

/** POST /api/v1/auth/change-password — Bearer JWT + { oldPassword, newPassword }. */
export const changePassword = async (req: CustomRequest, res: Response) => {
  try {
    const { oldPassword, newPassword } = changePasswordSchema.parse(req.body);
    const uid = req.userId;

    if (!uid) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.UNAUTHORIZED,
        "Authorization Bearer token is required",
      );
    }

    const user = await UserModel.findOne({
      _id: uid,
      isDeleted: { $ne: true },
    });
    if (!user) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        ERROR_CONSTANTS.USER_NOT_FOUND,
      );
    }

    const isMatch = await compare(oldPassword, String(user.password));
    if (!isMatch) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.BAD_REQUEST,
        ERROR_CONSTANTS.OLD_PASSWORD_INCORRECT,
      );
    }

    user.password = await hash(newPassword, Number(AuthConfig.SALT));
    await user.save();

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {},
      AUTH_CONSTANTS.PASSWORD_CHANGED,
    );
  } catch (error: unknown) {
    return ResponseUtil.handleError(res, error);
  }
};

export const autoLogin = async (req: CustomRequest, res: Response) => {
  try {
    const userId = req.userId;
    const { device } = await autoLoginSchema.parseAsync(req.body);
    const user: any = await UserModel.findById(userId);

    if (user?.isDeleted)
      throw new Error(ERROR_CONSTANTS.ACCOUNT_ALREADY_DELETED);
    if (user?.isBanned) throw new Error(ERROR_CONSTANTS.ACCOUNT_SUSPENDED);

    if (device) {
      await registerDevice(userId as string, device);
    }

    const token = generateToken({
      email: String(user.email),
      id: String(userId),
      role: user.userType,
    });
    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      { user, token },
      AUTH_CONSTANTS.USER_DATA_FETCHED,
    );
  } catch (err) {
    ResponseUtil.handleError(res, err);
  }
};

export const logout = async (req: CustomRequest, res: Response) => {
  try {
    const { device } = await logoutSchema.parseAsync(req.body);

    const updatedDeviceToken = await DeviceModel.findOneAndUpdate(
      { deviceToken: device.device_token, userId: req.userId },
      { status: false },
      { new: true },
    );

    if (!updatedDeviceToken) {
      return ResponseUtil.errorResponse(
        res,
        STATUS_CODES.NOT_FOUND,
        AUTH_CONSTANTS.DEVICE_TOKEN_NOT_FOUND,
      );
    }

    return ResponseUtil.successResponse(
      res,
      STATUS_CODES.SUCCESS,
      {},
      AUTH_CONSTANTS.LOGGED_OUT,
    );
  } catch (err) {
    ResponseUtil.handleError(res, err);
  }
};
