import db from '@pbvision/firestore-orm'

import API from './api.js'
import { __RequestDone } from './exception.js'

/**
 * Thrown to avoid committing a transaction when an error occurs.
 * @access private
 */
export class RequestDoneAbortTransaction extends __RequestDone {
  constructor (code = 200, data = '') {
    super(undefined, data, code)
    this.retryable = false
  }
}

/**
 * An API whose response is computed inside a transaction.
 * @public
 * @class
 */
class DatabaseAPI extends API {
  /**
   * Whether this API can only read from Firestore. If false, the API's
   * computeResponse() method will run in a Firestore transaction.
   * @public
   */
  static IS_READ_ONLY = true

  /**
   * Options to pass the Firestore db.Context which wraps computeResponse().
   * @public
   */
  static CONTEXT_OPTIONS = {}

  async _computeResponse () {
    await this.preTxStart()

    let ret
    try {
      const opts = {
        ...this.constructor.CONTEXT_OPTIONS,
        readOnly: this.constructor.IS_READ_ONLY
      }
      ret = await db.Context.run(opts, async tx => {
        this.tx = tx
        this.req.tx = tx
        let respData = await super._computeResponse()
        if (this.__reply.statusCode < 400) {
          // pre-commit hook may change the response data (and status code!)
          respData = await this._callAndHandleRequestDone(
            this.preCommit, respData)
        }
        // if the response code indicates an error, then don't commit
        if (this.__reply.statusCode >= 400) {
          throw new RequestDoneAbortTransaction(
            this.__reply.statusCode, respData)
        }
        return respData
      })
    } catch (e) {
      if (e instanceof RequestDoneAbortTransaction) {
        return e.respData
      } else {
        throw e
      }
    } finally {
      delete this.tx
      delete this.req.tx
    }

    // _computeResponse() is called within _callAndHandleRequestDone; so we
    // don't need to wrap this call to postCommit() in it (redundant)
    return this.postCommit(ret)
  }

  /**
   * Called just before the transaction starts. Useful to do async computation
   * outside the transaction (reducing the window for contention).
   * @protected
   */
  async preTxStart () {}

  /**
   * Called just before the transaction ATTEMPTS to commit. May alter the
   * response by returning a new response value, or throwing RequestDone.
   * Throwing an error will be propagated and handled by Transaction.run(),
   * and will prevent the transaction from committing (unless the error is
   * retryable).
   *
   * @param {*} respData the response data
   * @returns {*} the (possibly updated) response data
   * @protected
   */
  async preCommit (respData) { return respData }

  /**
   * Called after the transaction commits (and ONLY if it commits
   * successfully). May alter the response by returning a new response value,
   * or throwing RequestDone.
   *
   * @param {*} respData the response data
   * @returns {*} the (possibly updated) response data
   * @protected
   */
  async postCommit (respData) { return respData }
}

export default DatabaseAPI