import type { TagCreate, Tag as TagType, TagUpdate } from "./tag.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 { createTagPayload } from "./helpers/payload"
import type { Model } from "mongoose"
export type TagBody = Partial<Omit<TagType, "_id">> & {
  _id?: string
}
export class TagService {
  private tagModel: Model<TagType>

  constructor(model: Model<TagType>) {
    this.tagModel = model
  }
  /**
   * Core function to create a new tag (returns raw data)
   */
  async _createTag(
    fastify: PluginFastifyInstance,
    tagBody: TagCreate,
  ): Promise<TagType> {
    try {
      if (tagBody._id) {
        const uuidValid = validate(tagBody._id)

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

        const duplicate = await this.tagModel.findById(tagBody._id)

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

      const tag = new this.tagModel(tagBody)

      await tag.save()
      fastify.log.info(
        `[API] Created tag #${tag._id} in project #${tag.project}`,
      )

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

  /**
   * Create a new tag with payload wrapping and event emission
   */
  async createTag(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    tagBody: TagCreate,
  ): Promise<FrontendPayload<TagType[]>> {
    const tag = await this._createTag(fastify, tagBody)

    const payload = await createTagPayload(fastify, [tag], requestData)

    fastify.emit("tagCreated", payload)

    return payload
  }

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

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

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

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

    return createTagPayload(fastify, [tag], requestData)
  }

  /**
   * Core function to read a tag by name – used for internal system functions mainly
   */
  async _readTagByTitle(
    fastify: PluginFastifyInstance,
    title: string,
  ): Promise<TagType> {
    try {
      const tags = await this.tagModel.find({ title }).lean()

      if (!tags) {
        throw new Error(`Tag with title ${title} not found`)
      }

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

  /**
   * Core function to read tags by ID or filter parameters (returns raw data)
   */
  async _readTags(
    fastify: PluginFastifyInstance,
    filter: Record<string, any> = { isDeleted: false },
    options: QueryOptions = {},
  ): Promise<TagType[]> {
    try {
      filter = { isDeleted: false, ...filter }
      // Start building the query
      let query = this.tagModel.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 tags by ID or filter parameters with payload wrapping
   */
  async readTags(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    filter: { id?: string; projectId?: string; isDeleted?: boolean } = {
      isDeleted: false,
    },
  ): Promise<FrontendPayload<TagType[]>> {
    try {
      if (filter.id) {
        // Get single tag
        const tag = await this._readTag(fastify, filter.id)

        return createTagPayload(fastify, [tag], requestData)
      } else {
        const tags = await this._readTags(fastify, filter, requestData.options)

        return createTagPayload(fastify, tags, requestData)
      }
    } catch (error: any) {
      fastify.log.error(error.message)
      throw error
    }
  }

  /**
   * Core function to update an existing tag (returns raw data)
   */
  async _updateTag(
    fastify: PluginFastifyInstance,
    id: string,
    tagBody: TagUpdate,
    userId: string,
  ): Promise<{ updated: TagType; original: TagType }> {
    try {
      const originalTag = await this.tagModel.findById(id)

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

      const updatedTag = await this.tagModel.findByIdAndUpdate(
        id,
        { ...tagBody, updatedBy: userId },
        { new: true },
      )

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

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

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

  /**
   * Update an existing tag with payload wrapping and event emission
   */
  async updateTag(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
    tagBody: TagUpdate,
    userId: string,
  ): Promise<FrontendPayload<TagType[]>> {
    const isLockedTag = await this.tagModel
      .findOne({ _id: id, locked: true })
      .lean()

    if (isLockedTag) {
      throw new Error(`Tag with ID ${id} is locked and can't be updated`)
    }
    const { updated, original } = await this._updateTag(
      fastify,
      id,
      tagBody,
      userId,
    )

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

    payload.body.prevVersion = original

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

    return payload
  }

  /**
   * Core function to soft delete a tag (returns raw data)
   */
  async _removeTag(
    fastify: PluginFastifyInstance,
    id: string,
  ): Promise<TagType> {
    try {
      // TODO: Add an universal childCount check
      const tag = await this.tagModel
        .findByIdAndUpdate(id, { isDeleted: true }, { new: true })
        .lean()

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

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

  /**
   * Soft delete a tag with payload wrapping and event emission
   */
  async removeTag(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<TagType[]>> {
    const isLockedTag = await this.tagModel
      .findOne({ _id: id, locked: true })
      .lean()
    if (isLockedTag) {
      throw new Error(`Tag with ID ${id} is locked and can't be deleted`)
    }

    const tag = await this._removeTag(fastify, id)

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

    payload.body.deleted = [tag._id]

    fastify.emit("tagDeleted", payload)

    return payload
  }

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

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

      const res = await tag.deleteOne()

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

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

  /**
   * Hard delete a tag with payload wrapping and event emission
   */
  async hardDeleteTag(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    id: string,
  ): Promise<FrontendPayload<TagType[]>> {
    const isLockedTag = await this.tagModel
      .findOne({ _id: id, locked: true })
      .lean()
    if (isLockedTag) {
      throw new Error(`Tag with ID ${id} is locked and can't be hard deleted`)
    }
    const tag = await this._hardDeleteTag(fastify, id)

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

    payload.body.deleted = [tag._id]

    fastify.emit("tagHardDeleted", payload)

    return payload
  }
  /**
   * Core function to create multiple tags (returns raw data)
   */
  async _createTags(
    fastify: PluginFastifyInstance,
    tagBodies: TagBody[],
    userId: string,
  ): Promise<TagType[]> {
    try {
      const tagsToCreate: TagBody[] = []

      // Validate and prepare all tags
      for (const tagBody of tagBodies) {
        if (tagBody._id) {
          const uuidValid = validate(tagBody._id)

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

          const duplicate = await this.tagModel.findOne({
            _id: tagBody._id,
          })

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

        tagsToCreate.push({
          ...tagBody,
          author: userId,
          lastEditor: userId,
        })
      }

      // Bulk insert
      const tags = await this.tagModel.insertMany(tagsToCreate)

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

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

  /**
   * Create multiple tags with payload wrapping and event emission
   */
  async createTags(
    fastify: PluginFastifyInstance,
    requestData: FrontendPayloadRequestData,
    tagBodies: TagBody[],
    userId: string,
  ): Promise<FrontendPayload<TagType[]>> {
    const tags = await this._createTags(fastify, tagBodies, userId)

    const payload = await createTagPayload(fastify, tags, requestData)

    fastify.emit("tagsCreated", payload)

    return payload
  }
}

export const createTagService = (model: Model<TagType>) => new TagService(model)
