// This file is part of Pa11y Dashboard.
//
// Pa11y Dashboard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pa11y Dashboard 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pa11y Dashboard.  If not, see <http://www.gnu.org/licenses/>.
'use strict';

const bodyParser = require('body-parser');
const compression = require('compression');
const createClient = require('pa11y-webservice-client-node');
const {EventEmitter} = require('events');
const express = require('express');
const hbs = require('express-hbs');
const morgan = require('morgan');
const {nanoid} = require('nanoid');
const http = require('http');
const pkg = require('./package.json');

module.exports = initApp;

function initApp(config, callback) {
	config = defaultConfig(config);

	let webserviceUrl = config.webservice;
	if (typeof webserviceUrl === 'object') {
		webserviceUrl = `http://${webserviceUrl.host}:${webserviceUrl.port}/`;
	}

	const app = new EventEmitter();

	app.express = express();
	app.server = http.createServer(app.express);
	app.webservice = createClient(webserviceUrl);

	loadMiddleware(app);

	// View engine
	loadViewEngine(app, config);

	// Load routes
	loadRoutes(app, config);

	// Error handling
	loadErrorHandling(app, config, callback);
}

function defaultConfig(config) {
	if (typeof config.noindex !== 'boolean') {
		config.noindex = true;
	}
	if (typeof config.readonly !== 'boolean') {
		config.readonly = false;
	}
	return config;
}

function loadMiddleware(app) {
	app.express.use(compression());

	// Adds an ID to every request, used later for logging
	app.express.use(addRequestId);

	// Logging middleware
	morgan.token('id', request => {
		return request.id;
	});

	// Log the start of all HTTP requests
	const startLog = '[:date[iso] #:id] Started :method :url for :remote-addr';
	// Immediate: true is required to log the request
	//  before the response happens
	app.express.use(morgan(startLog, {immediate: true}));

	// Log the end of all HTTP requests
	const endLog = '[:date[iso] #:id] Completed :status :res[content-length] in :response-time ms';
	app.express.use(morgan(endLog));

	// Public files
	app.express.use(express.static(`${__dirname}/public`, {
		maxAge: (process.env.NODE_ENV === 'production' ? 604800000 : 0)
	}));

	// General express config
	app.express.disable('x-powered-by');
	app.express.use(bodyParser.urlencoded({
		extended: true
	}));
}

function loadViewEngine(app, config) {
	app.express.engine('html', hbs.express4({
		extname: '.html',
		contentHelperName: 'content',
		layoutsDir: `${__dirname}/view/layout`,
		partialsDir: `${__dirname}/view/partial`,
		defaultLayout: `${__dirname}/view/layout/default`
	}));
	app.express.set('views', `${__dirname}/view`);
	app.express.set('view engine', 'html');

	// View helpers
	require('./view/helper/date')(hbs);
	require('./view/helper/string')(hbs);
	require('./view/helper/url')(hbs);
	require('./view/helper/conditionals')(hbs);

	// Populate view locals
	app.express.locals = {
		lang: 'en',
		year: (new Date()).getFullYear(),
		version: pkg.version,
		repo: pkg.homepage,
		bugtracker: pkg.bugs,
		noindex: config.noindex,
		readonly: config.readonly,
		siteMessage: config.siteMessage,
		settings: {}
	};

	app.express.use((request, response, next) => {
		response.locals.isHomePage = (request.path === '/');
		response.locals.host = request.hostname;
		next();
	});
}

function loadRoutes(app, config) {
	// Because there's some overlap between the different routes,
	//  they have to be loaded in a specific order in order to avoid
	//  passing mongo the wrong id which would result in
	//  "ObjectID generation failed." errors (e.g. #277)
	require('./route/index')(app);
	require('./route/result/download')(app);

	if (!config.readonly) {
		require('./route/new')(app);
		require('./route/task/delete')(app);
		require('./route/task/run')(app);
		require('./route/task/edit')(app);
		require('./route/task/ignore')(app);
		require('./route/task/unignore')(app);
	}

	// Needs to be loaded after `/route/new`
	require('./route/task/index')(app);
	// Needs to be loaded after `/route/task/edit`
	require('./route/result/index')(app);
}

function loadErrorHandling(app, config, callback) {
	app.express.get('*', (request, response) => {
		response.status(404);
		response.render('404');
	});
	app.express.use((error, request, response, next) => {
		/* eslint no-unused-vars: 'off' */
		if (error.code === 'ECONNREFUSED') {
			error = new Error('Could not connect to Pa11y Webservice');
		}
		app.emit('route-error', error);
		if (process.env.NODE_ENV !== 'production') {
			response.locals.error = error;
		}
		response.status(500);
		response.render('500');
	});

	app.server.listen(config.port, error => {
		callback(error, app);
	});
}

// Express middleware
function addRequestId(request, response, next) {
	// Create a random request (nano)id, 10 characters long
	// Nano ids are [0-9A-Za-z_-] so chance of collision is 1 in 64^10
	// If a site has so much traffic that this chance is too high
	//  we probably have worse things to worry about
	request.id = nanoid(10);
	next();
}
