local mymodule = {}

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

-- Set variables
local configfile = "/etc/ssh/sshd_config"
local processname = "sshd"
local packagename = "openssh"
local header = "SSH"

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

-- return "Yes" or "No" on true/false or value as string
local function config_value(value)
	if type(value) == "boolean" then
		if value then
			return "yes"
		else
			return "no"
		end
	end
	return tostring(value)
end

local function validate_config(config)
	local success = true

	if not validator.is_ipv4(config.value.ListenAddress.value) then
		config.value.ListenAddress.errtxt = "Invalid IP"
		success = false
	end
	if not validator.is_port(config.value.Port.value) then
		config.value.Port.errtxt = "Invalid Port"
		success = false
	end

	return success, config
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, header .. " status")
end

function mymodule.getconfigfile()
	return modelfunctions.getfiledetails(configfile)
end

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

function mymodule.read_config()
	local output = {}
	output.Port = cfe({ value=22, label="Port", seq=1 })
	output.ListenAddress = cfe({ value="0.0.0.0", label="Listen address", seq=2 })
	output.PermitRootLogin = cfe({ type="select", value="prohibit-password", label="Permit Root Login", option={"yes", "no", "prohibit-password", "without-password"}, seq=3 })
	output.PasswordAuthentication = cfe({ type="boolean", value=true, label="Password Authentication", seq=4 })
	output.UseDNS = cfe({ type="boolean", value=false, label="Use DNS", seq=5 })

	local config = format.parse_configfile(fs.read_file(configfile) or "")
	if config then	
		output.Port.value = config.Port or output.Port.value
		output.ListenAddress.value = config.ListenAddress or output.ListenAddress.value
		output.PermitRootLogin.value = config.PermitRootLogin or output.PermitRootLogin.value
		output.PasswordAuthentication.value = not (config.PasswordAuthentication == "no")
		output.UseDNS.value = (config.UseDNS == "yes")
	end

	return cfe({ type="group", value=output, label="OpenSSH Config" })
end

function mymodule.update_config(self, config)
	local success, config = validate_config(config)

	if success then
		for name,val in pairs(config.value) do
			val.line = name.." "..config_value(val.value)
		end

		local lines = {}
		for line in string.gmatch(fs.read_file(configfile) or "", "([^\n]*)\n?") do
			for name,val in pairs(config.value) do
				if val.line and string.find(line, "^%s*#?%s*"..name) then
					if string.find(line, "^%s*#") then
						lines[#lines+1] = val.line
					else
						line = val.line
					end
					val.line = nil
				end
			end
			lines[#lines+1] = line
		end

		for name,val in pairs(config.value) do
			if val.line then
				lines[#lines+1] = val.line
				val.line = nil
			end
		end
		fs.write_file(configfile, string.gsub(table.concat(lines, "\n"), "\n+$", ""))
	else
		config.errtxt = "Failed to save config"
	end

	return config
end

function mymodule.list_conn_peers()
	local output = cfe({ type="structure", value={}, label="Connected peers" })
	local netstat = {}
	local ps = {}
	local who = {}
	config = mymodule.read_config()
	local f = modelfunctions.run_executable({"netstat", "-lnaW"})
	local flines = format.search_for_lines(f, "ESTABLISHED")
	local g = modelfunctions.run_executable({"netstat", "-laW"})
	local glines = format.search_for_lines(g, "ESTABLISHED")
	for i,line in ipairs(flines) do
		local loc, peer = string.match(line, "^%S+%s+%S+%s+%S+%s+(%S+)%s+(%S+)")
		if loc then
			peer = string.match(peer, "%d+%.%d+%.%d+%.%d+")
			if string.find(loc, ":"..config.value.Port.value.."$") and peer then
				if not netstat[peer] then
					local name = string.match(glines[i], "^%S+%s+%S+%s+%S+%s+%S+%s+(%S+)")
					name = string.gsub(name, ":.*", "")
					netstat[peer] = {cnt=0, name=name}
				end
				netstat[peer].cnt = netstat[peer].cnt + 1
			end
		end
	end

	f = modelfunctions.run_executable({"ps"})
	for i,line in ipairs(format.search_for_lines(f, "sshd:")) do
		table.insert(ps, string.match(line,"@(%S+)"))
	end

	local who = modelfunctions.run_executable({"who"})
	for peer,v in pairs(netstat) do
		if not (netstat[peer].tty) then netstat[peer].tty = {} end
		for line in string.gmatch(who, "[^\n]+") do
			if string.find(line, peer) or (v.name ~= "" and string.find(line, v.name)) then
				for j,l in ipairs(ps) do
					if string.find(line, l) then
						local user,tty,idle,time = string.match(line, "^(%S*)%s*(%S*)%s*(%S*)%s*(%S*%s*%S*%s*%S*)")
						table.insert(netstat[peer].tty, {
							user=user,
							tty=tty,
							idle=idle,
							time=time,
						})
						break
					end
				end
			end
		end
		output.value[#output.value+1] = v
		output.value[#output.value].host = peer
	end

	return output
end

function mymodule.list_users()
	local users = {"root"}
	-- The only users we're going to worry about in this ACF are root and ones with home directories
	for user in posix.files("/home") do
		if fs.is_dir("/home/" .. user) and not string.find(user, "^%.") then users[#users + 1] = user end
	end
	table.sort(users)
	return cfe({ type="list", value=users, label="User list" })
end

local function parseauthline(line)
	local retval = {}
	local words = format.string_to_table(line, "%s")
	if words[1] and string.match(words[1], "^ssh%-%ws%w$") then
		retval.perm = ""
		retval.key = words[2]
		retval.id = table.concat(words, " ", 3)
	elseif words[2] and string.match(words[2], "^ssh%-%ws%w$") then
		retval.perm = words[1]
		retval.key = words[3]
		retval.id = table.concat(words, " ", 4)
	else
		retval = nil
	end
	return retval
end

function mymodule.list_auths(user)
	user = user or "root"
	local cmdresult = cfe({ type="group", value={}, label="Authorized Key List" })
	cmdresult.value.user = cfe({ value=user, label="User" })
	cmdresult.value.auth = cfe({ type="structure", value={}, label="Authorized Keys" })
	if not user == "root" and (string.find(user, "/") or not fs.is_dir("/home/"..user)) then
		cmdresult.value.user.errtxt = "Invalid user"
	else
		local file = "/"..user.."/.ssh/authorized_keys"
		if user ~= "root" then file = "/home"..file end
		local data = fs.read_file(file) or ""
		for line in string.gmatch(data, "([^\n]+)\n?") do
			table.insert(cmdresult.value.auth.value, parseauthline(line))
		end
	end
	table.sort(cmdresult.value.auth.value, function(a,b) return a.id < b.id end)
	return cmdresult
end

function mymodule.get_delete_auth(self, clientdata)
	local retval = {}
	retval.user = cfe({ value=clientdata.user or "root", label="User" })
	retval.auth = cfe({ value=clientdata.auth or "", label="Authorized Key" })
	return cfe({ type="group", value=retval, label="Delete Authorized Key" })
end

function mymodule.delete_auth(self, delauth)
	local user = delauth.value.user.value
	delauth.value.user.errtxt = "User not found"
	delauth.errtxt = "Failed to delete key"
	if user == "root" or (not string.find(user, "/") and fs.is_dir("/home/"..user)) then
		delauth.value.user.errtxt = nil
		delauth.value.auth.errtxt = "Key not found"

		local file = "/"..user.."/.ssh/authorized_keys"
		if user ~= "root" then file = "/home"..file end
		local data = fs.read_file(file)
		if data then
			local newdata = {}
			for line in string.gmatch(data, "([^\n]+)\n?") do
				local val = parseauthline(line)
				if val.id == delauth.value.auth.value then
					delauth.errtxt = nil
					delauth.value.auth.errtxt = nil
				else
					newdata[#newdata + 1] = line
				end
			end
			if not delauth.errtxt then
				fs.write_file(file, table.concat(newdata, "\n"))
			end
		end
	end
	return delauth
end

function mymodule.get_auth(user)
	local cmdresult = cfe({ type="group", value={}, label="Authorized Key List" })
	cmdresult.value.user = cfe({ value=user or "root", label="User", seq=1 })
	if user then
		cmdresult.value.user.readonly=true
	end
	cmdresult.value.cert = cfe({ type="longtext", label="SSH Certificate Contents", seq=2 })
	return cmdresult
end

function mymodule.create_auth(self, authstr)
	authstr.value.user.value = authstr.value.user.value or "root"
	local success = true
	if not authstr.value.user.value == "root" and (string.find(authstr.value.user.value, "/") or not fs.is_dir("/home/"..authstr.value.user.value)) then
		authstr.value.user.errtxt = "Invalid user"
		success = false
	end
	-- parse the current file to get existing keys
	local file = "/"..authstr.value.user.value.."/.ssh/authorized_keys"
	if authstr.value.user.value ~= "root" then file = "/home"..file end
	local lines = {}
	local auths = {}
	if success then
		local data = fs.read_file(file) or ""
		for line in string.gmatch(data, "([^\n]+)\n?") do
			auths[#auths+1] = parseauthline(line)
			lines[#lines+1] = line
		end
	end
	local certs = {}
	-- not sure how to validate the cert
	if not string.match(authstr.value.cert.value, "^%s*ssh") then
		authstr.value.cert.errtxt = "Invalid format - must start with 'ssh-...'"
		success = false
	else
		-- try to handle certs that wrap lines and multiple certs in the entry
		for line in string.gmatch(format.dostounix(authstr.value.cert.value), "([^\n]*)\n?") do
			if string.match(line, "^%s*ssh") then
				certs[#certs+1] = line
			elseif #certs > 0 then
				certs[#certs] = certs[#certs] .. line
			end
		end
	end
	for i,cert in ipairs(certs) do
		local val = parseauthline(cert)
		if not val then
			authstr.value.cert.errtxt = "Invalid format"
			success = false
			break
		end
		for j,au in ipairs(auths) do
			if val.id == au.id or val.key == au.key then
				authstr.value.cert.errtxt = "This key / ID already exists"
				success = false
				break
			end
		end
		if success then
			lines[#lines+1] = cert
			auths[#auths+1] = val
		else
			break
		end
	end
	if success then
		fs.write_file(file, table.concat(lines, "\n") or "")
	else
		authstr.errtxt = "Failed to add key"
	end
	return authstr
end

return mymodule
