local mymodule = {}

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

-- Set variables
local packagename = "iptables"
local servicename = "iptables"
local configfile = "/etc/conf.d/iptables"

local tables = {"filter", "nat", "mangle"}
local details

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

local getdetails = function()
	if not details then
		details = {}
		local cmd = {"iptables", "-t", "filter", "-n", "-L", "-v"}
		for i,tab in ipairs(tables) do
			details[tab] = {table=tab}
			cmd[3] = tab
			local file = modelfunctions.run_executable(cmd)
			for line in string.gmatch(file, "([^\n]+)") do
				if string.match(line, "^Chain") then
					local name = string.match(line, "^%w+%s+(%S+)")
					local policy = string.match(line, "policy (%w+)")
					local references = string.match(line, "(%d+) references")
					table.insert(details[tab], {name=name, policy=policy, references=references})
				elseif not string.match(line, "target%s+prot") then
					local block = {}
					block.packets, block.bytes, block.rule = string.match(line, "^%s*(%S+)%s+(%S+)%s+(.*%S)")
					table.insert(details[tab][#details[tab]], block)
					--table.insert(details[tab][#details[tab]], line)
				elseif not details[tab].header then
					details[tab].header = line
				end
			end
		end
	end
end

local find_chain = function(tab, chain)
	getdetails()
	if details[tab] then
		for i,chn in ipairs(details[tab]) do
			if chain == chn.name then
				return chn
			end
		end
	end
	return nil
end

local save = function()
	modelfunctions.run_executable({"/etc/init.d/"..servicename, "save"}, true)
	details = nil
end

local function validate_rule(rule)
	local success = true

	function basiccheck(val)
		if string.find(val.value, "[%s\'\"]") then
			val.errtxt = "Cannot contain spaces or quotes"
			success = false
		end
	end

	success = modelfunctions.validateselect(rule.value.table) and success
	basiccheck(rule.value.chain)
	if rule.value.position.value ~= "" and not validator.is_integer(rule.value.position.value) then
		rule.value.position.errtxt = "Must be a number"
		success = false
	end
	basiccheck(rule.value.protocol)
	basiccheck(rule.value.source)
	basiccheck(rule.value.destination)
	basiccheck(rule.value.jump)
	basiccheck(rule.value.goto)
	basiccheck(rule.value.in_interface)
	basiccheck(rule.value.out_interface)
	if #rule.value.fragment.value > 1 or string.match(rule.value.fragment.value, "[^%+!]") then
		rule.value.fragment.errtxt = "Invalid entry"
		success = false
	end
	if rule.value.set_counters.value ~= "" and not string.match(rule.value.set_counters.value, "^%d+%s+%d+$") then
		rule.value.set_counters.errtxt = "Invalid entry"
		success = false
	end
	basiccheck(rule.value.addrtype_src_type)
	basiccheck(rule.value.addrtype_dst_type)
	if string.find(rule.value.comment.value, "[\'\"]") then
		rule.value.comment.errtxt = "Cannot contain quotes"
		success = false
	end
	basiccheck(rule.value.icmp_type)
	basiccheck(rule.value.src_range)
	basiccheck(rule.value.dst_range)
	basiccheck(rule.value.mac_source)
	basiccheck(rule.value.sports)
	basiccheck(rule.value.dports)
	basiccheck(rule.value.ports)
	basiccheck(rule.value.state)
	basiccheck(rule.value.tcp_sport)
	basiccheck(rule.value.tcp_dport)
	basiccheck(rule.value.udp_sport)
	basiccheck(rule.value.udp_dport)

	return success, rule
end

local function generate_rule_specification(rule)
	local spec = {}

	-- notfirst parameter indicates that the "not" (!) must come before the option name
	function addparameter(value, option, notfirst)
		if value ~= "" then
			if string.find(value, "^!") then
				if notfirst then
					spec[#spec + 1] = '!'
					spec[#spec + 1] = option
					spec[#spec + 1] = string.sub(value, 2)
				else
					spec[#spec + 1] = option
					spec[#spec + 1] = '!'
					spec[#spec + 1] = string.sub(value,2)
				end
			else
				spec[#spec + 1] = option
				spec[#spec + 1] = value
			end
		end
	end
	function addmodule(values, mod)
		for i,value in ipairs(values) do
			if value ~= "" then
				spec[#spec + 1] = "-m"
				spec[#spec + 1] = mod
				break
			end
		end
	end

	addparameter(rule.value.protocol.value, "-p", true)
	addparameter(rule.value.source.value, "-s", true)
	addparameter(rule.value.destination.value, "-d", true)
	addparameter(rule.value.jump.value, "-j")
	addparameter(rule.value.goto.value, "-g")
	addparameter(rule.value.in_interface.value, "-i", true)
	addparameter(rule.value.out_interface.value, "-o", true)
	if rule.value.fragment.value == "!" then
		spec[#spec + 1] = "!"
		spec[#spec + 1] = "-f"
	elseif rule.value.fragment.value ~= "" then
		spec[#spec + 1] = "-f"
	end
	addparameter(rule.value.set_counters.value, "-c")
	addmodule({rule.value.addrtype_src_type.value, rule.value.addrtype_dst_type.value}, "addrtype")
	addparameter(rule.value.addrtype_src_type.value, "--src-type")
	addparameter(rule.value.addrtype_dst_type.value, "--dst-type")
	addmodule({rule.value.comment.value}, "comment")
	if rule.value.comment.value ~= "" then
		spec[#spec + 1] = "--comment"
		spec[#spec + 1] = rule.value.comment.value
	end
	addmodule({rule.value.icmp_type.value}, "icmp")
	addparameter(rule.value.icmp_type.value, "--icmp-type", true)
	addmodule({rule.value.src_range.value, rule.value.dst_range.value}, "iprange")
	addparameter(rule.value.src_range.value, "--src-range", true)
	addparameter(rule.value.dst_range.value, "--dst-range", true)
	addmodule({rule.value.mac_source.value}, "mac")
	addparameter(rule.value.mac_source.value, "--mac-source", true)
	addmodule({rule.value.sports.value, rule.value.dports.value, rule.value.ports.value}, "multiport")
	addparameter(rule.value.sports.value, "--sports", true)
	addparameter(rule.value.dports.value, "--dports", true)
	addparameter(rule.value.ports.value, "--ports", true)
	addmodule({rule.value.state.value}, "state")
	addparameter(rule.value.state.value, "--state")
	addmodule({rule.value.tcp_sport.value, rule.value.tcp_dport.value}, "tcp")
	addparameter(rule.value.tcp_sport.value, "--sport", true)
	addparameter(rule.value.tcp_dport.value, "--dport", true)
	addmodule({rule.value.udp_sport.value, rule.value.udp_dport.value}, "udp")
	addparameter(rule.value.udp_sport.value, "--sport", true)
	addparameter(rule.value.udp_dport.value, "--dport", true)

	return spec
end

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

function mymodule.getstatus()
	return modelfunctions.getstatus(servicename, packagename, "IPtables Status")
end

function mymodule.getstatusdetails()
	getdetails()
	local retval = {}
	for i,tab in ipairs(tables) do
		local chains = 0
		local rules = 0
		for i,chain in ipairs(details[tab]) do
			chains = chains + 1
			rules = rules + #chain
		end
		retval[tab] = {chains=chains, rules=rules}
	end

	return cfe({ type="structure", value=retval, label="IPtables Status Details" })
end

function mymodule.getrules(tab)
	getdetails()
	tab = tab or "filter"

	return cfe({ type="structure", value=details[tab] or {}, label=string.gsub(tab, "^.", string.upper).." Rules" })
end

function mymodule.read_chain(tab, chain)
	local retval = {}
	retval.table = cfe({ type="select", value=tab or "filter", label="Table", option=tables, seq=1 })
	retval.chain = cfe({ value=chain or "", label="Chain", seq=2 })
	getdetails()
	if tab and not details[tab] then
		retval.table.errtxt = "Invalid table"
	end
	if chain then
		retval.table.readonly = true
		retval.table.type = "text"
		retval.chain.readonly = true
						 
		local chn = find_chain(retval.table.value, chain)
		if not chn then
			retval.chain.errtxt = "Cannot find chain"
		elseif chn.policy then
			-- only built-in chains can have policies, and the target can only be DROP or ACCEPT
			retval.policy = cfe({ type="select", value=chn.policy, label="Policy", option={"DROP", "ACCEPT"}, seq=3 })
		end
	end
	return cfe({ type="group", value=retval, label="Chain" })
end

function mymodule.update_chain(self, chain)
	local success = true
	getdetails()
	if not details[chain.value.table.value] then
		chain.value.table.errtxt = "Invalid table"
		success = false
	elseif not find_chain(chain.value.table.value, chain.value.chain.value) then
		chain.value.chain.errtxt = "Invalid chain"
		success = false
	end

	if success then
		if chain.value.policy then
			chain.descr, chain.errtxt = modelfunctions.run_executable({"iptables", "-t", chain.value.table.value, "-P", chain.value.chain.value, chain.value.policy.value}, true)
			save()
		end
	else
		chain.errtxt = "Failed to update chain"
	end

	return chain
end

function mymodule.create_chain(self, chain)
	local success = true
	getdetails()
	if not details[chain.value.table.value] then
		chain.value.table.errtxt = "Invalid table"
		success = false
	elseif find_chain(chain.value.table.value, chain.value.chain.value) then
		chain.value.chain.errtxt = "Chain already exists"
		success = false
	end
	if string.find(chain.value.chain.value, "[%s\'\"]") then
		chain.value.chain.errtxt = "Cannot contain spaces or quotes"
		success = false
	end

	if success then
		chain.descr, chain.errtxt = modelfunctions.run_executable({"iptables", "-t", chain.value.table.value, "-N", chain.value.chain.value}, true)
		save()
	else
		chain.errtxt = "Failed to create chain"
	end

	return chain
end

function mymodule.get_delete_chain(self, clientdata)
	local retval = {}
	retval.table = cfe({ type="select", value=clientdata.table or "filter", label="Table", option=tables })
	retval.chain = cfe({ value=clientdata.chain or "", label="Chain" })
	return cfe({ type="group", value=retval, label="Delete Chain" })
end

function mymodule.delete_chain(self, chain)
	local chn = find_chain(chain.value.table.value, chain.value.chain.value)
	if not chn then
		chain.errtxt = "Could not find chain"
	elseif chn.policy then
		chain.errtxt = "Cannot delete built-in chain"
	elseif chn.references and tonumber(chn.references) > 0 then
		chain.errtxt = "Cannot delete chain with references"
	else
		chain.descr, chain.errtxt = modelfunctions.run_executable({"iptables", "-t", chain.value.table.value, "-X", chain.value.chain.value}, true)
		save()
	end

	return chain
end

function mymodule.read_rule(tab, chain, pos)
	local retval = {}
	-- Identification
	retval.table = cfe({ type="select", value=tab or "filter", label="Table", option=tables })
	retval.chain = cfe({ value=chain or "", label="Chain" })
	retval.position = cfe({ value=pos or "", label="Position" })
	-- Basics
	retval.protocol = cfe({ value="all", label="Protocol", descr="One of tcp, udp, icmp, or all, or a numeric value representing a protocol, or a protocol name from /etc/protocols. A '!' before the protocol inverts the test." })
	retval.source = cfe({ label="Source", descr="A network name or IP address (may have mask of type /xxx.xxx.xxx.xxx or /xx). A '!' before the address specification inverts the sense of the address." })
	retval.destination = cfe({ label="Destination", descr="A network name or IP address (may have mask of type /xxx.xxx.xxx.xxx or /xx). A '!' before the address specification inverts the sense of the address." })
	retval.jump = cfe({ label="Target", descr="Specify the target of the rule - one of ACCEPT, DROP, QUEUE, or RETURN, or the name of a user-defined chain." })
	retval.goto = cfe({ label="Goto", descr="Processing should continue in the specified chain" })
	retval.in_interface = cfe({ label="In Interface", descr="Name of an interface via which a packet was received. A '!' before the interface inverts the sense. A '+' ending the interface will match any interface that begins with this name." })
	retval.out_interface = cfe({ label="Out Interface", descr="Name of an interface via which a packet is going to be sent. A '!' before the interface inverts the sense. A '+' ending the interface will match any interface that begins with this name." })
	retval.fragment = cfe({ label="Fragment", descr="A '+' specifies the second and further packets of fragmented packets. A '!' specifies only head fragments or unfragmented packets." })
	retval.set_counters = cfe({ label="Set Counters", descr="'Number number' to initialize the packet and byte counters."})
	-- Extensions
	retval.addrtype_src_type = cfe({ type="select", label="Source Address Type", option={"", "UNSPEC", "UNICAST", "LOCAL", "BROADCAST", "ANYCAST", "MULTICAST", "BLACKHOLE", "UNREACHABLE", "PROHIBIT"} })
	retval.addrtype_dst_type = cfe({ type="select", label="Destination Address Type", option={"", "UNSPEC", "UNICAST", "LOCAL", "BROADCAST", "ANYCAST", "MULTICAST", "BLACKHOLE", "UNREACHABLE", "PROHIBIT"} })
	retval.comment = cfe({ label="Comment" })
	retval.icmp_type = cfe({ label="ICMP Type", descr="Type by name or number. A '!' before the type inverts the test." })
	retval.src_range = cfe({ label="Source IP Range", descr="'ip-ip' to match source IP. A '!' before the range inverts the test." })
	retval.dst_range = cfe({ label="Destination IP Range", descr="'ip-ip' to match destination IP. A '!' before the range inverts the test." })
	retval.mac_source = cfe({ label="Source MAC address", descr="'XX:XX:XX:XX:XX:XX' to match the ethernet source MAC. A '!' before the address inverts the test." })
	retval.sports = cfe({ label="Source Ports", descr="Up to 15 comma-separated ports can be specified. Port ranges (port:port) count as two ports. A '!' before the port definition inverts the test." })
	retval.dports = cfe({ label="Destination Ports", descr="Up to 15 comma-separated ports can be specified. Port ranges (port:port) count as two ports. A '!' before the port definition inverts the test." })
	retval.ports = cfe({ label="Ports", descr="Up to 15 comma-separated ports can be specified. Port ranges (port:port) count as two ports. A '!' before the port definition inverts the test." })
	retval.state = cfe({ label="State", descr="Comma-separated list of states to match. Possible states are INVALID, ESTABLISHED, NEW, and RELATED." })
	retval.tcp_sport = cfe({ label="TCP Source Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." })
	retval.tcp_dport = cfe({ label="TCP Destination Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." })
	retval.udp_sport = cfe({ label="UDP Source Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." })
	retval.udp_dport = cfe({ label="UDP Destination Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." })

	getdetails()
	if tab and not details[tab] then
		retval.table.errtxt = "Invalid table"
	end
	local chn
	if chain then
		chn = find_chain(retval.table.value, chain)
		if not chn then
			retval.chain.errtxt = "Cannot find chain"
		end
	end
	if pos and chn then
		if not chn[tonumber(pos)] then
			retval.position.errtxt = "Cannot find rule"
		else
			-- We found the rule, update the settings
			local words = {}
			for word in string.gmatch(chn[tonumber(pos)].rule, "%S+") do words[#words + 1] = word end
			-- the target might be missing, so we need to check
			if #words[2] == 2 and string.match(words[2], "^[%-!]") then
				table.insert(words, 1, "")
			end
			retval.jump.value = words[1] or ""
			retval.protocol.value = words[2] or ""
			if words[3] == "-f" then
				retval.fragment.value = "+"
			elseif words[3] == "!f" then
				retval.fragment.value = "!"
			end
			retval.in_interface.value = words[4] or ""
			if retval.in_interface.value == "*" then retval.in_interface.value = "" end
			retval.out_interface.value = words[5] or ""
			if retval.out_interface.value == "*" then retval.out_interface.value = "" end
			retval.source.value = words[6] or ""
			retval.destination.value = words[7] or ""

			local i=8
			while i <= #words do
				if words[i] == "[goto]" then
					retval.goto.value = retval.jump.value
					retval.jump.value = ""
				elseif words[i] == "src-type" then
					retval.addrtype_src_type.value = words[i+1]
					i = i+1
				elseif words[i] == "dst-type" then
					retval.addrtype_dst_type.value = words[i+1]
					i = i+1
				elseif words[i] == "icmptype" then
					retval.icmp_type.value = words[i+1]
					i = i+1
				elseif words[i] == "icmp" then
					retval.icmp_type.value = "!" .. words[i+2]
					i = i+2
				elseif words[i] == "source" and words[i+1] == "IP" and words[i+2] == "range" then
					if words[i+3] == "!" then
						retval.src_range.value = "!" .. words[i+4]
						i = i+4
					else
						retval.src_range.value = words[i+3]
						i = i+3
					end
				elseif words[i] == "destination" and words[i+1] == "IP" and words[i+2] == "range" then
					if words[i+3] == "!" then
						retval.dst_range.value = "!" .. words[i+4]
						i = i+4
					else
						retval.dst_range.value = words[i+3]
						i = i+3
					end
				elseif words[i] == "MAC" then
					if words[i+1] == "!" then
						retval.mac_source.value = "!" .. words[i+2]
						i = i+2
					else
						retval.mac_source.value = words[i+1]
						i = i+1
					end						
				elseif words[i] == "multiport" then
					if words[i+2] == "!" then
						retval[words[i+1]].value = "!" .. words[i+3]
						i = i+3
					else
						retval[words[i+1]].value = words[i+2]
						i = i+2
					end
				elseif words[i] == "state" then
					retval.state.value = words[i+1]
					i = i+1
				elseif words[i] == "tcp" then
					while words[i+1] do
						if string.match(words[i+1], "^spt") then
							retval.tcp_sport.value = string.match(words[i+1], ":(.+)$")
							i = i+1
						elseif string.match(words[i+1], "^dpt") then
							retval.tcp_dport.value = string.match(words[i+1], ":(.+)$")
							i = i+1
						else
							break
						end
					end
				elseif words[i] == "udp" then
					while words[i+1] do
						if string.match(words[i+1], "^spt") then
							retval.udp_sport.value = string.match(words[i+1], ":(.+)$")
							i = i+1
						elseif string.match(words[i+1], "^dpt") then
							retval.udp_dport.value = string.match(words[i+1], ":(.+)$")
							i = i+1
						else
							break
						end
					end
				end
				i = i+1
			end
			retval.comment.value = string.match(chn[tonumber(pos)].rule, "/%*%s+(.*)%s%*/") or ""
		end
	end
	return cfe({ type="group", value=retval, label="Rule" })
end

function mymodule.create_rule(self, rule)
	local success, rule = validate_rule(rule)

	if success then
		local spec = generate_rule_specification(rule)
		local cmd = {"iptables", "-t", rule.value.table.value}
		if tonumber(rule.value.position.value) then
			cmd[#cmd+1] = "-I"
			cmd[#cmd+1] = rule.value.chain.value
			cmd[#cmd+1] = rule.value.position.value
		else
			cmd[#cmd+1] = "-A"
			cmd[#cmd+1] = rule.value.chain.value
		end
		for i,s in ipairs(spec) do cmd[#cmd+1] = s end
		rule.descr, rule.errtxt = modelfunctions.run_executable(cmd, true)
		save()
	else
		rule.errtxt = "Failed to create rule"
	end

	return rule
end

function mymodule.update_rule(self, rule)
	local success, rule = validate_rule(rule)
	if not tonumber(rule.value.position.value) then
		rule.value.position.errtxt = "Must be a number"
		success = false
	end

	if success then
		local spec = generate_rule_specification(rule)
		local cmd = {"iptables", "-t", rule.value.table.value, "-R", rule.value.chain.value, rule.value.position.value}
		for i,s in ipairs(spec) do cmd[#cmd+1] = s end
		rule.descr, rule.errtxt = modelfunctions.run_executable(cmd, true)
		save()
	else
		rule.errtxt = "Failed to update rule"
	end

	return rule
end

function mymodule.get_delete_rule(self, clientdata)
	local retval = {}
	-- Identification
	retval.table = cfe({ type="select", value=clientdata.table or "filter", label="Table", option=tables })
	retval.chain = cfe({ value=clientdata.chain or "", label="Chain" })
	retval.position = cfe({ value=clientdata.position or "", label="Position" })
	return cfe({ type="group", value=retval, label="Delete Rule" })
end

function mymodule.delete_rule(self, rule)
	if "" == rule.value.table.value or "" == rule.value.chain.value or "" == rule.value.position.value then
		rule.errtxt = "Incomplete specification - must define table, chain, and position"
	else
		rule.descr, rule.errtxt = modelfunctions.run_executable({"iptables", "-t", rule.value.table.value, "-D", rule.value.chain.value, rule.value.position.value}, true)
		save()
	end
	return rule
end

function mymodule.readrulesfile()
	local rulesfile = format.get_ini_entry(fs.read_file(configfile) or "", "", "IPTABLES_SAVE")
	return modelfunctions.getfiledetails(rulesfile)
end

function mymodule.updaterulesfile(self, filedetails)
	local rulesfile = format.get_ini_entry(fs.read_file(configfile) or "", "", "IPTABLES_SAVE")
	return modelfunctions.setfiledetails(self, filedetails, {rulesfile})
end

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

return mymodule
