/*
 * Copyright (C) 2019 StApps
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
import {
  SCInternalServerErrorResponse,
  SCMethodNotAllowedErrorResponse,
  SCRoute,
  SCValidationErrorResponse,
} from '@openstapps/core';
import {ValidationError} from '@openstapps/core-tools/src/types/validator.js';
import {Logger} from '@openstapps/logger';
import {Application, Router} from 'express';
import PromiseRouter from 'express-promise-router';
import {isTestEnvironment, validator} from '../common.js';
import {isHttpMethod} from './http-types.js';

/**
 * Creates a router from a route class and a handler function which implements the logic
 *
 * The given router performs a request and response validation, sets status codes and checks if the given handler
 * only returns errors that are allowed for the client to see
 * @param routeClass Model of a route
 * @param handler Implements the logic of the route
 */
export function createRoute<REQUESTTYPE, RETURNTYPE>(
  routeClass: SCRoute,
  handler: (
    validatedBody: REQUESTTYPE,
    app: Application,
    parameters: {[parameterName: string]: string},
  ) => Promise<RETURNTYPE>,
): Router {
  // create router
  const router = PromiseRouter({mergeParams: true});

  // create route
  // the given type has no index signature so we have to cast to get the IRouteHandler when a HTTP method is given
  const route = router.route(routeClass.urlPath);

  const verb = routeClass.method.toLowerCase();

  // check if route has a valid http verb
  if (isHttpMethod(verb)) {
    // create a route handler for the given HTTP method
    route[verb](async (request, response) => {
      try {
        // validate request
        const requestValidation = validator.validate(request.body, routeClass.requestBodyName);

        if (requestValidation.errors.length > 0) {
          const error = new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
          response.status(error.statusCode);
          response.json(error);
          await Logger.error(error);

          return;
        }

        // hand over request to handler with path parameters
        const handlerResponse = await handler(request.body, request.app, request.params);

        // validate response generated by handler
        const responseErrors: ValidationError[] = validator.validate(
          handlerResponse,
          routeClass.responseBodyName,
        ).errors;

        if (responseErrors.length > 0) {
          const validationError = new SCValidationErrorResponse(responseErrors, isTestEnvironment);
          // The validation error is not caused by faulty user input, but through an error that originates somewhere in
          // the backend, therefore we use this "stacked" error.
          const internalServerError = new SCInternalServerErrorResponse(validationError, isTestEnvironment);
          response.status(internalServerError.statusCode);
          response.json(internalServerError);
          await Logger.error(internalServerError);

          return;
        }

        // set status code
        response.status(routeClass.statusCodeSuccess);

        // respond
        response.json(handlerResponse);
      } catch (error) {
        // if the error response is allowed on the route
        if (routeClass.errorNames.some(constructorType => error instanceof constructorType)) {
          // respond with the error from the handler
          response.status(error.statusCode);
          response.json(error);
          await Logger.error(error);
        } else {
          // the error is not allowed so something went wrong
          const internalServerError = new SCInternalServerErrorResponse(error, isTestEnvironment);
          response.status(internalServerError.statusCode);
          response.json(internalServerError);
          await Logger.error(error);
        }
      }
    });
  } else {
    throw new Error(
      'Invalid HTTP verb in route definition. Please check route definitions in `@openstapps/core`',
    );
  }

  // return a SCMethodNotAllowedErrorResponse on all other HTTP methods
  route.all((_request, response) => {
    const error = new SCMethodNotAllowedErrorResponse(isTestEnvironment);
    response.status(error.statusCode);
    response.json(error);
    Logger.warn(error);
  });

  return router;
}
