local mymodule = {}

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

-- Set variables
local configfile = "/etc/dnsmasq.conf"
local processname = "dnsmasq"
local packagename = "dnsmasq"
local leasefile = "/var/lib/misc/dnsmasq.leases"
local dnsfilterfile = "/etc/dnsmasq-dnsfilter.conf"

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

local function update_file (file, search_name, value_in)
	if not file or not search_name or search_name == "" then
		return file, false
	end

	-- Since we clear out the value, this prevents us from changing parent's value
	local value = value_in
	if type(value_in) == "table" then
		value = {}
		for i,val in ipairs(value_in) do
			value[#value + 1] = val
		end
	end

	local new_conf_file = {}
	local skip_lines = {}
	for l in string.gmatch(file, "([^\n]*)\n?") do
		if string.find ( l, "\\%s*$" ) then
			skip_lines[#skip_lines+1] = string.match(l, "^(.*)\\%s*$")
			l = nil
		else
			if #skip_lines then
				skip_lines[#skip_lines+1] = l
				l = table.concat(skip_lines, " ")
			end
			-- check if comment line
			if not string.find ( l, "^%s*#" ) then
				-- find name
				local a = string.match ( l, "^%s*([^=]*%S)%s*=" )
				if a and (search_name == a) then
					-- Figure out the value
					local b = string.match ( l, '=%s*(.*%S)%s*$' ) or ""
					-- no need to remove comments from end of line, they
					-- should not be there, see 'man dnsmasq'
					-- We found the name, change the value
					if not value then
						l = nil
					elseif type(value) == "string" then
						if value ~= b then
							l = search_name.."="..value
						end
						value = nil
					else
						local temp = l
						l = nil
						for i,val in ipairs(value) do
							if val == b then
								l = temp
								table.remove(value, i)
								break
							end
						end
					end
					skip_lines = {}	-- replacing line
				end
			end
			if #skip_lines > 0 then
				for i,line in ipairs(skip_lines) do
					new_conf_file[#new_conf_file + 1] = line
				end
				skip_lines = {}
				l = nil
			end
		end
		new_conf_file[#new_conf_file + 1] = l
	end

	if value then
		-- we didn't find the searchname, add it now
		if type(value) == "string" then
			new_conf_file[#new_conf_file + 1] = search_name.."="..value
		else
			for i,val in ipairs(value) do
				new_conf_file[#new_conf_file + 1] = search_name.."="..val
			end
		end
	end

	file = table.concat(new_conf_file, '\n')

	return file, true
end

-- Parse string for name=value pairs, returned in a table
local function parse_file (file)
	if not file or file == "" then
		return nil
	end
	local opts = nil
	local skip_lines = {}
	for l in string.gmatch(file, "([^\n]*)\n?") do
		if string.find ( l, "\\%s*$" ) then
			skip_lines[#skip_lines+1] = string.match(l, "^(.*)\\%s*$")
		else
			if #skip_lines then
				skip_lines[#skip_lines+1] = l
				l = table.concat(skip_lines, " ")
				skip_lines = {}
			end
			-- check if comment line
			if not string.find ( l, "^%s*#" ) then
				-- find name
				local a = string.match ( l, "^%s*([^=]*%S)%s*=" )
				if a then
					-- Figure out the value
					local b = string.match ( l, '=%s*(.*%S)%s*$' ) or ""
					-- no need to remove comments from end of line, they
					-- should not be there, see 'man dnsmasq'
					if not (opts) then opts = {} end
					if not opts[a] then
						opts[a] = {b}
					else
						table.insert(opts[a], b)
					end
				end
			end
		end
	end

	return opts
end

local function validateconfig(self, config)
	local success = true

	function testlist(param, test, errtxt)
		if #param.value > 0 then
			for i,val in ipairs(param.value) do
				if test(val) then
					param.errtxt = errtxt
					success = false
					break
				end
			end
		end
	end

	function testtext(param, test, errtxt)
		if test(param.value) then
			param.errtxt = errtxt
			success = false
		end
	end

	local interfaces = {}
	local interfacescontroller = self:new("alpine-baselayout/interfaces")
	local ints = interfacescontroller.model.get_interfaces()
	interfacescontroller:destroy()
	for i,v in ipairs(ints.value) do
		interfaces[v] = i
	end

	testlist(config.value.domain, function(v) return string.find(v, "%s") end, "Cannot contain spaces")
	testlist(config.value.interface, function(v) return not interfaces[v] end, "Invalid interface")
	testlist(config.value.listen_address, function(v) return not validator.is_ipv4(v) end, "Invalid IP Address")
	testlist(config.value.dhcp_range, function(v) return string.find(v, "%s") end, "Cannot contain spaces")
	testlist(config.value.no_dhcp_interface, function(v) return not interfaces[v] end, "Invalid interface")
	testlist(config.value.dhcp_host, function(v) return string.find(v, "%s") end, "Cannot contain spaces")
	--testlist(config.value.dhcp_option, function(v) return string.find(v, "%s") end, "Cannot contain spaces")
	testlist(config.value.mx_host, function(v) return string.find(v, "%s") end, "Cannot contain spaces")
	if config.value.dns_filtering.value then
		-- validate ip address only if dns filtering is enabled
		testtext(config.value.dns_default_address, function(v) return not validator.is_ipv4(v) end, "Invalid IP Address")
	end

	return success, config
end

local function dns_filtering_is_enabled(conf)
	-- Returns true if dnsfilterfile is included in configfile.
	-- conf - config variable from parse_file()
	local conf_files = conf["conf-file"] or {}
	for i,conf_file in ipairs(conf_files) do
		if conf_file == dnsfilterfile then
			return true
		end
	end
	return false
end

local function set_dns_filtering(file, enable)
	-- Returns new table of conf["conf-file"].
	-- Includes existing conf-files and adds or removes dnsfilterfile.
	-- file - config data
	-- enabled - boolean for dns filtering
	local conf = parse_file(file) or {}
	local enabled = dns_filtering_is_enabled(conf)
	local conf_files = conf["conf-file"] or {}
	if enabled ~= enable then
		-- state changed, we have to modify conf_files table
		if enable then
			-- add dnsfilterfile to conf-files
			table.insert(conf_files, dnsfilterfile)
		else
			-- remove dnsfilterfile from conf-files
			for i,conf_file in ipairs(conf_files) do
				if conf_file == dnsfilterfile then
					table.remove(conf_files, i)
					break
				end
			end
		end
	end
	return conf_files
end

local function update_dns_whitelist(servers, default_address)
	-- Updates dnsfilterfile
	-- servers - value from web form as table

	local file = fs.read_file(dnsfilterfile) or ""
	local serverlines = {}
	for i,server in ipairs(servers or {}) do
		local modified = "/" .. server .. "/#"
		table.insert(serverlines, modified)
	end
	file = update_file(file, "server", serverlines)
	-- update address to point to default_address
	file = update_file(file, "address", "/#/" .. default_address)
	fs.write_file(dnsfilterfile, file)
	return true
end

local function parse_whitelist(file)
	-- Parses dnsfilterfile and returns table of servers and
	-- default address.
	-- Format should be in:
	-- server=/whitelisted.com/#
	-- server=/another.com/#
	-- address=/#/192.168.1.1
	local servers = {}
	local parsed = parse_file(file) or {}
	local serverlines = parsed["server"] or {}
	for i,value in ipairs(serverlines) do
		-- remove '/' and '/#' from value
		local server = string.match(value, "^/%s*([^=]*%S)%s*/#")
		table.insert(servers, server)
	end
	-- parse default address
	local default_address = ""
	local addresses = parsed["address"] or {}
	if #addresses > 0 then
		-- get first address, anyway there shouldn't be more than 1?
		default_address = string.match(addresses[1], "^/#/(.*%S)")
	end
	return servers, default_address
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

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

function mymodule.getconfig()
	local conf = parse_file(fs.read_file(configfile) or "") or {}
	local dns_filtering = {}
	dns_filtering.enabled = dns_filtering_is_enabled(conf)
	dns_filtering.whitelist, dns_filtering.default_address = parse_whitelist(fs.read_file(dnsfilterfile) or "")

	local output = {}
	output.domain = cfe({ type="list", value=conf.domain or {}, label="Local Domain",
		descr="List of internal domain(s) for your LAN 'domain[,address_range]'.  Address range can be a single IP address, a range specified by 'IP,IP', or IP/netmask." })
	output.interface = cfe({ type="list", value=conf.interface or {}, label="Interface", descr="List of interfaces to listen on." })
	output.listen_address = cfe({ type="list", value=conf["listen-address"] or {}, label="List of IP addresses to listen on" })
	output.dhcp_range = cfe ({ type="list", value=conf["dhcp-range"] or {},	label="Range of DHCP IPs",
		descr="List of Start,End,Netmask,Time in seconds/minutes(m)/hours(h) ie. 169.254.0.10,169.254.0.100,255.255.255.0,12h" })
	output.no_dhcp_interface = cfe({ type="list", value=conf["no-dhcp-interface"] or {}, label="No DHCP Interface", descr="List of interfaces which should have DNS but not DHCP." })
	output.dhcp_host = cfe({ type="list", value=conf["dhcp-host"] or {}, label="DHCP Host Parameter", descr="List of per host parameters for the DHCP server. See dnsmasq documentation." })
	output.dhcp_option = cfe({ type="list", value=conf["dhcp-option"] or {}, label="DHCP Option", descr="List of different or extra options to DHCP clients. See dnsmasq documentation." })
	output.mx_host = cfe({ type="list", value=conf["mx-host"] or {}, label="MX Record", descr="List of MX records 'mx_name,hostname'." })

	output.dns_filtering = cfe({ type="boolean", value=dns_filtering.enabled or false, label="DNS filtering enabled", descr="State of DNS filtering." })
	output.dns_whitelist = cfe({ type="list", value=dns_filtering.whitelist or {}, label="DNS whitelist", descr="DNS whitelist for filtering. Type one allowed domain per one line." })
	output.dns_default_address = cfe({ type="text", value=dns_filtering.default_address or "", label="Blocked page server address", descr="All DNS requests which are not on the white list, will be redirected to this IP address. This address should have a web server running that shows an address blocked page. Example: 192.168.1.1" })
	return cfe({ type="group", value=output, label="DNS Masq Config" })
end

function mymodule.setconfig(self, config)
	local success, config = validateconfig(self, config)

	if success then
		local file = fs.read_file(configfile) or ""
		file = update_file(file,"domain",config.value.domain.value)
		file = update_file(file,"interface",config.value.interface.value)
		file = update_file(file,"listen-address",config.value.listen_address.value)
		file = update_file(file,"dhcp-range",config.value.dhcp_range.value)
		file = update_file(file,"no-dhcp-interface",config.value.no_dhcp_interface.value)
		file = update_file(file,"dhcp-host",config.value.dhcp_host.value)
		file = update_file(file,"dhcp-option",config.value.dhcp_option.value)
		file = update_file(file,"mx-host",config.value.mx_host.value)
		-- dns filtering
		local conf_file_value = set_dns_filtering(file, config.value.dns_filtering.value)
		file = update_file(file,"conf-file",conf_file_value)
		fs.write_file(configfile, file)
	else
		config.errtxt = "Failed to set config"
	end

	-- update dns filter file
	update_dns_whitelist(config.value.dns_whitelist.value, config.value.dns_default_address.value)

	return config
end

function mymodule.getconfigfile()
	-- FIXME Validate
	return modelfunctions.getfiledetails(configfile)
end

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

function mymodule.getleases()
	return modelfunctions.getfiledetails(leasefile)
end

function mymodule.get_logfile(self, clientdata)
	local retval = cfe({ type="group", value={}, label="Log File Configuration" })
	retval.value.facility = cfe({value="daemon", label="Syslog Facility"})
	retval.value.grep = cfe({ value="dnsmasq", label="Grep" })

	local conf = parse_file(fs.read_file(configfile) or "") or {}
	if conf["log-facility"] then
		local facility = conf["log-facility"][1]
		if string.find(facility, "/") then
			retval.value.filename = cfe({value=facility, label="File name"})
			retval.value.facility = nil
			retval.value.grep = nil
		elseif not string.find(facility, "^%-$") then
			retval.value.facility.value = string.lower(facility)
		end
	end

	return retval
end

return mymodule
