#!/usr/bin/env lua

dkjson = require("dkjson")
uci	= require("uci")

UCI = {}

--- Return the configuration defaults as a table suitable for JSON output
--
-- Mostly taken from yggdrasil -genconf -json
-- @return table with configuration defaults
function UCI.defaults()
	return { 
		AdminListen = "unix:///var/run/yggdrasil.sock", IfName = "ygg0", 
		NodeInfoPrivacy = false, IfTAPMode = false,
		LinkLocalTCPPort = 0, IfMTU = 65535,

		Peers = { }, Listen = { }, MulticastInterfaces = { }, AllowedEncryptionPublicKeys = { },
		InterfacePeers = setmetatable({ }, {__jsontype = "object"}),
		NodeInfo = setmetatable({ }, {__jsontype = "object"}),

		SessionFirewall = {
			Enable = false,
			AllowFromDirect = true,
			AllowFromRemote = true,
			AlwaysAllowOutbound = true,
			WhitelistEncryptionPublicKeys = { },
			BlacklistEncryptionPublicKeys = { }
		},
		TunnelRouting = {
			Enable = false,
			IPv6RemoteSubnets = setmetatable({ }, {__jsontype = "object"}),
			IPv6LocalSubnets = { },
			IPv4RemoteSubnets = setmetatable({ }, {__jsontype = "object"}),
			IPv4LocalSubnets = { }
		},
		SwitchOptions = { MaxTotalQueueSize = 4194304 }
	}
end

--- Return the yggdrasil configuration as a table suitable for JSON output
--
-- @return table with yggdrasil configuration
function UCI.get()
	local obj = UCI.defaults()

	local cursor = uci.cursor()
	local config = cursor:get_all("yggdrasil", "yggdrasil")
	if not config then return obj end

	obj.EncryptionPublicKey = config.EncryptionPublicKey
	obj.EncryptionPrivateKey = config.EncryptionPrivateKey
	obj.SigningPublicKey = config.SigningPublicKey
	obj.SigningPrivateKey = config.SigningPrivateKey
	obj.AdminListen = config.AdminListen or obj.AdminListen
	obj.IfName = config.IfName or obj.IfName
	obj.NodeInfo = dkjson.decode(config.NodeInfo) or obj.NodeInfo
	for _, v in pairs({ "NodeInfoPrivacy", "IfTAPMode" }) do
		if config[v] ~= nil then obj[v] = to_bool(config[v]) end
	end
	for _, v in pairs({ "LinkLocalTCPPort", "IfMTU" }) do
		if config[v] ~= nil then obj[v] = tonumber(config[v]) end
	end

	cursor:foreach("yggdrasil", "peer", function (s) 
		table.insert(obj.Peers, s.uri)
	end)
	cursor:foreach("yggdrasil", "listen_address", function (s) 
		table.insert(obj.Listen, s.uri)
	end)
	cursor:foreach("yggdrasil", "multicast_interface", function (s) 
		table.insert(obj.MulticastInterfaces, s.name)
	end)
	cursor:foreach("yggdrasil", "allowed_encryption_public_key", function (s) 
		table.insert(obj.AllowedEncryptionPublicKeys, s.key)
	end)

	cursor:foreach("yggdrasil", "interface_peer", function (s) 
		if obj.InterfacePeers[s.interface] == nil then
			obj.InterfacePeers[s.interface] = {}
		end
		table.insert(obj.InterfacePeers[s["interface"]], s.uri)
	end)

	-- session firewall config
	local session_firewall_config = { "Enable", "AllowFromDirect", "AllowFromRemote", "AlwaysAllowOutbound" }
	for _, v in pairs(session_firewall_config) do
		if config["SessionFirewall_"..v] ~= nil then
			obj.SessionFirewall[v] = to_bool(config["SessionFirewall_"..v])
		end
	end
	cursor:foreach("yggdrasil", "whitelisted_encryption_public_key", function (s) 
		table.insert(obj.SessionFirewall.WhitelistEncryptionPublicKeys, s.key)
	end)
	cursor:foreach("yggdrasil", "blacklisted_encryption_public_key", function (s) 
		table.insert(obj.SessionFirewall.BlacklistEncryptionPublicKeys, s.key)
	end)
	-- /session firewall config

	-- tunnel routing config
	if config.TunnelRouting_Enable ~= nil then
		obj.TunnelRouting.Enable = to_bool(config.TunnelRouting_Enable)
	end
	cursor:foreach("yggdrasil", "ipv6_remote_subnet", function (s) 
		obj.TunnelRouting.IPv6RemoteSubnets[s.subnet] = s.key
	end)
	cursor:foreach("yggdrasil", "ipv6_local_subnet", function (s) 
		table.insert(obj.TunnelRouting.IPv6LocalSubnets, s.subnet)
	end)
	cursor:foreach("yggdrasil", "ipv4_remote_subnet", function (s) 
		obj.TunnelRouting.IPv4RemoteSubnets[s.subnet] = s.key
	end)
	cursor:foreach("yggdrasil", "ipv4_local_subnet", function (s) 
		table.insert(obj.TunnelRouting.IPv4LocalSubnets, s.subnet)
	end)
	-- /tunnel routing config

	if config.SwitchOptions_MaxTotalQueueSize ~= nil then
		obj.SwitchOptions.MaxTotalQueueSize = tonumber(config.SwitchOptions_MaxTotalQueueSize)
	end

	return obj
end

--- Parse and save updated configuration from JSON input
--
-- Transforms general settings into UCI sections, and replaces the UCI config's
-- contents with them.
-- @param table JSON input
-- @return Boolean whether saving succeeded
function UCI.set(obj)
	local cursor = uci.cursor()

	for i, section in pairs(cursor:get_all("yggdrasil")) do
		cursor:delete("yggdrasil", section[".name"])
	end


	cursor:set("yggdrasil", "yggdrasil", "yggdrasil")
	cursor:set("yggdrasil", "yggdrasil", "EncryptionPublicKey", obj.EncryptionPublicKey) 
	cursor:set("yggdrasil", "yggdrasil", "EncryptionPrivateKey", obj.EncryptionPrivateKey) 
	cursor:set("yggdrasil", "yggdrasil", "SigningPublicKey", obj.SigningPublicKey) 
	cursor:set("yggdrasil", "yggdrasil", "SigningPrivateKey", obj.SigningPrivateKey)
	cursor:set("yggdrasil", "yggdrasil", "AdminListen", obj.AdminListen) 
	cursor:set("yggdrasil", "yggdrasil", "IfName", obj.IfName) 
	cursor:set("yggdrasil", "yggdrasil", "NodeInfoPrivacy", to_int(obj.NodeInfoPrivacy)) 
	cursor:set("yggdrasil", "yggdrasil", "NodeInfo", dkjson.encode(obj.NodeInfo)) 
	cursor:set("yggdrasil", "yggdrasil", "IfTAPMode", to_int(obj.IfTAPMode)) 
	cursor:set("yggdrasil", "yggdrasil", "LinkLocalTCPPort", obj.LinkLocalTCPPort)
	cursor:set("yggdrasil", "yggdrasil", "IfMTU", obj.IfMTU)

	set_values(cursor, "peer", "uri", obj.Peers)
	set_values(cursor, "listen_address", "uri", obj.Listen)
	set_values(cursor, "multicast_interface", "name", obj.MulticastInterfaces)
	set_values(cursor, "allowed_encryption_public_key", "key", obj.AllowedEncryptionPublicKeys)

	for interface, peers in pairs(obj.InterfacePeers) do
		for _, v in pairs(peers) do
			local name = cursor:add("yggdrasil", "interface_peer")
			cursor:set("yggdrasil", name, "interface", interface)
			cursor:set("yggdrasil", name, "uri", v)
		end
	end

	-- session firewall config
	cursor:set("yggdrasil", "yggdrasil", "SessionFirewall_Enable", to_int(obj.SessionFirewall.Enable)) 
	cursor:set("yggdrasil", "yggdrasil", "SessionFirewall_AllowFromDirect", to_int(obj.SessionFirewall.AllowFromDirect)) 
	cursor:set("yggdrasil", "yggdrasil", "SessionFirewall_AllowFromRemote", to_int(obj.SessionFirewall.AllowFromRemote)) 
	cursor:set("yggdrasil", "yggdrasil", "SessionFirewall_AlwaysAllowOutbound", to_int(obj.SessionFirewall.AlwaysAllowOutbound)) 
	set_values(cursor, "whitelisted_encryption_public_key", "key", obj.SessionFirewall.WhitelistEncryptionPublicKeys)
	set_values(cursor, "blacklisted_encryption_public_key", "key", obj.SessionFirewall.BlacklistEncryptionPublicKeys)
	-- /session firewall config

	-- tunnel routing config
	cursor:set("yggdrasil", "yggdrasil", "TunnelRouting_Enable", to_int(obj.TunnelRouting.Enable)) 
	if obj.TunnelRouting.IPv6RemoteSubnets ~= nil then
		for subnet, key in pairs(obj.TunnelRouting.IPv6RemoteSubnets) do
			local name = cursor:add("yggdrasil", "ipv6_remote_subnet")
			cursor:set("yggdrasil", name, "subnet", subnet)
			cursor:set("yggdrasil", name, "key", key)
		end
	end
	set_values(cursor, "ipv6_local_subnet", "subnet", obj.TunnelRouting.IPv6LocalSubnets)
	if obj.TunnelRouting.IPv4RemoteSubnets ~= nil then
		for subnet, key in pairs(obj.TunnelRouting.IPv4RemoteSubnets) do
			local name = cursor:add("yggdrasil", "ipv4_remote_subnet")
			cursor:set("yggdrasil", name, "subnet", subnet)
			cursor:set("yggdrasil", name, "key", key)
		end
	end
	set_values(cursor, "ipv4_local_subnet", "subnet", obj.TunnelRouting.IPv4LocalSubnets)
	-- /tunnel routing config

	cursor:set("yggdrasil", "yggdrasil", "SwitchOptions_MaxTotalQueueSize", obj.SwitchOptions.MaxTotalQueueSize) 

	return cursor:commit("yggdrasil")
end

function set_values(cursor, section_name, parameter, values) 
	if values == nil then return false end

	for k, v in pairs(values) do
		local name = cursor:add("yggdrasil", section_name)
		cursor:set("yggdrasil", name, parameter, v)
	end
end

function to_int(bool) return bool and '1' or '0' end

function to_bool(int) return int ~= '0' end

function help()
	print("JSON interface to /etc/config/yggdrasil\n\nExamples: \
	ygguci get > /tmp/etc/yggdrasil.conf \
	cat /tmp/etc/yggdrasil.conf | ygguci set \
	uci changes \
	ygguci get | yggdrasil -useconf")
end

-- main 

if arg[1] == "get" then
	local json = dkjson.encode(UCI.get(), { indent = true })
	print(json)
elseif arg[1] == "set" then
	local json = io.stdin:read("*a")
	local obj, pos, err = dkjson.decode(json, 1, nil)

	if obj then
		UCI.set(obj)
	else
		print("dkjson: " .. err)
		os.exit(1)
	end
else
	help()
end
