local mymodule = {}

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

-- Set variables
local DatabaseName = "provisioning"
local DatabaseUser = "postgres"
local DatabasePassword

local baseurl = "/etc/provisioning/templates/"
local updatedevicescriptfile = "/etc/provisioning/update_device.lua"
local updatedeviceparamsscriptfile = "/etc/provisioning/update_device_params.lua"
local deletedevicescriptfile = "/etc/provisioning/delete_device.lua"
local determineclassscriptfile = "/etc/provisioning/determine_class.lua"
local processputscriptfile = "/etc/provisioning/process_put.lua"
local scriptfiles = {updatedevicescriptfile, updatedeviceparamsscriptfile, deletedevicescriptfile, determineclassscriptfile, processputscriptfile}

local provdb = db.create(db.engine.postgresql, DatabaseName, DatabaseUser, DatabasePassword)
provdb.table_creation_scripts = require("provisioning/provisioning-scripts")

local saved_devices = {}
local saved_device_params = {}

-- These are the functions that may be called from within loaded Lua code
-- The functions must be added after they're declared
local functions

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

-- This function is used by scripts, do not change prototype
local runsqlcommand = function(sql)
	mymodule.logevent(sql)
	return provdb.runsqlcommand(sql, true)
end

-- This function is used by scripts, do not change prototype
local getselectresponse = function(sql)
	mymodule.logevent(sql)
	return provdb.getselectresponse(sql, true)
end

local function assert (v, m)
	if not v then
		m = m or "Assertion failed!"
		error(m, 0)
	end
	return v, m
end

local createdatabase = function()
        local result = {}
	local cmdresult, errtxt
	-- First, create the user
	if DatabaseUser ~= "postgres" then
		local cmd = "CREATE USER "..DatabaseUser
		if DatabasePassword then
			cmd = cmd .. " WITH PASSWORD '"..DatabasePassword.."'"
		end
		cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true)
		table.insert(result, errtxt)
		table.insert(result, cmdresult)
	end
	
	-- Create the database
	local cmd = "CREATE DATABASE "..DatabaseName.." WITH OWNER "..DatabaseUser
	cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true)
	table.insert(result, errtxt)
	table.insert(result, cmdresult)

	cmdresult, errtxt = modelfunctions.run_executable({"createlang", "-U", "postgres", "plpgsql", DatabaseName}, true)
	table.insert(result, errtxt)
	table.insert(result, cmdresult)

	mymodule.logevent(table.concat(result, "\n"))

	return table.concat(result, "\n")
end
	
-- Delete the database and roles
local deletedatabase = function()
	local result = {}
	
	local cmd = "DROP DATABASE "..DatabaseName
	local cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true)
	table.insert(result, errtxt)
	table.insert(result, cmdresult)

	if DatabaseUser ~= "postgres" then
		cmd = "DROP ROLE "..DatabaseUser
		local cmdresult, errtxt = modelfunctions.run_executable({"psql", "-U", "postgres", "-c", cmd}, true)
		table.insert(result, errtxt)
		table.insert(result, cmdresult)
	end

	mymodule.logevent(table.concat(result, "\n"))

	return table.concat(result, "\n")
end

local databaseconnect = function(unlocked)
	local result = false
	local res, err = pcall(function()
		result = provdb.databaseconnect()
	end)
	if not res and err then
		if string.match(err, 'Error connecting to database.') or string.match(string.lower(err), 'database "provisioning" does not exist') then
			createdatabase()
			result = provdb.databaseconnect()
			-- Let's create all the tables from the start
			for n,v in pairs(provdb.table_creation_scripts) do
				if not string.match(n, "^_") then
					provdb.runsqlcommand("SELECT * FROM "..provdb.escape(n).." LIMIT 1")
				end
			end
		else
			assert(res, err)
		end
	end
	if result then
		local sql
		sql = "BEGIN TRANSACTION"
		mymodule.logevent(sql)
		provdb.runsqlcommand(sql)
		if not unlocked then
			sql = "LOCK TABLE provisioning_values IN ACCESS EXCLUSIVE MODE"
			mymodule.logevent(sql)
			provdb.runsqlcommand(sql)
		end
	end
	return result
end

local databasedisconnect = function()
	local sql = "COMMIT"
	mymodule.logevent(sql)
	provdb.runsqlcommand(sql)
	provdb.databasedisconnect()
end

local handlesqlexception = function(connected, err)
	-- Can't trust cache, so clear it
	saved_devices = nil
	saved_device_params = nil
	if connected then
		local sql = "ROLLBACK"
		mymodule.logevent(sql)
		pcall(function() con:execute(sql) end)
		provdb.databasedisconnect()
	elseif provdb.isconnected() then
		-- database connection ocurred in calling function, propagate the error
		error(err)
	else
		-- failure during connection, no cleanup needed
	end
end

local validateparam
validateparam = function(p, allowdefault)
	if p.type == "group" then
		local success = true
		for n,p2 in pairs(p.value) do
			success = validateparam(p2, allowdefault) and success
		end
		return success
	elseif allowdefault and p.value == p.default then
		return true
	elseif p.type == "select" then
		return modelfunctions.validateselect(p)
	elseif p.type == "text" and p.regexp and p.regexp ~= "" then
		if not string.match(p.value, p.regexp) then
			p.errtxt = "Invalid value"
			return false
		end
	end
	return true
end

local validateparamcoded
validateparamcoded = function(p, top)
	top = top or p
	local success = true
	if p.type == "group" then
		for n,p2 in pairs(p.value) do
			success = validateparamcoded(p2, top) and success
		end
		return success
	end
	if p.validate and p.validate ~= "" then
		-- We have Lua validation code
		local f
		local env = {}
		setmetatable (env, {__index = _G})
		local IS_52_LOAD = pcall(load, '')
		if IS_52_LOAD then
			f = load(p.validate, nil, "bt", env)
		else
			-- loadfile loads into the global environment
			-- so we set env 0, not env 1
			setfenv (0, env)
			f = loadstring(p.validate)
			setfenv (0, _G)
		end
		if (f) then
			functions.logevent = mymodule.logevent
			local res, err = pcall(function()
				p.value, p.errtxt = f(p.value, functions, top)
				if p.errtxt then success = false end
			end)
			if not res and err then
				success = false
				p.errtxt = "Exception in validate code\n"..err
			end
		end
	end
	return success
end

local function callscript(script, ...)
	local result={}
	local f
	local env = {}
	setmetatable (env, {__index = _G})
	local IS_52_LOAD = pcall(load, '')
	if IS_52_LOAD then
		f = loadfile(script, "bt", env)
	else
		-- loadfile loads into the global environment
		-- so we set env 0, not env 1
		setfenv (0, env)
		f = loadfile(script)
		setfenv (0, _G)
	end
	if f then
		functions.logevent = mymodule.logevent
	        local res, err = pcall(function(...)
			result = { f(functions, ...) }
		end, ...)
		if not res and err then
			assert(res, "Exception in "..script.." script\n"..err)
		end
	elseif posix.stat(script) then
		-- file exists, but wouldn't load
		error("Failed to load "..script, 0)
	end
	return unpack(result)
end

local function validateluacode(code)
	local success = true

	-- Validate that contents are valid lua code
	local f,errtxt
	local env = {}
	setmetatable (env, {__index = _G})
	local IS_52_LOAD = pcall(load, '')
	if IS_52_LOAD then
		f = load(code, nil, "bt", env)
	else
		-- loadfile loads into the global environment
		-- so we set env 0, not env 1
		setfenv (0, env)
		f,errtxt = loadstring(code)
		setfenv (0, _G)
	end
	if not f then
		success = false
	end

	return success, errtxt
end

local function validatefiledetails(filedetails)
	local success, errtxt = validateluacode(filedetails.value.filecontent.value)
	if not success then
		filedetails.value.filecontent.errtxt = "Invalid Lua code\n"..(errtxt or "")
	end
	return success, filedetails
end

local duplicatestructure
duplicatestructure = function(value, saved)
	saved = saved or {}
	if type(value) == "table" then
		if saved[value] then
			return saved[value]
		else
			local output = {}
			saved[value] = output
			for k,v in pairs(value) do
				output[k] = duplicatestructure(v, saved)
			end
			return output
		end
	else
		return value
	end
end

local checkgroupdefaultoverride = function(device_id)
	sql = "SELECT p.param_id "..
		"FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING(class_id) JOIN provisioning_groups g USING(group_id) "..
		"JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id) JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name))"..
		"WHERE d2t.device_id='"..provdb.escape(device_id).."' AND v.value IS NOT NULL AND g2p.value IS NOT NULL AND v.value!=g2p.value"
	local tmp = getselectresponse(sql)
	return (#tmp ~= 0)
end

-- This function is used by scripts, do not change prototype
local get_device = function(device_id, create)
	local connected
	local retval = {}
	retval.device_id = cfe({value=device_id or "", label="Device ID", seq=1})
	retval.classes = cfe({type="group", value={}, label="Classes", seq=2})
	local errtxt
	local res, err = pcall(function()
		local classes={}
		connected = databaseconnect(true)
		if not create and device_id and device_id ~= "" then
			-- Get the device-to-class mappings
			local sql = "SELECT class_id FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."'"
			local tmp = getselectresponse(sql)
			for i,g in ipairs(tmp) do
				classes[g.class_id] = true
			end
		end
		-- Finally, get the class options
		sql = "SELECT class_id, g.name, g.label AS group, c.label, c.seq FROM provisioning_classes c JOIN provisioning_class_groups g USING(class_group_id) ORDER BY g.seq ASC, g.label ASC, c.seq ASC, c.label ASC"
		tmp = getselectresponse(sql)
		for i,c in ipairs(tmp) do
			if not retval.classes.value[c.name] then
				retval.classes.value[c.name] = cfe({type="select", label=c.group, option={{value="", label=""}}, seq=i})
			end
			local class = retval.classes.value[c.name]
			class.option[#class.option + 1] = {value=c.class_id, label=c.label}
			if classes[c.class_id] then
				class.value = c.class_id
			end
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	-- Save the device for later use
	local output = cfe({ type="group", value=retval, label="Provisioning Device", errtxt=errtxt })
	if not create and device_id and device_id ~= "" and not errtxt then saved_devices[device_id] = duplicatestructure(output) end
	return output
end

-- This function is used by scripts, do not change prototype
local get_device_params = function(device_id, editable)
	local connected
	local retval = {}
	retval.device_id = cfe({value=device_id or "", label="Device ID", seq=0})
	local errtxt = "Cannot find device"
	if device_id and device_id ~= "" then
		local res, err = pcall(function()
			connected = databaseconnect(true)
			-- First, just check to see if device_id exists
			local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."' LIMIT 1"
			local tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				errtxt = nil
				-- Next, get all of the param groups
				sql = "SELECT * FROM provisioning_groups"
				local tmp = getselectresponse(sql)
				-- Loop through the groups and put them into the result
				for i,g in ipairs(tmp) do
					retval[g.name] = g
					retval[g.name].label = g.name
					retval[g.name].type="group"
				end
				-- Then, get all of the parameters for this device
				sql = "SELECT g.name AS group, g.label AS grouplabel, p.param_id, p.name, p.type, p.label, p.descr, p.seq, p.regexp, p.validate, CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value, CASE WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS default, g2p.value AS groupdefault "..
					"FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) JOIN provisioning_groups g USING(group_id) "..
						"JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) "..
					"WHERE d2t.device_id='"..provdb.escape(device_id).."'"
				if editable then
					sql = sql.." AND g2p.editable='t'"
				end
				local tmp = getselectresponse(sql)
				-- Loop through the params to figure out options and put them into the groups
				for i,p in ipairs(tmp) do
					-- Options
					if (p.type == "select") then
						sql = "SELECT * FROM provisioning_options WHERE param_id='"..provdb.escape(p.param_id).."' ORDER BY seq ASC"
						p.option = getselectresponse(sql) or {}
					end
					-- Groups
					if not retval[p.group].value then
						retval[p.group].value = {}
					end
					retval[p.group].label = p.grouplabel
					local value = retval[p.group].value
					if p.type == "boolean" then
						p.value = (p.value == "true")
						p.default = (p.default == "true")
						if p.groupdefault then
							p.groupdefault = (p.groupdefault == "true")
						end
					end
					value[p.name] = p
				end
				-- Finally, loop through the result and remove empty groups
				for name,val in pairs(retval) do
					if not val.value then
						retval[name] = nil
					end
				end
				-- Even more finally, add in a flag to show if group defaults have been overridden
				retval.groupdefaultoverride = cfe({ type="boolean", value=checkgroupdefaultoverride(device_id), label="Group defaults have been overridden", readonly=true })
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			errtxt = err
		end
	end

	-- Save the device for later use
	local output = cfe({ type="group", value=retval, label="Provisioning Device Parameters", errtxt=errtxt })
	if device_id and device_id ~= "" and not editable then saved_device_params[device_id] = duplicatestructure(output) end
	return output
end

-- This function is used by scripts, do not change prototype
local set_device_params = function(params, editable)
	local connected
	-- Validate the settings
	local success = validateparam(params)
	local errtxt
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			success = validateparamcoded(params)
			local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(params.value.device_id.value).."' LIMIT 1"
			local tmp = getselectresponse(sql)
			if not tmp or #tmp == 0 then
				success = false
				errtxt = "Device does not exist"
			end
			if success then
				if not saved_device_params[params.value.device_id.value] then get_device_params(params.value.device_id.value) end

				local sql
				if not editable then
					-- Delete all values for this device (can't do this if only updating editable)
					sql = "DELETE FROM provisioning_values WHERE device_id='"..provdb.escape(params.value.device_id.value).."'"
					runsqlcommand(sql)
				end
				-- Loop through the groups and params
				for group,v in pairs(params.value) do
					if v.type == "group" then
						for name,param in pairs(v.value) do
							if editable then
								sql = "DELETE FROM provisioning_values WHERE device_id='"..provdb.escape(params.value.device_id.value).."' AND group_name='"..provdb.escape(group).."' AND param_id='"..provdb.escape(param.param_id).."'"
								runsqlcommand(sql)
							end
							if param.value ~= param.default then
								sql = "INSERT INTO provisioning_values VALUES('"..provdb.escape(params.value.device_id.value).."', '"..provdb.escape(group).."', '"..provdb.escape(param.param_id).."', '"..provdb.escape(tostring(param.value)).."')"
								runsqlcommand(sql)
							end
						end
					end
				end
				
				local tmp = saved_device_params[params.value.device_id.value]
				local p = params
				if not editable then
					saved_device_params[params.value.device_id.value] = params
				else
					p = get_device_params(params.value.device_id.value)
				end
				callscript(updatedeviceparamsscriptfile, p, tmp)
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		params.errtxt = errtxt or "Failed to save device parameters"
	end
	return params
end

local get_device_values = function(device_id)
	local connected
	local retval = {}
	local errtxt
	if device_id and device_id ~= "" then
		local res, err = pcall(function()
			connected = databaseconnect(true)
			local sql = "SELECT g.name AS group, p.name, p.type, CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value "..
				"FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING(class_id) JOIN provisioning_groups g USING(group_id) "..
					"JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) "..
				"WHERE d2t.device_id='"..provdb.escape(device_id).."'"
			local tmp = getselectresponse(sql)
			-- Loop through the params and put them into the groups
			for i,p in ipairs(tmp) do
				if p.type == "boolean" then
					p.value = (p.value == "true")
				end
				if not retval[p.group] then
					retval[p.group] = {}
				end
				retval[p.group][p.name] = p.value
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			errtxt = err
		end
	else
		errtxt = "Invalid device id"
	end

	return cfe({type="structure", value=retval, label="Parameter Values", errtxt=errtxt})
end

-- These are the functions that may be called from within loaded Lua code
-- The functions must be added after they're declared
-- All scripts are called within DB transactions
functions = {
	getselectresponse=getselectresponse,
	runsqlcommand=runsqlcommand,
	escape=provdb.escape,
	get_device=get_device,
	get_device_params=get_device_params,
	set_device_params=set_device_params,
}

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

mymodule.list_templates = function()
	local connected
	local retval = {}
	local errtxt
	-- Get the templates from the DB
        local res, err = pcall(function()
		connected = databaseconnect(true)
		local sql = "SELECT value AS filename, label, seq FROM provisioning_options WHERE param_id = (SELECT param_id FROM provisioning_params WHERE name = 'template') ORDER BY seq ASC, label ASC, value ASC"
		retval = getselectresponse(sql)
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end
	local reversed = {}
	for i,t in ipairs(retval) do
		reversed[t.filename] = i
	end
	-- Get the file stats for each template and add in any template files that aren't in the DB
	fs.create_directory(baseurl)
	for f in posix.files(baseurl) do
		local file = baseurl..f
                local details = fs.stat(file)
		if details.type == "regular" then
			if not reversed[file] then
				retval[#retval+1] = {filename=file}
				reversed[file] = #retval
			end
			local t = retval[reversed[file]]
			for n,v in pairs(details) do
				t[n] = v
			end
		end
	end

	return cfe({ type="structure", value=retval, label="Templates", errtxt=errtxt })
end

mymodule.get_template = function(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local filename = clientdata.filename
	local retval = {}
	if filename and not string.match(filename, "/") then
		filename = baseurl .. filename
	end
	retval.filename = cfe({value=filename or "", label="File Name", descr="Must be in "..baseurl, seq=1})
	retval.label = cfe({label="Label", seq=2})
	retval.seq = cfe({label="Sequence", seq=3})
	local errtxt
	if filename and filename ~= "" then
	        local res, err = pcall(function()
			connected = databaseconnect(true)
			local sql = "SELECT * FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(filename).."' ORDER BY seq ASC, label ASC, value ASC"
			local tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				for n,v in pairs(tmp[1]) do
					if n == "value" then
						retval.filename.value = v
					elseif retval[n] then
						retval[n].value = v
					end
				end
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			errtxt = err
		end
	end
	local filedetails = modelfunctions.getfiledetails(retval.filename.value, function(filename) return validator.is_valid_filename(filename, baseurl) end)
	for i,n in ipairs({"filecontent", "filesize", "mtime"}) do
		retval[n] = filedetails.value[n]
	end

	return cfe({ type="group", value=retval, label="Provisioning Template", errtxt=errtxt })
end

mymodule.create_template = function(self, template, action)
	return mymodule.update_template(self, template, action, true)
end

mymodule.update_template = function(self, template, action, create)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
	if template.value.filename and not string.match(template.value.filename.value, "/") then
		template.value.filename.value = baseurl .. template.value.filename.value
	end
	if not validator.is_valid_filename(template.value.filename.value, baseurl) then
		success = false
		template.value.filename.errtxt = "Invalid filename"
	end
	if template.value.label.value == "" then
		success = false
		template.value.label.errtxt = "Cannot be blank"
	end
	if not validator.is_integer(template.value.seq.value) then
		success = false
		template.value.seq.errtxt = "Must be an integer"
	end
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			local sql = "SELECT * FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(template.value.filename.value).."'"
			local tmp = getselectresponse(sql)
			if not create and (not tmp or #tmp == 0) then
				success = false
				errtxt = "Template does not exist"
			elseif create and #tmp > 0 then
				success = false
				errtxt = "Template already exists"
			end
			if success then
				if create then
					sql = "INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='template'), '"..provdb.escape(template.value.label.value).."', '"..provdb.escape(template.value.filename.value).."', '"..provdb.escape(template.value.seq.value).."')"
				else
					sql = "UPDATE provisioning_options SET (label, seq) = ('"..provdb.escape(template.value.label.value).."', '"..provdb.escape(template.value.seq.value).."') WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(template.value.filename.value).."'"
				end
				runsqlcommand(sql)

				fs.write_file(template.value.filename.value, string.gsub(format.dostounix(template.value.filecontent.value), "\n+$", ""))
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		if create then
			template.errtxt = errtxt or "Failed to create template"
		else
			template.errtxt = errtxt or "Failed to save template"
		end
	end
	return template
end

mymodule.get_delete_template = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.filename = cfe({ value=clientdata.filename or "", label="File Name" })
	return cfe({ type="group", value=retval, label="Delete Template" })
end

mymodule.delete_template = function(self, delreq)
	local connected
	local filename = delreq.value.filename.value
	delreq.errtxt = "Failed to delete template"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(filename).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.filename.errtxt = "Template does not exist"
		else
			-- Remove the template
			sql = "DELETE FROM provisioning_options WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name = 'template') AND value='"..provdb.escape(filename).."'"
			runsqlcommand(sql)
			-- Delete the template file
			os.remove(filename)
			delreq.errtxt = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.list_class_groups = function()
	local connected
	local retval = {}
	local errtxt
	-- Get the groups from the DB
        local res, err = pcall(function()
		connected = databaseconnect(true)
		local sql = "SELECT * FROM provisioning_class_groups ORDER BY seq ASC, label ASC"
		retval = getselectresponse(sql)
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="structure", value=retval, label="Class Groups", errtxt=errtxt })
end

mymodule.get_class_group = function(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local class_group_id = clientdata.class_group_id
	local retval = {}
	retval.class_group_id = cfe({value=class_group_id or "", label="Class Group ID", readonly=true, seq=1})
	retval.name = cfe({label="Name", seq=2})
	retval.label = cfe({label="Label", seq=3})
	retval.seq = cfe({label="Sequence", seq=4})
	local errtxt
	local res, err = pcall(function()
		connected = databaseconnect(true)
		if class_group_id and class_group_id ~= "" then
			sql = "SELECT * FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(class_group_id).."'"
			tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				for n,v in pairs(tmp[1]) do
					if retval[n] then
						retval[n].value = v
					end
				end
			end
		else
			retval.class_group_id = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="group", value=retval, label="Provisioning Class Group", errtxt=errtxt })
end

mymodule.create_class_group = function(self, group, action)
	return mymodule.update_class_group(self, group, action, true)
end

mymodule.update_class_group = function(self, group, action, create)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
	if group.value.name.value == "" then
		success = false
		group.value.name.errtxt = "Cannot be blank"
	elseif string.find(group.value.name.value, "%W") then
		success = false
		group.value.name.errtxt = "Must only contain alphanumeric characters"
	end
	if group.value.label.value == "" then
		success = false
		group.value.label.errtxt = "Cannot be blank"
	end
	if not validator.is_integer(group.value.seq.value) then
		success = false
		group.value.seq.errtxt = "Must be an integer"
	end
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			if not create then
				local sql = "SELECT * FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(group.value.class_group_id.value).."'"
				local tmp = getselectresponse(sql)
				if not tmp or #tmp == 0 then
					success = false
					errtxt = "Group does not exist"
				end
			end
			if success then
				local sql
				if create then
					sql = "INSERT INTO provisioning_class_groups VALUES(DEFAULT, '"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."')"
					runsqlcommand(sql)
					sql = "SELECT class_group_id FROM provisioning_class_groups WHERE label='"..provdb.escape(group.value.label.value).."'"
					local tmp = getselectresponse(sql)
					if tmp and #tmp>0 then
						group.value.class_group_id = cfe({value=tmp[1].class_group_id, label="Class Group ID", readonly=true, seq=1})
					else
						error("Failed to insert class group", 0)
					end
				else
					sql = "UPDATE provisioning_class_groups SET (name, label, seq) = ('"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."') WHERE class_group_id='"..provdb.escape(group.value.class_group_id.value).."'"
					runsqlcommand(sql)
				end
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		if create then
			group.errtxt = errtxt or "Failed to create class group"
		else
			group.errtxt = errtxt or "Failed to save class group"
		end
	end
	return group
end

mymodule.get_delete_class_group = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.class_group_id = cfe({ value=clientdata.class_group_id or "", label="Class Group ID" })
	return cfe({ type="group", value=retval, label="Delete Class Group" })
end

mymodule.delete_class_group = function(self, delreq)
	local connected
	local class_group_id = delreq.value.class_group_id.value
	delreq.errtxt = "Failed to delete class group"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(class_group_id).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.class_group_id.errtxt = "Group does not exist"
		else
			sql = "DELETE FROM provisioning_class_groups WHERE class_group_id='"..provdb.escape(class_group_id).."'"
			runsqlcommand(sql)
			delreq.errtxt = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.list_classes = function()
	local connected
	local retval = {}
	local errtxt
	-- Get the classes from the DB
        local res, err = pcall(function()
		connected = databaseconnect(true)
		local sql = "SELECT class_id, g.label AS group, g.name, c.label, c.seq FROM provisioning_classes c JOIN provisioning_class_groups g USING(class_group_id) ORDER BY g.seq ASC, g.label ASC, c.seq ASC, c.label ASC"
		retval = getselectresponse(sql)
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="structure", value=retval, label="Classes", errtxt=errtxt })
end

mymodule.get_class = function(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local class_id = clientdata.class_id
	local retval = {}
	retval.class_id = cfe({value=class_id or "", label="Class ID", readonly=true, seq=1})
	retval.class_group_id = cfe({type="select", label="Class Group", option={}, seq=2})
	retval.label = cfe({label="Label", seq=3})
	retval.seq = cfe({label="Sequence", seq=4})
	retval.groups = cfe({type="group", value={}, label="Parameter Groups", seq=5})
	local errtxt
	local res, err = pcall(function()
		local groups = {}
		connected = databaseconnect(true)
		if class_id and class_id ~= "" then
			local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(class_id).."'"
			local tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				for n,v in pairs(tmp[1]) do
					if retval[n] then
						retval[n].value = v
					end
				end
			end
			-- Now, get the class-to-paramgroup mappings
			sql = "SELECT group_id FROM classes_to_param_groups WHERE class_id='"..provdb.escape(class_id).."'"
			tmp = getselectresponse(sql)
			for i,g in ipairs(tmp) do
				groups[g.group_id] = true
			end
		else
			retval.class_id = nil
		end
		-- Get the class_group_id options
		sql = "SELECT * from provisioning_class_groups ORDER BY seq ASC, label ASC"
		tmp = getselectresponse(sql)
		for i,g in ipairs(tmp) do
			retval.class_group_id.option[#retval.class_group_id.option + 1] = {value=g.class_group_id, label=g.label}
		end
		-- Finally, get the paramgroup options
		sql = "SELECT group_id, name, label FROM provisioning_groups ORDER BY seq ASC, name ASC"
		tmp = getselectresponse(sql)
		for i,g in ipairs(tmp) do
			if not retval.groups.value[g.name] then
				retval.groups.value[g.name] = cfe({type="select", label=g.name, option={{value="", label=""}}, seq=i})
			end
			local group = retval.groups.value[g.name]
			group.option[#group.option + 1] = {value=g.group_id, label=g.label}
			if groups[g.group_id] then
				group.value = g.group_id
			end
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="group", value=retval, label="Provisioning Class", errtxt=errtxt })
end

mymodule.create_class = function(self, class, action)
	return mymodule.update_class(self, class, action, true)
end

mymodule.update_class = function(self, class, action, create)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
	success = modelfunctions.validateselect(class.value.class_group_id) and success
	if class.value.label.value == "" then
		success = false
		class.value.label.errtxt = "Cannot be blank"
	end
	if not validator.is_integer(class.value.seq.value) then
		success = false
		class.value.seq.errtxt = "Must be an integer"
	end
	for n,g in pairs(class.value.groups.value) do
		success = modelfunctions.validateselect(g) and success
	end
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			if not create then
				local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(class.value.class_id.value).."'"
				local tmp = getselectresponse(sql)
				if not tmp or #tmp == 0 then
					success = false
					errtxt = "Class does not exist"
				end
			end
			if success then
				local sql
				if create then
					sql = "INSERT INTO provisioning_classes VALUES(DEFAULT, '"..provdb.escape(class.value.class_group_id.value).."', '"..provdb.escape(class.value.label.value).."', '"..provdb.escape(class.value.seq.value).."')"
					runsqlcommand(sql)
					sql = "SELECT class_id FROM provisioning_classes WHERE class_group_id='"..provdb.escape(class.value.class_group_id.value).."' AND label='"..provdb.escape(class.value.label.value).."'"
					local tmp = getselectresponse(sql)
					if tmp and #tmp>0 then
						class.value.class_id = cfe({value=tmp[1].class_id, label="Class ID", readonly=true, seq=1})
					else
						error("Failed to insert class", 0)
					end
				else
					sql = "UPDATE provisioning_classes SET (class_group_id, label, seq) = ('"..provdb.escape(class.value.class_group_id.value).."', '"..provdb.escape(class.value.label.value).."', '"..provdb.escape(class.value.seq.value).."') WHERE class_id='"..provdb.escape(class.value.class_id.value).."'"
					runsqlcommand(sql)
					sql = "DELETE FROM classes_to_param_groups WHERE class_id='"..provdb.escape(class.value.class_id.value).."'"
					runsqlcommand(sql)
				end
				-- Insert the class to group entries
				for n,g in pairs(class.value.groups.value) do
					if g.value ~= "" then
						sql = "INSERT INTO classes_to_param_groups VALUES('"..provdb.escape(class.value.class_id.value).."', '"..provdb.escape(g.value).."')"
						runsqlcommand(sql)
					end
				end
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		if create then
			class.errtxt = errtxt or "Failed to create class"
		else
			class.errtxt = errtxt or "Failed to save class"
		end
	end
	return class
end

mymodule.get_delete_class = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.class_id = cfe({ value=clientdata.class_id or "", label="Class ID" })
	return cfe({ type="group", value=retval, label="Delete Class" })
end

mymodule.delete_class = function(self, delreq)
	local connected
	local class_id = delreq.value.class_id.value
	delreq.errtxt = "Failed to delete class"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(class_id).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.class_id.errtxt = "Class does not exist"
		else
			sql = "DELETE FROM classes_to_param_groups WHERE class_id='"..provdb.escape(class_id).."'"
			runsqlcommand(sql)
			sql = "DELETE FROM provisioning_classes WHERE class_id='"..provdb.escape(class_id).."'"
			runsqlcommand(sql)
			delreq.errtxt = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.list_groups = function()
	local connected
	local retval = {}
	local errtxt
	-- Get the groups from the DB
        local res, err = pcall(function()
		connected = databaseconnect(true)
		local sql = "SELECT * FROM provisioning_groups ORDER BY seq ASC, name ASC, label ASC"
		retval = getselectresponse(sql)
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="structure", value=retval, label="Parameter Groups", errtxt=errtxt })
end

mymodule.get_group = function(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local group_id = clientdata.group_id
	local retval = {}
	retval.group_id = cfe({value=group_id or "", label="Group ID", readonly=true, seq=1})
	retval.name = cfe({label="Name", seq=2})
	retval.label = cfe({label="Label", seq=3})
	retval.seq = cfe({label="Sequence", seq=4})
	retval.params = cfe({type="multi", value={}, label="Parameters", option={}, descr="Each selected parameter will be included in the group", seq=5})
	retval.editable = cfe({type="multi", value={}, label="Editable Parameters", option={}, descr="Each selected parameter will be user editable", seq=6})
	retval.override = cfe({type="multi", value={}, label="Overridden Parameters", option={}, descr="Each selected parameter will override the parameter default from the group defaults", seq=7})
	retval.defaults = cfe({type="group", value={}, label="Parameter Defaults", seq=8})
	local errtxt
	local res, err = pcall(function()
		connected = databaseconnect(true)
		-- First, let's get all the parameters to set up the params.options and defaults
		local sql = "SELECT * FROM provisioning_params ORDER BY seq ASC, name ASC"
		local tmp = getselectresponse(sql)
		for i,p in ipairs(tmp) do
			retval.params.option[#retval.params.option + 1] = {value=p.param_id, label=p.label}
			retval.editable.option[#retval.editable.option + 1] = {value=p.param_id, label=p.label}
			retval.override.option[#retval.override.option + 1] = {value=p.param_id, label=p.label}
			p.seq = i
			if p.type == "select" then
				sql = "SELECT * FROM provisioning_options WHERE param_id='"..provdb.escape(p.param_id).."' ORDER BY seq ASC"
				p.option = getselectresponse(sql) or {}
			end
			if p.type == "boolean" then
				p.value = (p.value == "true")
			end
			p.default = p.value
			retval.defaults.value[p.param_id] = p
		end
		if group_id and group_id ~= "" then
			sql = "SELECT * FROM provisioning_groups WHERE group_id='"..provdb.escape(group_id).."'"
			tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				for n,v in pairs(tmp[1]) do
					if retval[n] then
						retval[n].value = v
					end
				end
			end
			-- Now, get the paramgroup-to-param mappings
			sql = "SELECT * FROM param_groups_to_params WHERE group_id='"..provdb.escape(group_id).."'"
			tmp = getselectresponse(sql)
			for i,p in ipairs(tmp) do
				retval.params.value[#retval.params.value + 1] = p.param_id
				if (p.editable == "t") then
					retval.editable.value[#retval.editable.value + 1] = p.param_id
				end
				if p.value then
					retval.override.value[#retval.override.value + 1] = p.param_id
					if retval.defaults.value[p.param_id].type == "boolean" then
						retval.defaults.value[p.param_id].value = (p.value == "true")
					else
						retval.defaults.value[p.param_id].value = p.value
					end
				end
			end
		else
			retval.group_id = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="group", value=retval, label="Provisioning Parameter Group", errtxt=errtxt })
end

mymodule.create_group = function(self, group, action)
	return mymodule.update_group(self, group, action, true)
end

mymodule.update_group = function(self, group, action, create)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
	success = modelfunctions.validatemulti(group.value.params) and success
	success = modelfunctions.validatemulti(group.value.editable) and success
	success = modelfunctions.validatemulti(group.value.override) and success
	if group.value.name.value == "" then
		success = false
		group.value.name.errtxt = "Cannot be blank"
	-- Group name cannot be device_id, name, or label or will mess up get_device_params
	elseif group.value.name.value == "device_id" or group.value.name.value == "name" or  group.value.name.value == "label" then 
		success = false
		group.value.name.errtxt = "Illegal name"
	elseif string.find(group.value.name.value, "%W") then
		success = false
		group.value.name.errtxt = "Must only contain alphanumeric characters"
	end
	if group.value.label.value == "" then
		success = false
		group.value.label.errtxt = "Cannot be blank"
	end
	if not validator.is_integer(group.value.seq.value) then
		success = false
		group.value.seq.errtxt = "Must be an integer"
	end
	-- Validate the param defaults
	for i,p in pairs(group.value.defaults.value) do
		success = validateparam(p, true) and success
	end
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			local devices = {}
			if not create then
				local sql = "SELECT * FROM provisioning_groups WHERE group_id='"..provdb.escape(group.value.group_id.value).."'"
				local tmp = getselectresponse(sql)
				if not tmp or #tmp == 0 then
					success = false
					errtxt = "Group does not exist"
				else
					-- Find all devices that have this group so we can notify on a param default value change
					sql = "SELECT d2t.device_id "..
						"FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id)) "..
						"WHERE t2g.group_id='"..provdb.escape(group.value.group_id.value).."'"
					devices = getselectresponse(sql)
				end
			end
			if success then
				if create then
					sql = "INSERT INTO provisioning_groups VALUES(DEFAULT, '"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."')"
					runsqlcommand(sql)
					sql = "SELECT group_id FROM provisioning_groups WHERE name='"..provdb.escape(group.value.name.value).."' AND label='"..provdb.escape(group.value.label.value).."'"
					local tmp = getselectresponse(sql)
					if tmp and #tmp>0 then
						group.value.group_id = cfe({value=tmp[1].group_id, label="Group ID", readonly=true, seq=1})
					else
						error("Failed to insert parameter group", 0)
					end
				else
					sql = "UPDATE provisioning_groups SET (name, label, seq) = ('"..provdb.escape(group.value.name.value).."', '"..provdb.escape(group.value.label.value).."', '"..provdb.escape(group.value.seq.value).."') WHERE group_id='"..provdb.escape(group.value.group_id.value).."'"
					runsqlcommand(sql)
					sql = "DELETE FROM param_groups_to_params WHERE group_id='"..provdb.escape(group.value.group_id.value).."'"
					runsqlcommand(sql)
				end
				-- Reverse the editable table for ease of use below
				local reverseeditable = {}
				for i,p in ipairs(group.value.editable.value) do
					reverseeditable[p] = i
				end
				-- Reverse the override table for ease of use below
				local reverseoverride = {}
				for i,p in ipairs(group.value.override.value) do
					reverseoverride[p] = "'"..provdb.escape(tostring(group.value.defaults.value[p].value)).."'"
				end
				-- Insert the group to param entries
				for i,p in ipairs(group.value.params.value) do
					sql = "INSERT INTO param_groups_to_params VALUES('"..provdb.escape(group.value.group_id.value).."', '"..provdb.escape(p).."', "..(reverseoverride[p] or "null")..", '"..provdb.escape(tostring(reverseeditable[p] ~= nil)).."')"
					runsqlcommand(sql)
				end
			end
			if connected then databasedisconnect() end

			-- Update the devices after databasedisconnect so device failure doesn't rollback the change
			if success then
				group.descr = {"Parameter Group Saved"}
				-- Notify the devices that their params might have changed (this will also validate)
				for i,d in ipairs(devices) do
					local r = set_device_params(get_device_params(d.device_id, false), false)
					if r.errtxt then
						group.descr[#group.descr+1] = "ERROR: Failed to update params for device "..d.device_id.."\n"..r:print_errtxt()
						self.logevent(group.descr[#group.descr])
					end
				end
				group.descr = table.concat(group.descr, "\n")
			end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		if create then
			group.errtxt = errtxt or "Failed to create parameter group"
		else
			group.errtxt = errtxt or "Failed to save parameter group"
		end
	end
	return group
end

mymodule.get_delete_group = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.group_id = cfe({ value=clientdata.group_id or "", label="Group ID" })
	return cfe({ type="group", value=retval, label="Delete Group" })
end

mymodule.delete_group = function(self, delreq)
	local connected
	local group_id = delreq.value.group_id.value
	delreq.errtxt = "Failed to delete parameter group"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_groups WHERE group_id='"..provdb.escape(group_id).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.group_id.errtxt = "Group does not exist"
		else
			sql = "DELETE FROM param_groups_to_params WHERE group_id='"..provdb.escape(group_id).."'"
			runsqlcommand(sql)
			sql = "DELETE FROM provisioning_groups WHERE group_id='"..provdb.escape(group_id).."'"
			runsqlcommand(sql)
			delreq.errtxt = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.list_params = function()
	local connected
	local retval = {}
	local errtxt
	-- Get the params from the DB
        local res, err = pcall(function()
		connected = databaseconnect(true)
		local sql = "SELECT * FROM provisioning_params ORDER BY seq ASC, name ASC, label ASC"
		retval = getselectresponse(sql)
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="structure", value=retval, label="Parameters", errtxt=errtxt })
end

mymodule.get_param = function(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local param_id = clientdata.param_id
	local retval = {}
	retval.param_id = cfe({value=param_id or "", label="Param ID", readonly=true, seq=1})
	retval.name = cfe({label="Name", seq=2})
	retval.type = cfe({type="select", label="Type", option={"text", "boolean", "select"}, seq=3})
	retval.label = cfe({label="Label", seq=4})
	retval.descr = cfe({label="Description", seq=5})
	retval.value = cfe({label="Default Value", descr="Warning, this value is not validated", seq=6})
	retval.regexp = cfe({label="Regular Expression", descr="Lua regular expression for validating the text parameter value", seq=7})
	retval.validate = cfe({type="longtext", label="Validation Code", descr="Lua code to validate the parameter value. Returns updated value and error text. Not used to validate group defaults.", seq=8})
	retval.seq = cfe({label="Sequence", seq=9})
	local errtxt
	local res, err = pcall(function()
		connected = databaseconnect(true)
		if param_id and param_id ~= "" then
			sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'"
			tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				for n,v in pairs(tmp[1]) do
					if retval[n] then
						retval[n].value = v
					end
				end
			end
		else
			retval.param_id = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end

	return cfe({ type="group", value=retval, label="Provisioning Parameter", errtxt=errtxt })
end

mymodule.create_param = function(self, param, action)
	return mymodule.update_param(self, param, action, true)
end

mymodule.update_param = function(self, param, action, create)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
	success = modelfunctions.validateselect(param.value.type)
	if param.value.name.value == "" then
		success = false
		param.value.name.errtxt = "Cannot be blank"
	end
	if param.value.label.value == "" then
		success = false
		param.value.label.errtxt = "Cannot be blank"
	end
	if not validator.is_integer(param.value.seq.value) then
		success = false
		param.value.seq.errtxt = "Must be an integer"
	end
	local s,e = validateluacode(param.value.validate.value)
	if not s then
		success = false
		param.value.validate.errtxt = "Invalid Lua code"
		if e and e ~= "" then
			param.value.validate.errtxt = param.value.validate.errtxt.."\n"..e
		end
	end
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			local devices = {}
			if not create then
				local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param.value.param_id.value).."'"
				local tmp = getselectresponse(sql)
				if not tmp or #tmp == 0 then
					success = false
					errtxt = "Param does not exist"
				else
					-- Find all devices that have this param so we can notify on a param default value change
					sql = "SELECT d2t.device_id FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) "..
						"JOIN provisioning_groups g USING(group_id) JOIN param_groups_to_params g2p USING(group_id))"..
						"WHERE g2p.param_id='"..provdb.escape(param.value.param_id.value).."'"
					devices = getselectresponse(sql)
				end
			end
			if success then
				local sql
				if create then
					sql = "INSERT INTO provisioning_params VALUES(DEFAULT, '"..provdb.escape(param.value.name.value).."', '"..provdb.escape(param.value.type.value).."', '"..provdb.escape(param.value.label.value).."', '"..provdb.escape(param.value.descr.value).."', '"..provdb.escape(param.value.value.value).."', '"..provdb.escape(param.value.seq.value).."', '"..provdb.escape(param.value.regexp.value).."', '"..provdb.escape(format.dostounix(param.value.validate.value)).."')"
					runsqlcommand(sql)
					sql = "SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(param.value.name.value).."' AND label='"..provdb.escape(param.value.label.value).."'"
					local tmp = getselectresponse(sql)
					if tmp and #tmp>0 then
						param.value.param_id = cfe({value=tmp[1].param_id, label="Param ID", readonly=true, seq=1})
					else
						error("Failed to insert parameter", 0);
					end
				else
					sql = "UPDATE provisioning_params SET (name, type, label, descr, value, seq, regexp, validate) = ('"..provdb.escape(param.value.name.value).."', '"..provdb.escape(param.value.type.value).."', '"..provdb.escape(param.value.label.value).."', '"..provdb.escape(param.value.descr.value).."', '"..provdb.escape(param.value.value.value).."', '"..provdb.escape(param.value.seq.value).."', '"..provdb.escape(param.value.regexp.value).."', '"..provdb.escape(format.dostounix(param.value.validate.value)).."') WHERE param_id='"..provdb.escape(param.value.param_id.value).."'"
					runsqlcommand(sql)
				end
			end
			if connected then databasedisconnect() end

			-- Update the devices after databasedisconnect so device failure doesn't rollback the change
			if success then
				param.descr = {"Parameter Saved"}
				-- Notify the devices that their params might have changed (this will also validate)
				for i,d in ipairs(devices) do
					local r = set_device_params(get_device_params(d.device_id, false), false)
					if r.errtxt then
						param.descr[#param.descr+1] = "ERROR: Failed to update params for device "..d.device_id.."\n"..r:print_errtxt()
						self.logevent(param.descr[#param.descr])
					end
				end
				param.descr = table.concat(param.descr, "\n")
			end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		if create then
			param.errtxt = errtxt or "Failed to create parameter"
		else
			param.errtxt = errtxt or "Failed to save parameter"
		end
	end
	return param
end

mymodule.get_delete_param = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.param_id = cfe({ value=clientdata.param_id or "", label="Param ID" })
	return cfe({ type="group", value=retval, label="Delete Param" })
end

mymodule.delete_param = function(self, delreq)
	local connected
	local param_id = delreq.value.param_id.value
	delreq.errtxt = "Failed to delete parameter"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.param_id.errtxt = "Parameter does not exist"
		else
			sql = "DELETE FROM provisioning_options WHERE param_id='"..provdb.escape(param_id).."'"
			runsqlcommand(sql)
			sql = "DELETE FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'"
			runsqlcommand(sql)
			delreq.errtxt = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.list_devices = function(self, clientdata)
	local connected
	local retval = cfe({ type="group", value={}, label="Provisioning Devices" })
	retval.value.page = cfe({ value=0, label="Page Number", descr="0 indicates ALL", key=true })
	retval.value.pagesize = cfe({ value=10, label="Page Size", key=true })
	retval.value.rowcount = cfe({ value=0, label="Row Count" })
	-- orderby must be an array of tables with column name and direction
	retval.value.orderby = cfe({ type="structure", value={{column="device_id", direction="asc"}}, label="Order By", key=true })
	-- filter is a table with a string filter for each column
	retval.value.filter = cfe({ type="structure", value={}, label="Filter", key=true })
	self.handle_clientdata(retval, clientdata)
	retval.value.result = cfe({ type="structure", value={}, label="Provisioning Devices" })
	retval.value.groups = cfe({ type="structure", value={}, label="Provisioning Class Groups" })

	-- Process the incoming page data
	local page = tonumber(retval.value.page.value) or 0
	retval.value.page.value = page
	local pagesize = tonumber(retval.value.pagesize.value) or 10
	retval.value.pagesize.value = pagesize

	-- Get the devices from the DB
        local res, err = pcall(function()
		connected = databaseconnect(true)

		-- First, get the columns / class_groups
		local sql = "SELECT * from provisioning_class_groups ORDER BY seq ASC"
		retval.value.groups.value = getselectresponse(sql)

		-- Set up the dynamic tables for order/filter and the sql statement for each column / class_group
		local columns = {device_id="device_id"}
		local selects = {"device_id"}
		local joins = {"devices_to_classes"}
		for i,g in ipairs(retval.value.groups.value) do
			columns[g.name] = provdb.escape(g.name)..".label"
			selects[#selects+1] = provdb.escape(g.name)..".label AS "..provdb.escape(g.name)
			joins[#joins+1] = "(SELECT device_id, c.label FROM devices_to_classes d2c JOIN provisioning_classes c USING(class_id) WHERE c.class_group_id='"..g.class_group_id.."') "..provdb.escape(g.name).." USING(device_id)"
		end

		-- Handle the sorting
		local orderby = {}
		local directions = {asc="ASC", desc="DESC", ASC="ASC", DESC="DESC"}
		for i,o in ipairs(retval.value.orderby.value) do
			if columns[o.column] and directions[o.direction] then
				orderby[#orderby+1] = columns[o.column].." "..directions[o.direction]
			end
		end
		if #orderby == 0 then
			orderby[#orderby+1] = "device_id ASC"
		end

		-- Handle the filtering
		local filter = {}
		for c,f in pairs(retval.value.filter.value) do
			if columns[c] and f ~= "" then
				if c == "device_id" then
					if tonumber(f) then
						filter[#filter+1] = columns[c].."='"..provdb.escape(f).."'"
					else
						filter[#filter+1] = columns[c].." IS NULL"
					end
				else
					filter[#filter+1] = columns[c].."~'"..provdb.escape(f).."'"
				end
			end
		end

		-- Set up the joins and filtering first
		sql = " FROM "..table.concat(joins, " LEFT JOIN ")
		if #filter>0 then
			sql = sql.." WHERE "..table.concat(filter, " AND ")
		end
		-- If we're paginating, get the full count here
		if page > 0 then
			local count = getselectresponse("SELECT count(DISTINCT device_id)"..sql)
			retval.value.rowcount.value = count[1].count
		end
		-- Then we can finish the sql with the selects and sorting
		sql = "SELECT DISTINCT "..table.concat(selects, ", ")..sql
		sql = sql.." ORDER BY "..table.concat(orderby, ", ")
		if page > 0 then
			sql = sql.." LIMIT "..pagesize.." OFFSET "..(page - 1)*pagesize
		end
		retval.value.result.value = getselectresponse(sql) or {}

		if page <= 0 then
			retval.value.rowcount.value = #retval.value.result.value
		end

		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		retval.errtxt = err
	end

	return retval
end

mymodule.get_existing_device = function(self, clientdata)
	clientdata = clientdata or {}
	return get_device(clientdata.device_id, false)
end

mymodule.get_new_device = function(self, clientdata)
	clientdata = clientdata or {}
	return get_device(clientdata.device_id, true)
end

mymodule.create_device = function(self, device, action)
	return mymodule.update_device(self, device, action, true)
end

mymodule.update_device = function(self, device, action, create)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
	local something = false
	for n,c in pairs(device.value.classes.value) do
		success = modelfunctions.validateselect(c) and success
		if c.value ~= "" then something = true end
	end
	if not something then
		success = false
	end
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			if not create then
				local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(device.value.device_id.value).."' LIMIT 1"
				local tmp = getselectresponse(sql)
				if not tmp or #tmp == 0 then
					success = false
					device.value.device_id.errtxt = "Device does not exist"
				end
			end
			if success then
				if not saved_devices[device.value.device_id.value] then get_device(device.value.device_id.value) end
				if not saved_device_params[device.value.device_id.value] then get_device_params(device.value.device_id.value) end

				if create then
					sql = "SELECT nextval('provisioning_device_seq')"
					local tmp = getselectresponse(sql)
					if tmp and #tmp>0 then
						device.value.device_id.value = tmp[1].nextval
					end
				end
				sql = "DELETE FROM devices_to_classes WHERE device_id='"..provdb.escape(device.value.device_id.value).."'"
				runsqlcommand(sql)
				-- Insert the device to class entries
				for n,c in pairs(device.value.classes.value) do
					if c.value ~= "" then
						sql = "INSERT INTO devices_to_classes VALUES('"..provdb.escape(device.value.device_id.value).."', '"..provdb.escape(c.value).."')"
						runsqlcommand(sql)
					end
				end

				device.descr = "Device Created"

				-- Notify the device that its params might have changed (this will also validate)
				local s = saved_device_params[device.value.device_id.value]
				local r = set_device_params(get_device_params(device.value.device_id.value, false), false)
				if r.errtxt then
					if create then
						device.descr = device.descr.."\nERROR: Failed to update params\n"..r:print_errtxt()
					else
						error(r:print_errtxt())
					end
				end

				callscript(updatedevicescriptfile, device, saved_devices[device.value.device_id.value], r, s)

				saved_devices[device.value.device_id.value] = device
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		device.descr = nil
		if create then
			device.errtxt = errtxt or "Failed to create device"
		else
			device.errtxt = errtxt or "Failed to save device"
		end
	end
	return device
end

mymodule.get_delete_device = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.device_id = cfe({ value=clientdata.device_id or "", label="Device ID" })
	return cfe({ type="group", value=retval, label="Delete Device" })
end

mymodule.delete_device = function(self, delreq)
	local connected
	local device_id = delreq.value.device_id.value
	delreq.errtxt = "Failed to delete device"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."' LIMIT 1"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.device_id.errtxt = "Device does not exist"
		else
			if not saved_device_params[device_id] then get_device_params(device_id) end
			if not saved_devices[device_id] then get_device(device_id) end
			
			sql = "DELETE FROM provisioning_values WHERE device_id='"..provdb.escape(device_id).."'"
			runsqlcommand(sql)
			sql = "DELETE FROM devices_to_classes WHERE device_id='"..provdb.escape(device_id).."'"
			runsqlcommand(sql)
			delreq.errtxt = nil

			callscript(deletedevicescriptfile, saved_devices[device_id], saved_device_params[device_id])
			saved_devices[device_id] = nil
			saved_device_params[device_id] = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.get_editable_device_params = function(self, clientdata, action)
	clientdata = clientdata or {}
	return get_device_params(clientdata.device_id, true)
end

mymodule.get_all_device_params = function(self, clientdata, action)
	clientdata = clientdata or {}
	return get_device_params(clientdata.device_id, false)
end

mymodule.get_class_options = function(self, clientdata)
	clientdata = clientdata or {}
	local class_id = clientdata.class_id
	local retval = {}
	retval.class_id = cfe({value=class_id or "", label="Class ID", seq=1})
	return cfe({ type="group", value=retval, label="Provisioning Class Values" })
end

mymodule.get_class_values = function(self, retval)
	local connected
	retval.errtxt = "Failed to find class"
	if not validator.is_integer(retval.value.class_id.value) then
		retval.value.class_id.errtxt = "Invalid class ID"
	else
		local res, err = pcall(function()
			connected = databaseconnect(true)
			-- First, just check to see if class_id exists
			local sql = "SELECT * FROM provisioning_classes WHERE class_id='"..provdb.escape(retval.value.class_id.value).."'"
			local tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				retval.errtxt = nil
				retval.value.label = cfe({ value=tmp[1].label or "", label="Label", seq=3})
				retval.value.values = cfe({type="structure", value={}, label="Parameter Values"})
					
				local sql = "SELECT g.name AS group, p.name, p.type, CASE WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value "..
					"FROM (provisioning_classes t JOIN classes_to_param_groups t2g USING(class_id) JOIN provisioning_groups g USING(group_id) "..
						"JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) "..
					"WHERE t.class_id='"..provdb.escape(retval.value.class_id.value).."'"
				local tmp = getselectresponse(sql)
				-- Loop through the params and put them into the groups
				for i,p in ipairs(tmp) do
					if p.type == "boolean" then
						p.value = (p.value == "true")
					end
					if not retval.value.values.value[p.group] then
						retval.value.values.value[p.group] = {}
					end
					retval.value.values.value[p.group][p.name] = p.value
				end
			else
				retval.value.class_id.errtxt = "Failed to find Class ID"
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			retval.errtxt = err
		end
	end

	return retval
end

mymodule.set_editable_device_params = function(self, params)
	return set_device_params(params, true)
end

mymodule.set_all_device_params = function(self, params)
	return set_device_params(params, false)
end

mymodule.fetch_device_values = function(self, search)
	local connected
	local res, err = pcall(function()
		connected = databaseconnect(true)
		search = mymodule.search_device_values(self, search)
		if search.errtxt then
			-- Just return the error
		elseif #search.value.result.value == 0 then
			search.errtxt = "Device not found"
		elseif #search.value.result.value > 1 then
			search.errtxt = "Multiple devices found"
		else
			search.value.values = get_device_values(search.value.result.value[1].device_id)
			search.value.values.seq = 5
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		search.errtxt = err
	end

	search.label="Provisioning Device Parameter Values"
	return search
end

mymodule.get_search_options = function()
	local connected
	local errtxt
	retval = {}
	retval.id = cfe({type="select", value="device_id", label="Parameter", option={"device_id"}, seq=1})
	retval.comparison = cfe({type="select", value="=", label="Comparison", option={"=", "!=", "~", "!~", "~*", "!~*"}, seq=2})
	retval.value = cfe({label="Parameter Value", descr="Parameter value or SQL regular expression", seq=3})
        local res, err = pcall(function()
		connected = databaseconnect(true)
		-- Get the group/parameter options
		local sql = "SELECT g.name AS group_name, p.name AS param_name FROM provisioning_groups g JOIN param_groups_to_params USING(group_id) JOIN provisioning_params p USING(param_id) GROUP BY g.name, p.name ORDER BY g.name ASC, p.name ASC"
		local tmp = getselectresponse(sql)
		local blankopt = {}
		local blankexists = {}
		for i,v in ipairs(tmp) do
			retval.id.option[#retval.id.option + 1] = v.group_name.."."..v.param_name
			if not blankexists[v.param_name] then
				blankopt[#blankopt+1] = "."..v.param_name
				blankexists[v.param_name] = true
			end
		end
		table.sort(blankopt)
		for i,o in ipairs(blankopt) do
			retval.id.option[#retval.id.option + 1] = o
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		errtxt = err
	end
	return cfe({type="group", value=retval, label="Device Search", errtxt=errtxt})
end

mymodule.search_device_values = function(self, search)
	local connected
	local success = true
	success = modelfunctions.validateselect(search.value.id) and success
	success = modelfunctions.validateselect(search.value.comparison) and success
	if search.value.id.value == "device_id" then
		if string.match(search.value.comparison.value, "~") then
			success = false
			search.value.comparison.errtxt = "Cannot do regular expression comparison to device_id"
		end
		if not validator.is_integer(search.value.value.value) then
			success = false
			search.value.value.errtxt = "Must be an integer for comparison to device_id"
		end
	end
	if success then
		retval.result = cfe({type="structure", value={}, label="Devices", seq=4 })
        	local res, err = pcall(function()
			connected = databaseconnect(true)
			-- Get the devices from the DB
			sql = "SELECT d2t.device_id, "
			local group, param = string.match(search.value.id.value, "([^%.]*)%.(.*)")
			if not group then
				sql = sql.."'"..provdb.escape(search.value.id.value).."' AS param, d2t."..provdb.escape(search.value.id.value).." AS value FROM devices_to_classes d2t WHERE d2t."..provdb.escape(search.value.id.value)..
					provdb.escape(search.value.comparison.value).."'"..provdb.escape(search.value.value.value).."' GROUP BY device_id"
			else
				sql = sql.."g.name as group, p.name as param, CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END as value FROM "..
					"devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING(class_id) "..
					"JOIN provisioning_groups g USING(group_id) JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id) "..
					"LEFT JOIN provisioning_values v ON(d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) "
				if group and group ~= "" then
					sql = sql.."WHERE g.name='"..provdb.escape(group).."' AND"
				else
					sql = sql.."WHERE"
				end
				sql = sql.." p.name='"..provdb.escape(param).."' AND CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END"..provdb.escape(search.value.comparison.value).."'"..provdb.escape(search.value.value.value).."'"
			end
			sql = sql.." ORDER BY d2t.device_id ASC"
			search.value.result.value = getselectresponse(sql)
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			search.errtxt = err
		end
	else
		search.errtxt = "Search failed"
	end
	return search
end

mymodule.get_param_options = function(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local param_id = clientdata.param_id
	local retval = {}
	retval.param_id = cfe({value=param_id or "", label="Parameter ID", readonly=true, seq=0})
	retval.name = cfe({label="Name", readonly=true, seq=1})
	retval.label = cfe({label="Label", readonly=true, seq=2})
	retval.options = cfe({type="list", value={}, label="Value Options", descr="Ordered list of options where each option is made up of 'value' or 'value,label'", seq=3})
	local errtxt = "Cannot find parameter"
	if param_id and param_id ~= "" then
		local res, err = pcall(function()
			connected = databaseconnect(true)
			-- First, just check to see if param_id exists
			local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(param_id).."'"
			local tmp = getselectresponse(sql)
			if tmp and #tmp > 0 then
				errtxt = nil
				retval.name.value = tmp[1].name
				retval.label.value = tmp[1].label
				-- Next, get all of the param options
				sql = "SELECT * FROM provisioning_options WHERE param_id='"..provdb.escape(param_id).."' ORDER BY seq ASC"
				local tmp = getselectresponse(sql) or {}
				for i,t in ipairs(tmp) do
					retval.options.value[#retval.options.value + 1] = t.value..","..t.label
				end
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			errtxt = err
		end
	end
	return cfe({ type="group", value=retval, label="Provisioning Parameter Options", errtxt=errtxt })
end

mymodule.set_param_options = function(self, options)
	local connected
	local success = true
	local errtxt
	-- Validate the settings
--FIXME
	if success then
        	local res, err = pcall(function()
			connected = databaseconnect()
			local sql = "SELECT * FROM provisioning_params WHERE param_id='"..provdb.escape(options.value.param_id.value).."'"
			local tmp = getselectresponse(sql)
			if not tmp or #tmp == 0 then
				success = false
				errtxt = "Parameter does not exist"
			end
			if success then
				-- Delete all options for this device
				local sql = "DELETE FROM provisioning_options WHERE param_id='"..provdb.escape(options.value.param_id.value).."'"
				runsqlcommand(sql)
				-- Loop through the options
				for i,o in ipairs(options.value.options.value) do
					local v,l = string.match(o, "^%s*([^,]*),%s*(.*%S?)%s*$")
					if v then
						v = string.match(v, "^(.*%S?)%s*$")
					else
						v = string.match(o, "^%s*(.*%S?)%s*$")
						l = v
					end
					sql = "INSERT INTO provisioning_options VALUES('"..provdb.escape(options.value.param_id.value).."', '"..provdb.escape(l).."', '"..provdb.escape(v).."', '"..i.."')"
					runsqlcommand(sql)
				end
			end
			if connected then databasedisconnect() end
		end)
		if not res and err then
			handlesqlexception(connected, err)
			success = false
			errtxt = err
		end
	end
	if not success then
		options.errtxt = errtxt or "Failed to save options"
	end
	return options
end

function mymodule.get_filedetails(self, clientdata)
	clientdata = clientdata or {}
	return modelfunctions.getfiledetails(clientdata.filename, scriptfiles)
end

function mymodule.update_filedetails(self, filedetails)
	return modelfunctions.setfiledetails(self, filedetails, scriptfiles, validatefiledetails)
end

function mymodule.list_files()
	local retval = {}
	for i,file in ipairs(scriptfiles) do
		local details = fs.stat(file) or {}
		details.filename = file
		table.insert(retval, details)
	end
	table.sort(retval, function(a,b) return a.filename < b.filename end)
	return cfe({ type="structure", value=retval, label="Provisioning Script Files" })
end

function mymodule.get_database()
	local devices = cfe({ type="boolean", value=false, label="Include Device Data" })
	return cfe({ type="group", value={devices=devices}, label="Provisioning Database Dump" })
end

function mymodule.dump_database(self, db)
	local connected

	local function escape_or_null(value)
		if not value then return "null" end
		return "'"..provdb.escape(value).."'"
	end

	local res, err = pcall(function()
		connected = databaseconnect(true)
		local lines = {}
		local sql = "SELECT name, label, seq FROM provisioning_class_groups ORDER BY name, label"
		local tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO provisioning_class_groups VALUES(default, "..escape_or_null(t.name)..", "..escape_or_null(t.label)..", "..escape_or_null(t.seq)..");"
		end
		sql = "SELECT g.name AS group, c.label, c.seq FROM provisioning_classes c JOIN provisioning_class_groups g USING(class_group_id) ORDER BY g.name, c.label"
		tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='"..provdb.escape(t.group).."'), "..escape_or_null(t.label)..", "..escape_or_null(t.seq)..");"
		end
		sql = "SELECT name, label, seq FROM provisioning_groups ORDER BY name, label"
		tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO provisioning_groups VALUES(default, "..escape_or_null(t.name)..", "..escape_or_null(t.label)..", "..escape_or_null(t.seq)..");"
		end
		sql = "SELECT name, type, label, descr, value, seq, regexp, validate FROM provisioning_params ORDER BY name"
		tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO provisioning_params VALUES(default, "..escape_or_null(t.name)..", "..escape_or_null(t.type)..", "..escape_or_null(t.label)..", "..escape_or_null(t.descr)..", "..escape_or_null(t.value)..", "..escape_or_null(t.seq)..", "..escape_or_null(t.regexp)..", "..escape_or_null(t.validate)..");"
		end
		sql = "SELECT p.name AS param, o.label, o.value, o.seq FROM provisioning_options o JOIN provisioning_params p USING(param_id) ORDER BY p.name, o.seq, o.label"
		tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(t.param).."'), "..escape_or_null(t.label)..", "..escape_or_null(t.value)..", "..escape_or_null(t.seq)..");"
		end
		sql = "SELECT c.label AS class, g.label AS group FROM provisioning_classes c JOIN classes_to_param_groups USING(class_id) JOIN provisioning_groups g USING(group_id) ORDER BY c.label, g.label"
		tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='"..provdb.escape(t.class).."'), (SELECT group_id FROM provisioning_groups WHERE label='"..provdb.escape(t.group).."'));"
		end
		sql = "SELECT p.name AS param, g.label AS group, t.value, t.editable FROM provisioning_params p JOIN param_groups_to_params t USING(param_id) JOIN provisioning_groups g USING(group_id) ORDER BY p.name, g.label"
		tmp = getselectresponse(sql)
		for i,t in ipairs(tmp) do
			lines[#lines+1] = "INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='"..provdb.escape(t.group).."'), (SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(t.param).."'), "..escape_or_null(t.value)..", "..escape_or_null(t.editable)..");"
		end
		if db.value.devices and db.value.devices.value then
			sql = "SELECT device_id FROM devices_to_classes GROUP BY device_id ORDER BY device_id ASC"
			devices = getselectresponse(sql)
			for i,d in ipairs(devices) do
				sql = "SELECT label FROM devices_to_classes JOIN provisioning_classes USING(class_id) WHERE device_id='"..provdb.escape(d.device_id).."' ORDER BY label ASC"
				tmp = getselectresponse(sql)
				for j,t in ipairs(tmp) do
					lines[#lines+1] = "INSERT INTO devices_to_classes VALUES("..provdb.escape(i)..", (SELECT class_id FROM provisioning_classes WHERE label='"..provdb.escape(t.label).."'));"
				end
				sql = "SELECT group_name, p.name AS param, v.value FROM provisioning_values v JOIN provisioning_params p USING(param_id) WHERE device_id='"..provdb.escape(d.device_id).."' ORDER BY group_name, p.name"
				tmp = getselectresponse(sql)
				for j,t in ipairs(tmp) do
					lines[#lines+1] = "INSERT INTO provisioning_values VALUES("..provdb.escape(i)..", "..escape_or_null(t.group_name)..", (SELECT param_id FROM provisioning_params WHERE name='"..provdb.escape(t.param).."'), "..escape_or_null(t.value)..");"
				end
			end
			lines[#lines+1] = "SELECT pg_catalog.setval('provisioning_device_seq', "..provdb.escape(#devices)..", true);"
		end
		db.value.data = cfe({ type="longtext", value=table.concat(lines, "\n") or "", label="Database Values" })
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		db.errtxt = err
	end
	return db
end

function mymodule.get_file(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local mac = clientdata.mac
	local ip = clientdata.ip
	local agent = clientdata.agent
	local result = cfe()

	-- Error if there's no mac
	if not mac or mac == "" then
		result.errtxt = "No MAC Address"
		return result
	end

	local res, err = pcall(function()
		connected = databaseconnect(true)

		-- Add the device to the table of requests
		local sql = "SELECT * FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(mac)).."'"
		local requests = getselectresponse(sql)
		if requests and #requests > 0 then
			sql = "UPDATE provisioning_requests SET ip='"..provdb.escape(ip).."', agent='"..provdb.escape(agent).."', date=now() WHERE mac='"..provdb.escape(string.upper(mac)).."'"
		else
			sql = "INSERT INTO provisioning_requests VALUES('"..provdb.escape(string.upper(mac)).."', '"..provdb.escape(ip).."', '"..provdb.escape(agent).."', now())"
		end
		runsqlcommand(sql)

		-- Now, let's see if this device exists
		result = mymodule.get_search_options()
		result.value.id.value = "device.mac"
		result.value.value.value = string.upper(mac)
		result = mymodule.fetch_device_values(self, result)

		-- If can't find the device, return a file with default settings
		if #result.value.result.value == 0 then
			-- Determine which class to use (need a class that specifies a template)
			local c = mymodule.list_classes()
			local class = callscript(determineclassscriptfile, agent, c)

			if class then
				local options = mymodule.get_class_options(self, {})
				options.value.class_id.value = class
				result = mymodule.get_class_values(self, options)
			else
				result.errtxt = "Unknown device"
			end
		end

		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		result.errtxt = err
	end

	return result
end

function mymodule.put_file(self, clientdata)
	local connected
	clientdata = clientdata or {}
	local mac = clientdata.mac
	local data = clientdata.data
	local retval = cfe({ label="PUT Data" })
	
	-- Error if there's no mac
	if not mac or mac == "" then
		retval.errtxt = "No MAC Address"
		return retval
	end

	local res, err = pcall(function()
		connected = databaseconnect()
		
		-- Now, let's see if this device exists
		local search = mymodule.get_search_options()
		search.value.id.value = "device.mac"
		search.value.value.value = string.upper(mac)
		search = mymodule.search_device_values(self, search)
		if search.errtxt then
			retval.errtxt = search.errtxt
		elseif #search.value.result.value == 0 then
			retval.errtxt = "Device not found"
		elseif #search.value.result.value > 1 then
			retval.errtxt = "Multiple devices found"
		else
			retval.value, retval.errtxt = callscript(processputscriptfile, mac, data, search.value.result.value[1].device_id)
			-- If the script doesn't exist, allow the write
			retval.value = retval.value or data
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		retval.errtxt = err
	end
	return retval
end

mymodule.list_requests = function(self, clientdata)
	local connected
	local retval = cfe({ type="group", value={}, label="Requests" })
	retval.value.page = cfe({ value=0, label="Page Number", descr="0 indicates ALL", key=true })
	retval.value.pagesize = cfe({ value=10, label="Page Size", key=true })
	retval.value.rowcount = cfe({ value=0, label="Row Count" })
	-- orderby must be an array of tables with column name and direction
	retval.value.orderby = cfe({ type="structure", value={{column="date", direction="asc"}}, label="Order By", key=true })
	-- filter is a table with a string filter for each column
	retval.value.filter = cfe({ type="structure", value={}, label="Filter", key=true })
	self.handle_clientdata(retval, clientdata)
	retval.value.result = cfe({ type="structure", value={}, label="Requests" })

	-- Process the incoming page data
	local page = tonumber(retval.value.page.value) or 0
	retval.value.page.value = page
	local pagesize = tonumber(retval.value.pagesize.value) or 10
	retval.value.pagesize.value = pagesize
	local orderby = {}
	local columns = {device_id="c.device_id", agent="r.agent", date="r.date", ip="r.ip", mac="r.mac"}
	local directions = {asc="ASC", desc="DESC", ASC="ASC", DESC="DESC"}
	for i,o in ipairs(retval.value.orderby.value) do
		if columns[o.column] and directions[o.direction] then
			orderby[#orderby+1] = columns[o.column].." "..directions[o.direction]
		end
	end
	if #orderby == 0 then
		orderby[#orderby+1] = "r.date DESC"
	end

        local res, err = pcall(function()
		connected = databaseconnect(true)
		
		local filter = {}
		columns.data = nil -- Cannot regex filter based on date because of the timestamp type
		for c,f in pairs(retval.value.filter.value) do
			if columns[c] and f ~= "" then
				if c == "device_id" then
					if tonumber(f) then
						filter[#filter+1] = columns[c].."='"..provdb.escape(f).."'"
					else
						filter[#filter+1] = columns[c].." IS NULL"
					end
				else
					filter[#filter+1] = columns[c].."~'"..provdb.escape(f).."'"
				end
			end
		end

		local sql = " FROM (provisioning_requests r LEFT JOIN (SELECT v.device_id, v.value FROM (provisioning_values v JOIN provisioning_params p USING(param_id)) WHERE p.name='mac') c ON r.mac=c.value)"
		if #filter>0 then
			sql = sql.." WHERE "..table.concat(filter, " AND ")
		end
		if page > 0 then
			local count = getselectresponse("SELECT count(*)"..sql)
			retval.value.rowcount.value = count[1].count
		end
		sql = sql.." ORDER BY "..table.concat(orderby, ", ")
		if page > 0 then
			sql = sql.." LIMIT "..pagesize.." OFFSET "..(page - 1)*pagesize
		end
		retval.value.result.value = getselectresponse("SELECT r.*, c.device_id"..sql) or {}
		if page <= 0 then
			retval.value.rowcount.value = #retval.value.result.value
		end

		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		retval.errtxt = err
	end

	return retval
end

mymodule.get_delete_request = function(self, clientdata)
	clientdata = clientdata or {}
	local retval = {}
	retval.mac = cfe({ value=clientdata.mac or "", label="MAC Address" })
	return cfe({ type="group", value=retval, label="Delete Request" })
end

mymodule.delete_request = function(self, delreq)
	local connected
	local mac = delreq.value.mac.value
	delreq.errtxt = "Failed to delete request"
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(mac)).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			delreq.value.mac.errtxt = "Request does not exist"
		else
			-- Remove the request
			sql = "DELETE FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(mac)).."'"
			runsqlcommand(sql)
			delreq.errtxt = nil
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		delreq.errtxt = err
	end

	return delreq
end

mymodule.get_request = function(self, clientdata)
	clientdata = clientdata or {}
	local mac = clientdata.mac
	local retval = {}
	retval.mac = cfe({ value=mac or "", label="MAC Address" })
	return cfe({ type="group", value=retval, label="Create Device from Request" })
end

mymodule.create_from_request = function(self, request)
	local connected
	success = false
       	local res, err = pcall(function()
		connected = databaseconnect()
		local sql = "SELECT * FROM provisioning_requests WHERE mac='"..provdb.escape(string.upper(request.value.mac.value)).."'"
		local tmp = getselectresponse(sql)
		if #tmp == 0 then
			request.value.mac.errtxt = "Request does not exist"
		else
			-- Determine which class to use (need a class that specifies a template)
			local c = mymodule.list_classes()
			local class, group = callscript(determineclassscriptfile, tmp[1].agent, c)

			-- Create the device
			local device = get_device(nil, true)
			if class and group and device.value.classes.value[group] then
				device.value.classes.value[group].value = class
				device = mymodule.create_device(self, device)
				if device.errtxt then
					request.errtxt = device:print_errtxt()
				else
					local params = get_device_params(device.value.device_id.value)
					-- Set the MAC Address
					if params.value.device and params.value.device.value.mac then
						params.value.device.value.mac.value = string.upper(request.value.mac.value)
						params = set_device_params(params)
					end
					if params.errtxt then
						request.errtxt = params:print_errtxt()
						local req = mymodule.get_delete_device(self, {device_id = device.value.device_id.value})
						mymodule.delete_device(self, req)
					else
						success = true
						request.value.device_id = device.value.device_id
					end
				end
			else
				request.errtxt = "Failed to create device - could not determine class"
			end
		end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		request.errtxt = err
	end
	
	if not success and not request.errtxt then
		request.errtxt = "Failed to create device"
	end
	return request
end

mymodule.get_bulk_create_request = function(self, clientdata)
	local retval = {}
	retval.bulkdevicedata = cfe({ type="list", value={}, label="Bulk device list", descr="List of devices as comma-separated fields (first row must contain column headers)" })
	return cfe({ type="group", value=retval, label="Create Multiple Devices" })
end

mymodule.bulk_create_devices = function(self, devicelist)
	local connected
	-- To allow uploading a file, check the bulkdevicedata format
	-- Haserl will pass a temporary file name if a file is uploaded
	if #devicelist.value.bulkdevicedata.value == 1 and string.find(devicelist.value.bulkdevicedata.value[1], "^/tmp/[^.]+$") and fs.is_file(devicelist.value.bulkdevicedata.value[1]) then
		-- Need to account for possible DOS line endings
		--devicelist.value.bulkdevicedata.value = fs.read_file_as_array(devicelist.value.bulkdevicedata.value[1]) or {}
		devicelist.value.bulkdevicedata.value = format.string_to_table(fs.read_file(devicelist.value.bulkdevicedata.value[1]), "\r?\n")
	end
	local res, err = pcall(function()
		connected = databaseconnect()

		-- Need to split on ",", but not if found in quoted field
		function csv_parseline(line,sep)
			local res = {}
			local pos = 1
			sep = sep or ','
			while true do
				local c = string.sub(line,pos,pos)
				if (c == "") then break end
				if (c == '"') then
					-- quoted value (ignore separator within)
					local txt = ""
					repeat
						local startp,endp = string.find(line,'^%b""',pos)
						txt = txt..string.sub(line,startp+1,endp-1)
						pos = endp + 1
						c = string.sub(line,pos,pos)
						if (c == '"') then txt = txt..'"' end
						-- check first char AFTER quoted string, if it is another
						-- quoted string without separator, then append it
						-- this is the way to "escape" the quote char in a quote. example:
					until (c ~= '"')
					table.insert(res,txt)
					pos = pos + 1
				else
					-- no quotes used, just look for the first separator
					local startp,endp = string.find(line,sep,pos)
					if (startp) then
						table.insert(res,string.sub(line,pos,startp-1))
						pos = endp + 1
					else
						-- no separator found -> use rest of string and terminate
						table.insert(res,string.sub(line,pos))
						break
					end
				end
			end
			return res
		end

		local groups = mymodule.list_class_groups(true)

		local headers = csv_parseline(devicelist.value.bulkdevicedata.value[1], ",")
		local reverseheaders = {}
		for i,h in ipairs(headers) do
			reverseheaders[h] = i
		end

		-- The repeat at the end of the line gives us a 'continue'-like feature by calling break
		-- see http://stackoverflow.com/a/13825260
		for i=2,#devicelist.value.bulkdevicedata.value do repeat
			-- continue if blank line
			if "" == devicelist.value.bulkdevicedata.value[i] then break end
			local values = csv_parseline(devicelist.value.bulkdevicedata.value[i], ",")

			-- Create the device
			local device = get_device(nil, true)
			for name,class in pairs(device.value.classes.value) do
				if reverseheaders[name] then
					local value = values[reverseheaders[name]]
					-- Allow the class value to be the class_id number or a unique string
					local found = false
					if tonumber(value) then
						for k,c in ipairs(class.option) do
							if tonumber(value) == tonumber(c.value) then
								found = true
								class.value = value
								break
							end
						end
					end
					if not found then
						-- Check for string match to label
						for k,c in ipairs(class.option) do
							if c.label == value then
								found = c.value
								break
							elseif string.find(c.label, format.escapemagiccharacters(value)) then
								if found then
									error("Ambiguous "..name.." class in line "..i)
								end
								found = c.value
							end
						end
						if found then
							class.value = found
						else
							error("Invalid "..name.." class in line "..i)
						end
					end
				end
			end

			device = mymodule.update_device(self, device, "Create", true, true)

			if device.errtxt then
				error("Failed to create device in line "..i.."\n"..device:print_errtxt())
			end

			-- Set the params
			local params = get_device_params(device.value.device_id.value)
			for j,h in ipairs(headers) do
				if values[j] and string.find(h, "%.") then
					local class,param = string.match(h, "^([^.]+)%.(.*)")
					if params.value[class] and params.value[class].value[param] then
						if params.value[class].value[param].type == "select" then
							-- Allow the select value to be the value or a unique substring of the label
							local found = false
							for k,o in ipairs(params.value[class].value[param].option) do
								if values[j] == o.value or values[j] == o.label then
									found = o.value
									break
								elseif values[j] ~= "" and string.find(o.label, format.escapemagiccharacters(values[j])) then
									if found then
										error("Ambiguous "..h.." value in line "..i)
									end
									found = o.value
								end
							end
							if found then
								params.value[class].value[param].value = found
							else
								error("Invalid "..h.." value in line "..i)
							end
						elseif params.value[class].value[param].type == "boolean" then
							params.value[class].value[param].value = ("true" == string.lower(values[j]))
						else
							params.value[class].value[param].value = values[j]
						end
					end
				end
			end

			params = set_device_params(params, false, true)

			if params.errtxt then
				error("Failed to create device in line "..i.."\n"..params:print_errtxt())
			end
		until true end
		if connected then databasedisconnect() end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		devicelist.errtxt = err
	end

	return devicelist
end

mymodule.get_bulk_dump_request = function(self, clientdata)
	local retval = {}
	return cfe({ type="group", value=retval, label="Dump Multiple Devices" })
end

mymodule.bulk_dump_devices = function(self, dumprequest)
	local connected
	dumprequest.value.devices = cfe({type="raw", value={}, label="devices.csv"})
	local res, err = pcall(function()
		connected = databaseconnect(true)

		local classes = {}
		local reverseclasses = {}
		local devicevalues = {}
		local columns = {}
		local reversecolumns = {}
		local devices = mymodule.list_devices(self, {})
		for i,d in ipairs(devices.value.result.value) do
			-- Capture all of the classes
			for class in pairs(d) do
				if class ~= "device_id" and not reverseclasses[class] then
					classes[#classes+1] = class
					reverseclasses[class] = #classes
				end
			end

			-- Capture all of the params
			devicevalues[i] = get_device_values(d.device_id)
			for class,value in pairs(devicevalues[i].value) do
				for param in pairs(value) do
					if not reversecolumns[class.."."..param] then
						columns[#columns+1] = class.."."..param
						reversecolumns[class.."."..param] = #columns
					end
				end
			end
		end

		if connected then databasedisconnect() end

		table.sort(classes)
		table.sort(columns)

		function csv_escape(val)
			-- Replace " with "" and surround with "'s if contains " or ,
			local v,c = string.gsub(val or "", '"', '""')
			if c>0 or string.find(v, ",") then
				return '"'..v..'"'
			else
				return v
			end
		end

		local device = {}
		for i,c in ipairs(classes) do device[#device+1] = csv_escape(c) end
		for i,p in ipairs(columns) do device[#device+1] = csv_escape(p) end
		dumprequest.value.devices.value[#dumprequest.value.devices.value+1] = table.concat(device, ",")
		for i,d in ipairs(devicevalues) do
			device = {}
			for j,c in ipairs(classes) do
				device[#device+1] = csv_escape(devices.value.result.value[i][c])
			end
			for j,p in ipairs(columns) do
				local class,param = string.match(p, "([^.]*)%.(.*)")
				if d.value[class] and d.value[class][param] ~= nil then
					device[#device+1] = csv_escape(tostring(d.value[class][param]))
				else
					device[#device+1] = ""
				end
			end

			dumprequest.value.devices.value[#dumprequest.value.devices.value+1] = table.concat(device, ",")
		end
	end)
	if not res and err then
		handlesqlexception(connected, err)
		dumprequest.errtxt = err
	end

	dumprequest.value.devices.value = table.concat(dumprequest.value.devices.value, "\n")

	return dumprequest
end

return mymodule
