import mongoose from 'mongoose';
import validator from 'validator';
import bcrypt from 'bcryptjs';

import { IUserDoc, IUserModel } from '@/modules/user/user.interfaces';
import { paginate, toJSON } from '@/shared/utils/plugins/index';
import {
  DeviceType,
  MemberType,
  Status,
  UserType,
} from '@/shared/constants/enum.constant';

const userSchema = new mongoose.Schema<IUserDoc, IUserModel>(
  {
    firstName: {
      type: String,
      required: true,
      trim: true,
    },
    lastName: {
      type: String,
      required: true,
      trim: true,
    },
    email: {
      type: String,
      required: true,
      trim: true,
      lowercase: true,
      validate(value: string) {
        if (!validator.isEmail(value)) throw new Error('Invalid email');
      },
    },
    password: {
      type: String,
      trim: true,
      minlength: 8,
      validate(value: string) {
        if (!value.match(/\d/) || !value.match(/[a-zA-Z]/))
          throw new Error(
            'Password must contain at least one letter and one number',
          );
      },
    },

    isEmailVerified: {
      type: Boolean,
      default: false,
    },
    phone: {
      dialCode: {
        type: Number,
        required: true,
      },
      number: {
        type: Number,
        required: true,
      },
    },
    team: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Team',
      },
    ],
    company: {
      id: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Company',
      },
      role: [
        {
          type: mongoose.Schema.Types.ObjectId,
          ref: 'Role',
        },
      ],
      _id: false,
    },
    companyType: {
      type: String,
    },

    subCompany: {
      id: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Company',
      },
      role: [
        {
          type: mongoose.Schema.Types.ObjectId,
          ref: 'Role',
        },
      ],
      _id: false,
    },
    reportingTo: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
    },
    joiningDate: {
      type: Date,
      default: Date.now,
    },
    status: {
      type: String,
      enum: Object.values(Status),
      default: Status.PENDING,
    },
    isDeleted: {
      type: Boolean,
      default: false,
    },
    memberType: {
      type: String,
      enum: Object.values(MemberType),
      default: MemberType.INTERNAL,
    },
    userType: {
      type: String,
      enum: UserType,
      default: UserType.CRMUSER,
    },

    deviceType: {
      type: String,
      enum: DeviceType,
      default: 'web',
    },
    deviceToken: {
      type: [String],
    },
    createdBy: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
    },
    updatedBy: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
    },
    lastPermissionUpdate: {
      type: Date,
      select: true,
    },

    resetTokenVersion: {
      type: Number,
    },
    myoperator: {
      uuid: {
        type: String,
      },
      extension: {
        type: Number,
      },
      syncedAt: {
        type: Date,
      },
      syncStatus: {
        type: String,
        default: 'not_synced',
      },
    },
  },
  {
    timestamps: true,
  },
);

// add plugin that converts mongoose to json
userSchema.plugin(toJSON);
userSchema.plugin(paginate);

userSchema.index(
  { 'phone.number': 1, 'company.id': 1 },
  { unique: true, partialFilterExpression: { status: Status.ACTIVE } },
);

userSchema.index(
  { 'email': 1, 'company.id': 1 },
  { unique: true, partialFilterExpression: { status: Status.ACTIVE } },
);

/**
 * Check if email is taken
 * @param {string} email - The user's email
 * @param {ObjectId} [excludeUserId] - The id of the user to be excluded
 * @returns {Promise<boolean>}
 */
userSchema.static(
  'isEmailTaken',
  async function (
    email: string,
    excludeUserId: mongoose.ObjectId,
  ): Promise<boolean> {
    const user = await this.findOne({
      email,
      status: Status.ACTIVE,
      _id: { $ne: excludeUserId },
    });
    return !!user;
  },
);

/**
 * Check if password matches the user's password
 * @param {string} password
 * @returns {Promise<boolean>}
 */
userSchema.method('isPasswordMatch', async function (password: string): Promise<
  boolean | void
> {
  const user = this;
  return bcrypt.compare(password, user.password);
});

userSchema.pre('save', async function (next) {
  const user = this;

  if (user.isModified('password'))
    user.password = await bcrypt.hash(user.password, 8);

  next();
});

userSchema.pre('findOneAndUpdate', async function (next) {
  const update = this.getUpdate();

  if (!update || Array.isArray(update) || typeof update !== 'object')
    return next();

  if ('lastPermissionUpdate' in update && !update.__allowLastPermissionUpdate)
    delete update.lastPermissionUpdate;

  const updateObj = {
    ...(typeof update.$set === 'object' ? update.$set : {}),
    ...update,
  };

  if (updateObj.password) {
    const hashed = await bcrypt.hash(updateObj.password, 8);

    this.setUpdate({
      ...update,
      $set: {
        ...(typeof update.$set === 'object' ? update.$set : {}),
        password: hashed,
      },
    });
  }

  next();
});

const User = mongoose.model<IUserDoc, IUserModel>('User', userSchema);

export default User;
