Source: models/user.mongo.js

import { Schema, model } from "mongoose";
import bcrypt from "bcrypt";

import logger from "../config/logger.js";
import {
  EMAIL_REGEX,
  isPasswordValidOrGoogleUser,
} from "../utils/validators.js";

/**
 * Schema for individual items in the user's cart.
 *
 * - Embedded directly in the user document for fast access.
 * - References a Product by ObjectId.
 * - Does not generate its own _id to keep the structure lean.
 */
const cartItemSchema = new Schema(
  {
    productId: {
      type: Schema.Types.ObjectId,
      ref: "Product",
      required: true,
    },
    quantity: {
      type: Number,
      required: true,
      min: 1,
    },
  },
  { _id: false }
);

/**
 *  User schema definition.
 *
 * Fields:
 * - email {String} required, unique, trimmed, lowercase
 * - password {String} required (hashed before save)
 * - role {String} enum: 'user' | 'admin'
 * - cartItems {Array<CartItem>} embedded for quick access
 * - previousOrders {Array<ObjectId>} references Order documents
 */
const userSchema = new Schema(
  {
    displayName: {
      type: String,
      trim: true,
      default: "",
    },
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
      match: EMAIL_REGEX,
    },
    googleId: {
      type: String,
    },
    password: {
      type: String,
      required: function () {
        return !this.googleId;
      },
      validate: {
        validator: function (value) {
          const isNewOrChanged = this.isModified("password");
          return !isNewOrChanged || isPasswordValidOrGoogleUser(value, this);
        },
        message: "Password required for local accounts",
      },
    },
    role: {
      type: String,
      enum: ["user", "admin"],
      default: "user",
    },
    cartItems: [cartItemSchema], // Embedded for fast access and frequent updates
    previousOrders: [
      {
        type: Schema.Types.ObjectId,
        ref: "Order", // Referenced for historical lookup and scalability
      },
    ],
  },
  { timestamps: true }
);

/**
 * Hash the password before saving the user.
 *
 * - Only hashes if the password field has been modified.
 * - Uses bcrypt with a salt round of 10.
 */
userSchema.pre("save", async function (next) {
  try {
    const shouldHash = this.isModified("password") && this.password;
    if (!shouldHash) return next();
    this.password = await bcrypt.hash(this.password, 10);
    next();
  } catch (error) {
    logger.error(`[users.mongo] Error hashing password: ${error.message}`);
    next(error);
  }
});

/**
 * Compare a candidate password with the stored hash.
 *
 * @param {string} candidatePassword - Plain text password to verify
 * @returns {Promise<boolean>} - Whether the password matches
 */
userSchema.methods.comparePassword = async function (candidatePassword) {
  try {
    return bcrypt.compare(candidatePassword, this.password);
  } catch (error) {
    logger.error(`[users.mongo] Error comparing password: ${error.message}`);
    throw error;
  }
};

userSchema.virtual("authProvider").get(function () {
  return this.googleId ? "google" : "local";
});

const User = model("User", userSchema);
export default User;