// Other autoconfigs tend to declare globals for 'Services', 'FileUtils', etc.
// as well, and all autoconfigs run in the same global scope.  Put our globals
// in 'pqcg' to avoid conflicts.
const pqcg = {};
pqcg.Services = globalThis.Services;
pqcg.log_str = "pqccomms-client-autoconfig\n"

function log(s) {
	pqcg.log_str += s;
	pqcg.log_str += "\n";
}

function getClassService(classPath, service) {
	return Components.classes[classPath].getService(service);
}

function openFileLineInputStream(filePath) {
	let istream = Components.classes["@mozilla.org/network/file-input-stream;1"].
		createInstance(Components.interfaces.nsIFileInputStream);
	istream.init(new pqcg.FileUtils.File(filePath), 1, 0444, 0);
	istream.QueryInterface(Components.interfaces.nsILineInputStream);
	return istream;
}

// Read an entire file into a string
function readFile(filePath) {
	let istream = Components.classes["@mozilla.org/network/file-input-stream;1"].
		createInstance(Components.interfaces.nsIFileInputStream);
	istream.init(new pqcg.FileUtils.File(filePath), 1, 0444, 0);
	let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
		createInstance(Components.interfaces.nsIConverterInputStream);
	cstream.init(istream, "UTF-8", 0, 0);

	let str = ""
	let read = 0
	let block = {}
	do {
		read = cstream.readString(0xFFFFFFFF, block);
		str += block.value
	} while (read != 0);

	cstream.close();
	istream.close();

	return str;
}

// Read an x.509 PEM certificate and return just the base64
// content (discard the BEGIN/END lines and line breaks)
function readCertificateContent(filePath) {
	let istream = openFileLineInputStream(filePath);

	let line = {};
	let more;
	// Look for the begin certificate line
	do {
		more = istream.readLine(line);
		if (line.value === "-----BEGIN CERTIFICATE-----")
			break;
	}
	while (more);

	// Capture the base64 content
	let content = "";
	do {
		more = istream.readLine(line);
		if (line.value === "-----END CERTIFICATE-----")
			break;
		content += line.value;
	}
	while (more);

	istream.close();
	return content;
}

function installCaCertificate(filePath) {
	let certDb = getClassService("@mozilla.org/security/x509certdb;1",
		Components.interfaces.nsIX509CertDB);
	try
	{
		let content = readCertificateContent(filePath);
		certDb.addCertFromBase64(content, "C,C,C", "");
	}
	catch (e)
	{
		// There's no CA certificate file yet, or some other error.
		// Log it and continue.
		log("no CA certificate yet: " + e.message);
	}
}

function readSiteDomain() {
	try
	{
		let siteConfigFilePath = getenv("HOME") + "/.pqccomms-client/site.json";
		log("check for site domain in: " + siteConfigFilePath);
		let siteConfigJson = readFile(siteConfigFilePath);
		let siteConfig = JSON.parse(siteConfigJson);
		return siteConfig.domain;
	}
	catch (e)
	{
		// There's no site configuration file yet, or some other error.
		// Log it and continue.
		log("no site config yet: " + e.message);
	}

	return null
}

function readChosenClientCn() {
	try
	{
		let cnFilePath = getenv("HOME") + "/.pqccomms-client/cn";
		log("check for CN in: " + cnFilePath);
		let istream = openFileLineInputStream(cnFilePath);
		let cnLine = {};
		istream.readLine(cnLine);
		// Remove line ending
		let cn = cnLine.value;
		while (cn.endsWith("\n"))
			cn = cn.slice(0, -1);
		log("found client CN: " + cn);
		return cn;
	}
	catch (e)
	{
		// There's no CN, file may not exist yet or is not readable.
		// Don't try to import a client cert.
		log("no client CN yet: " + e.message);
		return "";
	}
}

function installPkcs12ClientCertificate(filePath) {
	let certDb = getClassService("@mozilla.org/security/x509certdb;1",
		Components.interfaces.nsIX509CertDB);
	let errorCode = certDb.importPKCS12File(new pqcg.FileUtils.File(filePath), "");
	if (errorCode)
		throw new Error("PKCS#12 import failed: " + errorCode);
}

function enablePermissions(siteDomain) {
	let principal = pqcg.Services.scriptSecurityManager.createContentPrincipalFromOrigin(
		"https://" + siteDomain);
	pqcg.Services.perms.addFromPrincipal(principal, "desktop-notification", pqcg.Services.perms.ALLOW_ACTION);
	pqcg.Services.perms.addFromPrincipal(principal, "persistent-storage", pqcg.Services.perms.ALLOW_ACTION);
	pqcg.Services.perms.addFromPrincipal(principal, "camera", pqcg.Services.perms.ALLOW_ACTION);
	pqcg.Services.perms.addFromPrincipal(principal, "microphone", pqcg.Services.perms.ALLOW_ACTION);
}

function setClientCertForSite(filePath, siteDomain) {
	let certDb = getClassService("@mozilla.org/security/x509certdb;1",
		Components.interfaces.nsIX509CertDB);
	let content = readCertificateContent(filePath);
	let cert = certDb.constructX509FromBase64(content);
	let clientAuthRememberService = getClassService(
		"@mozilla.org/security/clientAuthRememberService;1",
		Components.interfaces.nsIClientAuthRememberService);
	let decisions = clientAuthRememberService.getDecisions();
	log("init decisions: " + JSON.stringify(decisions, null, 2));
	// This decision applies requests in the context of the site: loading
	// the site itself or the requests from those pages
	let sitePartitionKey = ChromeUtils.getPartitionKeyFromURL("https://" + siteDomain, "", false);
	clientAuthRememberService.rememberDecisionScriptable(siteDomain,
		{partitionKey: sitePartitionKey}, cert,
		Components.interfaces.nsIClientAuthRememberService.Permanent);
	// This decision applies to the API requests made by the service worker
	// (happens when you receive a Talk message and it tries to show a
	// notification)
	clientAuthRememberService.rememberDecisionScriptable(siteDomain,
		{}, cert,
		Components.interfaces.nsIClientAuthRememberService.Permanent);
	decisions = clientAuthRememberService.getDecisions();
	log("now decisions: " + JSON.stringify(decisions, null, 2));
}

log("import FileUtils");
pqcg.FileUtils = ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs").FileUtils;

try {
	log("Enable PQC hybrid key exchange");
	defaultPref("security.tls.enable_kyber", true);
	defaultPref("network.http.http3.enable_kyber", true);
	log("Set miscellaneous preferences");
	defaultPref("browser.startup.page", 3);	// Restore last session automatically
	log("install CA certificate");
	installCaCertificate("/usr/local/share/ca-certificates/pqccomms-client-ca.crt");
	let siteDomain = readSiteDomain();
	log("enable permissions");
	if (siteDomain) {
		enablePermissions(siteDomain);
	} else {
		log("no site domain, can't enable permissions for site");
	}
	log("check for client certificate");
	let cn = readChosenClientCn();
	if (cn) {
		log("found chosen client CN, install PKCS#12 certificate if it exists");
		// This cert might not exist yet if the CN was chosen but the
		// cert has not been authorized yet.  Just try and let it fail
		// if it doesn't exist.
		let p12Path = getenv("HOME") + "/.pqccomms-client/pki/private/" + cn + ".p12";
		log("try to install client cert from: " + p12Path);
		installPkcs12ClientCertificate(p12Path);
		log("installed client certificate");
		let certPath = getenv("HOME") + "/.pqccomms-client/pki/issued/" + cn + ".crt";
		if (siteDomain) {
			log("try to set client cert to site from: " + certPath);
			setClientCertForSite(certPath, siteDomain);
		} else {
			log("no site domain, can't set client cert for site");
		}
		log("set client cert for site");
	} else {
		log("no chosen client CN yet, do not try to install certificate");
	}
} catch (e) {
	log("error: " + e.message);
	// This appears in the browser console: hamburger > More Tools > Browser Console
	throw new Error(pqcg.log_str);
}
