-- acf model for /etc/dhcp/*
-- Copyright(c) 2007 A. Brodmann - Licensed under terms of GPL2
local mymodule = {}

--- get additional libraries
modelfunctions = require("modelfunctions")
validator = require("acf.validator")
fs = require("acf.fs")

local subnet = {}
local configfile = "/etc/dhcp/dhcpd.conf"
local confdfile = "/etc/conf.d/dhcpd"
local filelist = {configfile, confdfile}
local processname = "dhcpd"
local packagename = "dhcp"
local leasefile = "/var/lib/dhcp/dhcpd.leases"
local config

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

local replacemagiccharacters = function(str)
	return string.gsub(str, "[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
end

local replaceentry = function(file, configentry, newstring)
	if newstring then
		return string.gsub(file, string.gsub(replacemagiccharacters(table.concat(configentry, "\n")), "\n", "%%s+"), replacemagiccharacters(newstring), 1)
	else
		return string.gsub(file, "[^\n%S]*"..string.gsub(replacemagiccharacters(table.concat(configentry, "\n")), "\n", "%%s+").."%s*;%s*\n?", "", 1)
	end
end

local parseconfigfile = function(file)
	-- first, remove all comments
	file = file or ""
	lines = {}
	for line in string.gmatch(file, "([^\n]*)\n?") do
		lines[#lines+1] = string.gsub(line, "#.*$", "")
	end
	file = table.concat(lines, " ")
	
	-- each line either ends with ';' or with '{'...'}'
	-- build an array with one entry per statement, each entry having an array of elements, and possibly a subarray
	local config = {}
	local stack = {config}
	local entry
	for word in string.gmatch(file, "(%S+)") do
		if word == "{" then
			if not entry then
				entry = {}
				table.insert(stack[#stack], entry)
			end
			entry.sub = {}
			stack[#stack+1] = entry.sub
			entry = nil
		elseif word == "}" then
			stack[#stack] = nil
		else
			if not entry then
				entry = {}
				table.insert(stack[#stack], entry)
				entry[1] = word
			else
				entry[#entry+1] = word
			end
			if string.find(word, ";$") then
				entry[#entry] = string.sub(word, 1, -2)
				entry = nil
			end
		end
	end
	return config
end

local validate_host = function( host )
	local success = true
	if host.value.host.value == "" or string.find(host.value.host.value, "[^%w.-]") then
		host.value.host.errtxt = "Invalid host name"
		success = false
	end
	if not validator.is_mac(host.value.mac.value) then
		host.value.mac.errtxt = "Invalid mac address"
		success = false
	end
	if host.value.addresses.value ~= "" then
		for address in string.gmatch(host.value.addresses.value, "([^,%s]+),?%s*") do
			if string.find(address, "[^%w.-]") then
				host.value.addresses.errtxt = "Invalid domain name / IPv4 address"
				success = false
			end
		end
	end
	return success, host
end

local validate_subnet = function( net )
	local success = true
	if net.value.subnet.value == "" or string.find(net.value.subnet.value, "[^%w.-]") then
		net.value.subnet.errtxt = "Invalid domain name / IPv4 address"
		success = false
	end
	if not validator.is_ipv4(net.value.netmask.value) then
		net.value.netmask.errtxt = "Invalid IPv4 address"
		success = false
	end
	if net.value.defleasetime.value ~= "" and not validator.is_integer_in_range(net.value.defleasetime.value, 1800, 86400) then
		net.value.defleasetime.errtxt = "Lease time must be: 1800 < x < 86400"
		success = false
	end
	if net.value.maxleasetime.value ~= "" and not validator.is_integer_in_range(net.value.maxleasetime.value, 1800, 86400) then
		net.value.maxleasetime.errtxt = "Lease time must be: 1800 < x < 86400"
		success = false
	end
	if net.value.routers.value ~= "" then
		for router in string.gmatch(net.value.routers.value, "([^,%s]+),?%s*") do
			if string.find(router, "[^%w.-]") then
				net.value.routers.errtxt = "Invalid domain name / IPv4 address"
				success = false
				break
			end
		end
	end
	if string.find(net.value.domainname.value, "[^%w.-]") then
		net.value.domainname.errtxt = "Invalid domain name"
		success = false
	end
	if net.value.domainnameservers.value ~= "" then
		for server in string.gmatch(net.value.domainnameservers.value, "([^,%s]+),?%s*") do
			if string.find(server, "[^%w.-]") then
				net.value.domainnameservers.errtxt = "Invalid domain name / IPv4 address"
				success = false
			end
		end
	end
	if net.value.leaserangestart.value ~= "" and not validator.is_ipv4(net.value.leaserangestart.value) then
		net.value.leaserangestart.errtxt = "Invalid IPv4 address"
		success = false
	end
	if net.value.leaserangeend.value ~= "" then
		if not validator.is_ipv4(net.value.leaserangeend.value) then
			net.value.leaserangeend.errtxt = "Invalid IPv4 address"
			success = false
		elseif net.value.leaserangestart.value == "" then
			net.value.leaserangeend.errtxt = "Cannot define range end without range start"
			success = false
		end
	end
	success = modelfunctions.validateselect(net.value.unknownclients) and success

	return success, net
end

local validate_settings = function ( settings )
	local success = modelfunctions.validateselect(settings.value.dnsupdatestyle)
	if settings.value.defleasetime.value ~= "" and not validator.is_integer_in_range(settings.value.defleasetime.value, 1800, 86400) then
		settings.value.defleasetime.errtxt = "Out of range 1800 < x < 86400 or not integer"
		success = false
	end
	if settings.value.maxleasetime.value ~= "" and not validator.is_integer_in_range(settings.value.maxleasetime.value, 1800, 86400) then
		settings.value.maxleasetime.errtxt = "Out of range 1800 < x < 86400 or not integer"
		success = false
	end
	if string.find(settings.value.domainname.value, "[^%w.-]") then
		settings.value.domainname.errtxt = "Invalid domain name"
		success = false
	end
	if settings.value.domainnameservers.value ~= "" then
		for server in string.gmatch(settings.value.domainnameservers.value, "([^,%s]+),?%s*") do
			if string.find(server, "[^%w.-]") then
				settings.value.domainnameservers.errtxt = "Invalid domain name / IPv4 address"
				success = false
			end
		end
	end

	return success, settings
end

-- Give it the string and the position of the { and it will find the corresponding }
local find_section_end = function(file, section_start)
	local i = section_start+1
	local indent = 1
	while indent > 0 and i <= #file do
		local char = string.sub(file, i, i)
		if char == "}" then indent = indent-1
		elseif char == "{" then indent = indent+1
		elseif char == "#" then i = string.find(file, "\n", i)
		elseif char == "'" then i = string.find(file, "'", i)
		elseif char == '"' then i = string.find(file, '"', i)
		end
		i=i+1;
	end
	return i-1
end

local host_write = function(host)
	local file = fs.read_file(configfile) or ""
	config = config or parseconfigfile(file)

	-- First, add the host line if necessary
	local hostline = "host "..host.value.host.value
	local found = false
	for i,value in ipairs(config or {}) do
		if value[1] == "host" and value[2] == host.value.host.value then
			found = true
		end
	end
	if not found then file = file.."\n"..hostline.." {\n}" end

	-- Now, find the host section
	local host_start = select(2, string.find(file, replacemagiccharacters(hostline).."%s*{"))
	local host_end = find_section_end(file, host_start) - 1
	host_start = string.find(file, "\n", host_start) + 1
	local hostcontent = string.sub(file, host_start, host_end)

	-- Update the host data
	host.value.mac.replace = "hardware ethernet "..host.value.mac.value
	if host.value.addresses.value ~= "" then
		host.value.addresses.replace = "fixed-address "..host.value.addresses.value
	end

	local hostconfig = parseconfigfile(hostcontent)
	for i,value in ipairs(hostconfig or {}) do
		if value[1] == "hardware" and value[2] == "ethernet" then
			hostcontent = replaceentry(hostcontent, value, host.value.mac.replace)
			host.value.mac.replace = nil
		elseif value[1] == "fixed-address" then
			hostcontent = replaceentry(hostcontent, value, host.value.addresses.replace)
			host.value.addresses.replace = nil
		end
	end

	-- add in new lines at the top if they didn't exist
	local newlines = {}
	newlines[#newlines+1] = host.value.mac.replace
	host.value.mac.replace = nil
	newlines[#newlines+1] = host.value.addresses.replace
	host.value.addresses.replace = nil
	if #newlines > 0 then
		for i,line in ipairs(newlines) do newlines[i] = "  "..line end
		newlines[#newlines+1] = hostcontent
		hostcontent = table.concat(newlines, ";\n")
	end

	-- The host is updated, put it into the file
	file = string.sub(file, 1, host_start-1) .. hostcontent .. string.sub(file, host_end+1, -1)

	-- Finally, write out the new file
	fs.write_file(configfile, string.gsub(file, "\n*$", ""))
	config = nil
end

local subnet_write = function(net)
	local file = fs.read_file(configfile) or ""
	config = config or parseconfigfile(file)

	-- First, add or update the submet line
	local subnetline = "subnet "..net.value.subnet.value.." netmask "..net.value.netmask.value
	local found = false
	for i,value in ipairs(config or {}) do
		if value[1] == "subnet" and value[2] == net.value.subnet.value then
			file = replaceentry(file, value, subnetline)
			found = true
		end
	end
	if not found then file = file.."\n"..subnetline.." {\n}" end

	-- Now, find the subnet section
	local subnet_start = select(2, string.find(file, replacemagiccharacters(subnetline).."%s*{"))
	local subnet_end = find_section_end(file, subnet_start) - 1
	subnet_start = string.find(file, "\n", subnet_start) + 1
	local subnetcontent = string.sub(file, subnet_start, subnet_end)

	-- Update the subnet data
	if net.value.defleasetime.value ~= "" then
		net.value.defleasetime.replace = "default-lease-time "..net.value.defleasetime.value
	end
	if net.value.maxleasetime.value ~= "" then
		net.value.maxleasetime.replace = "max-lease-time "..net.value.maxleasetime.value
	end
	if net.value.routers.value ~= "" then
		net.value.routers.replace = "option routers "..net.value.routers.value
	end
	if net.value.domainname.value ~= "" then
		net.value.domainname.replace = 'option domain-name "'..net.value.domainname.value..'"'
	end
	if net.value.domainnameservers.value ~= "" then
		net.value.domainnameservers.replace = "option domain-name-servers "..net.value.domainnameservers.value
	end
	if net.value.leaserangestart.value ~= "" then
		net.value.leaserangestart.replace = "range "..net.value.leaserangestart.value
	end
	if net.value.leaserangeend.value ~= "" then
		net.value.leaserangestart.replace = net.value.leaserangestart.replace.." "..net.value.leaserangeend.value
	end
	if net.value.unknownclients.value ~= "" then
		net.value.unknownclients.replace = net.value.unknownclients.value.." unknown-clients"
	end

	local subnetconfig = parseconfigfile(subnetcontent)
	for i,value in ipairs(subnetconfig or {}) do
		if value[1] == "default-lease-time" then
			subnetcontent = replaceentry(subnetcontent, value, net.value.defleasetime.replace)
			net.value.defleasetime.replace = nil
		elseif value[1] == "max-lease-time" then
			subnetcontent = replaceentry(subnetcontent, value, net.value.maxleasetime.replace)
			net.value.maxleasetime.replace = nil
		elseif value[1] == "option" then
			if value[2] == "routers" then
				subnetcontent = replaceentry(subnetcontent, value, net.value.routers.replace)
				net.value.routers.replace = nil
			elseif value[2] == "domain-name" then
				subnetcontent = replaceentry(subnetcontent, value, net.value.domainname.replace)
				net.value.domainname.replace = nil
			elseif value[2] == "domain-name-servers" then
				subnetcontent = replaceentry(subnetcontent, value, net.value.domainnameservers.replace)
				net.value.domainnameservers.replace = nil
			end
		elseif value[1] == "range" then
			-- We need to steal the dynamic-bootp status
			if value[2] == "dynamic-bootp" and net.value.leaserangestart.replace then
				net.value.leaserangestart.replace = string.gsub(net.value.leaserangestart.replace, "range", "%1 "..value[2])
			end
			-- Need to use a pool if unknownclients defined
			if net.value.unknownclients.replace then
				subnetcontent = replaceentry(subnetcontent, value)
			else
				subnetcontent = replaceentry(subnetcontent, value, net.value.leaserangestart.replace)
				net.value.leaserangestart.replace = nil
			end
		-- We only support one pool per subnet
		elseif value[1] == "pool" then
			for x,y in ipairs(value.sub or {}) do
				if y[2] == "unknown-clients" then
					subnetcontent = replaceentry(subnetcontent, y, net.value.unknownclients.replace)
					net.value.unknownclients.replace = nil
				elseif y[1] == "range" then
					-- We need to steal the dynamic-bootp status
					if y[2] == "dynamic-bootp" and net.value.leaserangestart.replace then
						net.value.leaserangestart.replace = string.gsub(net.value.leaserangestart.replace, "range", "%1 "..y[2])
					end
					subnetcontent = replaceentry(subnetcontent, y, net.value.leaserangestart.replace)
					net.value.leaserangestart.replace = nil
				end
			end
			if net.value.leaserangestart.replace then
				subnetcontent = string.gsub(subnetcontent, "(pool%s*{%s*\n)", "%1"..replacemagiccharacters(net.value.leaserangestart.replace)..";\n")
				net.value.leaserangestart.replace = nil
			end
			if net.value.unknownclients.replace then
				subnetcontent = string.gsub(subnetcontent, "(pool%s*{%s*\n)", "%1"..replacemagiccharacters(net.value.unknownclients.replace)..";\n")
				net.value.unknownclients.replace = nil
			end
		end
	end

	-- add in new lines at the top if they didn't exist
	local newlines = {}
	newlines[#newlines+1] = net.value.defleasetime.replace
	net.value.defleasetime.replace = nil
	newlines[#newlines+1] = net.value.maxleasetime.replace
	net.value.maxleasetime.replace = nil
	newlines[#newlines+1] = net.value.routers.replace
	net.value.routers.replace = nil
	newlines[#newlines+1] = net.value.domainname.replace
	net.value.domainname.replace = nil
	newlines[#newlines+1] = net.value.domainnameservers.replace
	net.value.domainnameservers.replace = nil
	if net.value.leaserangestart.replace and not net.value.unknownclients.replace then
		newlines[#newlines+1] = net.value.leaserangestart.replace
		net.value.leaserangestart.replace = nil
	end
	if #newlines > 0 then
		for i,line in ipairs(newlines) do newlines[i] = "  "..line end
		newlines[#newlines+1] = subnetcontent
		subnetcontent = table.concat(newlines, ";\n")
	end
	if net.value.unknownclients.replace then
		local temp = "  pool {\n    "..net.value.unknownclients.replace..";\n"
		net.value.unknownclients.replace = nil
		if net.value.leaserangestart.replace then
			temp = temp .. "    " .. net.value.leaserangestart.replace .. ";\n"
			net.value.leaserangestart.replace = nil
		end
		subnetcontent = subnetcontent .. temp .. "  }\n"
	end

	-- The subnet is updated, put it into the file
	file = string.sub(file, 1, subnet_start-1) .. subnetcontent .. string.sub(file, subnet_end+1, -1)

	-- Finally, write out the new file
	fs.write_file(configfile, string.gsub(file, "\n*$", ""))
	config = nil
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, "DHCP Status")
end

mymodule.create_new_host = function()
	host = { 
	        host         = cfe({ label="Host Name", seq=1 }),
	        mac          = cfe({ label="MAC Address", seq=2 }),
	        addresses    = cfe({ label="Fixed Addresses", descr="Comma-separated addresses", seq=3 }),
	      }
	      
	return cfe({ type="group", value=host, label="Host" })
end

mymodule.host_read = function(self, clientdata)
	local name = clientdata.host
	config = config or parseconfigfile(fs.read_file(configfile) or "")
	local host = mymodule.create_new_host()
	host.value.host.value = name
	host.value.host.readonly = true

	for j,k in ipairs(config) do
		if k[1] == "host" and k[2] == name then
			for i,value in ipairs(k.sub or {}) do
				if value[1] == "hardware" and value[2] == "ethernet" then
					host.value.mac.value = value[3] or ""
				elseif value[1] == "fixed-address" then
					host.value.addresses.value = table.concat(value, " ", 2)
				end
			end
			break
		end
	end

	return host
end

mymodule.host_update = function(self, host)
	local success, host = validate_host( host )
	if not host.value.host.errtxt then
		local previous_success = success
		success = false
		host.value.host.errtxt = "This host does not exist"
		local hosts = mymodule.get_hosts()
		for i,ht in ipairs(hosts.value) do
			if ht == host.value.host.value then
				success = previous_success
				host.value.host.errtxt = nil
				break
			end
		end
	end
	if success then
		host_write(host)
	else
		host.errtxt = "Failed to update host"
	end
	
	return host
end

mymodule.host_create = function(self, host)
	local success, host = validate_host(host)
	if not host.value.host.errtxt then
		local hosts = mymodule.get_hosts()
		for i,ht in ipairs(hosts.value) do
			if ht == host.value.host.value then
				success = false
				host.value.host.errtxt = "This host already exists"
				break
			end
		end
	end
	if success then
		host_write(host)
	else
		host.errtxt = "Failed to create host"
	end

	return host
end

mymodule.get_host_delete = function()
	local host = cfe({ label="Host" })
	return cfe({ type="group", value={host=host}, label="Delete Host" })
end

mymodule.host_delete = function(self, host)
	local file = fs.read_file(configfile) or ""
	config = config or parseconfigfile(file)
	local cmdresult = cfe({ value="Failed to delete host - not found", label="Delete host result" })
	local hosts = mymodule.get_hosts()
	for i,h in ipairs(hosts.value) do
		if h == host.value.host.value then
			local start, endd = string.find(file, "host%s*"..replacemagiccharacters(h).."[^{]*{")
			endd = find_section_end(file, endd)
			endd = string.find(file, "\n", endd)
			file = string.sub(file, 1, start-1) .. string.sub(file, endd+1, -1)
			fs.write_file(configfile, string.gsub(file, "\n*$", ""))
			config = nil
			cmdresult.value = "Host Deleted"
		end
	end

	return cmdresult
end

mymodule.get_hosts = function ()
	config = config or parseconfigfile(fs.read_file(configfile) or "")
	local retval = {}
	for i,entry in ipairs(config) do
		if string.lower(entry[1] or "") == "host" then
			table.insert(retval, entry[2])
		end
	end

	return cfe({ type="list", value=retval, label="Host Declarations" })
end

mymodule.create_new_subnet = function()
	net = { 
	        subnet       = cfe({ label="Subnet", seq=1 }),
	        netmask      = cfe({ label="Netmask", seq=2 }),
	        defleasetime = cfe({ label="Default Lease Time", seq=9 }),
	        maxleasetime = cfe({ label="Maximum Lease Time", seq=10 }),
	        routers      = cfe({ label="Routers", descr="Comma-separated addresses", seq=8 }),
	        domainname   = cfe({ label="Domainname", seq=6 }),
		domainnameservers = cfe({ label="Domain Name Servers", descr="Comma-separated addresses", seq=7 }),
	        --wpad         = cfe({ label="Web Proxy Auto Discovery" }),
	        leaserangestart = cfe({ label="Lease Range Start", seq=3 }),
	        leaserangeend = cfe({ label="Lease Range End", seq=4 }),
	        unknownclients = cfe({ type="select", label="Unknown Clients", option={"", "allow", "deny"}, seq=5 }),
	      }
	      
	return cfe({ type="group", value=net, label="Subnet" })
end

mymodule.subnet_read = function(self, clientdata)
	local name = clientdata.subnet
	config = config or parseconfigfile(fs.read_file(configfile) or "")
	local net = mymodule.create_new_subnet()
	net.value.subnet.value = name
	net.value.subnet.readonly = true
	local pools = 0
	local ranges = 0

	for j,k in ipairs(config) do
		if k[1] == "subnet" and k[2] == name then
			net.value.netmask.value = k[4] or ""
			for i,value in ipairs(k.sub or {}) do
				if value[1] == "default-lease-time" then
					net.value.defleasetime.value = value[2] or ""
				elseif value[1] == "max-lease-time" then
					net.value.maxleasetime.value = value[2] or ""
				elseif value[1] == "option" then
					if value[2] == "routers" then
						net.value.routers.value = table.concat(value, " ", 3)
					elseif value[2] == "domain-name" then
						net.value.domainname.value = string.sub(value[3] or "", 2, -2)
					elseif value[2] == "domain-name-servers" then
						net.value.domainnameservers.value = table.concat(value, " ", 3)
					--elseif value[2] == "local-wpad-server" then
					--	net.value.wpad.value = string.sub(value[3] or "", 2, -2)
					end
				elseif value[1] == "range" then
					ranges = ranges + 1
					if value[2] == "dynamic-bootp" then
						net.value.leaserangestart.value = value[3] or ""
						net.value.leaserangeend.value = value[4] or ""
					else
						net.value.leaserangestart.value = value[2] or ""
						net.value.leaserangeend.value = value[3] or ""
					end
				-- We only support one pool per subnet
				elseif value[1] == "pool" then
					pools = pools + 1
					for x,y in ipairs(value.sub or {}) do
						if y[2] == "unknown-clients" then
							net.value.unknownclients.value = y[1]
						elseif y[1] == "range" then
							ranges = ranges + 1
							if y[2] == "dynamic-bootp" then
								net.value.leaserangestart.value = y[3] or ""
								net.value.leaserangeend.value = y[4] or ""
							else
								net.value.leaserangestart.value = y[2] or ""
								net.value.leaserangeend.value = y[3] or ""
							end
						end
					end
				end
			end
			break
		end
	end
	
	if pools > 1 or ranges > 1 then
		net.value.subnet.errtxt = "Warning! This subnet contains multiple pool/range definitions.  This is not supported by ACF.  Saving may break functionality!"
	end

	return net
end

mymodule.subnet_update = function(self, net)
	local success, net = validate_subnet( net )
	if not net.value.subnet.errtxt then
		local previous_success = success
		success = false
		net.value.subnet.errtxt = "This subnet does not exist"
		local subnets = mymodule.get_subnets()
		for i,subnet in ipairs(subnets.value) do
			if subnet == net.value.subnet.value then
				success = previous_success
				net.value.subnet.errtxt = nil
				break
			end
		end
	end
	if success then
		subnet_write(net)
	else
		net.errtxt = "Failed to update subnet"
	end
	
	return net
end

mymodule.subnet_create = function(self, net)
	local success, net = validate_subnet(net)
	if not net.value.subnet.errtxt then
		local subnets = mymodule.get_subnets()
		for i,subnet in ipairs(subnets.value) do
			if subnet == net.value.subnet.value then
				success = false
				net.value.subnet.errtxt = "This subnet already exists"
				break
			end
		end
	end
	if success then
		subnet_write(net)
	else
		net.errtxt = "Failed to create subnet"
	end

	return net
end

mymodule.get_subnet_delete = function()
	local subnet = cfe({ label="Subnet" })
	return cfe({ type="group", value={subnet=subnet}, label="Delete Subnet" })
end

mymodule.subnet_delete = function(self, subnet)
	local file = fs.read_file(configfile) or ""
	config = config or parseconfigfile(file)
	subnet.value.subnet.errtxt = "Subnet not found"
	subnet.errtxt = "Failed to delete subnet"
	local subnets = mymodule.get_subnets()
	for i,s in ipairs(subnets.value) do
		if s == subnet.value.subnet.value then
			local start, endd = string.find(file, "subnet%s*"..replacemagiccharacters(s).."%s*netmask[^{]*{")
			endd = find_section_end(file, endd)
			endd = string.find(file, "\n", endd)
			file = string.sub(file, 1, start-1) .. string.sub(file, endd+1, -1)
			fs.write_file(configfile, string.gsub(file, "\n*$", ""))
			config = nil
			subnet.value.subnet.errtxt = nil
			subnet.errtxt = nil
		end
	end

	return subnet
end

mymodule.get_subnets = function ()
	config = config or parseconfigfile(fs.read_file(configfile) or "")
	local retval = {}
	for i,entry in ipairs(config) do
		if string.lower(entry[1] or "") == "subnet" then
			table.insert(retval, entry[2])
		end
	end

	return cfe({ type="list", value=retval, label="Subnet Declarations" })
end

mymodule.read_settings = function()
	config = config or parseconfigfile(fs.read_file(configfile) or "")
	local settings = {}
	settings.domainname = cfe({ label="Domain Name", seq=1 })
	settings.domainnameservers = cfe({ label="Domain Name Servers", descr="Comma-separated addresses", seq=2 })
	settings.dnsupdatestyle = cfe({ type="select", label="DNS Update Style", option={"none", "ad-hoc", "interim"}, seq=3 })
	settings.defleasetime = cfe({ label="Default Lease Time", seq=4 })
	settings.maxleasetime = cfe({ label="Maximum Lease Time", seq=5 })
	for i,value in ipairs(config) do
		if value[1] == "option" then
			if value[2] == "domain-name" then
				settings.domainname.value = string.sub(value[3] or "", 2, -2)
			elseif value[2] == "domain-name-servers" then
				settings.domainnameservers.value = table.concat(value, " ", 3)
			end
		elseif value[1] == "default-lease-time" then
			settings.defleasetime.value = value[2] or ""
		elseif value[1] == "max-lease-time" then
			settings.maxleasetime.value = value[2] or ""
		elseif value[1] == "ddns-update-style" then
			settings.dnsupdatestyle.value = value[2] or ""
		end
	end
	
	return cfe({ type="group", value=settings, label = "Global settings" })
end

mymodule.update_settings = function (self, settings)
	success, settings = validate_settings(settings)
	if success then
		local file = fs.read_file(configfile) or ""
		config = config or parseconfigfile(file)

		-- set up the lines we want to enter
		if settings.value.domainname.value ~= "" then
			settings.value.domainname.replace = 'option domain-name "'..settings.value.domainname.value..'"'
		end
		if settings.value.domainnameservers.value ~= "" then
			settings.value.domainnameservers.replace = "option domain-name-servers "..settings.value.domainnameservers.value
		end
		settings.value.dnsupdatestyle.replace = "ddns-update-style "..settings.value.dnsupdatestyle.value
		if settings.value.defleasetime.value ~= "" then
			settings.value.defleasetime.replace = "default-lease-time "..settings.value.defleasetime.value
		end
		if settings.value.maxleasetime.value ~= "" then
			settings.value.maxleasetime.replace = "max-lease-time "..settings.value.maxleasetime.value
		end

		-- replace existing lines
		for i,value in ipairs(config) do
			if value[1] == "option" then
				if value[2] == "domain-name" then
					file = replaceentry(file, value, settings.value.domainname.replace)
					settings.value.domainname.replace = nil
				elseif value[2] == "domain-name-servers" then
					file = replaceentry(file, value, settings.value.domainnameservers.replace)
					settings.value.domainnameservers.replace = nil
				end
			elseif value[1] == "ddns-update-style" then
				file = replaceentry(file, value, settings.value.dnsupdatestyle.replace)
				settings.value.dnsupdatestyle.replace = nil
			elseif value[1] == "default-lease-time" then
				file = replaceentry(file, value, settings.value.defleasetime.replace)
				settings.value.defleasetime.replace = nil
			elseif value[1] == "max-lease-time" then
				file = replaceentry(file, value, settings.value.maxleasetime.replace)
				settings.value.maxleasetime.replace = nil
			end
		end

		-- add in new lines at the top if they didn't exist
		local newlines = {}
		newlines[#newlines+1] = settings.value.domainname.replace
		settings.value.domainname.replace = nil
		newlines[#newlines+1] = settings.value.domainnameservers.replace
		settings.value.domainnameservers.replace = nil
		newlines[#newlines+1] = settings.value.dnsupdatestyle.replace
		settings.value.dnsupdatestyle.replace = nil
		newlines[#newlines+1] = settings.value.defleasetime.replace
		settings.value.defleasetime.replace = nil
		newlines[#newlines+1] = settings.value.maxleasetime.replace
		settings.value.maxleasetime.replace = nil
		if #newlines > 0 then
			newlines[#newlines+1] = file
			file = table.concat(newlines, ";\n")
		end
		fs.write_file(configfile, string.gsub(file, "\n*$", ""))
		config = nil
	else
		settings.errtxt = "Failed to update global settings"
	end

	return settings
end

mymodule.listconfigfiles = function()
	local listed_files = {}
	for i,name in ipairs(filelist) do
		local filedetails = posix.stat(name) or {}
		filedetails.filename = name
		table.insert(listed_files, filedetails)
	end
	table.sort(listed_files, function (a,b) return (a.filename < b.filename) end )

	return cfe({ type="structure", value=listed_files, label="DHCP File List" })
end

mymodule.getconfigfile = function(self, clientdata)
	return modelfunctions.getfiledetails(clientdata.filename, filelist)
end

mymodule.setconfigfile = function(self, filedetails)
	return modelfunctions.setfiledetails(self, filedetails, filelist)
end

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

return mymodule
