import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils"
import { v4 } from "uuid"

const KEY_LOCKED_TIMEOUT = 1000

class IdempotencyKeyService extends BaseService {
  constructor({ manager, idempotencyKeyRepository, transactionService }) {
    super()

    /** @private @constant {EntityManager} */
    this.manager_ = manager

    /** @private @constant {IdempotencyKeyRepository} */
    this.idempotencyKeyRepository_ = idempotencyKeyRepository

    /** @private @constant {TransactionService} */
    this.transactionService_ = transactionService
  }

  /**
   * Execute the initial steps in a idempotent request.
   * @param {string} headerKey - potential idempotency key from header
   * @param {string} reqMethod - method of request
   * @param {string} reqParams - params of request
   * @param {string} reqPath - path of request
   * @return {Promise<IdempotencyKeyModel>} the existing or created idempotency key
   */
  async initializeRequest(headerKey, reqMethod, reqParams, reqPath) {
    return this.atomicPhase_(async (_) => {
      // If idempotency key exists, return it
      let key = await this.retrieve(headerKey)

      if (key) {
        return key
      }

      key = await this.create({
        request_method: reqMethod,
        request_params: reqParams,
        request_path: reqPath,
      })

      return key
    }, "SERIALIZABLE")
  }

  /**
   * Creates an idempotency key for a request.
   * If no idempotency key is provided in request, we will create a unique
   * identifier.
   * @param {object} payload - payload of request to create idempotency key for
   * @return {Promise<IdempotencyKeyModel>} the created idempotency key
   */
  async create(payload) {
    return this.atomicPhase_(async (manager) => {
      const idempotencyKeyRepo = manager.getCustomRepository(
        this.idempotencyKeyRepository_
      )

      if (!payload.idempotency_key) {
        payload.idempotency_key = v4()
      }

      const created = await idempotencyKeyRepo.create(payload)
      const result = await idempotencyKeyRepo.save(created)
      return result
    })
  }

  /**
   * Retrieves an idempotency key
   * @param {string} idempotencyKey - key to retrieve
   * @return {Promise<IdempotencyKeyModel>} idempotency key
   */
  async retrieve(idempotencyKey) {
    const idempotencyKeyRepo = this.manager_.getCustomRepository(
      this.idempotencyKeyRepository_
    )

    const key = await idempotencyKeyRepo.findOne({
      where: { idempotency_key: idempotencyKey },
    })

    return key
  }

  /**
   * Locks an idempotency.
   * @param {string} idempotencyKey - key to lock
   * @param {object} session - mongoose transaction session
   * @return {Promise} result of the update operation
   */
  async lock(idempotencyKey) {
    return this.atomicPhase_(async (manager) => {
      const idempotencyKeyRepo = manager.getCustomRepository(
        this.idempotencyKeyRepository_
      )

      const key = this.retrieve(idempotencyKey)

      if (key.locked_at && key.locked_at > Date.now() - KEY_LOCKED_TIMEOUT) {
        throw new MedusaError("conflict", "Key already locked")
      }

      const updated = await idempotencyKeyRepo.save({
        ...key,
        locked_at: Date.now(),
      })

      return updated
    })
  }

  /**
   * Locks an idempotency.
   * @param {string} idempotencyKey - key to update
   * @param {object} update - update object
   * @return {Promise} result of the update operation
   */
  async update(idempotencyKey, update) {
    return this.atomicPhase_(async (manager) => {
      const idempotencyKeyRepo = manager.getCustomRepository(
        this.idempotencyKeyRepository_
      )

      const iKey = await this.retrieve(idempotencyKey)

      for (const [key, value] of Object.entries(update)) {
        iKey[key] = value
      }

      const updated = await idempotencyKeyRepo.save(iKey)
      return updated
    })
  }

  /**
   * Performs an atomic work stage.
   * An atomic work stage contains some related functionality, that needs to be
   * transactionally executed in isolation. An idempotent request will
   * always consist of 2 or more of these phases. The required phases are
   * "started" and "finished".
   * @param {string} idempotencyKey - current idempotency key
   * @param {Function} func - functionality to execute within the phase
   * @return {IdempotencyKeyModel} new updated idempotency key
   */
  async workStage(idempotencyKey, func) {
    try {
      return await this.atomicPhase_(async (manager) => {
        let key

        const { recovery_point, response_code, response_body } = await func(
          manager
        )

        if (recovery_point) {
          key = await this.update(idempotencyKey, {
            recovery_point,
          })
        } else {
          key = await this.update(idempotencyKey, {
            recovery_point: "finished",
            response_body,
            response_code,
          })
        }

        return { key }
      }, "SERIALIZABLE")
    } catch (err) {
      return { error: err }
    }
  }
}

export default IdempotencyKeyService
