import httpStatus from 'http-status';
import mongoose, { PipelineStage } from 'mongoose';

import User from '@/modules/user/user.model';
import ApiError from '@/shared/utils/errors/ApiError';
import {
  IUserDoc,
  IUserPaginateResult,
  NewCreatedUser,
  UpdateUserBody,
  UserQuery,
} from '@/modules/user/user.interfaces';
import { PaginateOptions } from '@/shared/utils/plugins/paginate/paginate';
import { getObjectId, normalizeObjectIds } from '@/shared/utils/commonHelper';
import { TemplateName, CONSTANTS, EMAIL_FOR_TYPE } from '@/shared/constants';
import { Company } from '@/modules/company/company.model';
import { MemberType, Status, UserType } from '@/shared/constants/enum.constant';
import { Team } from '@/modules/teams/teams.model';
import { createSubCompany } from '@/modules/subCompany/subCompany.service';
import { sendSetPasswordEmail } from '@/modules/auth/auth.helper';
import { defaultStatus } from '@/shared/utils/responseCode/httpStatusAlias';
import { getEntityByIdWithQueryString } from '@/shared/utils/modelPopulateFields';
import { createBrevoContact } from '@/shared/brevo/brevoContactService';
import config from '@/shared/config/config';
import { sendEmailWithActiveTemplate } from '../communication/email/email.helper';
import { checkUserActivationUniqueness, createUserLookupStages } from './user.helper';
import { Tasks } from '../tasks/tasks.model';
import { getDateString } from '@/shared/utils/date';
import responseCodes from '@/shared/utils/responseCode/responseCode';

const { UserResponseCodes, CompanyResponseCodes } = responseCodes;

const checkActiveUserWithSameNumber = async (
  phoneNumber: number,
  dialCode: number,
  userId?: mongoose.Types.ObjectId,
) => {
  const query = {
    'phone.dialCode': dialCode,
    'phone.number': phoneNumber,
    status: CONSTANTS.ACTIVE,
  } as UserQuery;

  if (userId) query._id = { $ne: getObjectId(userId) };

  const existingActiveUserWithSameNumber = await User.findOne(query);

  if (existingActiveUserWithSameNumber)
    throw new ApiError(
      defaultStatus.BAD_REQUEST,
      'Phone number already associated with an active user.',
      true,
      '',
      UserResponseCodes.ACTIVE_USER_WITH_SAME_NUMBER_EXISTS,
    );
};

/**
 * Creates a new user in the system.
 *
 * @param {NewCreatedUser} userBody - The data of the user to be created.
 * @returns {Promise<IUserDoc & { emailSent: boolean }>} The created user document with an emailSent boolean.
 *
 */

export const createUser = async (
  userBody: NewCreatedUser,
): Promise<Partial<IUserDoc> & { emailSent: boolean }> => {
  try {
    const existingUserWithEmail = await User.findOne({
      email: userBody.email,
      status: CONSTANTS.ACTIVE,
    });

    if (existingUserWithEmail)
      throw new ApiError(
        httpStatus.OK,
        'Email already taken',
        true,
        '',
        UserResponseCodes.EMAIL_ALREADY_TAKEN,
      );

    const companyId = userBody.company?.id;

    const company = await Company.findById(companyId).select(
      'maxUserCount name companyType',
    );
    if (!company)
      throw new ApiError(
        httpStatus.OK,
        'Company not found',
        true,
        '',
        CompanyResponseCodes.COMPANY_NOT_FOUND,
      );

    // Add this check before calling User.create(userBody);

    const existingUserWithPhone = await User.findOne({
      $and: [
        { 'company.id': getObjectId(userBody.company.id) },
        { 'phone.number': userBody.phone.number },
      ],
    });

    if (existingUserWithPhone)
      throw new ApiError(
        httpStatus.CONFLICT,
        'Phone number already in use',
        true,
        '',
        UserResponseCodes.ALREADY_IN_USE_PHONE,
      );

    const currentActiveUserCount = await User.countDocuments({
      'company.id': company._id,
      status: CONSTANTS.ACTIVE,
    });

    if (company.maxUserCount && currentActiveUserCount >= company.maxUserCount)
      throw new ApiError(
        httpStatus.FORBIDDEN,
        `Company has reached the maximum allowed users (${company.maxUserCount})`,
        true,
        '',
        UserResponseCodes.MAX_USER_LIMIT_REACHED,
      );

    userBody.company.role = normalizeObjectIds(userBody.company?.role);
    userBody.team = normalizeObjectIds(userBody.team);
    userBody.companyType = company.companyType;

    const createdUser = await User.findById(userBody.createdBy).select(
      'firstName lastName',
    );

    await checkActiveUserWithSameNumber(
      userBody.phone.number,
      userBody.phone.dialCode,
    );

    // TODO: Handle revert user or compa if failed creation
    const user = await User.create(userBody);

    if (userBody.memberType === MemberType.EXTERNAL)
      await createSubCompany({
        name: userBody.subCompanyName,
        company: companyId,
      });

    if (Array.isArray(userBody.team))
      await Promise.all(
        userBody.team.map(async (teamId) => {
          await Team.findByIdAndUpdate(
            teamId,
            { $addToSet: { members: user._id } },
            { new: true },
          ).exec();
        }),
      );

    // Create brevo contact

    createBrevoContact({
      email: user.email,
      sms: `${user?.phone?.dialCode}${user?.phone?.number}`,
    });

    //TODO: Assign types for this
    const emailSent = await sendSetPasswordEmail({
      user: {
        id: user._id,
        firstName: user.firstName,
        lastName: user.lastName,
        userType: user.userType,
        email: user.email,
      },
      company: {
        id: company._id,
        name: company.name,
      },
      createdBy: {
        id: createdUser._id,
        firstName: createdUser.firstName,
        lastName: createdUser.lastName,
      },
      type: EMAIL_FOR_TYPE.SET_PASSWORD,
    });

    return {
      ...user.toObject(),
      emailSent,
    };
  } catch (error) {
    if (error instanceof ApiError) throw error;

    // Handle Mongoose validation error
    if (error?.name === 'ValidationError')
      throw new ApiError(
        httpStatus.BAD_REQUEST,
        error.message || 'Validation error',
        true,
        '',
        UserResponseCodes.VALIDATION_ERROR,
      );

    // Fallback for all other errors
    throw new ApiError(
      defaultStatus.INTERNAL_SERVER_ERROR,
      'Failed to create user',
      true,
      '',
      UserResponseCodes.CREATE_USER_ERROR,
    );
  }
};

/**
 * Query for users
 * @param {Object} filter - Mongo filter
 * @param {Object} options - Query options
 * @returns {Promise<QueryResult>}
 */

export const queryUsers = async (
  {
    company,
    userType,
    search,
    team,
    role,
    companyType,
    teamLeadId,
    isTeamLead,
    totalActiveUsers,
    ...rest
  }: Record<string, string>,
  options: PaginateOptions,
) => {
  // base AND-filters
  const base: Record<string, unknown> = {
    ...rest,
    ...(company && { 'company.id': getObjectId(company) }),
    ...(team && { team: getObjectId(team) }),
    ...(role && { 'company.role': getObjectId(role) }),
    ...(companyType && companyType !== 'all' && { companyType }),
    ...(search && {
      $or: [
        { firstName: { $regex: search, $options: 'i' } },
        { lastName: { $regex: search, $options: 'i' } },
        { email: { $regex: search, $options: 'i' } },
      ],
    }),
  };

  const defaultUserType =
    userType && userType !== UserType.SUPERADMIN
      ? userType
      : { $ne: UserType.SUPERADMIN };

  const or: Record<string, unknown>[] = [];
  if (userType === UserType.ADMIN) or.push({ userType: UserType.ADMIN });

  if (isTeamLead && teamLeadId) or.push({ _id: getObjectId(teamLeadId) }); // include a specific lead too

  const query: Record<string, unknown> =
    or.length > 0
      ? { ...base, $or: or }
      : { ...base, userType: defaultUserType };

  const pipeline: PipelineStage[] = [
    { $match: query },

    ...createUserLookupStages(),

    {
      $set: {
        isTeamLead: {
          $in: [
            '$_id',
            {
              $map: {
                input: { $ifNull: ['$team', []] },
                as: 't',
                in: '$$t.lead',
              },
            },
          ],
        },
      },
    },

    {
      $project: {
        firstName: 1,
        lastName: 1,
        email: 1,
        joiningDate: 1,
        status: 1,
        updatedAt: 1,
        company: 1,
        reportingTo: 1,
        team: 1,
        isTeamLead: 1,
        createdAt: 1,
        createdBy: 1,
        deviceType: 1,
        isDeleted: 1,
        isEmailVerified: 1,
        memberType: 1,
        phone: 1,
        subCompany: 1,
        updatedBy: 1,
        userType: 1,
        companyType: 1,
        companyDetails: 1,
        myoperator: 1,
      },
    },
  ];

  options.aggregation = pipeline;
  const user = (await User.paginate(query, options)) as IUserPaginateResult;

  if (totalActiveUsers)
    user.totalActiveUsers = await User.countDocuments({
      ...(company && { 'company.id': getObjectId(company) }),
      status: CONSTANTS.ACTIVE,
    });

  return user;
};

/**
 * Get user by id
 * @param {mongoose.Types.ObjectId} id
 * @returns {Promise<IUserDoc | null>}
 */
export const getUserById = async (
  id: mongoose.Types.ObjectId,
  fields?: string,
  populate?: string,
): Promise<IUserDoc | null> => {
  const user = await getEntityByIdWithQueryString({
    model: User,
    entityId: id,
    fields,
    populate,
    responseCode: UserResponseCodes.USER_NOT_FOUND,
  });

  return user;
};

/**
 * Get user by email
 * @param {string} email
 * @returns {Promise<IUserDoc | null>}
 */
export const getUserByEmail = async (
  email: string,
): Promise<IUserDoc | null> => {
  const user = await User.findOne({ email }).populate({
    path: 'company.id',
    select: 'companyType name pincode country state city area',
  });

  if (!user)
    throw new ApiError(
      httpStatus.OK,
      'User Not found',
      true,
      '',
      UserResponseCodes.USER_NOT_FOUND,
    );
  return user;
};

/**
 * Update user by id
 * @param {mongoose.Types.ObjectId} userId
 * @param {UpdateUserBody} updateBody
 * @returns {Promise<IUserDoc | null>}
 */
export const updateUserById = async (
  userId: mongoose.Types.ObjectId,
  updateBody: UpdateUserBody,
): Promise<IUserDoc | null> => {
  const user = await getUserById(userId);
  if (!user)
    throw new ApiError(
      httpStatus.OK,
      'User Not found',
      true,
      '',
      UserResponseCodes.USER_NOT_FOUND,
    );

  if (
    updateBody.status === Status.ACTIVE &&
    user.status !== Status.ACTIVE
  ) {
    await checkUserActivationUniqueness({
      userId,
      email: updateBody.email ?? user.email,
      phone: updateBody.phone ?? user.phone,
    });

    const companyId = user.company?.id;
    if (companyId) {
      const company = await Company.findById(companyId).select('maxUserCount');
      if (company?.maxUserCount) {
        const currentActiveUserCount = await User.countDocuments({
          'company.id': getObjectId(companyId),
          status: CONSTANTS.ACTIVE,
        });
        if (currentActiveUserCount >= company.maxUserCount)
          throw new ApiError(
            httpStatus.FORBIDDEN,
            `Company has reached the maximum allowed users (${company.maxUserCount})`,
            true,
            '',
            UserResponseCodes.MAX_USER_LIMIT_REACHED,
          );
      }
    }
  }

  if (updateBody.email && (await User.isEmailTaken(updateBody.email, userId)))
    throw new ApiError(
      httpStatus.OK,
      'Email already taken',
      true,
      '',
      UserResponseCodes.EMAIL_ALREADY_TAKEN,
    );

  if (updateBody?.phone?.number && updateBody?.phone?.dialCode)
    await checkActiveUserWithSameNumber(
      updateBody.phone.number,
      updateBody.phone.dialCode,
      userId,
    );

  const prevTeamIds = user.team.map((id) => id.toString());
  const newTeamIds = (updateBody.team || []).map((id) => id.toString());

  const removedTeamIds = prevTeamIds.filter((id) => !newTeamIds.includes(id));
  const addedTeamIds = newTeamIds.filter((id) => !prevTeamIds.includes(id));

  Object.assign(user, updateBody);
  await user.save();

  if (removedTeamIds.length > 0)
    await Team.updateMany(
      { _id: { $in: removedTeamIds } },
      { $pull: { members: userId } },
    );

  if (addedTeamIds.length > 0)
    await Team.updateMany(
      { _id: { $in: addedTeamIds } },
      { $addToSet: { members: userId } },
    );

  return user;
};

/**
 * Delete user by id
 * @param {mongoose.Types.ObjectId} userId
 * @returns {Promise<boolean | null>}
 */
export const deleteUserById = async (
  userId: mongoose.Types.ObjectId,
): Promise<boolean | null> => {
  const user = await getUserById(userId);
  if (!user)
    throw new ApiError(
      httpStatus.OK,
      'User Not found',
      true,
      '',
      UserResponseCodes.USER_NOT_FOUND,
    );

  if (user.status === CONSTANTS.INACTIVE)
    throw new ApiError(
      httpStatus.OK,
      'User is already inactive',
      true,
      '',
      UserResponseCodes.USER_ALREADY_INACTIVE,
    );

  await user.updateOne({ isDeleted: true, status: CONSTANTS.INACTIVE });
  return true;
};

export const resetPassword = async (
  userId: mongoose.Types.ObjectId,
  newPassword: string,
  oldPassword: string,
) => {
  try {
    const user = await getUserById(userId);
    if (!user)
      throw new ApiError(
        defaultStatus.OK,
        'User Not found',
        true,
        '',
        UserResponseCodes.USER_NOT_FOUND,
      );

    if (user.status === CONSTANTS.INACTIVE)
      throw new ApiError(
        defaultStatus.OK,
        'User is already inactive',
        true,
        '',
        UserResponseCodes.USER_ALREADY_INACTIVE,
      );

    if (!(await user.isPasswordMatch(oldPassword)))
      throw new ApiError(
        defaultStatus.OK,
        'Incorrect old password',
        true,
        '',
        UserResponseCodes.INCORRECT_OLD_PASSWORD,
      );

    if (await user.isPasswordMatch(newPassword))
      throw new ApiError(
        defaultStatus.OK,
        'New password is same as old password',
        true,
        '',
        UserResponseCodes.NEW_PASSWORD_SAME_AS_OLD_PASSWORD,
      );

    user.password = newPassword;
    await user.save();

    await sendEmailWithActiveTemplate({
      to: user.email,
      companyId: user.company.id,
      scenario: TemplateName.ResetPassword,
      templateParams: {
        fullName: `${user.firstName} ${user.lastName}`,
        loginUrl: `${config.clientUrl}${config.loginRoute}`,
      },
    });

    return true;
  } catch (error) {
    if (error instanceof ApiError) throw error;
  }
};

export const getSidebarAnalytics = async ({
  userId,
  company,
  userType,
}: {
  userId: mongoose.Types.ObjectId;
  company: mongoose.Types.ObjectId;
  userType: string;
}) => {
  try {
    const todayDate = getDateString(new Date(), 'YYYY-MM-DD');

    const taskFilterMatchStage =
      userType === 'admin'
        ? { companyId: getObjectId(company) }
        : {
            companyId: getObjectId(company),
            assignedTo: getObjectId(userId),
          };

    const pipeline = [
      {
        $match: {
          ...taskFilterMatchStage,
          status: {
            $in: ['pending', 'overdue'],
          },
          $expr: {
            $eq: [
              { $dateToString: { format: '%Y-%m-%d', date: '$activityDate' } },
              todayDate,
            ],
          },
        },
      },
      {
        $count: 'totalCount',
      },
    ];
    const taskCount = await Tasks.aggregate(pipeline);

    const count = taskCount.length > 0 ? taskCount[0].totalCount : 0;

    return {
      id: 'tasks',
      taskCount: count,
    };
  } catch (error) {
    console.error(error);
  }
};
