/*
 * Copyright 2020 Spotify AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import express, {
  Application,
  Router,
  Request,
  Response,
  NextFunction,
} from 'express';
import corsMiddleware from 'cors';
import compression from 'compression';
import bodyParser from 'body-parser';
import expressPromiseRouter from 'express-promise-router';
import morgan from 'morgan';
import { Server } from 'http';
import { Pool, ConnectionConfig } from 'pg';

import logger from '../logger';
import { runDbMigrations, awaitDbConnection, DbConnectionType } from '../db';
import { bindRoutes as bindAuditRoutes } from '../api/audits/routes';
import { bindRoutes as bindWebsiteRoutes } from '../api/websites/routes';
import { StatusCodeError } from '../errors';

const DEFAULT_PORT = 3003;

export interface LighthouseAuditServiceOptions {
  port?: number;
  cors?: boolean;
  postgresConfig?: ConnectionConfig;
}

function configureMiddleware(
  app: Application,
  options: LighthouseAuditServiceOptions,
) {
  if (options.cors) {
    app.use(corsMiddleware());
  }
  app.use(compression());
  app.use(bodyParser.json());
  app.use(
    morgan('combined', {
      stream: {
        write(message: String) {
          logger.info(message);
        },
      },
    }),
  );
}

export function configureErrorMiddleware(app: Application) {
  /* eslint-disable @typescript-eslint/no-unused-vars */
  app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
    /* eslint-enable @typescript-eslint/no-unused-vars */
    if (err instanceof StatusCodeError) res.status(err.statusCode);
    else res.status(500);
    res.send(err.message);
  });
}

function configureRoutes(router: Router, conn: DbConnectionType) {
  logger.debug('attaching routes...');
  router.get('/_ping', (_req, res) => res.sendStatus(200));
  bindAuditRoutes(router, conn);
  bindWebsiteRoutes(router, conn);
}

export async function getApp(
  options: LighthouseAuditServiceOptions = {},
  providedConn?: DbConnectionType,
): Promise<Application> {
  logger.info('building express app...');
  const conn = providedConn || new Pool(options.postgresConfig);

  await awaitDbConnection(conn);
  await runDbMigrations(conn);

  const app = express();
  app.set('connection', conn);
  configureMiddleware(app, options);

  const router = expressPromiseRouter();
  configureRoutes(router, conn);
  app.use(router);

  configureErrorMiddleware(app);

  return app;
}

export async function startServer(
  options: LighthouseAuditServiceOptions = {},
  providedConn?: DbConnectionType,
): Promise<Server> {
  const { port = DEFAULT_PORT } = options;
  const conn = providedConn || new Pool(options.postgresConfig);
  const app = await getApp(options, conn);

  logger.debug('starting application server...');
  return await new Promise((resolve, reject) => {
    const server = app.listen(port, (err?: Error) => {
      if (err) {
        reject(err);
        return;
      }
      logger.info(`listening on port ${port}`);
      resolve(server);
    });

    server.on('close', () => conn.end());
  });
}