import { Injectable } from "@nestjs/common"
import { CreateAppContentDto } from "./dto/create-app-content.dto"
import { UpdateAppContentDto } from "./dto/update-app-content.dto"
import { AppContentFilterDto } from "./dto/app-content-filter.dto"
import { successResponse, failureResponse } from "src/common/response/response"
import { code } from "src/common/response/response.code"
import {
  successMessage,
  errorMessage,
  validationMessage,
} from "src/utils/helpers"
import { messageKey } from "src/constants/message-keys"
import { isEmpty } from "class-validator"
import { AppContentRepository } from "./repository/app-content.repository"
import {
  CONTENT_FORMAT,
  CONTENT_FORMAT_VALUES,
} from "src/constants/app-content.constant"

const DATA_LABEL = "App Content"
const REF_NO_MIN_LENGTH = 3

@Injectable()
export class AppContentService {
  constructor(private readonly appContentRepository: AppContentRepository) {}

  /**
   * Ref numbers are stored zero padded ("001"), so the highest one is found by
   * comparing them as numbers rather than as text. Soft-deleted rows are
   * included because a ref number must never be handed out twice.
   */
  private async getNextFieldRefNo(): Promise<string> {
    const records: any = await this.appContentRepository.getByParams({
      withDeleted: true,
    })

    const list = Array.isArray(records) ? records : records?.data || []

    const highest = list.reduce((max: number, item: any) => {
      const value = Number(item.field_ref_no)
      return Number.isFinite(value) && value > max ? value : max
    }, 0)

    return String(highest + 1).padStart(REF_NO_MIN_LENGTH, "0")
  }

  /**
   * Rich text editors submit markup such as "<p><br></p>" for an empty field,
   * so emptiness is judged on the text content rather than the raw string.
   */
  private hasContent(value?: string) {
    if (isEmpty(value)) {
      return false
    }
    return (
      `${value}`
        .replace(/<[^>]*>/g, "")
        .replace(/&nbsp;/g, " ")
        .trim().length > 0
    )
  }

  private validateFormat(field: string, value?: string) {
    if (!isEmpty(value) && !CONTENT_FORMAT_VALUES.includes(value)) {
      return validationMessage(messageKey.invalid, { ":field": field })
    }
    return null
  }

  async create(createDto: CreateAppContentDto) {
    try {
      if (
        !this.hasContent(createDto.title) &&
        !this.hasContent(createDto.description)
      ) {
        return failureResponse(
          code.SUCCESS,
          validationMessage(messageKey.either_field_required, {
            ":field1": "title",
            ":field2": "description",
          }),
        )
      }

      const titleFormatError = this.validateFormat(
        "title_format",
        createDto.title_format,
      )
      if (titleFormatError) {
        return failureResponse(code.SUCCESS, titleFormatError)
      }

      const descriptionFormatError = this.validateFormat(
        "description_format",
        createDto.description_format,
      )
      if (descriptionFormatError) {
        return failureResponse(code.SUCCESS, descriptionFormatError)
      }

      const fieldRefNo = await this.getNextFieldRefNo()

      await this.appContentRepository.save({
        field_ref_no: fieldRefNo,
        title: this.hasContent(createDto.title) ? createDto.title : null,
        title_format: createDto.title_format || CONTENT_FORMAT.TEXT,
        description: this.hasContent(createDto.description)
          ? createDto.description
          : null,
        description_format: createDto.description_format || CONTENT_FORMAT.TEXT,
      })

      return successResponse(
        code.SUCCESS,
        successMessage(messageKey.data_add, { ":data": DATA_LABEL }),
      )
    } catch (error) {
      return failureResponse(
        code.ERROR,
        errorMessage(messageKey.validation_error, { ":data": DATA_LABEL }),
      )
    }
  }

  async findAll(filterDto: AppContentFilterDto) {
    try {
      const { take, skip, search } = filterDto

      // Ordering by id keeps the true numeric sequence, which a zero padded
      // text column would break once the numbers reach four digits.
      const params: any = {
        orderBy: { id: "ASC" },
      }

      if (!isEmpty(take)) {
        params.take = take
        params.skip = skip
      }

      if (!isEmpty(search)) {
        params["search"] = {
          field_ref_no: search,
          title: search,
          description: search,
        }
      }

      const result: any = await this.appContentRepository.getByParams(params)

      return successResponse(
        code.SUCCESS,
        successMessage(messageKey.data_retrieve, { ":data": DATA_LABEL }),
        result,
      )
    } catch (error) {
      return failureResponse(
        code.ERROR,
        errorMessage(messageKey.validation_error, { ":data": DATA_LABEL }),
      )
    }
  }

  async findOne(id: number) {
    try {
      const record: any = await this.appContentRepository.getByParams({
        where: { id },
        findOne: true,
      })

      if (!record) {
        return failureResponse(
          code.SUCCESS,
          errorMessage(messageKey.data_not_found, { ":data": DATA_LABEL }),
        )
      }

      return successResponse(
        code.SUCCESS,
        successMessage(messageKey.detail_found, { ":data": DATA_LABEL }),
        record,
      )
    } catch (error) {
      return failureResponse(
        code.ERROR,
        errorMessage(messageKey.validation_error, { ":data": DATA_LABEL }),
      )
    }
  }

  async update(id: number, updateDto: UpdateAppContentDto) {
    try {
      const record = await this.appContentRepository.getByParams({
        where: { id },
        findOne: true,
      })

      if (!record) {
        return failureResponse(
          code.SUCCESS,
          errorMessage(messageKey.data_not_found, { ":data": DATA_LABEL }),
        )
      }

      const current: any = record
      const nextTitle =
        updateDto.title !== undefined ? updateDto.title : current.title
      const nextDescription =
        updateDto.description !== undefined
          ? updateDto.description
          : current.description

      if (!this.hasContent(nextTitle) && !this.hasContent(nextDescription)) {
        return failureResponse(
          code.SUCCESS,
          validationMessage(messageKey.either_field_required, {
            ":field1": "title",
            ":field2": "description",
          }),
        )
      }

      const titleFormatError = this.validateFormat(
        "title_format",
        updateDto.title_format,
      )
      if (titleFormatError) {
        return failureResponse(code.SUCCESS, titleFormatError)
      }

      const descriptionFormatError = this.validateFormat(
        "description_format",
        updateDto.description_format,
      )
      if (descriptionFormatError) {
        return failureResponse(code.SUCCESS, descriptionFormatError)
      }

      const payload: any = { id, ...updateDto }
      // field_ref_no is assigned once on create and is the mobile app's mapping key.
      delete payload.field_ref_no

      if (!this.hasContent(updateDto.title) && updateDto.title !== undefined) {
        payload.title = null
      }

      if (
        !this.hasContent(updateDto.description) &&
        updateDto.description !== undefined
      ) {
        payload.description = null
      }

      await this.appContentRepository.save(payload)

      return successResponse(
        code.SUCCESS,
        successMessage(messageKey.data_update, { ":data": DATA_LABEL }),
      )
    } catch (error) {
      return failureResponse(
        code.ERROR,
        errorMessage(messageKey.validation_error, { ":data": DATA_LABEL }),
      )
    }
  }

  async remove(id: number) {
    try {
      const record = await this.appContentRepository.getByParams({
        where: { id },
        findOne: true,
      })

      if (!record) {
        return failureResponse(
          code.SUCCESS,
          errorMessage(messageKey.data_not_found, { ":data": DATA_LABEL }),
        )
      }

      await this.appContentRepository.remove({ id }, undefined, false)

      return successResponse(
        code.SUCCESS,
        successMessage(messageKey.data_removed, { ":data": DATA_LABEL }),
      )
    } catch (error) {
      return failureResponse(
        code.ERROR,
        errorMessage(messageKey.validation_error, { ":data": DATA_LABEL }),
      )
    }
  }
}
