local json = require "cjson"
require "LuaOrm.Date"
require "rasa.logger" -- require utilizado para pm-cli/provisioningmanager
local utils = require "rasa.utils"
local publish = require "rasa.publish"
local apiLuis = require("luis"):new()
--------------------
-- version API 2.0
local apps2_0 = require "rasa.v2_0.apps"
local intents2_0 = require "rasa.v2_0.intents"
local runtime2_0 = require "rasa.v2_0.runtime"
local entities2_0 = require "rasa.v2_0.entities"
--------------------

apiLuis.EnableLogger(true)
local rasaModels = require("rasa.models"):GetModels()

----
local m_apiInstances = nil

module(..., package.seeall)

--------------------------------------
-- REDIS
local m_redis = nil
function SetRedis (redis)
	--LogDebug("[RASA_API:SetRedis] Set Redis '%s' to RASA.' ", redis)
	if m_redis then
		return
	end
	m_redis = redis
end
function GetRedis ()
	return m_redis
end

--------------------------------------
-- Server Config (INFORMACION DEL WEB HANDLER)
local m_serverConfig = nil
function SetServerConfig (serverConfig)
	--LogDebug("[RASA_API:SetServerConfig] Set Webhandler Server Config '%s' to RASA.' ", json.encode(serverConfig or {}))
	if m_serverConfig then
		return
	end
	m_serverConfig = serverConfig
end
function GetServerConfig ()
	return m_serverConfig
end
function GetWebHandlerServerConfig ()
	local config = GetServerConfig()
	if not config then
		return
	end
	return config.webhandler
end
function GetWebHandlerServerIP ()
	local webhandlerConfig = GetWebHandlerServerConfig()
	if not webhandlerConfig then
		return
	end
	return webhandlerConfig.IP
end

--------------------------------------
-- NODES (SERVIDORES DE LA INSTALACION)
local m_nodes = nil
function SetNodes (nodes)
	--LogDebug("[RASA_API:SetNodes] Set Nodes of Webhandlers Servers Config '%s' to RASA.' ", json.encode(nodes or {}))
	if m_nodes then
		return
	end
	m_nodes = nodes
end
function GetNodes ()
	--LogDebug("[RASA_API:GetNodes] Load Nodes of Webhandlers Servers Config. Nodes: %s' ", json.encode(m_nodes or {}))
	return m_nodes
end

-------------------------------------
-- DEFAULT APP VERSION
-------------------------------------
local m_defaultAppVersion = "0.1"

--[[ --------------------------------------
	API - VERSION
--------------------------------------]]--
local API_LAST_VERSION
local ALLOWED_API_VERSIONS = {
	["2.0"] = {last = true}
}
for apiVersion, info in pairs(ALLOWED_API_VERSIONS) do
	if info.last then
		API_LAST_VERSION = apiVersion
		break
	end
end
assert(API_LAST_VERSION)

local function InitApiVersionsModules (apiVersion)
	if apiVersion == "2.0" then
		_M.m_apiInstances[apiVersion].apps     = apps2_0
		_M.m_apiInstances[apiVersion].entities = entities2_0
		_M.m_apiInstances[apiVersion].intents  = intents2_0
		_M.m_apiInstances[apiVersion].runtime  = runtime2_0
	else
		assert(false, "Unrecognized RASA API version " .. tostring(apiVersion))
	end

	for name, module in pairs(_M.m_apiInstances[apiVersion]) do
		--print(tostring(name) .. " = " .. tostring(module))
		if type(module) == "table" and module.SetOwner then
			module.SetOwner(_M.m_apiInstances[apiVersion])
		end
	end

	utils.SetOwner(_M)
	publish.SetOwner(_M)
end

function EnableRasaLogger (enabled)
	-- Funcion "SetEnabledLogger" existente en rasa.logger
	print("SetEnabledRasaLogger " .. tostring(SetEnabledRasaLogger) .. " enabled " .. tostring(enabled))
	if type(SetEnabledRasaLogger) == "function" then
		SetEnabledRasaLogger(enabled)
	end
end

function MakeTaskVersion (versionId)
	--LogDebug("[RASA_API:MakeTaskVersion] Make Task Version. versionId '%s', m_defaultAppVersion: '%s'.", versionId, m_defaultAppVersion)
	return (versionId or ""):len() > 0 and versionId or m_defaultAppVersion
end

-- *******************************************************************************
-- API APPS
-- *******************************************************************************

--[[
]]--
function GetAssignedAzureAccountsResourceInApp (self, appId, info)
	assert(type(appId) == "string")
	assert(type(info) == "table")
	assert(type(info.tenant_id) == "string")
	assert(type(info.client_id) == "string")
	assert(type(info.client_secret) == "string")
	assert(type(info.resource) == "string")
	return self.apps.GetAssignedAzureAccountsResource(appId, info)
end

local function SetAppPublications (app)
	assert(app)
	local success, publications = publish.GetAppPublication(app)
	if success then
		local arrayPublications = {}
		for _, publication in pairs(publications or {}) do
			table.insert(arrayPublications, publication)
		end
		app.publications = arrayPublications
	end
end

--[[
]]--
function LoadApps (self, vcc, noIncludePublications)
	assert(type(vcc) == "string")

	local keyspace = utils.GetAgentKeySpace()
	local list = rasaModels.VCCs.GetApplications(vcc, keyspace)
	local total = utils.GetRowCount(list)
	--LogDebug("[RASA_API:LoadApps] Load Apps of vcc '%s' Total: '%s', Apps: %s", vcc, total, json.encode(list))
	local nowTime = LuaOrm.Date:time()

	if total > 0 then
		for _, app in pairs(list) do
			-- Se valida si la App se encuentra en estado Running y si ha sobrepasado el tiempo permitido.
			-- Si sobrepasó el tiempo, en esta misma función marca como fail.
			self.apps.ValidateRunningTrainTimeout(app, nowTime, keyspace)
			-- Las publicaciones de Redis hacia otros nodos.
			if not noIncludePublications then
				SetAppPublications(app)
			end
		end
	end
	keyspace:Close()

	--LogDebug("[RASA_API:LoadApps] Load Apps of vcc '%s' Total: '%s', Apps: %s", vcc, total, json.encode(list))
	return list, total
end

--[[_
]]--
function GetApp (self, appId, vcc, noIncludePublications)
	assert(type(appId) == "string")
	assert(type(vcc) == "string")
	local info = {
		id = appId,
		vcc = vcc
	}

	local success, encodedApp, app = self.apps.Get(info)
	if not success then
		return false, encodedApp, app
	end

	if not noIncludePublications then
		SetAppPublications(app)
		encodedApp = json.encode(app)
	end

	return true, encodedApp, app
end

-- Queda por compatibilidad con LUIS
GetAppFromDataBase = GetApp

--[[_
]]--
function GetAppByImportation (self, importedAppId, vcc, noIncludePublications)
	assert(type(importedAppId) == "string")
	assert(type(vcc) == "string")

	importedAppId = importedAppId:lower()
	local vccApps, total = self:LoadApps(vcc, noIncludePublications)
	if total == 0 then
		return false
	end
	for vccAppId, vccApp in pairs(vccApps) do
		if vccApp.sourceImportedApp and vccApp.sourceImportedApp.exportedAppId:lower() == importedAppId then
			return vccApp
		end
	end
	return false
end

--[[
	Al nombre de la "App" le concatena el VCC y el ClusterId.
	Ej:
		Recibe : ElNombre
		Retorna: ElNombre@ElVcc - i6BetaCaroteno
--]]
local function MakeAppNameForVcc (name, vcc)
	if not name then
		return
	end
	local newName = name
	-- Le agrego el VCC al Name
	if vcc then
		newName =  newName .. "@" .. vcc
	end
	-- Le agrego el ClusterId
	local clusterId = os.getenv("CLUSTER_ID") or "NONE"
	if clusterId then
		newName =  newName .. " - " .. clusterId
	end
	return newName
end

--[[
	A la descripción de la "App" le concatena la Versión del Build de i6 que se encuentra instalado.
	Ej:
		Recibe : LaDescripcion
		Retorna: LaDescripcion - 6.0.1674+MCWJ-1674
--]]
local function MakeAppDescriptionForVcc (description)
	description = description or ""
	local newDescription = description
	-- Le agrego la Version del Build de i6
	local iccVersion = os.getenv("ICC_VERSION") or "NONE"
	if iccVersion then
		newDescription =  newDescription .. " - " .. iccVersion
	end
	return newDescription
end

--[[
]]--
function LoadUnimportedLuisApps (self, vcc)
	assert(vcc)
	LogInfo("[RASA_API:LoadUnimportedLuisApps] Loading Unimported LUIS Apps, VCC: '%s' to RASA.' ", vcc)

	local luisApps, totalLuisApps = apiLuis.LoadApps(vcc)
	--print(totalLuisApps)
	if totalLuisApps == 0 then
		local err = {vcc = vcc, type = "UNABLE TO RETRIEVE UNIMPORTED LUIS APPS TO RASA - LUIS HAS NOT CONFIGURED APPS IN VCC"}
		LogInfo("[RASA_API:LoadUnimportedLuisApps] Unable to retrieve Unimported LUIS Apps to RASA - LUIS has not configured apps in VCC '%s'.", vcc)
		return {}, 0
	end

	local rasaApps, totalRasaApps = self:LoadApps(vcc)
	if totalRasaApps == 0 then
		return luisApps, totalLuisApps
	end

	local currentImportedApps = {}
	for _, rasaApp in pairs(rasaApps) do
		if rasaApp.sourceImportedApp and rasaApp.sourceImportedApp.exportedAppId then
			currentImportedApps[rasaApp.sourceImportedApp.exportedAppId:lower()] = true
		end
	end
	--print(json.encode(currentImportedApps))
	LogDebug("[RASA_API:LoadUnimportedLuisApps] Current LUIS Imported Apps to RASA. VCC: '%s': list: %s", vcc, json.encode(currentImportedApps))

	local unimportedLuisApps = {}

	for luisAppId, luisApp in pairs(luisApps) do
		luisAppId = luisAppId:lower()
		print(luisAppId)
		if not currentImportedApps[luisAppId] then
			table.insert(unimportedLuisApps, luisApp)
		end
	end

	local totalUnimportedLuisApps = #unimportedLuisApps
	--print(totalUnimportedLuisApps)

	LogInfo("[RASA_API:LoadUnimportedLuisApps] Total %d Unimported LUIS Apps to RASA. VCC: '%s'.", totalUnimportedLuisApps, vcc)

	return unimportedLuisApps, totalUnimportedLuisApps
end

--[[
]]--
function Import (self, appToImport, vcc)
	assert(type(vcc) == "string")
	assert(type(appToImport) == "table")
	assert(type(appToImport.inconcertAppInfo) == "table")
	assert(type(appToImport.inconcertAppInfo.id) == "string")
	assert(type(appToImport.inconcertAppInfo.name) == "string")
	assert(type(appToImport.inconcertAppInfo.description) == "string")
	assert(type(appToImport.inconcertAppInfo.culture) == "string")

	local startTime = os.time()
	local exportedAppId = appToImport.inconcertAppInfo.id
	LogInfo("[RASA_API:Import] 1 - + START Import App -Id '%s' Name: '%s'.", exportedAppId, appToImport.inconcertAppInfo.name)
	--LogDebug("[RASA_API:Import] --> START Import App Info '%s' ", json.encode(appToImport))

	local function MakeSourceApp ()
		local sourceApp = {exportedAppId = exportedAppId}
		for name, value in pairs(appToImport) do
			if type(value) == "table" then
				sourceApp[name] = #value
			else
				sourceApp[name] = value
			end
		end
		return sourceApp
	end

	-- 1 - Importa la App
	local appId = utils.SanitizeAppId(exportedAppId)
	local appInfo = {
		vcc = vcc,
		name = appToImport.inconcertAppInfo.name,
		description = appToImport.inconcertAppInfo.description,
		culture = appToImport.culture:sub(1, 2), -- Tengo que obtener los 2 primeros caracteres para RASA, ya que desde LUIS llegan 5 caracteres (Ej: es-mx desde LUIS)
		imported = true,
		sourceImportedApp = MakeSourceApp()
	}

	local success, response = self:AddApp(appInfo, vcc)
	if not success then
		LogInfo("[RASA_API:Import] - Unable to Import App '%s'.", appToImport.name, elapsedTime)
		return false, response
	end

	--[[
		Función que sanitiza las entidades Jerárquicas, se queda con la Hoja
			Ej: 
				Source: EntidadPadre::EntidadHija
				Target: EntidadHija
	]]--
	local function MakeEntityChildName (childName)
		assert(type(childName) == "string")
		local newChildName
		local startPos, endPos = childName:find("::")
		if startPos then
			newChildName = string.sub(childName, endPos + 1)
		else
			newChildName = childName
		end
		--LogDebug("[RASA_API:Import] MakeEntityChildName -------> childName: '%s', newChildName: '%s'", childName, newChildName)
		return newChildName
	end

	-- 2 - Importa las Entities
	local function CreateEntities (entities, treeLevel, luisEntityType)
		if not entities then
			return
		end

		treeLevel = treeLevel + 1
		for _, entity in ipairs(entities) do
			--LogInfo("[RASA_API:Import] 2.1 -------> entity: %s", json.encode(entity))
			local entityInfo = utils.DeepCopy(entity)
			entityInfo.vcc = vcc
			entityInfo.appId = appId
			entityInfo.typeId = 1 -- SIMPLE
			entityInfo.treeLevel = treeLevel
			entityInfo.hasChildren = #(entity.children or {}) > 0
			local _, _1 = self:CreateAppEntity(entityInfo)

			if entityInfo.hasChildren then
				for _, child in ipairs(entity.children) do
					local childEntity
					local childName = MakeEntityChildName(type(child) == "table" and child.name or child)

					if type(child) == "string" then
						--LogDebug("[RASA_API:Import] 2.1.1 -------> entity string child: %s", child)
						childEntity = utils.DeepCopy(entityInfo)
						childEntity.name        = childName
						childEntity.hasChildren = false
					else
						--LogDebug("[RASA_API:Import] 2.1.2 -------> entity object child: %s", json.encode(child))
						childEntity      = child
						childEntity.name = childName
					end

					childEntity.treeLevel  = treeLevel + 1
					childEntity.parentName = entity.name
					childEntity.path       = (entity.path and entity.path .. "_" or "") .. entity.name

					local _, _1 = self:CreateAppEntity(childEntity)

					--CreateEntities(childEntity.children)
				end
			end
		end
	end

	local entities = appToImport.entities or {}
	LogInfo("[RASA_API:Import] 2 - Importing %d Entities.", #entities)
	local treeLevel = 0
	CreateEntities(entities, treeLevel, "SIMPLE")
	local compositesEntities = appToImport.composites or {}
	LogInfo("[RASA_API:Import] 2.1 - Importing %d Composites Entities.", #compositesEntities)
	CreateEntities(compositesEntities, treeLevel, "COMPOSITE")

	-- 3 - Importa los Intents
	local intents = appToImport.intents or {}
	LogInfo("[RASA_API:Import] 3 - Importing %d Intents.", #intents)

	for _, intent in ipairs(intents) do
		local intentInfo = {
			vcc = vcc,
			appId = appId,
			name = intent.name
		}
		local _, _1 = self:CreateAppIntent(intentInfo)
	end

	-- -- 4 - Importa los Utterances
	local utterances = appToImport.utterances or {}
	LogInfo("[RASA_API:Import] 4 - Importing %d Utterances.", #utterances)

	for _, utterance in ipairs(utterances) do
		local utteranceInfo = {
			vcc          = vcc,
			appId        = appId,
			intentId     = utterance.intent,
			utterance    = utterance.text,
			entityLabels = {}
		}
		-- Tengo ue hacer una copia, por si tengo que Etiquetar con Entidades.
		--   Ya que la función "AddUtteranceAppIntent" modifica esta tabla "utteranceInfo".
		local copyUtteranceInfo = utils.DeepCopy(utteranceInfo)
		-- 1 - Agrego el Utterance
		local success = self:AddUtteranceAppIntent(utteranceInfo)

		if success and (#utterance.entities > 0) then
			for _, entity in ipairs(utterance.entities) do
				local entityLabel = utils.DeepCopy(entity)
				entityLabel.entityId       = MakeEntityChildName(entityLabel.entity)
				--LogDebug("[RASA_API:Import] 4.1 -------> Utterance label with entity sourceId: '%s' targetId: '%s'.", entityLabel.entity, entityLabel.entityId)
				entityLabel.entity         = nil
				entityLabel.startCharIndex = entityLabel.startPos
				entityLabel.startPos       = nil
				entityLabel.endCharIndex   = entityLabel.endPos
				entityLabel.endPos         = nil

				table.insert(copyUtteranceInfo.entityLabels, entityLabel)
			end
			-- 2 Etiqueto el Utterance con las Entidades
			self:UpdateUtteranceLabels(copyUtteranceInfo)
		else
			LogInfo("[RASA_API:Import] - Imported Utterance Without Entities Labeled App. Utterance: %s", json.encode(utteranceInfo))
		end
	end

	local endTime = os.time()
	local elapsedTime = endTime - startTime

	LogInfo("[RASA_API:Import] - END Import App '%s' ELAPSED Time '%s' seconds.", appToImport.name, elapsedTime)

	return true, response
end

--[[
]]--
function ImportLuisApp (self, luisAppId, vcc)
	assert(type(luisAppId) == "string")
	assert(type(vcc) == "string")

	luisAppId = luisAppId:lower()

	LogInfo("[RASA_API:ImportLuisApp] START Import LUIS App '%s', VCC: '%s' to RASA.' Retrieving Export Information... ", luisAppId, vcc)

	local encodedLuisApp = apiLuis.GetAppFromDataBase(vcc, luisAppId)
	--LogDebug("[RASA_API:ImportLuisApp] ==> luisApp from database: %s", encodedLuisApp)
	if not encodedLuisApp then
		local err = {luisAppId = luisAppId, vcc = vcc, type = "UNABLE TO IMPORT - LUIS APP DOES NOT EXIST"}
		LogError("[RASA_API:ImportLuisApp] LUIS App '%s' does not exist. Unable to Import LUIS App VCC: '%s'to RASA.", luisAppId, vcc)
		return false, err
	end

	local currentImportedApp = self:GetAppByImportation(luisAppId, vcc)
	if currentImportedApp then
		local err = {luisAppId = luisAppId, vcc = vcc, type = "UNABLE TO IMPORT - LUIS APP ALREADY IMPORTED"}
		LogError("[RASA_API:ImportLuisApp] LUIS App '%s' already Imported. Unable to Import LUIS App VCC: '%s'to RASA.", luisAppId, vcc)
		return false, err
	end

	local success, encodedExportedApp, exportedApp = apiLuis:ExportApp(luisAppId)
	--LogDebug("[RASA_API:ImportLuisApp] ==> encodedExportedApp: %s", encodedExportedApp)
	if not success then
		local err = encodedExportedApp
		print("[RASA_API:ImportLuisApp] Unable to Export LUIS App '%, VCC: '%s' for Import to RASA. Err: %s", luisAppId, vcc, err)
		return false, err
	end

	local luisApp = json.decode(encodedLuisApp)
	exportedApp.inconcertAppInfo = luisApp
	local success, response = self:Import(exportedApp, vcc)
	if not success then
		local err = response
		print("[RASA_API:ImportLuisApp] Unable to Import LUIS App '%, VCC: '%s' to RASA. Err: %s", luisAppId, vcc, err)
		return false, err
	end

	print("[RASA_API:ImportLuisApp] - END Import LUIS App '%s' ELAPSED App '%s' VCC '%s'.", luisAppId, vcc)

	return true, response
end

--[[
]]--
function AddApp (self, info, vcc)
	assert(type(info) == "table")
	assert(type(vcc) == "string")
	info.vcc = info.vcc or vcc
	info.__occName = MakeAppNameForVcc(info.name, vcc)
	info.__occDescription = MakeAppDescriptionForVcc(info.description)
	local success, response = self.apps.Add(info)
	return success, response
end

--[[
]]--
function RenameApp (self, info, vcc)
	assert(type(info) == "table")
	assert(type(vcc) == "string")
	info.vcc = info.vcc or vcc
	info.__occName = MakeAppNameForVcc(info.name, vcc)
	info.__occDescription = MakeAppDescriptionForVcc(info.description)
	local success, response = self.apps.Rename(info)
	return success, response
end

--[[
]]--
function DeleteApp (self, appId, vcc, forceDelete)
	assert(type(appId) == "string")
	assert(type(vcc) == "string")
	local info = {id = appId, vcc = vcc, forceDelete = forceDelete}
	local success, response = self.apps.Delete(info)
	--LogDebug("[RASA_API:DeleteApp] Deleting App: '%s' Success: '%s', Response: %s", json.encode(info), success, response)
	return success, response
end

--[[
]]--
function CreateAppFileSystemStructure (self, info)
	assert(type(info) == "table")
	assert(type(info.vcc) == "string")
	assert(type(info.appId) == "string")
	local success, response = self.apps.CreateAppFileSystemStructure(info)
	return success, response
end

--[[
]]--
function DeleteAppFileSystemStructure (self, info)
	assert(type(info) == "table")
	assert(type(info.vcc) == "string")
	assert(type(info.appId) == "string")
	local success, response = self.apps.DeleteAppFileSystemStructure(info)
	return success, response
end

--[[
]]--
function CreateTrainAppFileSystem (self, info)
	assert(type(info) == "table")
	assert(type(info.vcc) == "string")
	assert(type(info.appId) == "string")
	local success, response = self.apps.CreateTrainAppFileSystem(info)
	return success, response
end

--[[
]]--
function TrainApp (self, info)
	assert(type(info) == "table")
	--info.versionId = MakeTaskVersion(info.versionId)
	return self.apps.TrainApp(info)
end

--[[
]]--
function RunRasaApp (self, info)
	assert(type(info) == "table")
	--info.versionId = MakeTaskVersion(info.versionId)
	return self.runtime.RunRasaApp(info)
end

--[[
]]--
function DownRasaApp (self, info)
	assert(type(info) == "table")
	--info.versionId = MakeTaskVersion(info.versionId)
	return self.runtime.DownRasaApp(info)
end

--[[
]]--
function RunRasaVccApps (self, info)
	assert(type(info) == "table")
	--info.versionId = MakeTaskVersion(info.versionId)
	return self.runtime.RunRasaVccApps(info)
end

--[[
]]--
function DownRasaVccApps (self, info)
	assert(type(info) == "table")
	--info.versionId = MakeTaskVersion(info.versionId)
	return self.runtime.DownRasaVccApps(info)
end

--[[
]]--
function RunRasaAllVccsApps (self)
	return self.runtime.RunRasaAllVccsApps()
end

--[[
]]--
function DownRasaAllVccsApps (self)
	return self.runtime.DownRasaAllVccsApps()
end

--[[
]]--
function StopRasa (self)
	return self.runtime.StopRasa()
end

--[[
]]--
function MakeNLUFile (self, info)
	assert(type(info) == "table")
	return self.apps.MakeNLUFile(info)
end

--[[
]]--
function LoadAppCultures (self)
	return self.apps.LoadAppCultures()
end

-- *******************************************************************************
-- API INTENTS
-- *******************************************************************************

--[[
]]--
function LoadAllAppIntents (self, appId, vcc)
	assert(type(appId) == "string")
	assert(type(vcc) == "string")
	local list = rasaModels.VCCs.GetIntents(vcc, appId)
	local total = utils.GetRowCount(list)
	LogDebug("[RASA_API:LoadAllAppIntents] Load Apps '%s' Intents of vcc '%s' Total: '%s'", appId, vcc, total)
	--LogDebug("[RASA_API:LoadAllAppIntents] Load Apps '%s' Intents of vcc '%s' Total: '%s'. Intents: %s", appId, vcc, total, json.encode(list))
	return list, total
end

--[[
]]--
function GetAppIntent (self, appId, intentId, vcc)
	assert(type(appId) == "string")
	assert(type(intentId) == "string")
	assert(type(vcc) == "string")
	local info = {
		id = intentId,
		appId = appId,
		vcc = vcc
	}
	return self.intents.GetAppIntent(info)
end

--[[
]]--
function CreateAppIntent (self, info)
	assert(type(info) == "table")
	return self.intents.CreateAppIntent(info)
end

--[[
]]--
function RenameAppIntent (self, info)
	assert(type(info) == "table")
	return self.intents.RenameAppIntent(info)
end

--[[
]]--
function DeleteAppIntent (self, appId, intentId, vcc)
	assert(type(appId) == "string")
	assert(type(intentId) == "string")
	assert(type(vcc) == "string")
	local info = {
		id = intentId,
		appId = appId,
		vcc = vcc
	}
	return self.intents.DeleteAppIntent(info)
end

--[[
]]--
function LoadUtterancesByIntentId (self, appId, intentId, vcc)
	assert(type(appId) == "string")
	assert(type(intentId) == "string")
	assert(type(vcc) == "string")

	local utterances = rasaModels.VCCs.LoadUtterances(vcc, appId, intentId)
	if not utterances then
		return false
	end

	local encodedUtterances = json.encode(utterances)
	--LogDebug("[RASA:LoadUtterancesByIntentId] %s, %s, %s, %s, %s", vcc, appId, intentId, utteranceId, encodedUtterances)
	return true, encodedUtterances, utterances
end

--[[
]]--
function LoadUtterancesByIntentName (self, appId, intentName, vcc)
	assert(type(appId) == "string")
	assert(type(intentName) == "string")
	assert(type(vcc) == "string")

	local encodedIntent = rasaModels.VCCs.GetIntentByName(vcc, appId, intentName)
	if not encodedIntent then
		return false, {result = "UNABLE TO LOAD UTTERANCES - INTENT DOES NOT EXIST"}
	end

	local intent = json.decode(encodedIntent)
	return LoadUtterancesByIntentId(self, appId, intent.id, vcc)
end

--[[

]]--
function GetUtteranceByAppIntent (self, appId, intentId, utteranceId, vcc)
	assert(type(vcc) == "string")
	assert(type(appId) == "string")
	assert(type(intentId) == "string")
	assert(utteranceId)
	local encodedUtterance = rasaModels.VCCs.GetUtterance(vcc, appId, intentId, utteranceId)
--	LogDebug("[RASA:GetUtteranceByAppIntent] %s, %s, %s, %s, %s", vcc, appId, intentId, utteranceId, encodedUtterance)
	if not encodedUtterance then
		return false
	end
	local utterance = json.decode(encodedUtterance)
	return true, utterance, encodedUtterance
end

--[[
]]--
function AddUtteranceAppIntent (self, info)
	assert(type(info) == "table")
	return self.intents.AddUtteranceAppIntent(info)
end

--[[
]]--
function UpdateUtteranceLabels (self, info)
	assert(type(info) == "table")
	return self.intents.UpdateUtteranceLabels(info)
end

--[[
]]--
function DeleteAppUtterance (self, appId, utteranceId, intentId, vcc)
	assert(type(vcc) == "string")
	assert(type(appId) == "string")
	assert(type(intentId) == "string")
	assert(utteranceId)
	local info = {
		vcc = vcc,
		appId = appId,
		intentId = intentId,
		id = utteranceId
	}
	return self.intents.DeleteAppUtterance(info)
end

-----------------------------------------
-- ENTITIES
-----------------------------------------

--[[
]]--
function LoadAllAppEntities (self, appId, vcc)
	assert(type(appId) == "string")
	assert(type(vcc) == "string")
	local list = rasaModels.VCCs.LoadEntities(vcc, appId)
	local total = utils.GetRowCount(list)
	LogDebug("[RASA_API:LoadAllAppEntities] Load Apps '%s' Entities of vcc '%s' Total: '%s', Apps: %s", appId, vcc, total, json.encode(list))
	return list, total
end

--[[
]]--
function GetAppEntity (self, appId, entityId, vcc)
	assert(type(appId) == "string")
	assert(type(entityId) == "string")
	assert(type(vcc) == "string")
	local info = {
		id = entityId,
		appId = appId,
		vcc = vcc
	}
	return self.entities.GetAppEntity(info)
end

--[[
]]--
function CreateAppEntity (self, info)
	assert(type(info) == "table")
	return self.entities.CreateAppEntity(info)
end

--[[
]]--
function RenameAppEntity (self, info)
	assert(type(info) == "table")
	return self.entities.RenameAppEntity(info)
end

--[[
]]--
function DeleteAppEntity (self, appId, entityId, vcc)
	assert(type(appId) == "string")
	assert(type(entityId) == "string")
	assert(type(vcc) == "string")
	local info = {
		id = entityId,
		appId = appId,
		vcc = vcc
	}
	return self.entities.DeleteAppEntity(info)
end


-----------------------------------------
-- RUNTIME
-----------------------------------------

--[[
]]--
function AppStatus (self, info)
	assert(type(info) == "table")
	assert(type(info.appId) == "string")
	assert(type(info.vcc) == "string")

	local success, encodedApp, appInfo = self:GetApp(info.appId, info.vcc)
	--LogDebug(" ---> success: '%s', appInfo: '%s'", success, appInfo)
	if success then
		info.runtime = appInfo.runtime
	else
		-- Si a la app No la ha encontrado por su ID, la busca por el ID de la App de LUIS (el de la importacion).
		local luisAppId = info.appId
		local importedApp = self:GetAppByImportation(luisAppId, info.vcc)
		if not importedApp then
			LogError("[RASA_API:AppStatus] RASA App '%s' does not exist in vcc: '%s'.", info.appId, info.vcc)
			return false, {appId = info.appId, vcc = info.vcc, result = "RASA APPLICATION DOES NOT EXIST IN VCC"}
		end
		info.appId = importedApp.id
		info.runtime = importedApp.runtime
	end

	if not info.runtime or not info.runtime.port then
		LogError("[RASA_API:AppStatus] RASA App '%s' vcc: '%s' has not configured runtime port. Runtime Info: %s", info.appId, info.vcc, json.encode(info.runtime))
		return false, {appId = info.appId, vcc = info.vcc, result = "RASA APPLICATION HAS NOT CONFIGURED RUNTIME PORT"}
	end

	local success, rasaResponse = self.runtime.AppStatus(info)
	if not success then
		local err = rasaResponse
		return false, err
	end

	return true, rasaResponse
end

--[[
]]--
function Query (self, info)
	assert(type(info) == "table")
	assert(type(info.appId) == "string")
	assert(type(info.vcc) == "string")

	local noIncludePublications = true
	local success, encodedApp, appInfo = self:GetApp(info.appId, info.vcc, noIncludePublications)
	--LogDebug(" ---> success: '%s', appInfo: '%s'", success, appInfo)
	if success then
		info.runtime = appInfo.runtime
	else
		-- Si a la app No la ha encontrado por su ID, la busca por el ID de la App de LUIS (el de la importacion).
		local luisAppId = info.appId
		local importedApp = self:GetAppByImportation(luisAppId, info.vcc, noIncludePublications)
		if not importedApp then
			LogError("[RASA_API:Query] RASA App '%s' does not exist in vcc: '%s'.", info.appId, info.vcc)
			return false, {appId = info.appId, vcc = info.vcc, result = "RASA APPLICATION DOES NOT EXIST IN VCC"}
		end
		info.appId = importedApp.id
		info.runtime = importedApp.runtime
	end

	if not info.runtime or not info.runtime.port then
		LogError("[RASA_API:Query] RASA App '%s' vcc: '%s' has not configured runtime port. Runtime Info: %s", info.appId, info.vcc, json.encode(info.runtime))
		return false, {appId = info.appId, vcc = info.vcc, result = "RASA APPLICATION HAS NOT CONFIGURED RUNTIME PORT"}
	end

	local success, rasaResponse = self.runtime.Query(info)
	if not success then
		local err = rasaResponse
		return false, err
	end
	if not info.convertOnLuis20Response then
		return true, rasaResponse
	end

	local luisResponse = self:ConvertQueryResponseOnLuis20Response(rasaResponse)
	return true, rasaResponse, luisResponse
end

--[[
	Convierte la respuesta de un Query de RASA al formato de respuesta de Query de LUIS 2.0.
	
	1) source "RASA response":
		{
			"text": "si",
			"intent": {
				"id": -4030525273585851668,
				"name": "Afirmacion",
				"confidence": 0.9981739521026611
			},
			"entities": [
				{
					"entity": "distancia",
					"start": 0,
					"end": 9,
					"confidence_entity": 0.8797520399,
					"value": "estupendo",
					"extractor": "DIETClassifier"
				}
			],
			"intent_ranking": [
				{
					"id": -4030525273585851668,
					"name": "Afirmacion",
					"confidence": 0.9981739521026611
				},
				{
					"id": -3099473888966252626,
					"name": "None",
					"confidence": 0.001787551329471171
				},
				{
					"id": -5607697797905275342,
					"name": "Despedida",
					"confidence": 1.98633370018797e-05
				},
				{
					"id": 5798939286001829386,
					"name": "Bienvenida",
					"confidence": 8.538708243577275e-06
				},
				{
					"id": -5155584666881836646,
					"name": "Prestamo_Vehicular",
					"confidence": 4.440808424988063e-06
				},
				{
					"id": -6759223048379147668,
					"name": "Prestamo",
					"confidence": 3.979007033194648e-06
				},
				{
					"id": 2834177783678420691,
					"name": "Prestamo_Minorista",
					"confidence": 4.4522940356728213e-07
				},
				{
					"id": 4583268402097880838,
					"name": "Prestamo_MiCasa",
					"confidence": 4.2478006889723474e-07
				},
				{
					"id": 8868469969782498637,
					"name": "Horario_Sucursales",
					"confidence": 4.0596822259431065e-07
				},
				{
					"id": 6561969706177761722,
					"name": "Negacion",
					"confidence": 3.994383916960942e-07
				}
			],
			"response_selector": {
				"all_retrieval_intents": [],
				"default": {
				"response": {
					"id": null,
					"response_templates": null,
					"confidence": 0.0,
					"intent_response_key": null,
					"template_name": "utter_None"
				},
				"ranking": []
				}
			}
		}


	2) target "LUIS response":
		{
			"query": "si",
			"topScoringIntent": {
				"intent": "Afirmacion",
				"score": 0.9209893
			},
			"intents": [
				{
					"intent": "Afirmacion",
					"score": 0.9209893
				},
				{
					"intent": "Prestamo",
					"score": 0.459878355
				},
				{
					"intent": "Despedida",
					"score": 0.0281440374
				},
				{
					"intent": "Bienvenida",
					"score": 0.0144929672
				},
				{
					"intent": "Horario_Sucursales",
					"score": 0.0107543394
				},
				{
					"intent": "None",
					"score": 0.00640365
				},
				{
					"intent": "Prestamo_Vehicular",
					"score": 0.005326915
				},
				{
					"intent": "Negacion",
					"score": 0.004407885
				},
				{
					"intent": "Humano",
					"score": 0.00139135262
				},
				{
					"intent": "Prestamo_MiCasa",
					"score": 0.00106048794
				},
				{
					"intent": "Prestamo_Minorista",
					"score": 0.0007567182
				}
			],

			// ESTO NO VI QUE LUIS LO RETORNE!!
			"entities": [],
		}
]]--
function ConvertQueryResponseOnLuis20Response (self, rasaResponse)
	assert(type(rasaResponse) == "table")
	LogInfo("[RASA_API:ConvertQueryResponseOnLuis20Response] Convert Query Rasa Response On Luis 2.0 Response.")
	--LogDebug("[RASA_API:ConvertQueryResponseOnLuis20Response] 1 - source -> rasaResponse: %s.", json.encode(rasaResponse))

	-- Función que genera un Intent según el formato esperado por LUIS
	local function MakeLuisIntent (rasaIntent)
		if not rasaIntent then
			return
		end
		return {
			intent = rasaIntent.name,
			score  = rasaIntent.confidence
		}
	end

	local luisResponse = {
		query            = rasaResponse.text,                         -- la palabra buscada
		topScoringIntent = MakeLuisIntent(rasaResponse.intent) or {}, -- el intent "top"
		intents          = {},                                        -- lista de intents de la respuesta
		entities         = {}                                         -- list ade entities de la respuesta
	}

	-- Recorro la lista de intents retornados por RASA y los convierto según el formato de LUIS.
	for _, rasaIntent in ipairs(rasaResponse.intent_ranking) do
		local luisIntent = MakeLuisIntent(rasaIntent)
		if luisIntent then
			table.insert(luisResponse.intents, luisIntent)
		end
	end

	-- Función que genera una Entidad según el formato esperado por LUIS
	local function MakeLuisEntity (rasaEntity)
		if not rasaEntity then
			return
		end
		--TODO: Ver si hay que formatear segun el formato de LUIS. Segun las pruebas que hice LUIS no retorno info ahí.
		return rasaEntity
	end

	-- Recorro la lista de entidades retornados por RASA y los convierto según el formato de LUIS.
	for _, rasaEntity in ipairs(rasaResponse.entities or {}) do
		local luisEntity = MakeLuisEntity(rasaEntity)
		if luisEntity then
			table.insert(luisResponse.entities, luisEntity)
		end
	end

	--LogDebug("[RASA_API:ConvertQueryResponseOnLuis20Response] 2 - target -> luisResponse: %s.", json.encode(luisResponse))

	return luisResponse
end

-- PUBLISH ----
function ListenerPublications (self)
	--LogDebug("[RASA_API:ListenerPublications] START Listiner Publications.")
	return publish.ListenerPublications()
end

function LoadPublications (self)
	--LogDebug("[RASA_API:LoadPublications] Load Publications.")
	return publish.LoadPublications()
end

-----------------------------------
function new (self, apiVersion)
	apiVersion = apiVersion or API_LAST_VERSION

	LogInfo("[RASA_API:new] RASA New Instance. API Version: '%s'.", apiVersion)

	assert(ALLOWED_API_VERSIONS[apiVersion], "Unrecognized API Version " .. tostring(apiVersion))

	if not (_M.m_apiInstances) then
		_M.m_apiInstances = {}
	end

	if _M.m_apiInstances[apiVersion] then
		return _M.m_apiInstances[apiVersion]
	end

	for version in pairs(ALLOWED_API_VERSIONS) do
		-- TODO: Los metodos deberian agregar de forma explicita !!! (Definirlos como privados y exponerlos)
		_M.m_apiInstances[version] = _M.m_apiInstances[version] or {
			apiVersion = version
		}
		for n, v in pairs(_M) do
			if type(v) == "function" then
				--LogDebug("----------> _M.n = " .. tostring(n) .. " " .. type(v))
				_M.m_apiInstances[version][n] = v
			end
		end

		InitApiVersionsModules(version)
		--SetVersionBaseURIs(version)
	end

	return _M.m_apiInstances[apiVersion]
end
