/*
 * 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 {
  SCNotFoundErrorResponse,
  SCRequestBodyTooLargeErrorResponse,
  SCSyntaxErrorResponse,
  SCUnsupportedMediaTypeErrorResponse,
} from '@openstapps/core';
import {Logger} from '@openstapps/logger';
import cors from 'cors';
import {Express} from 'express';
import morgan from 'morgan';
import path from 'path';
import {DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common.js';
import {getPrometheusMiddleware} from './middleware/prometheus.js';
import {MailQueue} from './notification/mail-queue.js';
import {bulkAddRouter} from './routes/bulk-add-route.js';
import {bulkDoneRouter} from './routes/bulk-done-route.js';
import {bulkRouter} from './routes/bulk-route.js';
import {indexRouter} from './routes/index-route.js';
import {multiSearchRouter} from './routes/multi-search-route.js';
import {pluginRegisterRouter} from './routes/plugin-register-route.js';
import {searchRouter} from './routes/search-route.js';
import {thingUpdateRouter} from './routes/thing-update-route.js';
import {virtualPluginRoute} from './routes/virtual-plugin-route.js';
import {BulkStorage} from './storage/bulk-storage.js';
import {DatabaseConstructor} from './storage/database.js';
import {backendConfig} from './config.js';
import {fileURLToPath} from 'url';

/**
 * Configure the backend
 */
export async function configureApp(app: Express, databases: {[name: string]: DatabaseConstructor}) {
  let integrationTestTimeout: NodeJS.Timeout;
  // request loggers have to be the first middleware to be set in express
  app.use(
    morgan('dev', {
      skip: (_request, response) => {
        if (process.env.NODE_ENV === 'integration-test') {
          clearTimeout(integrationTestTimeout);
          integrationTestTimeout = setTimeout(() => {
            process.exit(1);
          }, DEFAULT_TIMEOUT);

          return false;
        }

        return response.statusCode < 400;
      },
      stream: process.stdout,
    }),
  );

  /* istanbul ignore if */
  if (process.env.PROMETHEUS_MIDDLEWARE === 'true') {
    app.use(getPrometheusMiddleware());
  }

  const corsOptions = {
    allowedHeaders: [
      'DNT',
      'Keep-Alive',
      'User-Agent',
      'X-Requested-With',
      'If-Modified-Since',
      'Cache-Control',
      'Content-Type',
      'X-StApps-Version',
    ],
    credentials: true,
    maxAge: 1_728_000,
    methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
    optionsSuccessStatus: 204,
  };

  // allow all origins on all routes
  app.use(cors(corsOptions));
  // TODO: See if it can handle options request with no content-type

  // allow cors preflight requests on every route
  app.options('*', [cors(corsOptions)]);

  // only accept json as content-type for all requests
  app.use((request, response, next) => {
    // Only accept json as content-type
    if (request.is('application/json') !== 'application/json') {
      // return an error in the response
      const error = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment);
      response.status(error.statusCode);
      response.json(error);

      return;
    }

    const bodyBuffer: Buffer[] = [];
    // we don't know the full size, the only way we can get is by adding up all individual chunk sizes
    let bodySize = 0;
    const chunkGatherer = (chunk: Buffer) => {
      bodySize += chunk.byteLength;
      // when adding each chunk size to the total size, check how large it now is.
      if (bodySize > backendConfig.backend.maxRequestBodySize) {
        request.off('data', chunkGatherer);
        request.off('end', endCallback);
        // return an error in the response
        const error = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment);
        response.status(error.statusCode);
        response.json(error);

        return;
      }
      // push the chunk in the buffer
      bodyBuffer.push(chunk);
    };

    const endCallback = () => {
      request.body = Buffer.concat(bodyBuffer).toString();

      try {
        request.body = JSON.parse(request.body);
        next();
      } catch (error) {
        const error_ = new SCSyntaxErrorResponse(error.message, isTestEnvironment);
        response.status(error_.statusCode);
        response.json(error_);

        return;
      }
    };
    request.on('data', chunkGatherer).on('end', endCallback);
  });

  // validate config file
  const directory = path.dirname(fileURLToPath(import.meta.url));
  await validator.addSchemas(
    path.join(directory, '..', 'node_modules', '@openstapps', 'core', 'lib', 'schema'),
  );

  // validate the config file
  const configValidation = validator.validate(backendConfig, 'SCConfigFile');

  // validation failed
  if (configValidation.errors.length > 0) {
    throw new Error(
      `Validation of config file failed. Errors were: ${JSON.stringify(configValidation.errors)}`,
    );
  }

  // check if a database name was given
  if (!backendConfig.internal.database?.name) {
    throw new Error('You have to configure a database');
  }

  const database = new databases[backendConfig.internal.database.name](
    backendConfig,
    // mailQueue
    mailer !== undefined && backendConfig.internal.monitoring ? new MailQueue(mailer) : undefined,
  );

  await database.init();

  if (database === undefined) {
    throw new TypeError('No implementation for configured database found. Please check your configuration.');
  }

  Logger.ok('Validated config file successfully');

  // treats /foo and /foo/ as two different routes
  // see http://expressjs.com/en/api.html#app.set
  app.enable('strict routing');

  // make the bulk storage available to all http middlewares/routes
  app.set('bulk', new BulkStorage(database));

  app.set('env', process.env.NODE_ENV);

  // load routes before plugins
  // they now can be used or overwritten by any plugin
  app.use(
    bulkAddRouter,
    bulkDoneRouter,
    bulkRouter,
    indexRouter,
    multiSearchRouter,
    pluginRegisterRouter,
    searchRouter,
    thingUpdateRouter,
  );

  // for plugins, as Express doesn't really want you to unregister routes (and doesn't offer any method to do so at all)
  app.all('*', async (request, response, next) => {
    // if the route exists, call virtual route on the plugin that registered that route
    if (plugins.has(request.originalUrl)) {
      try {
        response.json(await virtualPluginRoute(request, plugins.get(request.originalUrl)!));
      } catch (error) {
        // in case of an error: send an error response
        response.status(error.statusCode);
        response.json(error);
      }
    } else {
      // pass to the next matching route (which is 404)
      next();
    }
  });

  // add a route for a missing resource (404)
  app.use((_request, response) => {
    const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment);
    response.status(errorResponse.statusCode);
    response.json(errorResponse);
  });
}
