import type {
  PublicUser,
  UserCreate,
  User as UserType,
  UserUpdate,
} from "./user.schema"
import type {
  FrontendPayload,
  FrontendPayloadRequestData,
} from "@c/payload/payloadTypes"
import type { QueryOptions } from "@m/database/providers/mongodb/service"
import type { PluginFastifyInstance } from "types"
import { v4 as uuidv4, validate } from "uuid"
import { createUserPayload } from "./helpers/payload"
import { NotFoundError } from "@/utils/errors"
import type { Model } from "mongoose"

export class UserService {
  private userModel: Model<UserType>

  constructor(model: Model<UserType>) {
    this.userModel = model
  }
  /**
   * Core function to create a user (returns raw data)
   */
  async _createUser(
    fastify: PluginFastifyInstance,
    userBody: UserCreate,
  ): Promise<UserType> {
    try {
      if (userBody._id) {
        const uuidValid = validate(userBody._id)

        if (!uuidValid) {
          throw new Error("Invalid ID - not a valid uuid v4")
        }

        const duplicate = await this.userModel.findById(userBody._id)

        if (duplicate) {
          throw new Error("An entry with this id already exists")
        }
      } else {
        userBody._id = uuidv4()
      }

      const user = new this.userModel(userBody)

      await user.save()

      fastify.log.info(`[API] Created user #${user._id}`)

      return user.toObject ? user.toObject() : user
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Create a new user with payload wrapping and event emission
   */
  async createUser(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    userBody: UserCreate,
  ): Promise<FrontendPayload<PublicUser[]>> {
    const user = await this._createUser(fastify, userBody)

    const payload = await createUserPayload(fastify, [user], requestData)

    fastify.emit("userCreated", payload)

    return payload
  }

  /**
   * Core function to read a specific user by ID (returns raw data)
   */
  async _readUser(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<UserType> {
    try {
      const user = await this.userModel.findById(id).lean()

      if (!user) {
        throw new NotFoundError(`User with ID ${id} not found`)
      }

      return user
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Read a specific user by ID with payload wrapping
   */
  async readUser(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<PublicUser[]>> {
    const user = await this._readUser(fastify, id)

    return createUserPayload(fastify, [user], requestData)
  }

  /**
   * Convenience method for email-based user lookup with password
   */
  async _readUserByEmail(email: string): Promise<PublicUser | null> {
    return await this.userModel
      .findOne({ email, isDeleted: false })
      .populate("account")
      .select("+password")
  }

  /**
   * Core function to read multiple users by filter (returns raw data)
   */
  async _readCount(fastify: PluginFastifyInstance): Promise<Number> {
    return await this.userModel.countDocuments({ isDeleted: false })
  }

  /**
   * Core function to read multiple users by filter (returns raw data)
   */
  async _readUsers(
    fastify: PluginFastifyInstance,
    filter: Record<string, any> = { isDeleted: false },
    options: QueryOptions = {},
  ): Promise<UserType[]> {
    try {
      if (filter.isDeleted !== true && filter.isDeleted !== "true")
        filter.isDeleted = false
      // Start building the query
      let query = this.userModel.find(filter)

      // Apply pagination if provided
      if (options.limit !== undefined) {
        query = query.limit(options.limit)
      }
      if (options.offset !== undefined) {
        query = query.skip(options.offset)
      }

      // Apply population if provided
      if (options.populate && Array.isArray(options.populate)) {
        options.populate.forEach((populateOption) => {
          query = query.populate(populateOption as any)
        })
      }

      // Execute query
      return await query.lean()
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Read multiple users by filter with payload wrapping
   */
  async readUsers(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    filter: Record<string, any> = {},
  ): Promise<FrontendPayload<PublicUser[]>> {
    const users = await this._readUsers(fastify, filter, requestData.options)

    return createUserPayload(fastify, users, requestData)
  }

  /**
   * Core function to update an existing user (returns raw data)
   */
  async _updateUser(
    fastify: PluginFastifyInstance,
    id: string,
    userBody: Partial<UserUpdate>,
  ): Promise<{ updated: UserType; original: UserType }> {
    try {
      const originalUser = await this.userModel.findById(id)

      if (!originalUser) {
        throw new NotFoundError(`User with ID ${id} not found`)
      }
      delete userBody._id
      // Handle password separately if provided
      if (userBody.password) {
        originalUser.password = userBody.password
        await originalUser.save()
        delete userBody.password
      }

      const updatedUser = await this.userModel.findByIdAndUpdate(
        id,
        { ...userBody },
        { new: true },
      )

      if (!updatedUser) {
        throw new Error(`Failed to update user with ID ${id}`)
      }

      fastify.log.info(`[API] Updated user #${updatedUser._id}`)

      return {
        updated: updatedUser.toObject ? updatedUser.toObject() : updatedUser,
        original: originalUser.toObject
          ? originalUser.toObject()
          : originalUser,
      }
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Update an existing user with payload wrapping and event emission
   */
  async updateUser(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
    userBody: Partial<UserUpdate>,
  ): Promise<FrontendPayload<PublicUser[]>> {
    const { updated, original } = await this._updateUser(fastify, id, userBody)

    const payload = await createUserPayload(fastify, [updated], requestData)

    payload.body.prevVersion = original

    if (requestData.broadcast) {
      fastify.emit("userUpdated", payload)
    }

    return payload
  }

  /**
   * Core function to soft delete a user (returns raw data)
   */
  async _removeUser(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<UserType> {
    try {
      const user = await this.userModel
        .findByIdAndUpdate(id, { isDeleted: true }, { new: true })
        .lean()

      if (!user) {
        throw new NotFoundError(`User with ID ${id} not found`)
      }

      return user
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Soft delete a user with payload wrapping and event emission
   */
  async removeUser(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<PublicUser[]>> {
    const user = await this._removeUser(fastify, id)

    const payload = await createUserPayload(fastify, [user], {
      ...requestData,
      type: "dataDelete",
    })

    payload.body.deleted = [user._id]

    fastify.emit("userDeleted", payload)

    return payload
  }

  /**
   * Core function to hard delete an interaction link (returns raw data)
   */
  async _hardDeleteUser(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<UserType> {
    try {
      const user = await this.userModel.findOne({
        _id: id,
        isDeleted: true,
      })

      if (!user) {
        throw new Error(
          `User with ID ${id} not found or not marked for deletion`,
        )
      }

      const res = await user.deleteOne()

      if (res?.deletedCount === 1) {
        return user.toObject ? user.toObject() : user
      }

      throw new Error(`Deletion of User with ID ${id} was not successful.`)
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }
  /**
   * Hard delete a user with payload wrapping and event emission
   */
  async hardDeleteUser(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<UserType[]>> {
    const user = await this._hardDeleteUser(fastify, id)

    const payload = await createUserPayload(fastify, [user], {
      ...requestData,
      type: "dataHardDeleted",
    })

    payload.body.deleted = [user._id]

    fastify.emit("userHardDeleted", payload)

    return payload
  }
  /**
   * Core function to update user's last accessed project (returns raw data)
   */
  async _updateLastProject(
    fastify: PluginFastifyInstance,
    userId: string,
    projectId: string,
  ): Promise<UserType> {
    try {
      const user = await this.userModel
        .findOneAndUpdate(
          { _id: userId },
          { "ux.lastProjectId": projectId },
          { new: true },
        )
        .lean()

      if (!user) {
        throw new NotFoundError(`User with ID ${userId} not found`)
      }

      fastify.log.info(
        `[API] Updated last project for user #${userId} to ${projectId}`,
      )

      return user
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Update user's last accessed project with payload wrapping
   */
  async updateLastProject(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    userId: string,
    projectId: string,
  ): Promise<FrontendPayload<PublicUser[]>> {
    const user = await this._updateLastProject(fastify, userId, projectId)

    return createUserPayload(fastify, [user], {
      ...requestData,
      type: "dataUpdate",
    })
  }
}

export const createUserService = (model: Model<UserType>) =>
  new UserService(model)
