local mymodule = {}

modelfunctions = require("modelfunctions")
posix = require("posix")
format = require("acf.format")
fs = require("acf.fs")
processinfo = require("acf.processinfo")
validator = require("acf.validator")
date = require("acf.date")

local processname = "openvpn"
local packagename = "openvpn"
local configfile = "/etc/openvpn/openvpn.conf"
local baseurl = "/etc/openvpn/"
local certurl = "/etc/openvpn/openvpn_certs/"

function mymodule.set_processname(p)
	processname = p
	configfile = "/etc/openvpn/"..processname..".conf"
	certurl = "/etc/openvpn/"..processname.."_certs/"
end

-- ################################################################################
-- LOCAL FUNCTIONS

local function config_content( f )
	local config = {}
	local lines = format.parse_linesandwords(fs.read_file(f) or "", "[#;]")
	-- there can be multiple entries
	for i,linetable in ipairs(lines) do
		if config[linetable[1]] then
			config[linetable[1]] = config[linetable[1]] .. "\n" .. (table.concat(linetable, " ", 2) or "")
		else
			config[linetable[1]] = table.concat(linetable, " ", 2) or ""
		end
	end

	config.name = f
	if config.remote then
		config.remoteport = string.match ( config.remote, "^%S+%s+(%S*)" )
	end
	if not ( config.log ) then
		config.log = config["log-append"]
	end
	if not ( config["max-clients"] ) then
		config["max-clients"] = "Unlimited"
	end
	if not ( config["local"] ) then
		config["local"] = "0.0.0.0"
	end
	return config
end

local function check_valid_config (config)
	config.errtxt = nil
	if not (config.client) or not (config.ca) or not (config.cert) or not (config.key) or not (config.dev) or not (config.proto) or not (config.remote) then
		config.errtxt = ""
		if not (config.ca) then
			config.errtxt = config.errtxt .. "Check CA; "
		end
		if not (config.cert) then
			config.errtxt = config.errtxt .. "Check CERT; "
		end
		if not (config.key) then
			config.errtxt = config.errtxt .. "Check KEY; "
		end
		if not (config.dev) then
			config.errtxt = config.errtxt .. "Check DEV; "
		end
		if not (config.proto) then
			config.errtxt = config.errtxt .. "Check PROTO; "
		end
		if (config.client) or not (config.ca) or not (config.cert) or not (config.key) or not (config.dev) or not (config.proto) or not (config.port) then
			config.type = nil
		else
			config.type = "server"
			config.errtxt = nil
		end
	else
		config.type = "client"
		config.errtxt = nil
	end
	if not (config.type) then config.type = "unknown" end
	return config.type, config.errtxt
end

local function clientlist( statusfile )
	local clientlist = {}
	local routinglist = {}
	local datechange = {}
	local list = {}
	if (statusfile) then
		local f = fs.read_file_as_array( statusfile )
		local clientlst = false
		local routinglst = false
		if ( f ) then
			for k,v in ipairs(f) do
				local col = format.string_to_table(v, ",")
				if ( col[1] == "ROUTING TABLE" ) or ( col[1] == "GLOBAL STATS" ) then
					clientlst = false
					routinglst = false
				end
				if ( clientlst ) then
					table.insert(clientlist, { CN=col[1],
						REALADDR=col[2],
						BYTESRCV=col[3],
						BYTESSND=col[4],
						CONN=col[5] } )
				end
				if ( routinglst ) then
					table.insert(routinglist, { VIRTADDR=col[1],
						CN=col[2],
						REALADDR=col[3],
						LAST=col[4] } )
					if (col[4]) then
						local month,day,hour,min,sec,year = string.match(col[4],"^%S+%s+(%S+)%s+(%S+)%s+(%d%d):(%d%d):(%d%d)%s+(%S+)")
						table.insert(datechange, { year=year,
							month=date.abr_month_num(month),
							day=day,
							hour=hour,
							min=min,
							sec=sec } )
					end
				end
				if ( col[1] == "Virtual Address" ) then
					routinglst = true
				end
				if ( col[1] == "Common Name" ) then
					clientlst = true
				end

			end
		end
	end
	-- JOIN 'CLIENT_LIST' and 'ROUTING_LIST' TABLES INTO ONE TABLE AND LATER ON PRESENT THIS TABLE
	for k,v in ipairs(clientlist) do
		for kk,vv in ipairs(routinglist) do
			if ( v.CN == vv.CN ) then
				table.insert(list, { CN=v.CN, REALADDR=v.REALADDR, BYTESRCV=v.BYTESRCV, BYTESSND=v.BYTESSND, VIRTADDR=vv.VIRTADDR, CONN=v.CONN, LAST = LAST } )
			end
		end
	end
	local lastdatechangetxt, lastdatechangediff
	if ((#clientlist > 0) and (#datechange > 0)) then
		local lastdatechange = date.date_to_seconds(datechange)
		lastdatechangetxt = os.date("%c", lastdatechange[#lastdatechange])
		lastdatechangediff = os.time() - os.date(lastdatechange[#lastdatechange])
		if (lastdatechangediff > 60) then
			lastdatechangediff = math.modf(lastdatechangediff / 60) .. " min"
		else
			lastdatechangediff = lastdatechangediff  .. " sec"
		end
	end
	return list, #clientlist, lastdatechangetxt, lastdatechangediff
end

-- ################################################################################
-- PUBLIC FUNCTIONS

function mymodule.getstatus()
	return modelfunctions.getstatus(processname, packagename, "OpenVPN Status")
end

function mymodule.get_startstop(self, clientdata)       
        return modelfunctions.get_startstop(processname)
end

function mymodule.startstop_service(self, startstop, action)        
        return modelfunctions.startstop_service(startstop, action)
end

function mymodule.getclientinfo()
	local config = config_content(configfile)
	return cfe({ type="structure", value=clientlist(config.status), label="Client info" })
end

function mymodule.get_config()
	local config = config_content(configfile)
	check_valid_config(config)
	if config.type == "server" then
		local clientlist, client_count, client_lastupdate, client_lastdatechangediff = clientlist(config.status)
		config["client_lastupdate"] = client_lastupdate or "?"
		config["client_lastdatechangediff"] = client_lastdatechangediff or "? min"
		config["client_count"] = client_count or 0
	end
	return cfe({ type="structure", value=config, label="OpenVPN Config" })
end

function mymodule.get_logfile(self, clientdata)
	local retval = cfe({ type="group", value={}, label="Log File Configuration" })
	local config = config_content(configfile)
	if config.log then
		retval.value.filename = cfe({value=config.log, label="File name"})
	else
		retval.value.facility = cfe({value="daemon", label="Syslog Facility"})
		retval.value.grep = cfe({ value="openvpn", label="Grep" })
	end
	return retval
end

function mymodule.get_filecontent()
	--FIXME validate
	return modelfunctions.getfiledetails(configfile)
end

function mymodule.update_filecontent(self, filedetails)
	--FIXME validate
	return modelfunctions.setfiledetails(self, filedetails, {configfile})
end

function mymodule.list_certs()
	local list = {}
	for file in fs.find(".*%.pem", certurl) do
		list[#list+1] = file
	end
	return cfe({ type="list", value=list, label="OpenVPN Certificates" })
end

function mymodule.get_delete_cert(self, clientdata)
	local retval = {}
	retval.cert = cfe({ value=clientdata.cert or "", label="Certificate Local Name" })
	return cfe({ type="group", value=retval, label="Delete Certificate" })
end

function mymodule.delete_cert(self, delcert)
	local list = mymodule.list_certs()
	delcert.value.cert.errtxt = "Invalid cert name"
	delcert.errtxt = "Failed to delete certificate"
	for i,cert in ipairs(list.value) do
		if cert == delcert.value.cert.value then
			os.remove(cert)
			delcert.value.cert.errtxt = nil
			delcert.errtxt = nil
			break
		end
	end
	return delcert
end

function mymodule.new_upload_cert()
	local value = {}
	value.cert = cfe({ type="raw", value=0, label="Certificate", descr='File must be a password protected ".pfx" file', seq=1 })
	value.password = cfe({ type="password", label="Certificate Password", seq=2 })
	value.name = cfe({ label="Certificate Local Name", seq=3 })
	return cfe({ type="group", value=value })
end

function mymodule.upload_cert(self, newcert)
	local success = true
	-- Trying to upload a cert/key
	-- The way haserl works, cert contains the temporary file name
	-- First, get the cert
	local cmd, f, cmdresult, errtxt
	if validator.is_valid_filename(newcert.value.cert.value, "/tmp/") and fs.is_file(newcert.value.cert.value) then
		cmdresult, errtxt = modelfunctions.run_executable({"openssl", "pkcs12", "-in", newcert.value.cert.value, "-out", newcert.value.cert.value.."cert.pem", "-password", "pass:"..newcert.value.password.value, "-nokeys", "-clcerts"}, true)
		local filestats = posix.stat(newcert.value.cert.value.."cert.pem")
		if not filestats or filestats.size == 0 then
			newcert.value.cert.errtxt = "Could not open certificate\n"..(errtxt or cmdresult)
			success = false
		end
	else
		newcert.value.cert.errtxt = "Invalid certificate"
		success = false
	end

	-- Now, get the key and the ca certs
	if success then
		cmdresult, errtxt = modelfunctions.run_executable({"openssl", "pkcs12", "-in", newcert.value.cert.value, "-out", newcert.value.cert.value.."key.pem", "-password", "pass:"..newcert.value.password.value, "-nocerts", "-nodes"}, true)
		filestats = posix.stat(newcert.value.cert.value.."key.pem")
		if not filestats or filestats.size == 0 then
			newcert.value.cert.errtxt = "Could not find key\n"..(errtxt or cmdresult)
			success = false
		end

		cmdresult, errtxt = modelfunctions.run_executable({"openssl", "pkcs12", "-in", newcert.value.cert.value, "-out", newcert.value.cert.value.."ca.pem", "-password", "pass:"..newcert.value.password.value, "-nokeys", "-cacerts"}, true)
		filestats = posix.stat(newcert.value.cert.value.."ca.pem")
		if not filestats or filestats.size == 0 then
			newcert.value.cert.errtxt = "Could not find CA certs\n"..(errtxt or cmdresult)
			success = false
		end
	end

	if newcert.value.name.value == "" then
		newcert.value.name.errtxt = "Cannot be blank"
		success = false
	elseif posix.stat(certurl..newcert.value.name.value.."-cert.pem") or posix.stat(certurl..newcert.value.name.value.."-key.pem") or posix.stat(certurl..newcert.value.name.value.."-ca.pem") then
		newcert.value.name.errtxt = "Certificate of this name already exists"
		success = false
	end

	if success then
		if not posix.stat(certurl) then
			fs.create_directory(certurl)
		end
		-- copy the keys
		fs.move_file(newcert.value.cert.value.."cert.pem", certurl..newcert.value.name.value.."-cert.pem")
		fs.move_file(newcert.value.cert.value.."key.pem", certurl..newcert.value.name.value.."-key.pem")
		fs.move_file(newcert.value.cert.value.."ca.pem", certurl..newcert.value.name.value.."-ca.pem")
		posix.chmod(certurl..newcert.value.name.value.."-key.pem", "rw-------")
	else
		newcert.errtxt = "Failed to upload certificate"
	end

	-- Delete the temporary files
	if validator.is_valid_filename(newcert.value.cert.value, "/tmp/") and fs.is_file(newcert.value.cert.value) then
		os.remove(newcert.value.cert.value.."cert.pem")
		os.remove(newcert.value.cert.value.."key.pem")
		os.remove(newcert.value.cert.value.."ca.pem")
	end

	return newcert
end

mymodule.view_cert = function(certname)
	local cmdresult = "Invalid cert name"
	local errtxt
	if not string.find(certname, "/") then
		certname = certurl..certname
	end
	if validator.is_valid_filename(certname, certurl) or validator.is_valid_filename(certname, baseurl) then
		cmdresult, errtxt = modelfunctions.run_executable({"openssl", "x509", "-in", certname, "-noout", "-text"})
		cmdresult = cmdresult .. "Content:\n" .. (fs.read_file(certname) or "")
	end
	return cfe({ type="structure", value={name=certname, value=cmdresult}, label="Certificate", errtxt=errtxt })
end

mymodule.get_generate_dh_params = function(self, clientdata)
	local retval = {}
	return cfe({ type="group", value=retval, label="Generate Diffie Hellman parameters" })
end

mymodule.generate_dh_params = function(self, gen)
	if not posix.stat(certurl) then
		fs.create_directory(certurl)
	end
	gen.descr, gen.errtxt = modelfunctions.run_executable({"openssl", "dhparam", "-out", certurl.."dh1024.pem", "1024"}, true)
	return gen
end

return mymodule
