-- util to easily merge multiple sets of LuaSec context options

local type = type;
local pairs = pairs;
local rawset = rawset;
local rawget = rawget;
local error = error;
local t_concat = table.concat;
local t_insert = table.insert;
local setmetatable = setmetatable;
local resolve_path = require"prosody.util.paths".resolve_relative_path;

local _ENV = nil;
-- luacheck: std none

local handlers = { };
local finalisers = { };
local id = function (v) return v end

-- All "handlers" behave like extended rawset(table, key, value) with extra
-- processing usually merging the new value with the old in some reasonable
-- way
-- If a field does not have a defined handler then a new value simply
-- replaces the old.


-- Convert either a list or a set into a special type of set where each
-- item is either positive or negative in order for a later set of options
-- to be able to remove options from this set by filtering out the negative ones
function handlers.options(config, field, new)
	local options = config[field] or { };
	if type(new) ~= "table" then new = { new } end
	for key, value in pairs(new) do
		if value == true or value == false then
			options[key] = value;
		else -- list item
			options[value] = true;
		end
	end
	rawset(config, field, options)
end

handlers.verifyext = handlers.options;

-- finalisers take something produced by handlers and return what luasec
-- expects it to be

-- Produce a list of "positive" options from the set
function finalisers.options(options)
	local output = {};
	for opt, enable in pairs(options) do
		if enable then
			output[#output+1] = opt;
		end
	end
	return output;
end

finalisers.verifyext = finalisers.options;

-- We allow ciphers to be a list

function finalisers.ciphers(cipherlist)
	if type(cipherlist) == "table" then
		return t_concat(cipherlist, ":");
	end
	return cipherlist;
end

-- Curve list too
finalisers.curveslist = finalisers.ciphers;

-- TLS 1.3 ciphers
finalisers.ciphersuites = finalisers.ciphers;

-- Path expansion
function finalisers.key(path, config)
	if type(path) == "string" then
		return resolve_path(config._basedir, path);
	else
		return nil
	end
end
finalisers.certificate = finalisers.key;
finalisers.cafile = finalisers.key;
finalisers.capath = finalisers.key;

function finalisers.dhparam(value, config)
	if type(value) == "string" then
		if value:sub(1, 10) == "-----BEGIN" then
			-- literal value
			return value;
		else
			-- assume a filename
			return resolve_path(config._basedir, value);
		end
	end
end

-- protocol = "x" should enable only that protocol
-- protocol = "x+" should enable x and later versions

local protocols = { "sslv2", "sslv3", "tlsv1", "tlsv1_1", "tlsv1_2", "tlsv1_3" };
for i = 1, #protocols do protocols[protocols[i] .. "+"] = i - 1; end

-- this interacts with ssl.options as well to add no_x
local function protocol(config)
	local min_protocol = protocols[config.protocol];
	if min_protocol then
		config.protocol = "sslv23";
		for i = 1, min_protocol do
			t_insert(config.options, "no_"..protocols[i]);
		end
	end
end

-- Merge options from 'new' config into 'config'
local function apply(config, new)
	rawset(config, "_cache", nil);
	if type(new) == "table" then
		for field, value in pairs(new) do
			-- exclude keys which are internal to the config builder
			if type(field) == "string" and field:sub(1, 1) ~= "_" then
				(handlers[field] or rawset)(config, field, value);
			end
		end
	end
	return config
end

-- Finalize the config into the form LuaSec expects
local function final(config)
	local output = { };
	for field, value in pairs(config) do
		-- exclude keys which are internal to the config builder
		if field:sub(1, 1) ~= "_" then
			output[field] = (finalisers[field] or id)(value, config);
		end
	end
	-- Need to handle protocols last because it adds to the options list
	protocol(output);
	return output;
end

local function build(config)
	local cached = rawget(config, "_cache");
	if cached then
		return cached, nil
	end

	local ctx, err = rawget(config, "_context_factory")(config:final(), config);
	if ctx then
		rawset(config, "_cache", ctx);
	end
	return ctx, err
end

local sslopts_mt = {
	__index = {
		apply = apply;
		final = final;
		build = build;
	};
	__newindex = function()
		error("SSL config objects cannot be modified directly. Use :apply()")
	end;
};


-- passing basedir through everything is required to avoid sslconfig depending
-- on prosody.paths.config
local function new(context_factory, basedir)
	return setmetatable({
		_context_factory = context_factory,
		_basedir = basedir,
		options={},
	}, sslopts_mt);
end

local function clone(config)
	local result = new();
	for k, v in pairs(config) do
		-- note that we *do* copy the internal keys on clone -- we have to carry
		-- both the factory and the cache with us
		rawset(result, k, v);
	end
	return result
end

sslopts_mt.__index.clone = clone;

return {
	apply = apply;
	final = final;
	_new = new;
};
