import { Server, IncomingMessage, ServerResponse } from 'http';
import express from 'express';

import { InvalidHandler } from './errors.js';
import { HandlersInput, Handler } from './handler.js';
import correlationIdMiddleware from './middlewares/correlationId.js';
import credentialsMiddleware from './middlewares/credentials.js';
import errorsMiddleware from './middlewares/errors.js';
import filtersMiddleware from './middlewares/filters.js';
import finishMiddleware from './middlewares/finish.js';
import notFoundMiddleware from './middlewares/notFound.js';
import loggerMiddleware from './middlewares/logger.js';
import startMiddleware from './middlewares/start.js';
import secretsMiddleware from './middlewares/secrets.js';
import searchMiddleware from './middlewares/search.js';
import selectsMiddleware from './middlewares/selects.js';
import relationsMiddleware from './middlewares/relations.js';
import signalMiddleware from './middlewares/signal.js';
import healthMiddleware from './middlewares/health.js';

function printErrorMessage(message: string) {
  console.error();
  console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
  console.error(message);
}

type Options = {
  port?: number;
};

/**
 * Main class for the Integration SDK providing an abstraction layer between the Integration's Graph definition
 * and the underlying HTTP server.
 *
 * An `Integration` instance can have multiple handlers configured to handle different routes. Upon receiving a request,
 * the Integration will parse the request to extract meaninful information, match the request to the appropriate handler
 * method and forward that information in the form a {@link Context} object.
 * The Integration also offer standardized error handling and logging to help you build a robust
 * and reliable Integration.
 *
 * See our {@link https://dev.unito.io/docs/ | documentation} for more examples on how to build an integration.
 */
export default class Integration {
  private handlers: Handler[];

  private instance: Server<typeof IncomingMessage, typeof ServerResponse> | undefined = undefined;

  private port: number;

  /**
   * Creates a new Integration instance with default port set to 9200.
   *
   * @param options The {@link Options} to configure the Integration instance. Can be used to override the default port.
   */
  constructor(options: Options = {}) {
    this.port = options.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 9200);
    this.handlers = [];
  }

  /**
   * Adds a group of common handlers to the integration.
   *
   * Handlers added to the integration can be one of the following:
   * - `ItemHandlers`: A group of handlers defining the implementation of the Operations available for a given item.
   * - `CredentialAccountHandlers`: A handler returning the CredentialAccount linked to the caller's credentials.
   * - `ParseWebhookHandlers`: A handler parsing the content of an incoming webhook.
   * - `WebhookSubscriptionHandlers`: A handler subscribing or unsubscribing to a particular webhook.
   * - `AcknowledgeWebhookHandlers`: A handler acknowledging the reception of a webhook.
   *
   * To accomodate the fact that ItemHandlers may specify multiple operations, some at the collection level, some at the
   * item level, we need a way to define the route for each of these operations.
   * To achieve this, we assume that if the last part of the path is a variable, then it is the item identifier.
   *
   * @example The following path: `/trainer/:trainerId/pokemons/:pokemonId` will lead to the following
   * routes:
   * - getCollection will be called for `GET /trainer/:trainerId/pokemons/` requests
   * - getItem will be called for `GET /trainer/:trainerId/pokemons/:pokemonId` requests
   * - createItem will be called for `POST /trainer/:trainerId/pokemons/` requests
   * - updateItem will be called for `PATCH /trainer/:trainerId/pokemons/:pokemonId` requests
   * - deleteItem will be called for `DELETE /trainer/:trainerId/pokemons/:pokemonId` requests
   *
   * @param path The path to be used as Route for the handlers.
   * @param handlers The Handlers definition.
   */
  public addHandler(path: string, handlers: HandlersInput): void {
    if (this.instance) {
      printErrorMessage(`
  It seems like you're trying to add a handler after the server has already started. This is probably
  a mistake as calling the start() function essentially starts the server and ignore any further change.
  To fix this error, move all your addHandler() calls before the start() function.
      `);
      process.exit(1);
    }

    try {
      this.handlers.push(new Handler(path, handlers));
    } catch (error) {
      if (error instanceof InvalidHandler) {
        printErrorMessage(`
  It seems like you're trying to add an invalid handler. The exact error message is:

  > ${error.message}

  You must address this issue before trying again.
        `);
      } else {
        printErrorMessage(`
  An unexpected error happened as we were trying to add your handler. The exact error message is;

  >  ${(error as Error).message}
        `);
      }
      process.exit(1);
    }
  }

  /**
   * Starts the server and listens on the specified port (default to 9200).
   *
   * @remarks
   * This function should be called after all the handlers have been added to the integration
   * and any other configuration is completed.
   */
  public start() {
    // Express Server initialization
    const app: express.Application = express();

    // Parse query strings with https://github.com/ljharb/qs.
    app.set('query parser', 'simple');

    app.use(express.json());

    // Must be one of the first handlers (to catch all the errors).
    app.use(finishMiddleware);

    // Instantiate internal middlewares.
    app.use(startMiddleware);
    app.use(correlationIdMiddleware);
    app.use(loggerMiddleware);

    // Making sure we log all incoming requests (except to '/health'), prior any processing.
    app.use((req, res, next) => {
      if (req.originalUrl !== '/health') {
        res.locals.logger.info(`Initializing request for ${req.originalUrl}`);
      }
      next();
    });

    // Instantiate application middlewares. These can throw, so they have an implicit dependency on the internal
    // middlewares such as the logger, the correlationId, and the error handling.
    app.get('/health', healthMiddleware);
    app.use(credentialsMiddleware);
    app.use(secretsMiddleware);
    app.use(filtersMiddleware);
    app.use(searchMiddleware);
    app.use(selectsMiddleware);
    app.use(relationsMiddleware);
    app.use(signalMiddleware);

    // Load handlers as needed.
    if (this.handlers.length) {
      for (const handler of this.handlers) {
        app.use(handler.generate());
      }
    } else {
      printErrorMessage(`
  It seems like you're trying to start the server without any handler. This is probably a mistake as the
  server wouldn't expose any route. To fix this error, add at least one handler before calling the start()
  function.
      `);
      process.exit(1);
    }

    // Must be the (last - 1) handler.
    app.use(errorsMiddleware);

    // Must be the last handler.
    app.use(notFoundMiddleware);

    // Start the server.
    this.instance = app.listen(this.port, error => {
      if (error) {
        printErrorMessage(`
          Error starting server:

          > ${error.message}
        `);
        process.exit(1);
      } else {
        console.info(`Server started on port ${this.port}.`);
      }
    });
  }
}
