// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
  BindingScope,
  config,
  Context,
  inject,
  injectable,
  ValueOrPromise,
} from '@loopback/core';
import {
  InvokeMiddleware,
  InvokeMiddlewareOptions,
  MiddlewareGroups,
  MiddlewareView,
} from '@loopback/express';
import debugFactory from 'debug';
import {RestBindings, RestTags} from './keys';
import {RequestContext} from './request-context';
import {FindRoute, InvokeMethod, ParseParams, Reject, Send} from './types';
const debug = debugFactory('loopback:rest:sequence');

const SequenceActions = RestBindings.SequenceActions;

/**
 * A sequence function is a function implementing a custom
 * sequence of actions to handle an incoming request.
 */
export type SequenceFunction = (
  context: RequestContext,
  sequence: DefaultSequence,
) => ValueOrPromise<void>;

/**
 * A sequence handler is a class implementing sequence of actions
 * required to handle an incoming request.
 */
export interface SequenceHandler {
  /**
   * Handle the request by running the configured sequence of actions.
   *
   * @param context - The request context: HTTP request and response objects,
   * per-request IoC container and more.
   */
  handle(context: RequestContext): Promise<void>;
}

/**
 * The default implementation of SequenceHandler.
 *
 * @remarks
 * This class implements default Sequence for the LoopBack framework.
 * Default sequence is used if user hasn't defined their own Sequence
 * for their application.
 *
 * Sequence constructor() and run() methods are invoked from [[http-handler]]
 * when the API request comes in. User defines APIs in their Application
 * Controller class.
 *
 * @example
 * User can bind their own Sequence to app as shown below
 * ```ts
 * app.bind(CoreBindings.SEQUENCE).toClass(MySequence);
 * ```
 */
export class DefaultSequence implements SequenceHandler {
  /**
   * Optional invoker for registered middleware in a chain.
   * To be injected via SequenceActions.INVOKE_MIDDLEWARE.
   */
  @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true})
  protected invokeMiddleware: InvokeMiddleware = () => false;

  /**
   * Constructor: Injects findRoute, invokeMethod & logError
   * methods as promises.
   *
   * @param findRoute - Finds the appropriate controller method,
   *  spec and args for invocation (injected via SequenceActions.FIND_ROUTE).
   * @param parseParams - The parameter parsing function (injected
   * via SequenceActions.PARSE_PARAMS).
   * @param invoke - Invokes the method specified by the route
   * (injected via SequenceActions.INVOKE_METHOD).
   * @param send - The action to merge the invoke result with the response
   * (injected via SequenceActions.SEND)
   * @param reject - The action to take if the invoke returns a rejected
   * promise result (injected via SequenceActions.REJECT).
   */
  constructor(
    @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
    @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
    @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
    @inject(SequenceActions.SEND) public send: Send,
    @inject(SequenceActions.REJECT) public reject: Reject,
  ) {}

  /**
   * Runs the default sequence. Given a handler context (request and response),
   * running the sequence will produce a response or an error.
   *
   * Default sequence executes these steps
   *  - Executes middleware for CORS, OpenAPI spec endpoints
   *  - Finds the appropriate controller method, swagger spec
   *    and args for invocation
   *  - Parses HTTP request to get API argument list
   *  - Invokes the API which is defined in the Application Controller
   *  - Writes the result from API into the HTTP response
   *  - Error is caught and logged using 'logError' if any of the above steps
   *    in the sequence fails with an error.
   *
   * @param context - The request context: HTTP request and response objects,
   * per-request IoC container and more.
   */
  async handle(context: RequestContext): Promise<void> {
    try {
      const {request, response} = context;
      // Invoke registered Express middleware
      const finished = await this.invokeMiddleware(context);
      if (finished) {
        // The response been produced by the middleware chain
        return;
      }
      const route = this.findRoute(request);
      const args = await this.parseParams(request, route);
      const result = await this.invoke(route, args);

      debug('%s result -', route.describe(), result);
      this.send(response, result);
    } catch (error) {
      this.reject(context, error);
    }
  }
}

/**
 * Built-in middleware groups for the REST sequence
 */
export namespace RestMiddlewareGroups {
  /**
   * Invoke downstream middleware to get the result or catch errors so that it
   * can produce the http response
   */
  export const SEND_RESPONSE = 'sendResponse';

  /**
   * Enforce CORS
   */
  export const CORS = MiddlewareGroups.CORS;

  /**
   * Server OpenAPI specs
   */
  export const API_SPEC = MiddlewareGroups.API_SPEC;

  /**
   * Default middleware group
   */
  export const MIDDLEWARE = MiddlewareGroups.MIDDLEWARE;
  export const DEFAULT = MIDDLEWARE;

  /**
   * Find the route that can serve the request
   */
  export const FIND_ROUTE = 'findRoute';

  /**
   * Perform authentication
   */
  export const AUTHENTICATION = 'authentication';

  /**
   * Parse the http request to extract parameter values for the operation
   */
  export const PARSE_PARAMS = 'parseParams';

  /**
   * Invoke the target controller method or handler function
   */
  export const INVOKE_METHOD = 'invokeMethod';
}

/**
 * A sequence implementation using middleware chains
 */
@injectable({scope: BindingScope.SINGLETON})
export class MiddlewareSequence implements SequenceHandler {
  private middlewareView: MiddlewareView;

  static defaultOptions: InvokeMiddlewareOptions = {
    chain: RestTags.REST_MIDDLEWARE_CHAIN,
    orderedGroups: [
      // Please note that middleware is cascading. The `sendResponse` is
      // added first to invoke downstream middleware to get the result or
      // catch errors so that it can produce the http response.
      RestMiddlewareGroups.SEND_RESPONSE,

      RestMiddlewareGroups.CORS,
      RestMiddlewareGroups.API_SPEC,
      RestMiddlewareGroups.MIDDLEWARE,

      RestMiddlewareGroups.FIND_ROUTE,

      // authentication depends on the route
      RestMiddlewareGroups.AUTHENTICATION,

      RestMiddlewareGroups.PARSE_PARAMS,

      RestMiddlewareGroups.INVOKE_METHOD,
    ],

    /**
     * Reports an error if there are middleware groups are unreachable as they
     * are ordered after the `invokeMethod` group.
     */
    validate: groups => {
      const index = groups.indexOf(RestMiddlewareGroups.INVOKE_METHOD);
      if (index !== -1) {
        const unreachableGroups = groups.slice(index + 1);
        if (unreachableGroups.length > 0) {
          throw new Error(
            `Middleware groups "${unreachableGroups.join(
              ',',
            )}" are not invoked as they are ordered after "${
              RestMiddlewareGroups.INVOKE_METHOD
            }"`,
          );
        }
      }
    },
  };

  /**
   * Constructor: Injects `InvokeMiddleware` and `InvokeMiddlewareOptions`
   *
   * @param invokeMiddleware - invoker for registered middleware in a chain.
   * To be injected via RestBindings.INVOKE_MIDDLEWARE_SERVICE.
   */
  constructor(
    @inject.context()
    context: Context,

    @inject(RestBindings.INVOKE_MIDDLEWARE_SERVICE)
    readonly invokeMiddleware: InvokeMiddleware,
    @config()
    readonly options: InvokeMiddlewareOptions = MiddlewareSequence.defaultOptions,
  ) {
    this.middlewareView = new MiddlewareView(context, options);
    debug('Discovered middleware', this.middlewareView.middlewareBindingKeys);
  }

  /**
   * Runs the default sequence. Given a handler context (request and response),
   * running the sequence will produce a response or an error.
   *
   * Default sequence executes these groups of middleware:
   *
   *  - `cors`: Enforces `CORS`
   *  - `openApiSpec`: Serves OpenAPI specs
   *  - `findRoute`: Finds the appropriate controller method, swagger spec and
   *    args for invocation
   *  - `parseParams`: Parses HTTP request to get API argument list
   *  - `invokeMethod`: Invokes the API which is defined in the Application
   *    controller method
   *
   * In front of the groups above, we have a special middleware called
   * `sendResponse`, which first invokes downstream middleware to get a result
   * and handles the result or error respectively.
   *
   *  - Writes the result from API into the HTTP response (if the HTTP response
   *    has not been produced yet by the middleware chain.
   *  - Catches error logs it using 'logError' if any of the above steps
   *    in the sequence fails with an error.
   *
   * @param context - The request context: HTTP request and response objects,
   * per-request IoC container and more.
   */
  async handle(context: RequestContext): Promise<void> {
    debug(
      'Invoking middleware chain %s with groups %s',
      this.options.chain,
      this.options.orderedGroups,
    );
    const options: InvokeMiddlewareOptions = {
      middlewareList: this.middlewareView.middlewareBindingKeys,
      validate: MiddlewareSequence.defaultOptions.validate,
      ...this.options,
    };
    await this.invokeMiddleware(context, options);
  }
}