import type { Composition as CompositionType } from "./composition.schema"
import type {
  FrontendPayload,
  FrontendPayloadRequestData,
} from "@c/payload/payloadTypes"
import type { PluginFastifyInstance } from "types"
import type { Model } from "mongoose"
import { v4 as uuidv4, validate } from "uuid"
import { createCompositionPayload } from "./helpers/payload"

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

// Using a class to preserve 'this' context
export class CompositionService {
  private compositionModel: Model<CompositionType>

  constructor(model: Model<CompositionType>) {
    this.compositionModel = model
  }
  /**
   * Core function to create a composition (returns raw data)
   */
  async _createComposition(
    fastify: PluginFastifyInstance,
    compositionBody: CompositionBody,
    userId: string,
  ): Promise<CompositionType> {
    try {
      if (compositionBody._id) {
        const uuidValid = validate(compositionBody._id)

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

        const duplicate = await this.compositionModel.findOne({
          id: compositionBody._id,
        })

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

      const composition = new this.compositionModel({
        ...compositionBody,
        author: userId,
        lastEditor: userId,
      })

      await composition.save()

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

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

  /**
   * Create a new composition with payload wrapping and event emission
   */
  async createComposition(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    compositionBody: CompositionBody,
    userId: string,
  ): Promise<FrontendPayload<CompositionType[]>> {
    const composition = await this._createComposition(
      fastify,
      compositionBody,
      userId,
    )

    const payload = await createCompositionPayload(
      fastify,
      [composition],
      requestData,
    )

    fastify.emit("compositionCreated", payload)

    return payload
  }

  /**
   * Core function to get all compositions (returns raw data)
   */
  async _readAllCompositions(
    fastify: PluginFastifyInstance,
    filters: Record<string, any> = { isDeleted: false },
  ): Promise<CompositionType[]> {
    try {
      filters = { isDeleted: false, ...filters }
      return await this.compositionModel
        .find({
          ...filters,
        })
        .lean()
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Get all compositions with payload wrapping
   */
  async getAllCompositions(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    filters: Record<string, any> = {},
  ): Promise<FrontendPayload<CompositionType[]>> {
    const compositions = await this._readAllCompositions(fastify, filters)

    return createCompositionPayload(fastify, compositions, requestData)
  }

  /**
   * Core function to get a specific composition (returns raw data)
   */
  async _readComposition(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<CompositionType> {
    try {
      const composition = await this.compositionModel
        .findOne({ _id: id })
        .lean()

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

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

  /**
   * Get a specific composition with payload wrapping
   */
  async readComposition(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<CompositionType[]>> {
    const composition = await this._readComposition(fastify, id)

    return createCompositionPayload(fastify, [composition], requestData)
  }

  /**
   * Core function to update a composition (returns raw data)
   */
  async _updateComposition(
    fastify: PluginFastifyInstance,
    id: string,
    compositionBody: CompositionBody,
    userId: string,
  ): Promise<{ updated: CompositionType; original: CompositionType }> {
    try {
      const originalComposition = await this.compositionModel.findOne({
        _id: id,
      })

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

      // Remove protected fields
      delete compositionBody._id

      const updatedComposition = await this.compositionModel
        .findOneAndUpdate(
          { _id: id },
          {
            ...compositionBody,
            lastEditor: userId,
          },
          { new: true },
        )
        .lean()

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

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

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

  /**
   * Update a composition with payload wrapping and event emission
   */
  async updateComposition(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
    compositionBody: CompositionBody,
    userId: string,
  ): Promise<FrontendPayload<CompositionType[]>> {
    const { updated, original } = await this._updateComposition(
      fastify,
      id,
      compositionBody,
      userId,
    )

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

    payload.body.prevVersion = original

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

    return payload
  }

  /**
   * Core function to soft delete a composition (returns raw data)
   */
  async _removeComposition(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<CompositionType> {
    try {
      const composition = await this.compositionModel
        .findOneAndUpdate({ _id: id }, { isDeleted: true }, { new: true })
        .lean()

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

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

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

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

    payload.body.deleted = [composition._id]
    fastify.emit("compositionDeleted", payload)

    return payload
  }

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

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

      const res = await composition.deleteOne()

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

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

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

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

    payload.body.deleted = [composition._id]
    fastify.emit("compositionHardDeleted", payload)

    return payload
  }
  /**
   * Core function to create multiple compositions (returns raw data)
   */
  async _createCompositions(
    fastify: PluginFastifyInstance,
    compositionBodies: CompositionBody[],
    userId: string,
  ): Promise<CompositionType[]> {
    try {
      const compositionsToCreate: CompositionBody[] = []

      // Validate and prepare all compositions
      for (const compositionBody of compositionBodies) {
        if (compositionBody._id) {
          const uuidValid = validate(compositionBody._id)

          if (!uuidValid) {
            throw new Error(
              `Invalid ID - not a valid uuid v4: ${compositionBody._id}`,
            )
          }

          const duplicate = await this.compositionModel.findOne({
            _id: compositionBody._id,
          })

          if (duplicate) {
            throw new Error(
              `An entry with this id already exists: ${compositionBody._id}`,
            )
          }
        } else {
          compositionBody._id = uuidv4()
        }

        compositionsToCreate.push({
          ...compositionBody,
          author: userId,
          lastEditor: userId,
        })
      }

      // Bulk insert
      const compositions =
        await this.compositionModel.insertMany(compositionsToCreate)

      fastify.log.info(
        `[API] Created ${compositions.length} compositions in bulk`,
      )

      return compositions.map((comp) =>
        comp.toObject ? comp.toObject() : comp,
      )
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Create multiple compositions with payload wrapping and event emission
   */
  async createCompositions(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    compositionBodies: CompositionBody[],
    userId: string,
  ): Promise<FrontendPayload<CompositionType[]>> {
    const compositions = await this._createCompositions(
      fastify,
      compositionBodies,
      userId,
    )

    const payload = await createCompositionPayload(
      fastify,
      compositions,
      requestData,
    )

    fastify.emit("compositionsCreated", payload)

    return payload
  }
}

export const createCompositionService = (model: Model<CompositionType>) =>
  new CompositionService(model)
