local LuaConfigurationControl = require "LuaConfigurationControl"
local WrapRedis = require "wrapredis"
local cassandra = require "lua_cassandra"
local json = require "cjson"
local helpers = require "presencefilter.helpers"

module((...), package.seeall)

--a table with a metatable pointing to itself
local API = {}
API.__index = API

--map that contains filter instances, one for each app that requires it
local filterInstances = {}

local redis_client = nil

local configControl = nil

---
-- App status control table for the appReadyFilter
-- It validates the configuration received from the config server
--
app_status = {
	actualStatus = "waiting",

	CheckDependencies = function (newConfig, checkList)
		for _, dep in ipairs(checkList) do
			if newConfig[dep] == nil or newConfig:DiffState(dep) == "deleted" then
				LogError("[PresenceFilter] Dependency '%s' is not met", dep)
				app_status.actualStatus = "invalid"
				error("Unmet dependency "..dep)
			end
		end
	end,

	-- Actually this is the only function called from the appReadyFilter
	CheckStatus = function ()
		local hasChanged, newConfig = configControl:ConfigurationHasChanged()
		if not hasChanged and app_status.actualStatus ~= "invalid" then
			return app_status.actualStatus
		end

		newConfig = newConfig or app_status.current
		app_status.CheckDependencies(newConfig, {"redis", "cassandra"})

		app_status.current = newConfig

		LogInfo("[PresenceFilter] Connecting to Redis on %s:%s", newConfig["redis"]["host"], newConfig["redis"]["port"])
		
		--Configure redis
		redis_client = WrapRedis(newConfig["redis"]["host"], tonumber(newConfig["redis"]["port"]), newConfig["redis"]["pwd"])
		
		--Configure Cassandra
		for key, value, state in newConfig["cassandra"]:childs() do
			if state ~= "deleted" then
				LogInfo("Connecting to cassandra on %s:%s", value.host, tostring(value.port))
				cassandra.seed(value.host, value.port)
			end
		end

		app_status.actualStatus = "ready"
		return "ready"
	end
}

local m_vccs = helpers.Cache(function()
	LogDebug("prcesencefilter::FetchVccs")
	local ks_agent = cassandra:CassandraConnection("Agent")
	local vccs_c = ks_agent:GetRangeSlices("VCCs",  --column family
		"",  --supercolumn
		"", --startkey
		"", --endkey
		{ --predicate
			 column_names = { "SessionTimeouts" },
			 slice_range = {
				start = "",
				finish = "",
				reversed = false,
				count = 100
			}
		},
		1000 --total keys
	)
	ks_agent:Close()

	local vccs = {}
	if vccs_c then
		for vccId, vccRow in vccs_c:Iterator() do
			local vcc = {}
			for _, column in pairs(vccRow) do
				if column.name == "SessionTimeouts" and type(column.value) == "string" and column.value ~= "" then
					vcc.SessionTimeouts = json.decode(column.value)
				end
			end
			vcc.SessionTimeouts = vcc.SessionTimeouts or {}
			vcc.SessionTimeouts.WEB = vcc.SessionTimeouts.WEB or 40
			vcc.SessionTimeouts.MOBILE = vcc.SessionTimeouts.MOBILE or (3 * 60 * 60)
			vccs[vccId] = vcc
		end
	end
	return vccs
end, 180)

---
-- Updates in redis the new timestamp for a given session
--
local function update_presence_timestamp (authUser)
	local session_token = authUser.authToken
	local session_key = helpers.get_user_session_time_key(authUser)
	local new_timestamp = os.time()
	
	redis_client:zadd(session_key, new_timestamp, session_token)
	LogDebug("[PresenceFilter] Updated timestamp for key '%s' session '%s' with value '%s'", 
		session_key, session_token, new_timestamp)
end

---
-- The filter itself
--
local function Filter (self, appToFilter, options)

	local function dispatch_request (wsapi_env, appToFilter)
		-- returns status, headers and body
		if type(appToFilter) == "table" and type(appToFilter["run"]) == "function" then
			return appToFilter.run(wsapi_env)
		else
			return appToFilter(wsapi_env)
		end
	end

	return function(wsapi_env)

		local presence_updated = false
		if inconcert.authUser and inconcert.authUser.userName == "redis" and
			inconcert.authUser.virtualContactCenter == "system"
		then
			-- we don't log out redis@system, ever, so we won't register its presence
			return dispatch_request(wsapi_env, appToFilter)
		end

		if inconcert.authUser and inconcert.authUser.authToken then
			update_presence_timestamp(inconcert.authUser)
			presence_updated = true
		end

		local status, headers, body = dispatch_request(wsapi_env, appToFilter)

		-- if a user was not authenticated before but it is now, update its presence
		-- this deals with 'authentication' endpoints without needing to know the url
		--
		if not presence_updated and inconcert.authUser and inconcert.authUser.authToken then
			update_presence_timestamp(inconcert.authUser)
		end
		return status, headers, body
	end
end


--
-- Initialize config client
--
function API:InitializeConfigClient (configServerData)

	configControl = LuaConfigurationControl.Client {
		type = "PRESENCEFILTER",
		address = configServerData.ip,
		port = configServerData.port,
		node_address = configServerData.myIp,
		node_port = configServerData.myPort,
	}

	configControl:TriggerNodeReady()
end

---
-- When you are inside config.lua preparing your filters
-- you should call this method to install the filter
--
function API:makeFilter (name, options)

	local auth_filter = assert(options.auth_filter)

	return function(appToFilter)
		return Filter(self, appToFilter, options)
	end
end

---
-- Removes sessions that have been inactive.
--

local function RemoveVccTypeDanglingSessions (self, session_type, vccId, timeout)
	local session_key = helpers.get_session_time_key(session_type, vccId)
	local now = os.time()
	local session_timeout = now - timeout
	local tokens = redis_client:zrangebyscore(session_key, "-inf", session_timeout, "withscores")
	for _, item in ipairs(tokens) do
		local token, timestamp = unpack(item)
		local user = self.auth_filter:GetUserFromToken(token)
		if user then
			LogWarning( "[PresenceFilter] Logging out user session '%s' due to time out check last seen= %d", token, os.difftime(now, timestamp))
			self.on_agent_logout(user, true)
			self.auth_filter:RevokeSession(token)
		end
		redis_client:zrem(session_key, token)
	end
end

local function RemoveVccDanglingSessions (self, vccId, vcc)
	for session_type, timeout in pairs(vcc.SessionTimeouts) do
		RemoveVccTypeDanglingSessions(self, session_type, vccId, timeout)
	end
end

local function RemoveDanglingSessions (self, wsapi_env)
	assert(type(wsapi_env) == "table")
	assert(self.auth_filter)

	LogInfo("[PresenceFilter] RemoveDanglingSessions - begin")

	local vccs = m_vccs.get()
	for vccId, vcc in pairs(vccs) do
		RemoveVccDanglingSessions(self, vccId, vcc)
	end

	LogInfo("[PresenceFilter] RemoveDanglingSessions - done")

	return 200, { ["Content-Type"] = "text/plain" }, coroutine.wrap(function()
						coroutine.yield("ok")
					end)
end



---
--
function API:makeCleanupHandler (name, options)

	self.auth_filter = assert(options.auth_filter)
	self.on_agent_logout = assert(options.on_agent_logout)

	return function (wsapi_env)
		return RemoveDanglingSessions(self, wsapi_env)
	end
end



getmetatable(_M).__call = function(_M, app_name)
	assert(type(app_name) == "string", "missing parameter app_name")
	app_name = app_name:upper()
	if filterInstances[app_name] then
		return filterInstances[app_name]
	end

	LogInfo("[PresenceFilter] Creating instance for app %s", app_name)

	-- creates a new table and assign the API metatable
	local new = setmetatable({ app_name = app_name }, API)

	filterInstances[app_name] = new
	return new
end
