local mymodule = {}

-- Load libraries
modelfunctions = require("modelfunctions")
fs = require("acf.fs")
format = require("acf.format")
validator = require("acf.validator")
authenticator = require("authenticator")
roles = require("roles")
posix = require("posix")

-- Set variables
local configfiles = {}
local configuser
local packagename = "tinydns"
local processname = "tinydns"
local configfile = "/etc/conf.d/" .. processname
local configdir = "/etc/"..processname
local descr = {
	prefix={
		['.']="Name server for your domain (NS + A + SOA)", 
		['&']="Delegate subdomain (NS + A)", 
		['=']="Host and reverse record (A + PTR)",
		['+']="Host record (A, no PTR)",
		['@']="Mail exchanger (MX)",
		["'"]="Text record (TXT)",
		['^']="Reverse record (PTR)", 
		['C']="Canonical name (CNAME)", 
		['Z']="SOA record (SOA)", 
		[':']="Generic record", 
		['%']="Client location", 
		['S']="Service location (SRV)", 
		['N']="Naming authority pointer (NAPTR)", 
	},
	fieldlabels={
		['.']={"Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, 
		['&']={"Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, 
		['=']={"Host", "IP address", "Time to live", "Timestamp", "Location", },
		['+']={"Host", "IP address", "Time to live", "Timestamp", "Location", },
		['@']={"Domain", "IP address", "Mail exchanger", "Distance", "Time to live", "Timestamp", "Location", }, 
		['\'']={"Domain", "Text Record", "Time to live", "Timestamp", "Location", }, 
		['^']={"PTR", "Domain name", "Time to live", "Timestamp", "Location", }, 
		['C']={"Domain", "Canonical name", "Time to live", "Timestamp", "Location", }, 
		['Z']={"Domain", "Primary name server", "Contact address", "Serial number", "Refresh time", "Retry time", "Expire time", "Minimum time", "Time to live", "Timestamp", "Location",}, 
		[':']={"Domain", "Record type", "Record data", "Time to live", "Timestamp", "Location", }, 
		['%']={"Location", "IP prefix", }, 
		['S']={"Domain Service", "IP address", "Server", "Port", "Priority", "Weight", "Time to live", "Timestamp", },
		['N']={"Domain", "Order", "Preference", "Flags", "Service", "Regular expression", "Replacement", "Time to live", "Timestamp", },
	},
}

function mymodule.set_processname(p)
	processname = p
	configfile = "/etc/conf.d/" .. processname
	configdir = "/etc/"..processname
end

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

-- Return a table with the config-content of a file
-- Commented/Blank lines are ignored
local function get_value_from_file(file)
	local output = {}
	local filecontent = fs.read_file_as_array(file) or {}
	for i=1,table.maxn(filecontent) do
		local l = filecontent[i]
		if not (string.find ( l, "^[;#].*" )) and not (string.find (l, "^%s*$")) then
			table.insert(output, string.match(l,"(.-)%s*$"))
		end
	end
	if (#output > 0) then
		return true, output
	else
		return false, output
	end

end

-- Function to recursively inserts all filenames in a dir into an array
local function recursedir(path, filearray)
	local k,v
	for k,v in pairs(posix.dir(path) or {}) do
		-- Ignore files that begins with a '.'
		if not string.match(v, "^%.") then
			local f = path .. "/" .. v
			-- If subfolder exists, list files in this subfolder
			if (posix.stat(f).type == "directory") then
				recursedir(f, filearray)
			else
				table.insert(filearray, f)
			end
		end
	end
end

-- Functin to split items into a table
local function split_config_items(orgitem)
	local delimiter = ":"
	local output = {}
	output = format.string_to_table(string.sub(orgitem,2),delimiter)
	output.type = string.sub(orgitem,1,1)
	output.label = descr['prefix'][output.type]
	return output
end

local function getallowedlist(self, userid)
	local allowedlist = {}
	local auth = authenticator.get_subauth(self)
	local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userid) or ""
	for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end
	
	-- also check to see if there are allowed files for this user's roles
	local userinfo = authenticator.get_userinfo(self, userid)
	-- add in the guest role
	userinfo.roles[#userinfo.roles + 1] = roles.guest_role
	for i,role in ipairs(userinfo.roles) do
		local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or ""
		for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end
	end
	return allowedlist
end

-- Feed the configfiles table with list of all configfiles that are available and allowed
-- Default to allowing all files if no userid or allowed list
local function searchforconfigfiles(self, userid)
	if #configfiles > 0 and configuser == userid then
		return configfiles
	end
	local cnffile = {}
	recursedir(configdir, cnffile)
	local allowedlist = getallowedlist(self, userid)
	if allowedlist and #allowedlist > 0 then
		local reverseallowed = {}
		for x,name in ipairs(allowedlist) do reverseallowed[name] = x end
		for k,v in pairs(cnffile) do
			if reverseallowed[v] then
				table.insert(configfiles, v)
			end
		end
	else
		configfiles = cnffile
	end
	configuser = userid
	table.sort(configfiles)
	return configfiles
end

local function validfilename(path)
	for k,v in pairs(configfiles) do
		if (v == path) then
			return true
		end
	end
	return false, "Not a valid filename!"
end

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

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

-- Present some general status
function mymodule.getstatus()
	local status = modelfunctions.getstatus(processname, packagename, "TinyDNS Status")

	status.value.configdir = cfe({
		label="Config directory",
		value=configdir,
		})

	local config = mymodule.getconfig()
	status.value.listen = config.value.listen

	return status
end

function mymodule.getconfig()
	local config = {}

	local listenaddr = format.parse_ini_file(fs.read_file(configfile) or "","","IP") or ""
	config.listen = cfe({
		label="IP address to listen on",
		value=listenaddr,
		 })
	local test, errtxt = validator.is_ipv4(config.listen.value)
	if not test then
		config.listen.errtxt = errtxt
	end

	return cfe({ type="group", value=config, label="TinyDNS Configuration" })
end

function mymodule.setconfig(self, conf)
	local test, errtxt = validator.is_ipv4(conf.value.listen.value)
	if not test then
		conf.value.listen.errtxt = errtxt
		conf.errtxt = "Failed to set configuration"
	else
		modelfunctions.write_file_with_audit(self, configfile, format.update_ini_file(fs.read_file(configfile) or "","","IP",conf.value.listen.value))
	end

	return conf
end

-- If you enter 'filter_type' (this should be one of the options found in local function check_signs() ) then
-- the output will be filtered to only contain this type of data.
function mymodule.getconfigobjects(self, file_name, userid, filter_type)
	configfiles = searchforconfigfiles(self, userid)
	local configobjects = {}
	--Loop through all available configfiles
	for i,filename in pairs(configfiles) do
		if not file_name or file_name == filename then
			local filecontent = fs.read_file_as_array(filename) or {}
			for linenumber,configline in ipairs(filecontent) do
				local domaindetails = {}
				local filecontent_table = split_config_items(configline)
				filecontent_table.configline = configline

				-- Use only configs that has a valid prefix
				-- If function is called with some filter options... then show only the filtered values
				if ( not (filter_type) or ((filter_type) and (filter_type == filecontent_table.type)) )
					and (filecontent_table.label) 
				then
					filecontent_table.filename = filename
					filecontent_table.linenumber = linenumber
					
					-- we're gonna add a reverse domain name to make it easier to sort
					local domain = {}
					for mt in string.gmatch(filecontent_table[1], "([^.]+)") do
						table.insert(domain, mt)
					end
					local reversedomain = {}
					for i=#domain,1,-1 do
						table.insert(reversedomain, domain[i])
					end
					filecontent_table.sort = table.concat(reversedomain, ".")

					-- add it to the table
					table.insert(configobjects, filecontent_table)
				end
			end
		end
	end

	-- Sort the table by domain name (entry 1)
	table.sort(configobjects, function(a,b)
			if a == b then
				return false
			elseif a.sort ~= b.sort then
				return a.sort < b.sort
			elseif a.configline ~= b.configline then
				return a.configline < b.configline
			end
			a.errtxt = "Duplicate entry"
			b.errtxt = "Duplicate entry"
			return false
		end)
	for i,entry in ipairs(configobjects) do
		entry.sort = nil
	end

	return cfe({ type="structure", value=configobjects, label="DNS Entries", filename=file_name, fieldlabels=descr.fieldlabels })
end

function mymodule.getfilelist(self, userid)
	configfiles = searchforconfigfiles(self, userid)
	local listed_files = {}
	for i,name in pairs(configfiles) do
		local filedetails = posix.stat(name) or {}
		filedetails.filename = name
		table.insert(listed_files, filedetails)
	end

	return cfe({ type="structure", value=listed_files, label="Config files" })
end

function mymodule.get_filedetails(self, path, userid)
	configfiles = searchforconfigfiles(self, userid)
	return modelfunctions.getfiledetails(path, validfilename)
end

function mymodule.set_filedetails (self, filedetails, userid)
	configfiles = searchforconfigfiles(self, userid)
	return modelfunctions.setfiledetails(self, filedetails, validfilename)
end

function mymodule.getnewconfigfile()
	local options = {}
	options.filename = cfe({ label="File Name" })
	return cfe({ type="group", value=options, label="New config file" })
end

function mymodule.createconfigfile(self, configfile, userid)
	configfile.errtxt = "Failed to create file"
	local path = configfile.value.filename.value
	if not string.find(path, "/") then
		path = configdir .. "/" .. path
	end
	if validator.is_valid_filename(path,configdir) then
		if (posix.stat(path)) then
			configfile.value.filename.errtxt = "File already exists"
		else
			fs.create_file(path)
			configfile.errtxt = nil

			-- We have to add this file to the allowed list, if there is one
			local allowed = getallowedlist(self, userid)
			if #allowed > 0 then
				local perm = mymodule.getuserpermissions(self, userid)
				perm.value.allowed.value[#perm.value.allowed.value + 1] = path
				mymodule.setuserpermissions(self, perm)
			end
		end
	else
		configfile.value.filename.errtxt = "Invalid filename"
	end

	return configfile
end

function mymodule.get_remove_file(self, clientdata)
	local retval = {}
	retval.filename = cfe({ value=clientdata.filename or "", label="File Name" })
	return cfe({ type="group", value=retval, label="Delete Config File" })
end

function mymodule.remove_file(self, delfile, userid)
	local path = delfile.value.filename.value
	configfiles = searchforconfigfiles(self, userid)
	delfile.errtxt = "Failed to delete file"
	if not (fs.is_file(path)) then
		delfile.value.filename.errtxt = "File doesn't exist!"
	elseif (validfilename(path)) then
		os.remove(path)
		delfile.errtxt = nil
	else
		delfile.value.filename.errtxt = "Not a valid filename!"
	end
	return delfile
end

function mymodule.getpermissionslist(self)
	local auth = authenticator.get_subauth(self)
	local users = authenticator.list_users(self)
	local userlist = {}
	for i,user in ipairs(users) do
		local allowedlist = {}
		local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, user) or ""
		for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end
		userlist[#userlist + 1] = {id=user, allowed=allowedlist}
	end
	-- Need to check for roles as well as users
	local rolelist = {}
	local rols = roles.list_all_roles(self)
	for i,role in ipairs(rols) do
		local allowedlist = {}
		local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or ""
		for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end
		rolelist[#rolelist + 1] = {id=role, allowed=allowedlist}
	end
	table.sort(userlist, function(a,b) return a.id < b.id end)
	return cfe({ type="structure", value={user=userlist, role=rolelist}, label="TinyDNS Permissions" })
end

local function validateuserpermissions(self, userpermissions)
	local success = true
	local userinfo = authenticator.get_userinfo(self, userpermissions.value.userid.value)
	if not userinfo then
		userpermissions.value.userid.errtxt = "Invalid user"
		success = false
	end
	success = modelfunctions.validatemulti(userpermissions.value.allowed) and success
	return success, userpermissions
end

local function validaterolepermissions(self, rolepermissions)
	local success = false
	rolepermissions.value.role.errtxt = "Invalid role"
	local rols = roles.list_all_roles(self)
	for i,role in ipairs(rols) do
		if rolepermissions.value.role.value == role then
			rolepermissions.value.role.errtxt = nil
			success = true
			break
		end
	end
	success = modelfunctions.validatemulti(rolepermissions.value.allowed) and success
	return success, rolepermissions
end

function mymodule.getuserpermissions(self, userid)
	local allowedlist = {}
	local auth = authenticator.get_subauth(self)
	local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userid) or ""
	for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end
	local cnffile = {}
	recursedir(configdir, cnffile)
	table.sort(cnffile)
	local allowed = cfe({ type="multi", value=allowedlist, label="TinyDNS Permissions", option=cnffile, descr="If no permissions are defined, then all are allowed", seq=1 })
	if #cnffile == 0 then
		allowed.errtxt = "No domains defined"
	end
	local user = cfe({ value=userid, label="User Name", readonly=true, seq=0 })
	local output = cfe({ type="group", value={userid=user, allowed=allowed}, label="TinyDNS Permissions" })
	validateuserpermissions(self, output)
	return output
end

function mymodule.setuserpermissions(self, userpermissions)
	local success, userpermissions = validateuserpermissions(self, userpermissions)

	if success then
		local auth = authenticator.get_subauth(self)
		auth.write_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userpermissions.value.userid.value, table.concat(userpermissions.value.allowed.value, ","))
	else
		userpermissions.errtxt = "Failed to set user permissions"
	end
	return userpermissions
end

function mymodule.getrolepermissions(self, role)
	local allowedlist = {}
	local auth = authenticator.get_subauth(self)
	local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or ""
	for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end
	local cnffile = {}
	recursedir(configdir, cnffile)
	table.sort(cnffile)
	local allowed = cfe({ type="multi", value=allowedlist, label="TinyDNS Permissions", option=cnffile, descr="If no permissions are defined, then all are allowed", seq=1 })
	if #cnffile == 0 then
		allowed.errtxt = "No domains defined"
	end
	local rol = cfe({ value=role, label="Role", readonly=true, seq=0 })
	local output = cfe({ type="group", value={role=rol, allowed=allowed}, label="TinyDNS Permissions" })
	validaterolepermissions(self, output)
	return output
end

function mymodule.setrolepermissions(self, rolepermissions)
	local success, rolepermissions = validaterolepermissions(self, rolepermissions)

	if success then
		local auth = authenticator.get_subauth(self)
		auth.write_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, rolepermissions.value.role.value, table.concat(rolepermissions.value.allowed.value, ","))
	else
		rolepermissions.errtxt = "Failed to set role permissions"
	end
	return rolepermissions
end

return mymodule
