
const http = require('http');
const express = require('express');
express.yaml ??= require('body-parser').yaml; // NOTE: Use own customised body-parser
const cookieParser = require('cookie-parser')

const maybeSendAlert = require("../lib/alerts");
const mw = require('./middleware');

const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
const verbose = process.env.NODE_ENV != 'test';
const app = express();

app.map = function(a, route){
	route = route || '';
	for (var key in a) {
		switch (typeof a[key]) {
			// { '/path': { ... }}
			case 'object':
				if (!Array.isArray(a[key])) {
					app.map(a[key], route + key);
					break;
				} // else drop through
				// get: function(){ ... }
			case 'function':
				if (verbose) INFO('%s %s', key, route);
				app[key](route, a[key]);
				break;
		}
	}
};

app.use(express.json({type: "application/json", strict: false, limit: '10mb'}));
app.use(express.yaml({type: "application/yaml", limit: '10mb'}));
app.use(express.urlencoded({ type: "application/x-www-form-urlencoded", extended: true }));
app.use(express.text({type: "text/*", limit: '10mb'}));
app.use((req, res, next) => {
	res.set("Access-Control-Allow-Origin", "*");
	res.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE");
	res.set("Access-Control-Allow-Headers", "Content-Type");
	next();
});

app.use(cookieParser());
app.use(mw.auth.jwt);

app.use((req, res, next) => {
	DEBUG("⟴ %s %s %s (%j)", req.method, req.originalUrl, req.body?.length ? `${req.body.length} bytes` : "", req.user);
	next();
});

// Adds arbitrary information to the request object
const meta = (key, value) => {
	return (req, res, next) => {
		if (!req.meta) {
			req.meta = {};
		}

		if (key) {
			req.meta[key] = value;
		}

		next();
	}
};

// Short for adding meta to all methods
const allMeta = (key, value) => {
	return { all: [ meta(key, value) ] };
};


//
// NOTICE These routes do not require authentication
//

app.map({
	'*': { all: [ meta() ] }, // Create the req.meta object
	'/login': {
		post: [ mw.user.login ]
	},
	'/logout': {
		get: [ mw.user.logout ],
		post: [ mw.user.logout ]
	},
	'/version': {
		get: [ mw.auth.operations, mw.version.get ],
		'/history': {
			get: [ /*mw.auth.user,*/ mw.version.history.get ],
		}
	},
	'/': {
		get: [ mw.openapi.get ]
	}
});

//
// WARNING Every route from here onwards requires authentication!
//
app.use(mw.auth.authentify);
// Users must be authenticated to access anything below here
app.use(mw.auth.access.user);

// Don't process the request if the data hasn't changed
app.use(mw.etag.ifNoneMatch);

// Use compression across the board
app.use(mw.compress);

// We must be authenticated before we can access these
app.map({
	'/project': {
		get: [ mw.project.get ],  // Get list of projects, filtered by `read` access
		post: [ mw.auth.access.admin, mw.project.post ], // Create a new project
	},
	'/project/:project': {
		get: [ mw.project.summary.get ],      // Get project data
		delete: [ mw.auth.access.edit, mw.project.delete ], // Delete a project (only if empty)
	},
	'/project/:project/summary': {
		get: [ mw.auth.access.read, mw.project.summary.get ],
	},
	'/project/:project/configuration': {
		patch: [ mw.auth.access.edit, mw.project.configuration.patch ],	// Modify project configuration
		put: [ mw.auth.access.edit, mw.project.configuration.put ],	// Overwrite configuration
	},
	'/project/:project/configuration/:path(*)?': {
		get: [ mw.auth.access.read, mw.configuration.get ],
	},

	/*
	 * GIS endpoints
	 */

	'/project/:project/gis': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.bbox ]
	},
	'/project/:project/gis/preplot': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.preplot ]
	},
	'/project/:project/gis/preplot/:featuretype(line|point)': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.preplot ]
	},
	'/project/:project/gis/raw/:featuretype(line|point)': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.raw ]
	},
	'/project/:project/gis/final/:featuretype(line|point)': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.final ]
	},
	'/project/:project/gis/layer': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.layer.get ]
	},
	'/project/:project/gis/layer/:name': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.gis.project.layer.get ]
	},

	/*
	 * Line endpoints
	 */

	'/project/:project/line/': {
		get: [ mw.auth.access.read, mw.line.list ],
	},
	'/project/:project/line/:line(\\d+)': {
		get: [ mw.auth.access.read, mw.line.get ],
		patch: [ mw.auth.access.write, mw.line.patch ],
	},
	'/project/:project/line/:class(sail|source)': {
		get: [ mw.auth.access.read, mw.line.get ],
	},

	/*
	 * Sequence endpoints
	 */

	'/project/:project/sequence/': {
		get: [ mw.auth.access.read, mw.sequence.list ],
	},
	'/project/:project/sequence/:sequence': {
		get: [ mw.auth.access.read, mw.sequence.get ],
		patch: [ mw.auth.access.write, mw.sequence.patch ],
		'/:point': {
			get: [ mw.auth.access.read, mw.sequence.point.get ]
		}
	},

	/*
	 * Planner endpoints
	 */

	'/project/:project/plan/': {
		get: [ mw.auth.access.read, mw.plan.list ],
		put: [ mw.auth.access.write, mw.plan.put ],
		post: [ mw.auth.access.write, mw.plan.post ]
	},
	'/project/:project/plan/:sequence': {
// 		get: [ mw.auth.access.read, mw.plan.get ],
		patch: [ mw.auth.access.write, mw.plan.patch ],
		delete: [ mw.auth.access.write, mw.plan.delete ]
	},

	/*
	 * Line name endpoints
	 *
	 */

	// Read access is sufficient for the next two endpoints

	'/project/:project/linename': {
		post: [ mw.auth.access.read, mw.linename.post ], // Get a linename
	},
	'/project/:project/linename/properties': {
		get: [ mw.auth.access.read, mw.linename.properties.get ], // Get linename properties
	},

	/*
	 * Event log endpoints
	 */

	'/project/:project/event/': {
		get: [ mw.auth.access.read, mw.event.list ],
		post: [ mw.auth.access.write, mw.event.post ],
		put: [ mw.auth.access.write, mw.event.put ],
		delete: [ mw.auth.access.write, mw.event.delete ],
		'changes/:since': {
			get: [ mw.auth.access.read, mw.event.changes ]
		},
		// NOTE: old alias for /sequence/:sequence
		'-/:sequence/': { // NOTE: We need to avoid conflict with the next endpoint ☹
			get: [ mw.auth.access.read, mw.event.sequence.get ],
		},
		'sequence/:sequence/': {
			get: [ mw.auth.access.read, mw.event.sequence.get ],
		},
		':id(\\d+)/': {
			get: [ mw.auth.access.read, mw.event.get ],
			put: [ mw.auth.access.write, mw.event.put ],
			patch: [ mw.auth.access.write, mw.event.patch ],
			delete: [mw.auth.access.write,  mw.event.delete ]
		},
		'import': {
			put: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
			post: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
			'/:filename': {
				put: [ mw.auth.access.read, mw.event.import.csv, mw.event.import.put ],
				post: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
				delete: [ mw.auth.access.write, mw.event.import.delete ]
			},
		},
	},

	/*
	 * QC endpoints
	 */

	'/project/:project/qc': {
		'/results': {
			// Get all QC results for :project
			get: [ mw.auth.access.read, mw.etag.noSave, mw.qc.results.get ],

			// Delete all QC results for :project
			delete: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.delete ],

			'/accept': {
				post: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.accept ]
			},

			'/unaccept': {
				post: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.unaccept ]
			},

			'/sequence/:sequence': {
				// Get QC results for :project, :sequence
				get: [ mw.etag.noSave, mw.qc.results.get ],

				// Delete QC results for :project, :sequence
				delete: [ mw.auth.access.write, mw.etag.noSave, mw.qc.results.delete ]
			}
		}
	},

	/*
	 * Other miscellaneous endpoints
	 */

	'/project/:project/label/': {
		get: [ mw.auth.access.read, mw.label.list ],
// 		post: [ mw.label.post ],
	},
	'/project/:project/info/:path(*)': {
		get: [ mw.auth.operations, mw.auth.access.read, mw.info.get ],
		post: [ mw.auth.operations, mw.auth.access.write, mw.info.post ],
		put: [ mw.auth.operations, mw.auth.access.write, mw.info.put ],
		delete: [ mw.auth.operations, mw.auth.access.write, mw.info.delete ]
	},
	'/project/:project/meta/': {
		put: [ mw.auth.access.write, mw.meta.put ],
	},
	'/project/:project/meta/:path(*)': {
		// Path examples:
		// GET:
		// `/raw/sequences/qc/missing_shots`,
		// `/final/points/qc/sync_warn/results
		get: [ mw.auth.access.read, mw.meta.get ],
// 		// PUT:
// 		// `/raw/qc/missing_shots` ← { sequence: …, value: … }
// 		put: [ mw.meta.put ]
	},
//
// 	'/project/:id/permissions/:mode(read|write)?': {
// 		get: [ mw.permissions.get ],
// 		put: [ mw.permissions.put ],
// // 		post: [ mw.permissions.post ],
// // 		delete: [ mw.permissions.delete ]
// 	},
	'/project/:project/files/:path(*)': {
		get: [ mw.files.get ]
	},
	'/files/?:path(*)': {
		get: [ mw.etag.noSave, mw.files.get ]
	},
	'/navdata/': { // TODO These endpoints should probably need read access auth
		get: [ mw.etag.noSave, mw.navdata.get ],
		'gis/:featuretype(line|point)': {
			get: [ mw.etag.noSave, mw.gis.navdata.get ]
		}
	},
	'/vessel/track': {
		get: [ /*mw.etag.noSave,*/ mw.vessel.track.get ], // JSON array
		'/line': {
			get: [ // GeoJSON Feature: type = LineString
				//mw.etag.noSave,
				(req, res, next) => { req.query.geojson = 'LineString'; next(); },
				mw.vessel.track.get
			]
		},
		'/point': {
			get: [ // GeoJSON FeatureCollection: feature types = Point
				//mw.etag.noSave,
				(req, res, next) => { req.query.geojson = 'Point'; next(); },
				mw.vessel.track.get
			]
		},
		'/points': {
			get: [ // JSON array of (Feature: type = Point)
				mw.etag.noSave,
				(req, res, next) => { req.query.geojson = true; next(); },
				mw.vessel.track.get
			],
		},
	},
	'/info/': {
		':path(*)': {
			get: [ mw.auth.operations, mw.info.get ],
			put: [ mw.auth.operations, mw.auth.access.write, mw.info.put ],
			post: [ mw.auth.operations, mw.auth.access.write, mw.info.post ],
			delete: [ mw.auth.operations, mw.auth.access.write, mw.info.delete ]
		}
	},

	/*
	 * 4D comparisons
	 */

	// FIXME no authentication yet!

	'/comparison/group': {
		get: [ mw.comparisons.groups.list ],
		'/:group': {
			get: [ mw.comparisons.groups.get ],
		},
	},



	/*
	 * Other endpoints
	 */

	'/queue/outgoing/': {
		'asaqc': {
			get: [ mw.etag.noSave, mw.queue.asaqc.get ],
			post: [ mw.auth.access.write, mw.queue.asaqc.post ],
			'/project/:project': {
				get: [ mw.etag.noSave, mw.queue.asaqc.get ],
				'/sequence/:sequence': {
					get: [ mw.etag.noSave, mw.queue.asaqc.get ],
				}
			},
			'/:id': {
				delete: [ mw.auth.access.write, mw.queue.asaqc.delete ]
			}
		}
	},
	'/diagnostics/': {
		get: [ mw.auth.access.write, mw.etag.noSave, mw.admin.diagnostics.get ]
	},
	'/rss/': {
		get: [ mw.rss.get ]
	},
//
	'/user': {
		get: [ mw.auth.access.read, mw.etag.noSave, mw.user.list ],
		post: [  mw.auth.access.edit, mw.etag.noSave, mw.user.post ],
	},
	'/user/:user_id': {
		get: [ mw.user.get ],
		put: [  mw.user.put ],
		delete: [ mw.user.delete ]
	},
//
});

app.use(mw.etag.save);
// Invalidate cache on database events
mw.etag.watch(app);

// Generic error handler. Stops stack dumps
// being sent to clients.
app.use(function (err, req, res, next) {
	const title = `HTTP backend error at ${req.method} ${req.originalUrl}`;
	const description = err.message;
	const message = err.message;
	const alert = {title, message, description, error: err};

	console.log("Error:", err);
	ERROR("%O", err)

	res.set("Content-Type", "application/json");
	if (err instanceof Error && err.name != "UnauthorizedError") {
// 		console.error(err.stack);
		ERROR(err.stack);
		res.set("Content-Type", "text/plain");
		res.status(500).send('General internal error');
		maybeSendAlert(alert);
	} else if (typeof err === 'string') {
		res.status(500).send({message: err});
		maybeSendAlert(alert);
	} else {
		res.status(err.status || 500).send({message: err.message || (err.inner && err.inner.message) || "Internal error"});
		if (!res.status) {
			maybeSendAlert(alert);
		}
	}
});

app.get("*", (req, res, next) => {
	if (!res.headersSent) {
		res.status(404).send({status: 404, message: "This endpoint does not exist"});
	}
});

app.disable('x-powered-by');
app.enable('trust proxy');
INFO('trust proxy is ' + (app.get('trust proxy')? 'on' : 'off'));

if (!module.parent) {
	const port = process.env.HTTP_PORT || 3000;
	const host = process.env.HTTP_HOST || "127.0.0.1";
	var server = http.createServer(app).listen(port, host);

	INFO('API started on port ' + port);
} else {
	app.start = function (port = 3000, host = "127.0.0.1", path) {

		var root = app;
		if (path) {
			root = express();
			['x-powered-by', 'trust proxy'].forEach(k => root.set(k, app.get(k)));
			root.use(path, app);
		}

		const server = http.createServer(root).listen(port, host);
		if (server) {
			console.log(`API started on port ${port}, prefix: ${path || "/"}`);
			INFO(`API started on port ${port}, prefix: ${path || "/"}`);
		}
		return server;
	}
	module.exports = app;
}
