
/**
 */
type type_conf = {
	server : {
		host : string;
		port : int;
	};
	authentication : {
		timestamp_tolerance : float;
		hash_salt : string;
	};
	authelia : {
		usersfile_path : string;
	};
};


/**
 */
type type_user_list_sparse = (
	null
	|
	{
		users : Record<
			string,
			{
				disabled ?: boolean;
				displayname ?: string;
				email ?: string;
				groups ?: Array<string>;
				password : string;
			}
		>;
	}
);


/**
 */
type type_user_list_complete = {
	users : Record<
		string,
		{
			disabled : boolean;
			displayname : string;
			email : string;
			groups : Array<string>;
			password : string;
		}
	>;
};


/**
 */
async function conf_get(
	path : string
) : Promise<type_conf>
{
	const raw : any = lib_plankton.json.decode(await lib_plankton.file.read(path));;
	const version_fallback : int = 1;
	if (! ("version" in raw)) {
		lib_plankton.log.warning(
			"conf_version_unspecified",
			{
				"assumed_version": version_fallback,
			}
		);
	}
	else {
		// do nothing
	}
	const version : int = (raw["version"] ?? version_fallback);
	switch (version) {
		default: {
			throw (new Error("invalid version: " + version.toFixed(0)));
			break;
		}
		case 1: {
			return {
				"server": {
					"host": ((raw["server"] ?? {})["host"] ?? "::"),
					"port": ((raw["server"] ?? {})["port"] ?? 7463),
				},
				"authentication": {
					"timestamp_tolerance": (raw["authentication"]["timestamp_tolerance"] ?? 2.0),
					"hash_salt": raw["authentication"]["hash_salt"], // required
				},
				"authelia": {
					"usersfile_path": ((raw["authelia"] ?? {})["usersfile_path"] ?? "/var/authelia/users.yaml"),
				},
			};
			break;
		}
	}
}


/**
 */
function encode_user_list(
	user_list : (null | type_user_list_sparse)
) : string
{
	let output : string = "";
	output += "users:\n";
	Object.entries((user_list === null) ? {} : user_list.users).forEach(
		([key, value]) => {
			output += ("  " + key + ":\n");
			output += ("    " + "disabled: " + ((value.disabled ?? false) ? "true" : "false") + "\n");
			output += ("    " + "displayname: " + (value.displayname ?? key) + "\n");
			if ("email" in value) {
				output += ("    " + "email: " + value.email + "\n");
			}
			else {
				// do nothing
			}
			output += ("    " + "groups: " + JSON.stringify(value.groups ?? []) + "\n");
			output += ("    " + "password: " + value.password + "\n");
		}
	);
	return output;
}


/**
 */
async function main(
	args_raw : Array<string>
) : Promise<void>
{
	lib_plankton.log.conf_push(
		[
			lib_plankton.log.channel_make(
				{
					"kind": "stdout",
					"data": {
						"threshold": "info",
						// "format": "human_readable",
					}
				}
			),
		]
	);
	
	// args
	const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler({
		"conf_path": lib_plankton.args.class_argument.volatile({
			"indicators_long": ["conf_path"],
			"indicators_short": ["c"],
			"type": lib_plankton.args.enum_type.string,
			"mode": lib_plankton.args.enum_mode.replace,
			"default": "conf.json",
			"info": "path to conf file",
			"name": "conf-path",
		}),
		"help": lib_plankton.args.class_argument.volatile({
			"indicators_long": ["help"],
			"indicators_short": ["h"],
			"type": lib_plankton.args.enum_type.boolean,
			"mode": lib_plankton.args.enum_mode.replace,
			"default": false,
			"info": "only show help and exit",
			"name": "help",
		}),
	});
	const args : Record<string, any> = arg_handler.read(lib_plankton.args.enum_environment.cli, args_raw.join(" "));	
	
	const conf : type_conf = await conf_get(args.conf_path);
	
	// exec
	const rest_subject : lib_plankton.rest.type_rest = lib_plankton.rest.make(
		{
			"title": "authelia-callbacks",
			"versioning_method": "header",
			"versioning_header_name": "X-Api-Version",
			"set_access_control_headers": true,
			"authentication": {
				"kind": "key_header",
				"parameters": {"name": "X-Session-Key"}
			},
		}
	);
	{
		lib_plankton.rest.register<
			null,
			string
		>
		(
			rest_subject,
			lib_plankton.http.enum_method.get,
			"/meta/ping",
			{
				"description": "sendet ein 'pong' zurück; gedacht um die Erreichbarkeit des Backends zu prüfen",
				"input_schema": () => ({
					"nullable": true,
				}),
				"output_schema": () => ({
					"nullable": false,
					"type": "string",
				}),
				// "restriction": restriction_none,
				"execution": () => {
					return Promise.resolve({
						"status_code": 200,
						"data": "pong",
					});
				},
			}
		);
		lib_plankton.rest.register<
			null,
			any
		>
		(
			rest_subject,
			lib_plankton.http.enum_method.get,
			"/meta/spec",
			{
				"description": "gibt die API-Spezifikation im OpenAPI-Format aus",
				"input_schema": () => ({
					"nullable": true,
				}),
				"output_schema": () => ({
				}),
				// "restriction": restriction_none,
				"execution": () => {
					return Promise.resolve({
						"status_code": 200,
						"data": lib_plankton.rest.to_oas(rest_subject),
					});
				},
			}
		);
		lib_plankton.rest.register<
			type_user_list_sparse,
			null
		>(
			rest_subject,
			lib_plankton.http.enum_method.put,
			"/users/set",
			{
				"description": "setzt die Nutzerliste",
				"query_parameters": [
					{
						"name": "timestamp",
						"required": true,
						"description": "current UNIX timestamp",
					},
					{
						"name": "auth",
						"required": true,
						"description": "authorization string",
					},
				],
				"input_schema": () => ({
					"nullable": true,
					"type": "object",
					"additionalProperties": false,
					"properties": {
						"users": {
							"nullable": true,
							"type": "object",
							"additionalProperties": {
								"nullable": false,
								"type": "object",
								"additionalProperties": false,
								"properties": {
									"disabled": {
										"nullable": false,
										"type": "boolean",
									},
									"displayname": {
										"nullable": false,
										"type": "string",
									},
									"email": {
										"nullable": false,
										"type": "string",
									},
									"groups": {
										"nullable": false,
										"type": "array",
										"items": {
											"nullable": false,
											"type": "string"
										}
									},
									"password": {
										"nullable": false,
										"type": "string",
									},
								},
								"required": [
									"password",
								]
							},
							"properties": {},
							"required": [],
						}
					},
					"required": [
						"users"
					]
				}),
				"output_schema": () => ({
					"nullable": true,
				}),
				"restriction": async (stuff) => {
					const timestamp_local : float = lib_plankton.base.get_current_timestamp();
					const timestamp_remote : float = parseFloat(stuff.query_parameters["timestamp"]);
					if (Math.abs(timestamp_local - timestamp_remote) > conf.authentication.timestamp_tolerance) {
						lib_plankton.log.notice(
							"restriction_access_denied_due_to_invalid_timestamp",
							{
								"timestamp_local": timestamp_local,
								"timestamp_remote": timestamp_remote,
							}
						);
						return false;
					}
					else {
						const authhash_is : string = stuff.query_parameters["auth"];
						const authhash_shall : string = lib_plankton.sha256.get(
							timestamp_remote.toFixed(0) + conf.authentication.hash_salt
						);
						if (authhash_is !== authhash_shall) {
							lib_plankton.log.notice(
								"restriction_access_denied_due_to_mismatching_hashes",
								{
									"timestamp": timestamp_remote,
									"authhash_is": authhash_is,
									"authhash_shall": authhash_shall,
								}
							);
							return false;
						}
						else {
							return true;
						}
					}
				},
				"execution": async (stuff) => {
					await lib_plankton.file.write(
						conf.authelia.usersfile_path,
						encode_user_list(stuff.input)
					);
					lib_plankton.log.notice(
						"userdata_updated",
						{
						}
					);
					return Promise.resolve({
						"status_code": 200,
						"data": null
					});
				},
			}
		);
	}
	
	const server : lib_plankton.server.type_subject = lib_plankton.server.make(
		async (input, metadata) => {
			const http_request : lib_plankton.http.type_request = lib_plankton.http.decode_request(input.toString());
			return (
				lib_plankton.rest.call(
					rest_subject,
					http_request
				)
				.catch(
					(error) => {
						lib_plankton.log.error(
							"server_request_processing_failed",
							{
								"error": String(error),
							}
						);
						return Promise.resolve<lib_plankton.http.type_response>(
							{
								"version": "HTTP/1.1",
								"status_code": 500,
								"headers": {},
								// @ts-ignore
								"body": Buffer.from(""),
							}
						);
					}
				)
				.then<string>(
					(http_response) => {
						const output : string = lib_plankton.http.encode_response(http_response);
						return Promise.resolve<string>(output);
					}
				)
			);
		},
		{
			"host": conf.server.host,
			"port": conf.server.port,
		}
	);
	
	lib_plankton.server.start(server);
	
	return Promise.resolve<void>(undefined);
}


(
	main(process.argv.slice(2))
	.then(
		() => {}
	)
	.catch(
		(error) => {process.stderr.write(String(error));}
	)
);

