local mymodule = {}

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

-- Set variables
local configfile = "/etc/lbu/lbu.conf"

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

local function get_version ()
	local programversion = modelfunctions.run_executable({"lbu"}, true)
	return string.match(programversion, "^[^\n]*")
end

local function getLbuStatus()
	local ret = {}
	local f = modelfunctions.run_executable({"lbu", "status", "-v"}, true)
	for line in string.gmatch(f, "[^\n]+") do
		if (string.match(line, "^Include files")) then break end
		if (string.match(line, "^Exclude files")) then break end
		local status, name = string.match(line, "^(%S)%s+(.+)$")
		if (status) and (name) then
			ret[string.gsub('/' .. name, "/+", "/")] = status
		end
	end
	return ret
end

local function availablemedias()
	local medias = {}
	local found = {}
	-- First, look in fstab
	local fstab = fs.read_file("/etc/fstab") or ""
	for media in string.gmatch(fstab, "/media/(%w+)%s") do
		if not string.find(media, "^cdrom") and not string.find(media, "^dvd") then
			medias[#medias+1] = media
			found[media] = true
		end
	end
	-- Then, look in result of 'mount'
	local mount = modelfunctions.run_executable({"mount"})
	for media in string.gmatch(mount, "/media/(%w+)%s") do
		if not string.find(media, "^cdrom") and not string.find(media, "^dvd") and not found[media] then
			medias[#medias+1] = media
		end
	end
	table.sort(medias)
	return medias
end

local function getciphers()
	local opensslciphers = {}
	local watchdog = nil
	local f = modelfunctions.run_executable({"openssl", "-v"}, true)
	for line in string.gmatch(f, "[^\n]+") do
		if (watchdog) then
			for cipher in string.gmatch(line, "(%S+)") do
				table.insert(opensslciphers,tostring(cipher))
			end
		end
		if (string.match(line, "^Cipher commands")) then watchdog="yes" end
	end
	return opensslciphers
end

local function validateconfig(config)
	-- Set errormessages when configs are wrong
	config.value.LBU_MEDIA.errtxt = "Invalid Media"
	for i,media in ipairs(config.value.LBU_MEDIA.option) do
		if media == config.value.LBU_MEDIA.value then
			config.value.LBU_MEDIA.errtxt = nil
			break
		end
	end
			
	config.value.DEFAULT_CIPHER.errtxt = "Invalid Cipher"
	for i,cipher in ipairs(config.value.DEFAULT_CIPHER.option) do
		if cipher == config.value.DEFAULT_CIPHER.value then
			config.value.DEFAULT_CIPHER.errtxt = nil
			break
		end
	end
	
	if config.value.PASSWORD.value == "" and config.value.ENCRYPTION.value then
		config.value.PASSWORD.errtxt = "Encryption without password is not allowed!\nDeactivate password protection or configure a password!"
	end

	if not validator.is_integer(config.value.BACKUP_LIMIT.value) then
		config.value.BACKUP_LIMIT.errtxt = "Must be an integer"
	end

	for name,value in pairs(config.value) do
		if value.errtxt then
			config.errtxt = "Invalid Config"
			break
		end
	end

	return config
end

local function replacestring (string, find, replace)
	local count
	string,count = string.gsub(string, find, replace)
	if 0 == count then
		string = string .. replace
	end
	return string
end

local function validatefilelist(filelist)
	local errors = {}
	local files = {}
	for line in string.gmatch(format.dostounix(filelist.value).."\n", "([^\n]*)\n") do
		if not string.match(line, "^%s*$") then
			local glob = posix.glob("/"..line)
			if not glob then
				errors[#errors+1] = '"' .. tostring(line) .. '" not a valid file/directory'
				files[#files+1] = line
			else
				for i,value in pairs(glob) do
					files[#files+1] = string.gsub(value, "^/+", "")
				end
			end
		end
	end
	filelist.value = table.concat(files, "\n")
	if #errors ~= 0 then
		filelist.errtxt = table.concat(errors, "\n")
	end
	return files
end

local function validatefilecontent (filedetails)
	local success = true
	local clientdata = clientdata or self.clientdata or {}
	clientdata.configcontents = filedetails.value.filecontent.value
	local config = mymodule.getconfig(nil, clientdata)
	local errors = {}
	for name,value in pairs(config.value) do
		if value.errtxt then	
			errors[#errors+1] = value.errtxt
		end
	end
	if #errors > 0 then
		success = false
		filedetails.value.filecontent.errtxt = table.concat(errors, "\n")
	end

	return success, filedetails
end

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

function mymodule.getstatus ()
	local status = {}
	status["version"] = cfe({
		value=get_version(),
		label="Program version",
		})

	status["committed"] = cfe({
		type="boolean",
		value=(#mymodule.list().value==0),
		descr=descrtxt,
		label="Program status",
		})

	local config = mymodule.getconfig()
	status["LBU_MEDIA"] = config.value.LBU_MEDIA
	if (config.value.ENCRYPTION.value) then
		status["DEFAULT_CIPHER"] = config.value.DEFAULT_CIPHER
	end
	
	return cfe({ type="group", value=status, label="LBU Status" })
end

function mymodule.list()
	local ret = {}
	local lbuStatus = getLbuStatus()
	for k,v in pairs(lbuStatus) do
		ret[#ret + 1] = { name=k, status=v }
	end
	table.sort(ret, function(a,b) return (a.name < b.name) end)
	return cfe({ type="structure", value=ret, label="List of changes" })
end

function mymodule.getconfig (self, clientdata)
	local clientdata = clientdata or {}
	local config = {}
	local configopts = {}
	if clientdata.configcontents then
		configopts = format.parse_ini_file(clientdata.configcontents, "") or {}
	elseif (fs.is_file(configfile)) then
		configopts = format.parse_ini_file(fs.read_file(configfile) or "", "") or {}
	end
	config["LBU_MEDIA"] = cfe({
		value=configopts.LBU_MEDIA or "",
		label="Media for commit",
		type="select",
		option=availablemedias() or {},
		})

	config["ENCRYPTION"] = cfe({
		value=(configopts.ENCRYPTION ~= nil),
		label="Password protected commits",
		type="boolean",
		})

	config["DEFAULT_CIPHER"] = cfe({
		value=configopts.DEFAULT_CIPHER or "",
		label="Cipher to use for encryption",
		option=getciphers() or {},
		type="select",
		})

	config["PASSWORD"] = cfe({
		value=configopts.PASSWORD or "",
		label="Password when encrypting",
		})

	config["BACKUP_LIMIT"] = cfe({
		value=configopts.BACKUP_LIMIT or "0",
		label="Backup archive limit",
		})

	retval = cfe({ type="group", value=config, label="LBU Config" })
	validateconfig(retval)

	return retval
end

function mymodule.setconfig (self, config)
	validateconfig(config)
	
	if not config.errtxt then
		local content = (fs.read_file(configfile) or "").."\n"
		
		-- LBU_MEDIA, ENCRYPTION, DEFAULT_CIPHER, PASSWORD
		content = replacestring(content, "[^\n%w]*LBU_MEDIA=[^\n]*\n", "LBU_MEDIA="..config.value.LBU_MEDIA.value.."\n")
		local newstring = "ENCRYPTION=$DEFAULT_CIPHER\n"
		if not config.value.ENCRYPTION.value then
			newstring = "#" .. newstring
		end
		content = replacestring(content, "[^\n%w]*ENCRYPTION=[^\n]*\n", newstring)
		content = replacestring(content, "[^\n%w]*DEFAULT_CIPHER=[^\n]*\n", "DEFAULT_CIPHER="..config.value.DEFAULT_CIPHER.value.."\n")
		content = replacestring(content, "[^\n%w]*PASSWORD=[^\n]*\n", "PASSWORD="..config.value.PASSWORD.value.."\n")
		content = replacestring(content, "[^\n%w]*BACKUP_LIMIT=[^\n]*\n", "BACKUP_LIMIT="..config.value.BACKUP_LIMIT.value.."\n")

		-- Write changes to file
		fs.write_file(configfile,content)
	end

	return config
end

function mymodule.getincluded ()
	local included = cfe({
		label="Included item(s)",
		type="longtext",
		descr="List one file/directory per line\nAssumes root directory, no leading '/' necessary",
		})

	-- Read the list
	included.value = modelfunctions.run_executable({"lbu", "include", "-l"})

	validatefilelist(included)

	return cfe({ type="group", value={included = included} })
end

function mymodule.setincluded (self, included)
	local files = validatefilelist(included.value.included)
	if not included.value.included.errtxt then
		local current = format.string_to_table(modelfunctions.run_executable({"lbu", "include", "-l"}), "\n")
		table.insert(current, 1, "-r")
		table.insert(current, 1, "include")
		table.insert(current, 1, "lbu")
		modelfunctions.run_executable(current)

		table.insert(files, 1, "include")
		table.insert(files, 1, "lbu")
		included.descr, included.errtxt = modelfunctions.run_executable(files)
	else
		included.errtxt = "Failed to set included"
	end
	return included
end

function mymodule.getexcluded ()
	local excluded = cfe({
		label="Excluded item(s)",
		type="longtext",
		descr="List one file/directory per line\nAssumes root directory, no leading '/' necessary",
		})

	-- Read the list
	excluded.value = modelfunctions.run_executable({"lbu", "exclude", "-l"})

	validatefilelist(excluded)

	return cfe({ type="group", value={excluded = excluded} })
end

function mymodule.setexcluded (self, excluded)
	local files = validatefilelist(excluded.value.excluded)
	if not excluded.value.excluded.errtxt then
		local current = format.string_to_table(modelfunctions.run_executable({"lbu", "exclude", "-l"}), "\n")
		table.insert(current, 1, "-r")
		table.insert(current, 1, "exclude")
		table.insert(current, 1, "lbu")
		modelfunctions.run_executable(current)

		table.insert(files, 1, "exclude")
		table.insert(files, 1, "lbu")
		excluded.descr, excluded.errtxt = modelfunctions.run_executable(files)
	else
		excluded.errtxt = "Failed to set excluded"
	end
	return excluded
end

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

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

function mymodule.getcommit()
	local simulate = cfe({ type="boolean", value=false, label="Simulate a commit" })
	local delete = cfe({ type="boolean", value=false, label="Remove existing overlays on commit" })
	return cfe({ type="group", value={simulate=simulate, delete=delete}, label="Commit changes" })
end

function mymodule.commit(self, input)
	if input.value.simulate.value and input.value.delete.value then
		input.errtxt = "Cannot delete overlays when simulating"
	else
		local flag = "-v"
		if input.value.simulate.value then flag=flag.." -n" end
		if input.value.delete.value then flag=flag.." -d" end
		input.descr, input.errtxt = modelfunctions.run_executable({"lbu", "commit", flag}, true)
	end
	
	return input
end

function mymodule.getbackupfiles()
	local files = {}
	local content = modelfunctions.run_executable({"lbu", "lb"})
	if not string.match(content, "usage: lbu") then
		for line in string.gmatch(content, "([^\n]+)\n?") do
			files[#files + 1] = line
		end
	end
	return cfe({ type="list", value=files, label="Backup archive list"})
end

function mymodule.get_selectbackup(self, clientdata)
	local result = {}
	result.backup = cfe({ type="select", value=clientdata.backup or "", label="Backup" })
	local files = mymodule.getbackupfiles()
	result.backup.option = files.value
	return cfe({ type="group", value=result, label="Revert to Backup" })
end

function mymodule.selectbackupfile(self, backuprequest)
	local success = modelfunctions.validate_select(backuprequest.value.backup)
	if success then
		backuprequest.descr, backuprequest.errtxt = modelfunctions.run_executable({"lbu", "revert", backuprequest.value.backup.value})
		if not backuprequest.errtxt and backuprequest.descr == "" then
			backuprequest.descr = "Reverted to backup "..file
		end
	else
		backuprequest.errtxt = "Failed to revert to backup"
	end
	return backuprequest
end

function mymodule.getpackage()
	-- create a temporary directory to store the file
	session = require("session")
	local tmp = "/tmp/"..session.random_hash(10)
	while posix.stat( tmp ) do
		tmp = "/tmp/"..session.random_hash(10)
	end
	posix.mkdir(tmp)
	local cmdresult, errtxt = modelfunctions.run_executable({"lbu", "package", tmp}, true)

	-- find and read the file
	local package = cfe({ type="raw", label="error", option="application/x-gzip", errtxt=errtxt })
	for name in fs.find(".*tar%.gz", tmp) do
		package.label = posix.basename(name)
		package.value = fs.read_file(name) or ""
		break
	end

	-- delete the temp directory and file
	fs.remove_directory(tmp)

	return package
end

return mymodule
