import Express from "express";

import { ControllerConfiguration } from "../controller/controller";
import { RouteConfigurator } from "../route/route";
import { RequestError } from "../error/request.error";
import { ConfigurationError } from "../error/configuration.error";

export class AppConfigurator {
  private _prefix = "";
  private readonly _express: Express.Application;
  private readonly _controllers: ControllerConfiguration<any>[] = [];
  private readonly logRequests: boolean;
  private _configured: boolean;

  /**
   * Create AppConfigurator
   * @param expressApplication - express application
   * @param logRequests - flag if requests should be logged, true by default
   */
  constructor(expressApplication: Express.Application, logRequests = true) {
    this._express = expressApplication;
    this._configured = false;
    this.logRequests = logRequests;
  }

  /**
   * Starts application, register controllers routes in express app
   * and connect to configured port.
   * @param printConfiguration - print express application routes enabled by default.
   */
  configure(printConfiguration = true): void {
    if (this._configured) {
      throw new ConfigurationError("Cannot configure application multiple times");
    } else {
      this._configured = true;
    }
    this.configureControllers();
    if (printConfiguration) {
      this.printExpressConfig();
    }
  }

  /**
   * Register prefix for all paths in application
   * @param prefix - prefix string eg: 'api'
   */
  prefix(prefix: string): AppConfigurator {
    this._prefix = prefix;
    return this;
  }

  /**
   * Register controller in application.
   * @param controller - registered controller
   */
  registerController(controller: ControllerConfiguration<any, any>): AppConfigurator {
    this._controllers.push(controller);
    return this;
  }

  /**
   * Register list of controllers in application.
   * @param controllers - controllers to register
   */
  registerControllers(...controllers: ControllerConfiguration<any, any>[]): AppConfigurator {
    controllers.forEach(controller => this.registerController(controller));
    return this;
  }

  // PRIVATE

  private printExpressConfig() {
    console.log("Routes registered by Express server:");
    this.getExpressRoutesAsStrings().forEach(route => console.log(route));
  }

  private configureControllers() {
    this._controllers.forEach(controller => this.configureController(controller));
  }

  private configureController(controller: ControllerConfiguration, controllersChain?: ControllerConfiguration[]) {
    if (!controllersChain) {
      controllersChain = [controller];
    } else {
      controllersChain = [...controllersChain, controller];
    }
    controller.getControllers().forEach(subController => {
      this.configureController(subController, controllersChain);
    });
    controller.getRoutes().forEach(route => {
      this.registerRouteInExpress(controllersChain, route);
    });
  }

  private registerRouteInExpress(controllersChain: ControllerConfiguration[], route: RouteConfigurator) {
    const controllersPrefix = controllersChain.reduce(
      (prefixAcc, controller) => AppConfigurator.getRoutePath(prefixAcc, controller.getPrefix()),
      this._prefix || ""
    );
    const routePath = AppConfigurator.getRoutePath(controllersPrefix, route.getPath());

    if (!route.getMethod()) {
      throw new ConfigurationError(`Route ${routePath} has no method specified`);
    }

    this._express[route.getMethod()](`/${routePath}`, this.createApplicationRequestHandler(route, controllersChain));
  }

  private createApplicationRequestHandler(
    route: RouteConfigurator,
    controllersChain: ControllerConfiguration[]
  ): Express.RequestHandler {
    return async (req: Express.Request, res: Express.Response) => {
      try {
        let context: unknown;

        for (const controller of controllersChain) {
          const newContextValue = await controller.getHandler()?.(req, res, context);
          context = newContextValue ? newContextValue : context;
        }

        const response = await route.getRequestHandler()(req, res, context);
        if (AppConfigurator.canSendResponse(res)) {
          if (!res.statusCode) {
            res.status(200);
          }
          res.send(response);
        }
      } catch (error) {
        if (AppConfigurator.canSendResponse(res)) {
          if (error instanceof RequestError) {
            res.status(error.httpCode || 500).send(error.response || "Internal error");
          } else {
            res.status(500).send("Internal error");
          }
        }
      } finally {
        this.logRequest(req, res);
      }
    };
  }

  private logRequest(req: Express.Request, res: Express.Response) {
    if (this.logRequests) {
      console.log(`Request ${req.method}:${req.path} Response status: ${res.statusCode}`);
    }
  }

  private getExpressRoutesAsStrings() {
    return this._express._router.stack.filter(r => r.route).map(AppConfigurator.expressRouteAsString);
  }
  // STATIC

  private static expressRouteAsString(r) {
    return `${Object.keys(r.route.methods)[0].toUpperCase()}:${r.route?.path}`;
  }

  /**
   * Get final route path
   * @param paths - array of paths
   * @private
   */
  private static getRoutePath(...paths: string[]) {
    return paths.filter(path => !!path).join(`/`);
  }

  private static canSendResponse(res: Express.Response) {
    return !res.writableEnded;
  }
}

type ApplicationOptions = {
  app: Express.Application;
  logRequests?: boolean;
};

/**
 * Creates application core
 * @param options.app - Express application used by application
 * @param options.logRequests - log requests, enabled by default
 */
export function Application(options: ApplicationOptions): AppConfigurator {
  return new AppConfigurator(options.app, options.logRequests);
}
