import * as core from './core.js';
import * as form from './form.js';

let gDatabase = new Database("auth");

const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;

function b64url(value) {
	value = value.replaceAll('+', '-').replaceAll('/', '_');
	let equals = value.indexOf('=');
	if (equals !== -1) {
		return value.substring(0, equals);
	} else {
		return value;
	}
}

function unb64url(value) {
	value = value.replaceAll('-', '+').replaceAll('_', '/');
	let remainder = value.length % 4;
	if (remainder == 3) {
		return value + '=';
	} else if (remainder == 2) {
		return value + '==';
	} else {
		return value;
	}
}

function makeJwt(payload) {
	let ids = ssb.getIdentities(':auth');
	let id;
	if (ids?.length) {
		id = ids[0];
	} else {
		id = ssb.createIdentity(':auth');
	}

	let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval}))));
	let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.');
	return jwt;
}

function readSession(session) {
	let jwt_parts = session?.split('.');
	if (jwt_parts?.length === 3) {
		let [header, payload, signature] = jwt_parts;
		header = JSON.parse(base64Decode(unb64url(header)));
		if (header.typ === 'JWT' && header.alg === 'HS256') {
			signature = unb64url(signature);
			let id = ssb.getIdentities(':auth');
			if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
				let result = JSON.parse(base64Decode(unb64url(payload)));
				let now = new Date().valueOf()
				if (now < result.exp) {
					print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
					return result;
				} else {
					print(`JWT expired by ${(now - result.exp) / 1000} seconds.`);
				}
			} else {
				print('JWT verification failed.');
			}
		} else {
			print('Invalid JWT header.');
		}
	} else {
		print('No session JWT.');
	}
}

function verifyPassword(password, hash) {
	return bCrypt.hashpw(password, hash) == hash;
}

function hashPassword(password) {
	let salt = bCrypt.gensalt(12);
	return bCrypt.hashpw(password, salt);
}

function noAdministrator() {
	return !core.globalSettings || !core.globalSettings.permissions || !Object.keys(core.globalSettings.permissions).some(function(name) {
		return core.globalSettings.permissions[name].indexOf("administration") != -1;
	});
}

function makeAdministrator(name) {
	if (!core.globalSettings.permissions) {
		core.globalSettings.permissions = {};
	}
	if (!core.globalSettings.permissions[name]) {
		core.globalSettings.permissions[name] = [];
	}
	if (core.globalSettings.permissions[name].indexOf("administration") == -1) {
		core.globalSettings.permissions[name].push("administration");
	}
	core.setGlobalSettings(core.globalSettings);
}

function getCookies(headers) {
	let cookies = {};

	if (headers.cookie) {
		let parts = headers.cookie.split(/,|;/);
		for (let i in parts) {
			let equals = parts[i].indexOf("=");
			let name = parts[i].substring(0, equals).trim();
			let value = parts[i].substring(equals + 1).trim();
			cookies[name] = value;
		}
	}

	return cookies;
}

function isNameValid(name) {
	let c = name.charAt(0);
	return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && name.split().map(x => x >= ('a' && x <= 'z') || x >= ('A' && x <= 'Z') || x >= ('0' && x <= '9'));
}

function handler(request, response) {
	let session = getCookies(request.headers).session;
	if (request.uri == "/login") {
		let formData = form.decodeForm(request.query);
		if (query(request.headers)?.permissions?.authenticated) {
			if (formData.return) {
				response.writeHead(303, {"Location": formData.return});
			} else {
				response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + '/', "Content-Length": "0"});
			}
			response.end();
			return;
		}

		let sessionIsNew = false;
		let loginError;

		if (request.method == "POST" || formData.submit) {
			sessionIsNew = true;
			formData = form.decodeForm(utf8Decode(request.body), formData);
			if (formData.submit == "Login") {
				let account = gDatabase.get("user:" + formData.name);
				account = account ? JSON.parse(account) : account;
				if (formData.register == '1') {
					if (!account &&
						isNameValid(formData.name) &&
						formData.password == formData.confirm) {
						let users = new Set();
						let users_original = gDatabase.get('users');
						try {
							users = new Set(JSON.parse(users_original));
						} catch {
						}
						if (!users.has(formData.name)) {
							users.add(formData.name);
						}
						users = JSON.stringify([...users].sort());
						if (users !== users_original) {
							gDatabase.set('users', users);
						}
						session = makeJwt({name: formData.name});
						account = {password: hashPassword(formData.password)};
						gDatabase.set('user:' + formData.name, JSON.stringify(account));
						if (noAdministrator()) {
							makeAdministrator(formData.name);
						}
					} else {
						loginError = 'Error registering account.';
					}
				} else if (formData.change == '1') {
					if (account &&
						isNameValid(formData.name) &&
						formData.new_password == formData.confirm &&
						verifyPassword(formData.password, account.password)) {
						session = makeJwt({name: formData.name});
						account = {password: hashPassword(formData.new_password)};
						gDatabase.set('user:' + formData.name, JSON.stringify(account));
					} else {
						loginError = 'Error changing password.';
					}
				} else {
					if (account &&
						account.password &&
						verifyPassword(formData.password, account.password)) {
						session = makeJwt({name: formData.name});
						if (noAdministrator()) {
							makeAdministrator(formData.name);
						}
					} else {
						loginError = 'Invalid username or password.';
					}
				}
			} else {
				// Proceed as Guest
				session = makeJwt({name: 'guest'});
			}
		}

		let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict`;
		let entry = readSession(session);
		if (entry && formData.return) {
			response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
			response.end();
		} else {
			File.readFile("core/auth.html").then(function(data) {
				let html = utf8Decode(data);
				let auth_data = {
					session_is_new: sessionIsNew,
					name: entry?.name,
					error: loginError,
					code_of_conduct: core.globalSettings.code_of_conduct,
					have_administrator: !noAdministrator(),
				};
				html = utf8Encode(html.replace('$AUTH_DATA', JSON.stringify(auth_data)));
				response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": html.length});
				response.end(html);
			}).catch(function(error) {
				response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
				response.end("404 File not found");
			});
		}
	} else if (request.uri == "/login/logout") {
		response.writeHead(303, {"Set-Cookie": `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT`, "Location": "/login" + (request.query ? "?" + request.query : "")});
		response.end();
	} else {
		response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
		response.end("Hello, " + request.client.peerName + ".");
	}
}

function getPermissions(session) {
	let permissions;
	let entry = readSession(session);
	if (entry) {
		permissions = getPermissionsForUser(entry.name);
		permissions.authenticated = entry.name !== "guest";
	}
	return permissions || {};
}

function getPermissionsForUser(userName) {
	let permissions = {};
	if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) {
		for (let i in core.globalSettings.permissions[userName]) {
			permissions[core.globalSettings.permissions[userName][i]] = true;
		}
	}
	return permissions;
}

function query(headers) {
	let session = getCookies(headers).session;
	let entry;
	let autologin = tildefriends.args.autologin;
	if (entry = autologin ? {name: autologin} : readSession(session)) {
		return {
			session: entry,
			permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
		};
	}
}

function make_refresh(credentials) {
	if (credentials?.session?.name) {
		return {
			token: makeJwt({name: credentials.session.name}),
			interval: kRefreshInterval,
		};
	}
}

export { handler, query, make_refresh };