import type {
  ProjectCreate,
  Project as ProjectType,
  ProjectUpdate,
} from "./project.schema"
import type {
  FrontendPayload,
  FrontendPayloadRequestData,
} from "@c/payload/payloadTypes"
import type { QueryOptions } from "@m/database/providers/mongodb/service"
import { v4 as uuidv4, validate } from "uuid"
import { createProjectPayload } from "./helpers/payload"
import type { Model } from "mongoose"
import type { PluginFastifyInstance } from "types"

export type ProjectBody = Partial<Omit<ProjectType, "_id">> & { _id?: string }

// Using a class to preserve 'this' context
export class ProjectService {
  private projectModel: Model<ProjectType>

  constructor(model: Model<ProjectType>) {
    this.projectModel = model
  }
  /**
   * Core function to create a new project (returns raw data)
   */
  async _createProject(
    fastify: PluginFastifyInstance,
    projectBody: ProjectCreate,
  ): Promise<ProjectType> {
    try {
      if (projectBody._id) {
        const uuidValid = validate(projectBody._id)

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

        const duplicate = await this.projectModel.findById(projectBody._id)

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

      const project = new this.projectModel(projectBody)

      await project.save()

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

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

  /**
   * Create a new project with payload wrapping and event emission
   */
  async createProject(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    projectBody: ProjectCreate,
  ): Promise<FrontendPayload<ProjectType[]>> {
    const project = await this._createProject(fastify, projectBody)

    const payload = await createProjectPayload(fastify, [project], requestData)

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

    return payload
  }

  /**
   * Core function to read all projects (returns raw data)
   */
  async _readProjects(
    fastify: PluginFastifyInstance,
    filter: Record<string, any> = { isDeleted: false },
    options: QueryOptions = {},
  ): Promise<ProjectType[]> {
    try {
      filter = { isDeleted: false, ...filter }
      // Start building the query
      let query = this.projectModel.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 all projects with payload wrapping
   */
  async readProjects(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    filter?: Record<string, any>,
    options: QueryOptions = {},
  ): Promise<FrontendPayload<ProjectType[]>> {
    const projects = await this._readProjects(fastify, filter, options)

    return createProjectPayload(fastify, projects, requestData)
  }

  /**
   * Core function to get a specific project (returns raw data)
   */
  async _readProject(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<ProjectType> {
    try {
      const project = await this.projectModel.findById(id).lean()

      if (!project) {
        throw new Error(`Project with ID ${id} not found`)
      }

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

  /**
   * Core function to the main project (used until spaces are coming)
   */
  async _readMainProject(fastify: PluginFastifyInstance): Promise<ProjectType> {
    try {
      const projects = await this.projectModel.find({ isDefault: true }).lean()

      if (!projects.length) {
        throw new Error(`No main project found. This is a crucial error!`)
      }

      return projects[0]
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Get a specific project with payload wrapping
   */
  async readProject(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<ProjectType[]>> {
    const project = await this._readProject(fastify, id)

    return createProjectPayload(fastify, [project], requestData)
  }

  /**
   * Core function to update an existing project (returns raw data)
   */
  async _updateProject(
    fastify: PluginFastifyInstance,
    id: string,
    projectBody: Partial<ProjectUpdate>,
    userId: string,
  ): Promise<{ updated: ProjectType; original: ProjectType }> {
    try {
      const originalProject = await this.projectModel.findById(id)

      if (!originalProject) {
        throw new Error(`Project with ID ${id} not found`)
      }

      // Remove protected fields
      delete projectBody._id
      delete projectBody.account

      const updatedProject = await this.projectModel
        .findByIdAndUpdate(
          id,
          { ...projectBody, updatedBy: userId },
          { new: true },
        )
        .lean()

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

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

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

  /**
   * Update an existing project with payload wrapping and event emission
   */
  async updateProject(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
    projectBody: Partial<ProjectUpdate>,
    userId: string,
  ): Promise<FrontendPayload<ProjectType[]>> {
    const { updated, original } = await this._updateProject(
      fastify,
      id,
      projectBody,
      userId,
    )

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

    payload.body.prevVersion = original

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

    return payload
  }

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

      if (!project) {
        throw new Error(`Project with ID ${id} not found`)
      }

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

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

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

    payload.body.deleted = [project._id]
    fastify.emit("projectDeleted", payload)

    return payload
  }

  /**
   * Core function to hard delete a project (returns raw data)
   */
  async _hardDeleteProject(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<ProjectType> {
    try {
      const project = await this.projectModel.findOne({
        _id: id,
        isDeleted: true,
      })

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

      const res = await project.deleteOne()

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

      throw new Error(`Deletion of Project with ID ${id} was not successful.`)
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Hard delete a project with payload wrapping and event emission
   */
  async hardDeleteProject(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<ProjectType[]>> {
    const project = await this._hardDeleteProject(fastify, id)

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

    payload.body.deleted = [project._id]
    fastify.emit("projectHardDeleted", payload)

    return payload
  }
}

export const createProjectService = (model: Model<ProjectType>) =>
  new ProjectService(model)
