import { invariant } from 'outvariant'
import { DeferredPromise } from '@open-draft/deferred-promise'
import { InterceptorError } from './InterceptorError'

const kRequestHandled = Symbol('kRequestHandled')
export const kResponsePromise = Symbol('kResponsePromise')

export class RequestController {
  /**
   * Internal response promise.
   * Available only for the library internals to grab the
   * response instance provided by the developer.
   * @note This promise cannot be rejected. It's either infinitely
   * pending or resolved with whichever Response was passed to `respondWith()`.
   */
  [kResponsePromise]: DeferredPromise<
    Response | Record<string, any> | undefined
  >;

  /**
   * Internal flag indicating if this request has been handled.
   * @note The response promise becomes "fulfilled" on the next tick.
   */
  [kRequestHandled]: boolean

  constructor(private request: Request) {
    this[kRequestHandled] = false
    this[kResponsePromise] = new DeferredPromise()
  }

  /**
   * Respond to this request with the given `Response` instance.
   * @example
   * controller.respondWith(new Response())
   * controller.respondWith(Response.json({ id }))
   * controller.respondWith(Response.error())
   */
  public respondWith(response: Response): void {
    invariant.as(
      InterceptorError,
      !this[kRequestHandled],
      'Failed to respond to the "%s %s" request: the "request" event has already been handled.',
      this.request.method,
      this.request.url
    )

    this[kRequestHandled] = true
    this[kResponsePromise].resolve(response)

    /**
     * @note The request controller doesn't do anything
     * apart from letting the interceptor await the response
     * provided by the developer through the response promise.
     * Each interceptor implements the actual respondWith/errorWith
     * logic based on that interceptor's needs.
     */
  }

  /**
   * Error this request with the given reason.
   *
   * @example
   * controller.errorWith()
   * controller.errorWith(new Error('Oops!'))
   * controller.errorWith({ message: 'Oops!'})
   */
  public errorWith(reason?: Error | Record<string, any>): void {
    invariant.as(
      InterceptorError,
      !this[kRequestHandled],
      'Failed to error the "%s %s" request: the "request" event has already been handled.',
      this.request.method,
      this.request.url
    )

    this[kRequestHandled] = true

    /**
     * @note Resolve the response promise, not reject.
     * This helps us differentiate between unhandled exceptions
     * and intended errors ("errorWith") while waiting for the response.
     */
    this[kResponsePromise].resolve(reason)
  }
}
