require "LuaOrm.Date"
local json = require "cjson"
local utils = require "rasa.utils"

module(..., package.seeall)

--[[
	Flag utilizado para el desarrollador o por si NO quiere que se ejecuten las actualizaciones en los Nodos.
]]--
local m_enabledListener = true
-- instancia del API DE RASA
local m_instanceOwner = nil
-- Las claves de RASA en REDIS
local REDIS_KEY_PUBLISH = "RasaPublish"

----------------------------------------

local function GetAppsModule ()
	local appsModule = require "rasa.v2_0.apps"
	return appsModule
end

local function GetAllowedPublishActions ()
	local allowedPublications = {
		["NEW APP"] = {
			executeAction = function(appInfo)
				return GetAppsModule().CreateAppFileSystemStructure(appInfo)
			end,
			name = "New App"
		},
		["REMOVED APP"] = {
			executeAction = function(appInfo)
				return GetAppsModule().DeleteAppFileSystemStructure(appInfo)
			end,
			name = "Removed App"
		},
		["TRAINED APP"] = {
			executeAction = function(appInfo)
				return GetAppsModule().CreateTrainAppFileSystem(appInfo)
			end,
			name = "Trained App"
		}
	}
	return allowedPublications
end

local function Publish (info)
	assert(type(info) == "table")
	assert(info.key)

	local redis = m_instanceOwner.GetRedis()
	if not redis then
		LogWarning("[RASA_PUBLISH:Publish] Unable to Publish Info '%s'. Instance of REDIS does not exist.", json.encode(info))
		return false
	end

	info.timestamp = LuaOrm.Date:time()
	local key = info.key
	local encodedInfo = json.encode(info)

	--LogDebug("[RASA_PUBLISH:Publish] ---> key: %s', encodedInfo %s", key, encodedInfo)

	redis:hset(REDIS_KEY_PUBLISH, key, encodedInfo)

	return true
end

local function RemovePublish (info)
	assert(type(info) == "table")
	assert(info.key)

	local redis = m_instanceOwner.GetRedis()
	if not redis then
		LogWarning("[RASA_PUBLISH:RemovePublish] Unable to Remove Publish Info '%s'. Instance of REDIS does not exist.", json.encode(info))
		return false
	end

	local key = info.key
	--LogDebug("[RASA_PUBLISH:RemovePublish] ---> deleting key: '%s'.", key)
	redis:hdel(REDIS_KEY_PUBLISH, key)

	return true
end

local function CompareIPs (sourceIP, destinationNodeIP)
	local resp = {
		result = nil,
		success = nil
	}

	if not destinationNodeIP then
		LogError("[RASA_PUBLISH:CompareIPs] 1-  Does Not exist your Destination Node IP.")
		resp.result = "NODE IP DESTINATION DOES NOT EXIST"
		resp.success = false
		return resp
	end

	if not sourceIP then
		LogError("[RASA_PUBLISH:CompareIPs] Does Not exist your Source IP.")
		resp.result = "NODE SOURCE IP DOES NOT EXIST"
		resp.success = false
		return resp
	end

	if sourceIP:lower() == destinationNodeIP:lower() then
		--LogDebug("[RASA_PUBLISH:CompareIPs] 3- The Nodes IP addresses are the same. IP: '%s'.", sourceIP)
		resp.result = "NODES IP ADDRESSES ARE EQUALS"
		resp.success = true
		return resp
	end

	--LogDebug("[RASA_PUBLISH:CompareIPs] 4-  The Nodes IP addresses are the differents. SourceIP: '%s', DestinationNodeIP: '%s'.", sourceIP, destinationNodeIP)
	resp.result = "NODES IP ADDRESSES ARE DIFFERENTS"
	resp.success = true

	return resp
end

local function PublishAppActionForNodes (info)
	assert(info)
	assert(info.id)
	assert(info.vcc)
	assert(info.action)
	--assert(info.appData)

	LogInfo("[RASA_PUBLISH:PublishAppActionForNodes] Publishing App Action All Nodes. Info: '%s'.", json.encode(info))

	local allowedPublications = GetAllowedPublishActions()
	local allowedPubl = allowedPublications[info.action]
	if not allowedPubl then
		LogError("[RASA_PUBLISH:PublishAppActionForNodes] RASA Action '%s' not allowed. Unable to Publish Action on the Node. Info '%s'.", info.action, json.encode(info))
		return false
	end

	local redis = m_instanceOwner.GetRedis()
	if not redis then
		LogWarning("[RASA_PUBLISH:PublishAppActionForNodes] Unable to Add Publish Info '%s'. Instance of REDIS does not exist.", json.encode(info))
		return false
	end
	
	local nodes = m_instanceOwner.GetNodes()
	local allNodes = nodes and (nodes.nodes or {}) or {}
	local totalNodes = #allNodes
	--LogDebug("[RASA_PUBLISH:PublishAppActionForNodes] --> Total Nodes %s --> nodes: %s", totalNodes, json.encode(nodes or {}))

	if totalNodes <= 1 then
		LogInfo("[RASA_PUBLISH:PublishAppActionForNodes] Unable to Publish App Action. Total Nodes %s. Info: %s", totalNodes, json.encode(info))
		return false
	end

	local myWebhandlerIP = m_instanceOwner.GetWebHandlerServerIP()
	local function MustAdd (destinationNodeIP)
		local resp = CompareIPs(myWebhandlerIP, destinationNodeIP)
		if resp.result ~= "NODES IP ADDRESSES ARE DIFFERENTS" then
			LogError("[RASA_PUBLISH:PublishAppActionForNodes.MustAdd] 1-  Unable to add publish. Err: %s.", resp.result)
			return false
		end
		--LogDebug("[RASA_PUBLISH:PublishAppActionForNodes.MustAdd] 2-  Can add publish.")
		return true
	end

	for _, node in pairs(allNodes) do
		local nodeIP = node.IP or ""
		local nodePort = node.port or ""

		local mustAdd = MustAdd(nodeIP)
		if mustAdd then
			local key = ("%s_%s"):format(utils.MakeAppId(info.vcc, info.id), nodeIP)
			local newApp = {
				key      = key,
				appId    = info.id,
				vcc      = info.vcc,
				nodeIP   = nodeIP,
				nodePort = nodePort,
				appData  = info.appData, -- solo utilizado para la accion "REMOVED APP"
				action   = info.action -- "NEW APP" o "REMOVED APP" o "TRAINED APP"
			}
			Publish(newApp)
		end
	end

	return true
end

---- Métodos públicos del API ---
--

function SetOwner (instanceOwner)
	m_instanceOwner = instanceOwner
end

function PublishNewApp (info)
	assert(info)
	assert(info.id)
	assert(info.vcc)
	LogInfo("[RASA_PUBLISH:PublishNewApp] Publishing New App '%s' vcc: '%s'.", info.id, info.vcc)
--	LogDebug("[RASA_PUBLISH:PublishNewApp] Publishing New App Info '%s'.", json.encode(info))
	info.action = "NEW APP"
	return PublishAppActionForNodes(info)
end

function PublishRemovedApp (info)
	assert(info)
	assert(info.id)
	assert(info.vcc)
	LogInfo("[RASA_PUBLISH:PublishRemovedApp] Publishing Removed App '%s' vcc: '%s'.", info.id, info.vcc)
--	LogDebug("[RASA_PUBLISH:PublishRemovedApp] Publishing Removed App Info '%s'.", json.encode(info))
	info.action = "REMOVED APP"
	return PublishAppActionForNodes(info)
end

function PublishTrainedApp (info)
	assert(info)
	assert(info.id)
	assert(info.vcc)
	LogInfo("[RASA_PUBLISH:PublishTrainedApp] Publishing Trained App '%s' vcc: '%s'.", info.id, info.vcc)
--	LogDebug("[RASA_PUBLISH:PublishTrainedApp] Publishing Trained App Info '%s'.", json.encode(info))
	info.action = "TRAINED APP"
	return PublishAppActionForNodes(info)
end

function GetAppPublication (info)
	assert(info)
	assert(info.id)
	assert(info.vcc)

	local keyAppPart = ("%s_"):format(utils.MakeAppId(info.vcc, info.id))
	local redis = m_instanceOwner.GetRedis()
	local appPublications = redis:hgetall(REDIS_KEY_PUBLISH)

	--LogDebug("[RASA_PUBLISH:GetAppPublication] --> appPublications: %s", #appPublications)
	local publications = {}
 
	for key, encodedPublication in pairs(appPublications or {}) do
		if key:match(keyAppPart) then
			--LogDebug("[RASA_PUBLISH:GetAppPublication] ----> _:'%s' key: '%s' - encodedPublication: %s", _, key, encodedPublication)
			local publication = utils.JSON_Decode(encodedPublication)
			if publication then
				publications[publication.key] = publication
			else
				LogError("[RASA_PUBLISH:GetAppPublication] Unable to decode key '%s'.", key)
			end
		end
	end

	--LogDebug("[RASA_PUBLISH:GetAppPublication] key: '%s' ----> publications %s", key, json.encode(publications))

	return true, publications
end

function LoadPublications ()
	local redis = m_instanceOwner.GetRedis()
	local allPublications = redis:hgetall(REDIS_KEY_PUBLISH)

	--LogDebug("[RASA_PUBLISH:LoadPublications] --> allPublications: %s", #allPublications)
	local publications = {}
 
	for key, encodedPublication in pairs(allPublications or {}) do
		--LogDebug("[RASA_PUBLISH:LoadPublications] ----> _:'%s' key: '%s' - encodedPublication: %s", _, key, encodedPublication)
		local publication = utils.JSON_Decode(encodedPublication)
		if publication then
			publications[publication.key] = publication
		else
			LogError("[RASA_PUBLISH:LoadPublications] Unable to decode key '%s'.", key)
		end
	end

	--LogDebug("[RASA_PUBLISH:LoadPublications] key: '%s' ----> publications %s", REDIS_KEY_PUBLISH, json.encode(publications))

	return true, publications
end

function ListenerPublications ()
	if not m_enabledListener then
		LogWarning("[RASA_PUBLISH:ListenerPublications] ---> Multi-Node UPDATE IS INACTIVE <-----")
		return false, {}
	end
	local errors = {}
	local _, publications = LoadPublications()
	local allowedPublications = GetAllowedPublishActions()

	local myWebhandlerIP = m_instanceOwner.GetWebHandlerServerIP()
	local function MustExecute (destinationNodeIP)
		local resp = CompareIPs(myWebhandlerIP, destinationNodeIP)
		if resp.result == "NODES IP ADDRESSES ARE EQUALS" then
			--LogDebug("[RASA_PUBLISH:ListenerPublications.MustExecute] 1- Can EXECUTE publish action.")
			return true
		end
		--LogDebug("[RASA_PUBLISH:ListenerPublications.MustExecute] 2- Unable to EXECUTE publish action. Err: %s.", resp.result)
		return false
	end

	for pubilshId, publish in pairs(publications) do
		local allowedPubl = allowedPublications[publish.action]
		if not allowedPubl then
			LogError("[RASA_PUBLISH:ListenerPublications] RASA Action '%s' not allowed. Unable to Synchronizing the Structure on the Node. AppInfo '%s'.", publish.action, json.encode(info))
		else
			local mustExecute = MustExecute(publish.nodeIP)
			if mustExecute then
				local appInfo = {
					appId   = publish.appId,
					vcc     = publish.vcc,
					appData = publish.appData
				}
				local success, response = allowedPubl.executeAction(appInfo)
				if success then
					LogInfo("[RASA_PUBLISH:ListenerPublications] Successfully Synchronized '%s' of Structure in the Node. AppId: '%s' Vcc: '%s'", publish.action, appInfo.appId, appInfo.vcc)
					-- Remuevo la publicacion de redis
					RemovePublish(publish)
				else
					local err = response
					table.insert(errors, err)
					LogError("[RASA_PUBLISH:ListenerPublications] Error Synchronizing the '%s' of the Structure on the Node. AppInfo '%s'. Error: '%s'", publish.action, json.encode(info), json.encode(err))
					-- TODO: Validar si tiene que remover la publicacion o no.
					--   Ej: si creo una nueva APP y el directorio ya existia (no deberia pasar) en este caso la publicacion debe ser eliminada. Quizas tener casos de errores conocidos. Revisar.
					RemovePublish(publish)
				end
			--else
				-- TODO: ALGUN LOG ????
			end
		end

		--RemovePublish(publish)
	end

	success = #errors == 0
	return success, errors
end
