---------------------------------------------------------------------
-- SF Permissions management
---------------------------------------------------------------------

SF.Permissions = {}

local P = SF.Permissions
P.privileges = {}
P.providers = {}
P.settings = setmetatable({},{__index = function(t,k) local r={} t[k]=r return r end})
P.filename = SERVER and "sf_perms_sv.txt" or "sf_perms_cl.txt"

do -- Delete old settings due to setting issues with new defaults
	local t = file.Time(P.filename, "DATA")
	if t>0 and t<1748452637 then file.Delete(P.filename) end
end

local Privilege = {
	__index = {
		buildcheck = function(self)
			local checks = {}
			local allAllow = true
			local anyBlock = false
			local overridable = false
			for providerid in pairs(self.providerconfig) do
				local provider = P.providers[providerid]
				if provider then
					local check = provider.checks[P.settings[self.id][providerid]]
					if provider.overridable then overridable = true end
					if check == "block" then
						if overridable then
							check = function() return false, "This function's permission is blocked!" end
						else
							allAllow = false
							anyBlock = true
							break
						end
					end
					if check ~= "allow" then
						allAllow = false
						checks[#checks+1] = check
					end
				end
			end

			if allAllow then
				self.check = function() return true end
			elseif anyBlock then
				self.check = function() return false, "This function's permission is blocked!" end
			elseif #checks==0 then
				allAllow = true
				self.check = function() return true end
			elseif #checks==1 then
				self.check = checks[1]
			else
				self.check = function(instance, target)
					for k, v in ipairs(checks) do
						local ok, reason = v(instance, target)
						if not ok then return false, reason end
					end
					return true
				end
			end

			if overridable and not allAllow then
				local check = self.check
				self.check = function(instance, target)
					if instance.permissionOverrides[self.id] then return true end
					return check(instance, target)
				end
			end
			self.overridable = overridable

		end,
		applySetting = function(self, providerid, setting)
			P.settings[self.id][providerid] = setting
			P.savePermissions()
			self:buildcheck()
		end,
		initSettings = function(self)
			local saveSettings = false
			for providerid, config in pairs(self.providerconfig) do
				local provider = P.providers[providerid]
				if provider then
					local setting = P.settings[self.id][providerid]
					if not setting or not provider.checks[setting] then
						P.settings[self.id][providerid] = config.default or provider.defaultsetting
						saveSettings = true
					end
				end
			end
			self:buildcheck()
			return saveSettings
		end,
	},
	__call = function(p, id, name, description, providerconfig)
		if not providerconfig then providerconfig = {} end
		if not providerconfig.usergroups then providerconfig.usergroups = {} end

		return setmetatable({
			id = id,
			name = name,
			description = description,
			providerconfig = providerconfig,
			check = function() error("Check function isn't set! id="..id.." name="..name) end
		}, p)
	end
}
setmetatable(Privilege, Privilege)

function P.registerProvider(provider)
	if P.providers[provider.id] and not SF.ReloadingLibrary then ErrorNoHaltWithStack("Registering same provider more than once! "..provider.name) end
	P.providers[provider.id] = provider
end

function P.registerPrivilege(id, name, description, providerconfig)
	if P.privileges[id] and not SF.ReloadingLibrary then ErrorNoHaltWithStack("Registering same privilege more than once! "..name) end
	P.privileges[id] = Privilege(id, name, description, providerconfig)
end

function P.check(instance, target, key)
	local ok, reason = P.privileges[key].check(instance, target)
	if not ok then SF.Throw("Permission " .. key .. ": " .. reason, 3) end
end

function P.hasAccess(instance, target, key)
	return P.privileges[key].check(instance, target)
end

function P.refreshSettingsCache()
	for providerid, provider in pairs(P.providers) do
		local settings = {}
		for privilegeid, privilege in pairs(P.privileges) do
			if privilege.providerconfig[providerid] then -- Check if this current provider manages privilege
				settings[privilegeid] = { privilege.name, privilege.description, P.settings[privilegeid][providerid] }
			end
		end
		provider.settings = settings
	end
end

local invalidators = {}

local printC = function(...) (SERVER and MsgC or chat.AddText)(Color(255, 255, 255), "[", Color(11, 147, 234), "Starfall", Color(255, 255, 255), "]: ", ...) if SERVER then MsgC("\n") end end

function P.savePermissions()
	local meta = getmetatable(P.settings)
	setmetatable(P.settings, nil)
	file.Write(P.filename, util.TableToJSON(P.settings, true))
	setmetatable(P.settings, meta)
end

-- Load the permission settings for each provider
function P.loadPermissions()
	if not pcall(P.loadPermissionsSafe) then
		-- Errored, try deleting the old config and trying again
		file.Delete(P.filename, "DATA")
		pcall(P.loadPermissionsSafe)
	end
end
function P.loadPermissionsSafe()
	local saveSettings = not file.Exists(P.filename, "DATA")
	P.settings = setmetatable(util.JSONToTable(file.Read(P.filename) or "") or {}, getmetatable(P.settings))

	local version = tonumber(P.settings.version) or 1
	while invalidators[version] do
		local issue = invalidators[version]
		if issue.realm then
			local changed = false
			for _, v in ipairs(issue.invalidate) do
				if issue.check == nil or issue.check(P.settings[v]) then
					P.settings[v] = nil
					changed = true
				end
			end
			if changed then
				printC("Your configuration has been modified due to a misconfiguration.")
				printC("Reason: " .. issue.message)
				printC("Changes: " .. table.concat(issue.invalidate, ", "))
			end
		end
		version = version + 1
	end
	if version ~= P.settings.version then
		P.settings.version = version
		saveSettings = true
	end

	for k, v in pairs(P.privileges) do
		if v:initSettings() then saveSettings = true end
	end

	if saveSettings then
		P.savePermissions()
	end
end

-- Find and include all provider files.
function P.includePermissions()
	local sv_dir = "starfall/permissions/providers_sv/"
	local sv_files = file.Find(sv_dir.."*.lua", "LUA")
	local sh_dir = "starfall/permissions/providers_sh/"
	local sh_files = file.Find(sh_dir.."*.lua", "LUA")
	local cl_dir = "starfall/permissions/providers_cl/"
	local cl_files = file.Find(cl_dir.."*.lua", "LUA")

	if SERVER then
		for _, file in pairs(sv_files) do
			include(sv_dir..file)
		end
		for _, file in pairs(sh_files) do
			AddCSLuaFile(sh_dir..file)
			include(sh_dir..file)
		end
		for _, file in pairs(cl_files) do
			AddCSLuaFile(cl_dir..file)
		end
	else
		for _, file in pairs(sh_files) do
			include(sh_dir..file)
		end
		for _, file in pairs(cl_files) do
			include(cl_dir..file)
		end
	end
end

local function changePermission(print, arg)
	if arg[1] then
		local privilege = P.privileges[arg[1]]
		if privilege then
			if arg[2] and privilege.providerconfig[arg[2]] then
				local val = tonumber(arg[3])
				if val and val>=1 and val<=#P.providers[arg[2]].settingsoptions then
					privilege:applySetting(arg[2], math.floor(val))
				else
					print("The setting's value is out of bounds or not a number.\n")
				end
			else
				print("Permission, " .. tostring(arg[2]) .. ", couldn't be found.\nHere's a list of permissions.\n")
				for id, _ in pairs(privilege.providerconfig) do print(id.."\n") end
			end
		else
			print("Privilege, " .. tostring(arg[1]) .. ", couldn't be found.\nHere's a list of privileges.\n")
			for id, _ in SortedPairs(P.privileges) do print(id.."\n") end
		end
	else
		print("Usage: sf_permission <privilege> <permission> <value>.\n")
	end
end

local function printfunc(ply)
	return ply:IsValid() and function(msg) ply:PrintMessage(HUD_PRINTCONSOLE, msg) end or Msg
end

-- Console commands for changing permissions.
if SERVER then
	concommand.Add("sf_permission", function(ply, com, arg)
		if ply:IsValid() and not ply:IsSuperAdmin() then return end
		changePermission(printfunc(ply), arg)
	end)
else
	concommand.Add("sf_permission_cl", function(ply, com, arg)
		changePermission(printfunc(ply), arg)
	end)
end

-- Networking for administrators to get the server's settings
if SERVER then
	util.AddNetworkString("sf_permissionsettings")
	net.Receive("sf_permissionsettings", function(len, ply)
		if ply:IsSuperAdmin() then
			P.refreshSettingsCache () -- Refresh cache first
			net.Start("sf_permissionsettings")

			net.WriteUInt(table.Count(P.providers), 8)
			for _, v in pairs(P.providers) do
				net.WriteString(v.id)
				net.WriteString(v.name)
				net.WriteUInt(#v.settingsoptions, 8)
				for _, option in pairs(v.settingsoptions) do
					net.WriteString(option)
				end
				net.WriteUInt(table.Count(v.settings), 8)
				for privid, setting in pairs(v.settings) do
					net.WriteString(privid)
					net.WriteString(setting[1])
					net.WriteString(setting[2])
					net.WriteUInt(setting[3], 8)
				end
			end

			net.Send(ply)
		end
	end)
else
	local reqCallback, reqTimeout
	function P.requestPermissions(callback)
		if not reqCallback or (reqTimeout and reqTimeout<CurTime()) then
			reqCallback = callback
			reqTimeout = CurTime() + 2
			net.Start("sf_permissionsettings")
			net.SendToServer()
		end
	end
	function P.permissionRequestSatisfied( instance )
		if not instance.permissionRequest then
			SF.Throw( 'There is no permission request' )
		end
		for id, _ in pairs( instance.permissionRequest.overrides ) do
			if not instance.permissionOverrides[ id ] then return false end
		end
		return true
	end
	net.Receive("sf_permissionsettings", function()
		if reqCallback then
			local providers = {}
			local nproviders = net.ReadUInt(8)
			for i = 1, nproviders do
				local provider = {
					id = net.ReadString(),
					name = net.ReadString(),
					settings = {},
					settingsoptions = {}
				}
				local noptions = net.ReadUInt(8)
				for j = 1, noptions do
					provider.settingsoptions[j] = net.ReadString()
				end
				local nsettings = net.ReadUInt(8)
				for j = 1, nsettings do
					provider.settings[net.ReadString()] = { net.ReadString(), net.ReadString(), net.ReadUInt(8) }
				end
				providers[provider.id] = provider
			end
			reqCallback(providers)
			reqCallback = nil
			reqTimeout = nil
		end
	end)
end
