require "LuaOrm.Date"
require "uuid"
local utf8 = require "icu.utf8"
local json = require "cjson"
local request = require "rasa.request"
local fileSystem = require "rasa.filesystem"
local intents = require "rasa.v2_0.intents"
local rasaCommandLine =  require "rasa.rasaCommandLine"
local utils = require "rasa.utils"
local rasaModels = require("rasa.models"):GetModels()
local runtime = require "rasa.v2_0.runtime"
local publish = require "rasa.publish"

module(..., package.seeall)

local m_instanceOwner
function SetOwner (instanceOwner)
	m_instanceOwner = instanceOwner
end

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

--[[
-----------------------------
APPS - GET APPLICATION INFO
-----------------------------
	Gets the application info.

]]--
function Get (info)
	assert(info)
	assert(info.id)
	assert(info.vcc)

	local appId = utils.SanitizeAppId(info.id):lower()
	local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId)
	if not encodedApp then
		local response = {result = "APPLICATION DOES NOT EXIST", appInfo = info}
		LogInfo("[RASA_APPS:Get] Get App '%s' Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_APPS:Get] Success Get App %s.", json.encode(info))

	--LogDebug("[RASA_APPS:Get] App: appId: '%s', data: '%s'", json.encode(info), encodedApp)
	local app = json.decode(encodedApp)
	return true, encodedApp, app
end

local function GetFreeRuntimeAppPort ()
	local initialPort = runtime.GetDefaultPort() -- sample: 5005
	local usedHighPort  = -1
	local allVCCsApps = rasaModels.VCCs.GetApplicationsAllVCCs() or {}

	--LogDebug("[RASA_APPS:GetFreeRuntimeAppPort] allVCCsApps %s.", json.encode(allVCCsApps))

	local usedPorts = {}
	for vcc, vccApps in pairs(allVCCsApps) do
		for appId, app in pairs(vccApps) do
			if app.runtime and app.runtime.port then
				usedHighPort  = app.runtime.port > usedHighPort and app.runtime.port or usedHighPort
				usedPorts[app.runtime.port] = app
			end
		end
	end

	if not next(usedPorts) then
		LogDebug("[RASA_APPS:GetFreeRuntimeAppPort] - Free Port: '%s'.", initialPort)
		return initialPort
	end

	local port = initialPort
	while (port <= usedHighPort ) do
		if not usedPorts[port] then
			break
		end
		port = port + 1
	end

	LogDebug("[RASA_APPS:GetFreeRuntimeAppPort] - Free Port: '%s', Used High Port: '%s'.", port, usedHighPort)

	return port
end

local function UpdateLanguageConfigYmlFile (appInfo, oldAppInfo)
	assert(appInfo)

	--LogInfo("[RASA_APPS:UpdateLanguageConfigYmlFile] App '%s' Vcc: '%s' Updating Config YML File '%s'.", appInfo.name, appInfo.vcc, appInfo.directories.fullConfigYMLName)

	if not appInfo.culture then
		LogWarning("[RASA_APPS:UpdateLanguageConfigYmlFile] Unable to Update Config File '%s'. App culture is required.", appInfo.directories.fullConfigYMLName)
		return
	end

	-- 1-  Leo el contenido del archivo de configuracion
	local success, contentFile = fileSystem.FileRead(appInfo.directories.fullConfigYMLName)
	if not success then
		LogWarning("[RASA_APPS:UpdateLanguageConfigYmlFile] could not read Config YML File '%s'.", appInfo.directories.fullConfigYMLName)
		return false
	end

	-- 2-  Actualizo el contenido del archivo de configuracion en memoria
	local oldCulture = oldAppInfo and oldAppInfo.culture or "es"
	local newCulture = appInfo.culture

	local templateLanguage = "language: *%s"
	local oldLanguageTag = templateLanguage:format(oldCulture)
	local newLanguageTag = utf8.gsub(templateLanguage:format(appInfo.culture), "*", "")

	local newContentFile, resp = utf8.gsub(contentFile, oldLanguageTag, newLanguageTag)

	--LogDebug(resp)
	if resp <= 0 then
		LogWarning("[RASA_APPS:UpdateLanguageConfigYmlFile] Unable to Update Config File '%s'. language not found", appInfo.directories.fullConfigYMLName)
		return false
	end
	--LogDebug(newContentFile)

	-- 3-  Actualizo el contenido del archivo de configuracion en HDD
	fileSystem.WriteTxtFile(appInfo.directories.fullConfigYMLName, newContentFile)

	return true
end

local function SetCulture (info)
	assert(info)
	assert(info.culture)
	local _, _1, cultures = LoadAppCultures()
	local defualtCulture
	local isAllowed = false
	for _, allowedCulture in ipairs(cultures) do
		if allowedCulture.code == info.culture then
			-- Han posteado una cultura permitida
			isAllowed = true
			break
		end
		if allowedCulture.default then
			defualtCulture = allowedCulture.code
		end
	end
	if not isAllowed then
		LogInfo("[RASA_APPS:SetCulture] Not Allowed Culture '%s'. Set default culture '%s'.", info.culture, defualtCulture)
		info.culture = defualtCulture
	else
		LogInfo("[RASA_APPS:SetCulture] Is Allowed Culture '%s'.", info.culture)
	end
end

--[[
	"version": "0.1",
	"createdDateTime": "2017-01-31T16:38:06Z",
	"lastModifiedDateTime": "2017-01-31T18:04:35Z",
	"lastTrainedDateTime": "2017-01-31T17:05:25Z",
	"lastPublishedDateTime": "2017-01-31T17:05:37Z",
	"endpointUrl": "DummyUrl",
		"assignedEndpointKey": {
		"SubscriptionKey": "",
		"SubscriptionName": ""
	},
	"externalApiKeys": {},
	"intentsCount": 3,
	"entitiesCount": 2,
	"endpointHitsCount": 0
]]--
local function StarProcessAddApp (info)
	LogInfo("[RASA_APPS:StarProcessAddApp] Start Process Adding app %s.", json.encode(info))

	local vcc = info.vcc:lower()
	--Si es una App Importada desde LUIS, el Id de RASA pasa a ser el mismo Id que en LUIS reemplazando los (- por _).
	-- En caso de que no sea un App Importada, sino que es una App Nativa de RASA, ahí sanitizo el Id según el Name.
	local appId = info.sourceImportedApp and info.sourceImportedApp.exportedAppId or info.name
	appId = utils.SanitizeAppId(appId):lower()

	local encodedApp = rasaModels.VCCs.GetApplication(vcc, appId)

	if encodedApp then
		return false, {result = "UNABLE TO CREATE APP - APP ALREADY EXIST", vcc = vcc, appId = appId}
	end

	local success, response = fileSystem.CreateAppFileSystemStructure(info)
	if not success then
		return false, response
	end
	-- Elimino caracteres del Id y actualiza la info de la App en el VCC.
	local api = require "rasa"

	info.id = appId
	info.fullId = utils.MakeAppId(vcc, appId)
	info.directories = response.directories
	info.activeVersion = info.activeVersion or api.MakeTaskVersion()
	info.createdDateTime = LuaOrm.Date:time()
	info.creatorUserId = info.userId or "ANONYMOUS"
	-- runtime / query - config 
	info.runtime = info.runtime or {}
	info.runtime.protocol = info.runtime.protocol or runtime.GetDefaultProtocol() -- sample: http
	info.runtime.domain = info.runtime.domain or runtime.GetDefaultDomain()	      -- sample: localhost
	info.runtime.port = tonumber(info.runtime.port) or GetFreeRuntimeAppPort()    -- sample: 5005
	-- clean up info
	info.userId = nil
	info.isNew = nil

	-- Si la cultura no es una de las permitidas, seteo una por defecto.
	SetCulture(info)

	rasaModels.VCCs.UpdateApplication(vcc, appId, info)

	-- Creo el Intent None
	local success, response = intents.CreateAppIntent({
		vcc = vcc,
		appId = appId,
		name = "None"
	})
	if not success then
		LogWarning("[RASA_APPS:StarProcessAddApp] Could not create 'Intent None' when creating App. Info: '%s'. Response: '%s'", json.encode(info), json.encode(response))
	end

	-- Actualizo el lenguage/culture en el archivo de configuración "config.yml"
	UpdateLanguageConfigYmlFile(info)

	-- Publica en REDIS que existe una nueva App
	publish.PublishNewApp({
		id = appId,
		vcc = info.vcc
	})

	return true, {appId = appId}
end

--[[
-----------------------------
APPS - ADD APP
-----------------------------
	Creats a new application
]]--
function Add (info)
	assert(type(info) == "table")
	assert(type(info.vcc) == "string")
	assert(type(info.name) == "string")
	assert(type(info.culture) == "string")

	local success, response = StarProcessAddApp(info)
	if not success then
		LogError("[RASA_APPS:Add] Unable to create App. Error: '%s'", json.encode(response))
		return false, response
	end

	local appId = response.appId
	LogInfo("[RASA_APPS:Add] Success added appId %s.", appId)

	return true, appId
end

local function StarProcessRenameApp (info)
	LogInfo("[RASA_APPS:StarProcessRenameApp] Start Process Renaming app %s.", json.encode(info))

	local appId = utils.SanitizeAppId(info.id):lower()
	local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId)
	if not encodedApp then
		return false, {result = "UNABLE TO RENAME APP - APP DOES NOT EXIST", vcc = info.vcc, appId = appId}
	end

	local oldApp = {}
	local currentApp = json.decode(encodedApp)
	oldApp.culture = currentApp.culture

	currentApp.name = info.name
	currentApp.description = info.description
	currentApp.culture = info.culture
	currentApp.lastModifiedDateTime = LuaOrm.Date:time()
	currentApp.lastModifiedUserId = info.userId or "ANONYMOUS"
	info.userId = nil
	info.isNew = nil

	-- Si la cultura no es una de las permitidas, seteo una por defecto.
	SetCulture(info)

	rasaModels.VCCs.UpdateApplication(info.vcc, appId, info)

	-- Actualizo el lenguage/culture en el archivo de configuración "config.yml"
	UpdateLanguageConfigYmlFile(currentApp, oldApp)

	return true, {appId = appId, vcc = info.vcc}
end

--[[
-----------------------------
APPS - RENAME APPLICATION
-----------------------------
	Updates the name or description of the application.
]]--
function Rename (info)
	assert(type(info) == "table")
	assert(type(info.id) == "string")
	assert(type(info.vcc) == "string")
	assert(type(info.name) == "string")

	local success, response = StarProcessRenameApp(info)
	if not success then
		LogError("[RASA_APPS:Rename] Unable to Renaming App. Error: '%s'", json.encode(response))
		return false, response
	end

	LogInfo("[RASA_APPS:Rename] Success Renamed appId %s.", info.id)

	return true, response
end

--[[
	Funcion que valida si la App se encuentra si tiene algún Train en Running.
]]--
local function IsRunningSomeTrainingForApp (info)
	assert(type(info) == "table")
	assert(info.appId)
	assert(info.vcc)

	LogInfo("[RASA_APPS:IsRunningSomeTrainingForApp] 1 - Validating Is Running Train for app: '%s' vcc: '%s'.", info.appId, info.vcc)

	local keyspace = utils.GetAgentKeySpace()
	if not info.app then
		-- 1 - Obtengo la App de Cassandra.
		--LogDebug("[RASA_APPS:IsRunningSomeTrainingForApp] 2 ----> Obtengo de la BD. app: '%s' vcc: '%s'.", info.appId, info.vcc)
		local appId = utils.SanitizeAppId(info.appId):lower()
		local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId, keyspace)

		if not encodedApp then
			keyspace:Close()
			LogError("[RASA_APPS:IsRunningSomeTrainingForApp] Error validating is Running Train for app: '%s' vcc: '%s'. Error: App does not exist.", info.appId, info.vcc)
			return false, {}
		end

		local app = json.decode(encodedApp)
		info.app = app
	end

	local success, lastTrainStepStatus = GetLastTrainAppStatus(info, keyspace)
	keyspace:Close()

	if success then
		-- Valido SI existe un TRAIN en Running de la App.
		if lastTrainStepStatus.isRunning then
			LogInfo("[RASA_APPS:IsRunningSomeTrainingForApp] + Is Running Train for app: '%s' vcc: '%s'. Train info: '%s'", info.appId, info.vcc, json.encode(lastTrainStepStatus))
			return true, lastTrainStepStatus
		end
	end

	LogInfo("[RASA_APPS:IsRunningSomeTrainingForApp] - Not Running a Train for app: '%s' vcc: '%s'.", info.appId, info.vcc)

	return false, lastTrainStepStatus
end

local function RemoveAppDirectory (app)
	if app.directories then
		local success, response = fileSystem.RemoveDirectory(app.directories.appVccDir)
		if not success then
			LogWarning("[RASA_APPS:RemoveAppDirectory] Error Deleting App '%s' Directory Error: '%s'", app.id, json.encode(response))
			return false, response
		end
		return true
	end
	LogWarning("[RASA_APPS:RemoveAppDirectory] Error Deleting App '%s' Error: App has not configured directories.", app.id)
	return false, {result = "UNABLE TO REMOVE DIRECTORY. APP NOT HAS CONFIGURED DIRECTORY"}
end

local function StarProcessDeleteApp (info, keyspace)
	LogInfo("[RASA_APPS:StarProcessDeleteApp] 1 - Start Process Deleting app %s, vcc %s.", info.id, info.vcc)

	-- 1 - Valida que la App exista.
	local appId = utils.SanitizeAppId(info.id):lower()
	local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId, keyspace)
	if not encodedApp then
		return false, {result = "UNABLE TO DELETE APP - APP DOES NOT EXIST", vcc = info.vcc, appId = appId}
	end

	info.appId = info.id
	local currentApp = json.decode(encodedApp)
	-- 2 - El atributo forceDelete es utilizado para desarrollo. Es para forzar el delete de una App sin realizar validaciones.
	if not info.forceDelete then
		-- 3 - Valido que NO exista un TRAIN en Running de la App.
		info.app = currentApp
		local isRunning, runningTrainStepStatus = IsRunningSomeTrainingForApp(info)
		info.app = nil
		if isRunning then
			-- 4 - Si existe un Train en curso para la App, cancelo.
			local err = {result = "UNABLE TO DELETE APP - TRAIN IS RUNNING FOR APP", vcc = info.vcc, appId = appId, status = json.encode(runningTrainStepStatus)}
			LogError("[RASA_APPS:StarProcessDeleteApp] Unable to delete app %s, vcc %s. App is Training. Train Step: %s.", info.id, info.vcc, json.encode(info))
			return false, err
		end
	end

	-- 5 - Se baja el modelo de RASA de memoria.
	LogInfo("[RASA_APPS:StarProcessDeleteApp] 2 - Deleting app %s, vcc %s. Down of Memory.", info.id, info.vcc)
	rasaCommandLine.DownRasaApp({
		appId = info.id,
		vcc = info.vcc
	})

	-- 6 - Pequeña pausa
	local waitSec = 2
	require("socket").sleep(waitSec)

	-- 7 - Elimino la app de la BD de Cassandra
	LogInfo("[RASA_APPS:StarProcessDeleteApp] 3 - Deleting app %s, vcc %s. Removing of Cassandra.", info.id, info.vcc)
	rasaModels.VCCs.RemoveApplication(info.vcc, appId, keyspace)

	-- 8 - Elimino del HDD la estructura de la app
	LogInfo("[RASA_APPS:StarProcessDeleteApp] 4 - Deleting app %s, vcc %s. Removing of HDD.", info.id, info.vcc)
	RemoveAppDirectory(currentApp)

	-- 9 - Publica en REDIS la App que se ha eliminado
	LogInfo("[RASA_APPS:StarProcessDeleteApp] 5 - Deleting app %s, vcc %s. Publishing of Multinode.", info.id, info.vcc)
	publish.PublishRemovedApp({
		id = appId,
		vcc = info.vcc,
		appData = currentApp
	})

	return true, {appId = appId, vcc = info.vcc}
end

--[[
-----------------------------
APPS - DELETE APPLICATION
-----------------------------
]]--
function Delete (info)
	assert(type(info) == "table")
	assert(type(info.id) == "string")
	assert(type(info.vcc) == "string")

	-- 1- Elimino la app de la BD, de HDD y publica en Redis para multinodo.
	local success, response = StarProcessDeleteApp(info)
	if not success then
		LogError("[RASA_APPS:Delete] Deleting App '%s' Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_APPS:Delete] Success deleted app %s.", json.encode(info))

	--LogDebug("[RASA_APPS:Delete] response: '%s'", json.encode(response))
	return true, response
end

--[[
-----------------------------
APPS - GET APPLICATION CULTURES
-----------------------------
	Gets the supported application cultures.
]]--
function LoadAppCultures ()
	local cultures = {
		{code = "en", name = "English"},
		{code = "es", name = "Spanish", default = true},
		{code = "fr", name = "French" }
	};
	local encodedCultures = json.encode(cultures)
	--LogDebug("[RASA_APPS:LoadAppCultures] Loaded Application Cultures, data: '%s'", encodedCultures)
	return true, encodedCultures, cultures
end

--[[
-----------------------------

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

]]--
local function GetAllTrainedAppStatus (info, keyspace)
	assert(type(info) == "table")
	local app = info.app
	if not app then
		assert(info.appId)
		assert(info.vcc)

		local appId = utils.SanitizeAppId(info.appId):lower()
		local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId, keyspace)

		if not encodedApp then
			local response = {result = "UNABLE TO GET LAST TRAIN APP STATUS - APP DOES NOT EXIST", info = info}
			--LogError("[RASA_APPS:GetAllTrainedAppStatus] Error GET Last TRAIN app '%s'. Error: '%s'", json.encode(info), json.encode(response))
			return false, response
		end
		app = json.decode(encodedApp)
	end

	LogInfo("[RASA_APPS:GetAllTrainedAppStatus] Retreive Last Train app stauts. app '%s' vcc '%s'.", app.id, app.vcc)

	if type(app.trainInfo) ~= "table" then
		local response = {result = "UNABLE TO GET LAST TRAIN APP STATUS - APP HAS NEVER BEEN TRAINED", info = info}
		--LogError("[RASA_APPS:GetAllTrainedAppStatus] Error GET Last TRAIN app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	return true, app.trainInfo
end

--[[
-----------------------------

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

]]--
function GetLastCompletedTrainAppStatus (info, keyspace)
	local success, allTrainSteps = GetAllTrainedAppStatus(info, keyspace)
	if not success then
		local error = allTrainSteps
		--LogError("[RASA_APPS:GetLastCompletedTrainAppStatus] Error GET Last COMPLETED TRAIN app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, error
	end

	local lastCompletedTrain
	local total = #allTrainSteps
	local i = total

	--LogDebug("-> allTrainSteps %s", json.encode(allTrainSteps))
	--LogDebug("-> total %s", total)

	while i > 0 do
		local steps = allTrainSteps[i]
		--LogDebug(" %d - steps: %s", i, json.encode(steps))
		for _, step in ipairs(steps or {}) do
			if step.statusId == "COMPLETED TRAIN" then
				lastCompletedTrain = step
				break
			end
		end
		if lastCompletedTrain then
			break
		end
		i = i - 1
	end

	if not lastCompletedTrain then
		--LogDebug("[RASA_APPS:GetLastCompletedTrainAppStatus] ---> There is no training that has been completed app '%s'.", json.encode(info))
		return false, {result = "UNABLE TO GET LAST COMPLETED TRAIN APP STATUS - HAVE NOT BEEN COMPLETED", info = info}
	end

	LogInfo("[RASA_APPS:GetLastCompletedTrainAppStatus] Last COMPLETED Train Step Status '%s' app '%s' vcc '%s'.", json.encode(lastCompletedTrain), info.appId, info.vcc)

	return true, lastCompletedTrain
end

--[[
-----------------------------
TRAIN - TRAIN APPLICATION VERSION
-----------------------------

]]--
function TrainApp (info)
	assert(type(info) == "table")
	assert(info.appId)
	assert(info.vcc)

	LogInfo("[RASA_APPS:TrainApp] 0.1- VALIDATE TRAIN app: '%s' vcc: '%s'.", info.appId, info.vcc)

	-- 1 - Obtengo la App de Cassandra.
	local keyspace = utils.GetAgentKeySpace()
	local appId = utils.SanitizeAppId(info.appId):lower()
	local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId, keyspace)

	if not encodedApp then
		keyspace:Close()
		local response = {result = "UNABLE TO TRAIN APP - APP DOES NOT EXIST", info = info}
		LogError("[RASA_APPS:TrainApp] Error TRAINING app: '%s' vcc: '%s'. Error: '%s'", info.appId, info.vcc, json.encode(response))
		return false, response
	end

	local app = json.decode(encodedApp)
	info.app = app
	local isRunning, runningTrainStepStatus = IsRunningSomeTrainingForApp(info)
	if isRunning then
		info.app = nil
		keyspace:Close()
		local err = {result = "UNABLE TO DOWN TRAIN APP - ANOTHER TRAIN IS RUNNING FOR APP", info = info, status = runningTrainStepStatus}
		LogWarning("[RASA_APPS:TrainApp] Unable to TRAIN RASA app: '%s' vcc: '%s' another train is running for app. Response: '%s'", info.appId, info.vcc, json.encode(err))
		return false, err
	end

	-- Data con los Steps del TRAIN
	local step = 1
	local stepTrain = {
		trainId = uuid.new(),
		startTrainDateTime = LuaOrm.Date:time(),
		userId = info.userId or "ANONYMOUS",
		statusId = "START RUNNING",
		isRunning = true,
		step = step,
		nodeIP = m_instanceOwner.GetWebHandlerServerIP()
	}

	LogInfo("[RASA_APPS:TrainApp] 1- START TRAINING app: '%s' vcc: '%s', trainStatus: '%s'.", info.appId, info.vcc, json.encode(stepTrain))
	--LogDebug("[RASA_APPS:TrainApp] START TRAINING app info: '%s'.", json.encode(info))

	app.trainInfo = app.trainInfo or {}

	-- 2 - Actualizo el primer Step (START) en la App de Cassandra.
	local trainSteps = {stepTrain}
	table.insert(app.trainInfo, trainSteps)
	rasaModels.VCCs.UpdateApplication(info.vcc, appId, app, keyspace)

	-- Funcion que actualiza los siguientes Steps del TRAIN.
	local function UpdateAppTrainState (statusId, newStepTrain)
		step = step + 1
		newStepTrain.statusId = statusId
		newStepTrain.step = step

		LogInfo("[RASA_APPS:TrainApp] TRAINING app: '%s' vcc: '%s', New current Status: '%s'.", info.appId, info.vcc, json.encode(newStepTrain))

		local currentTrainSteps = table.remove(app.trainInfo, #app.trainInfo)

		table.insert(currentTrainSteps, newStepTrain)
		table.insert(app.trainInfo, currentTrainSteps)
		rasaModels.VCCs.UpdateApplication(info.vcc, appId, app, keyspace)

		return newStepTrain
	end

	-- 3 - Obtengo el ultimo TRAIN realizado a la App.
	info.app = app
	local success, lastTrainStepStatus = GetLastTrainAppStatus(info, keyspace)
	info.app = nil
	local lastCompletedTrain

	if success then
		-- Valido que no exista otro TRAIN running.
		if lastTrainStepStatus.statusId ~= "COMPLETED TRAIN" and lastTrainStepStatus.trainId ~= stepTrain.trainId then
			local newStepTrainAlreadyRunning = utils.DeepCopy(stepTrain)
			newStepTrainAlreadyRunning.startDateTrainAPI = LuaOrm.Date:time()
			newStepTrainAlreadyRunning.anotherTrainId = stepTrain.trainId
			newStepTrainAlreadyRunning.isRunning = false
			newStepTrainAlreadyRunning = UpdateAppTrainState("ANOTHER TRAIN IS RUNNING FOR APP", newStepTrainAlreadyRunning)
			keyspace:Close()
			local err = {result = "UNABLE TO DOWN TRAIN APP - ANOTHER TRAIN IS RUNNING FOR APP", info = info, status = lastTrainStepStatus}
			LogError("[RASA_APPS:TrainApp] Error TRAINING RASA app: '%s' vcc: '%s'. Error: '%s', trainStatus: '%s'", info.appId, info.vcc, json.encode(err), json.encode(errStepTrainAPI))
			return false, err
		end

		--info.app = app
		local success, result = GetLastCompletedTrainAppStatus(info, keyspace)
		--info.app = nil
		if success then
			lastCompletedTrain = result
		end
	end

	-- 4 - Valido que la info de los directorios existan en la BD
	if not app.directories then
		keyspace:Close()
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP DIRECTORIES INFO DOES NOT EXIST IN DATABASE"}
		LogError("[RASA_APPS:TrainApp] Error TRAINING app: '%s' vcc: '%s'. Error: '%s'", info.appId, info.vcc, json.encode(response))
		response.info = info
		return false, response
	end

	-- 5 - Valido que el directorio Base de la App en el VCC exista.
	local isDirectory = fileSystem.isDirectory(app.directories.appVccDir)
	if not isDirectory then
		-- 4.1 - Si no existe el directorio Base del App, intento crear la estructura de directorios.
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP BASE DIRECTORY DOES NOT EXIST", directory = app.directories.appVccDir}
		LogWarning("[RASA_APPS:TrainApp] Warning TRAINING app: '%s' vcc: '%s'. Error: '%s'", info.appId, info.vcc, json.encode(response))
		local success, err = CreateAppFileSystemStructure(info)
		if not success then
			keyspace:Close()
			LogError("[RASA_APPS:TrainApp] Error TRAINING app: '%s' vcc: '%s'. Error: '%s'", info.appId, info.vcc, json.encode(response))
			response.info = info
			return false, response
		end
		-- Continuo ya que creo correctamente el Directorio Base de la App
	end

	-- 6 - Genero el archivo NLU segun la info de la BD.
	local newStepTrainNLU = utils.DeepCopy(stepTrain)
	if info.makeNLUFile then
		LogInfo("[RASA_APPS:TrainApp] 2 - Making NLU File for app: '%s' vcc: '%s'.", info.appId, info.vcc)
		-- 6.1 - Actualizo el Step en la App de Cassandra.
		newStepTrainNLU.startDateTrainMakeNLUFile = LuaOrm.Date:time()
		newStepTrainNLU.isRunning = true
		newStepTrainNLU = UpdateAppTrainState("MAKING NLU FILE", newStepTrainNLU)

		-- 6.2 - Creo el archivo NLU.
		local success, response = MakeNLUFile(info, keyspace)
		info.content = nil
		if not success then
			local errStepTrainAPI = utils.DeepCopy(newStepTrainNLU)
			errStepTrainAPI.message = "Rasa Rrain Error creating NLU File"
			errStepTrainAPI.err = response
			errStepTrainAPI.isRunning = false
			errStepTrainAPI = UpdateAppTrainState("FAIL TRAIN", errStepTrainAPI)
			keyspace:Close()
			LogError("[RASA_APPS:TrainApp] Error TRAINING app: '%s' vcc: '%s'. Error: '%s', trainStatus: '%s'", info.appId, info.vcc, json.encode(response), json.encode(errStepTrainAPI))
			return false, errStepTrainAPI
		end
	else
		newStepTrainNLU = stepTrain
	end

	-- 7 - Actualizo el Step en la App de Cassandra de que comienza el RUNNING del TRAIN contra RASA.
	local newStepTrainAPI = utils.DeepCopy(newStepTrainNLU)
	newStepTrainAPI.startDateTrainAPI = LuaOrm.Date:time()
	newStepTrainAPI.isRunning = true
	newStepTrainAPI = UpdateAppTrainState("RUNNING API RASA TRAIN", newStepTrainAPI)

	info.app = app

	-- 8 - Corro el RASA TRAIN !!!!!!!!!.
	LogInfo("[RASA_APPS:TrainApp] 3 - Run API RASA TRAIN for app: '%s' vcc: '%s'.", info.appId, info.vcc)
	local success, response = rasaCommandLine.Train(info)

	if not success then
		local errStepTrainAPI = utils.DeepCopy(newStepTrainAPI)
		errStepTrainAPI.message = "Error running command line rasa train"
		errStepTrainAPI.isRunning = false
		errStepTrainAPI = UpdateAppTrainState("FAIL TRAIN", errStepTrainAPI)
		keyspace:Close()
		LogError("[RASA_APPS:TrainApp] Error TRAINING app: '%s' vcc: '%s'. Error: '%s', trainStatus: '%s'", info.appId, info.vcc, json.encode(response), json.encode(errStepTrainAPI))
		return false, errStepTrainAPI
	end

	-- 9 - Bajo la instancia de RASA para luego volver a subirla y que los cambios queden disponibles.
	-- Despues de realizar el Train tengo que esperar unos segundos antes de realizar el Down del modelo de RASA, ya que sino las siguientes ejecuciones tiran el code error (30720) en el os.execute command line.
	LogInfo("[RASA_APPS:TrainApp] 4 - Down RASA Model for app: '%s' vcc: '%s'.", info.appId, info.vcc)

	local waitSec = 10
	require("socket").sleep(waitSec)
	rasaCommandLine.DownRasaApp(info) -- TODO: Ver si realizo algo con las respuestas

	app.lastTrainedDateTime = LuaOrm.Date:time()
	local newStepTrainEnd = utils.DeepCopy(newStepTrainAPI)
	newStepTrainEnd.endDateTrain = app.lastTrainedDateTime

	-- 10 - Subo nuevamente la instancia de RASA para que los cambios queden disponibles.
	-- Tengo que hacer una pausa para que haya bajado correctamente y luego volver a subirla.
	LogInfo("[RASA_APPS:TrainApp] 5 - Up RASA Model for app: '%s' vcc: '%s'.", info.appId, info.vcc)

	require("socket").sleep(2)
	rasaCommandLine.RunRasaApp(info)

	local oldModelFile = lastCompletedTrain and lastCompletedTrain.appStatus and lastCompletedTrain.appStatus.model_file

	-- 11 - Obtengo el status de la app para almacenar en el step del train completed.
	local success, statusResponse = runtime.StartAppStatusWithRetries({
		vcc     = info.vcc,
		appId   = info.appId,
		runtime = app.runtime
	})
	if success then
		newStepTrainEnd.appStatus = statusResponse
		local modelFileName = statusResponse.model_file
		--local modelFileName = "/var/lib/inconcert/rasa/vccs/vcc1/app1/models/video.mp4" -- prueba
		local success, contentModelFile = fileSystem.FileReadInParts(modelFileName)

		if not success then
			LogWarning("[RASA_APPS:TrainApp] Nodes cannot be synchronized. Could not open model file '%s'.", modelFileName)
		else
			LogInfo("[RASA_APPS:TrainApp] 6 - Synchronizing Nodes - Updating Binary model file of Train. App: '%s' vcc: '%s'\nmodelFileName: '%s'.", info.appId, info.vcc, modelFileName)
			--LogDebug("[RASA_APPS:TrainApp] 6.1 ==========> contentModelFile: %s", contentModelFile)

			app.lastTrain = statusResponse
			app.lastTrain.contentModelFile = contentModelFile
			app.lastTrain.oldModelFile = oldModelFile
			rasaModels.VCCs.UpdateApplication(info.vcc, appId, app, keyspace)

			-- Publica en REDIS la App que se ha entrenado
			publish.PublishTrainedApp({
				id = info.appId,
				vcc = info.vcc
			})
		end
	end

	-- 12 - Actualizo el Ultimo Step en la App de Cassandra.
	newStepTrainEnd.isRunning = false
	newStepTrainEnd = UpdateAppTrainState("COMPLETED TRAIN", newStepTrainEnd)

	keyspace:Close()

	-- 13 - Si el ultimo train que se había realizado se ha completado correctamente, elimino su archivo model de HDD.
	if oldModelFile  then
		LogInfo("[RASA_APPS:TrainApp] 7 - Removing Old Model File '%s' app: '%s' vcc: '%s'.", oldModelFile, info.appId, info.vcc)
		fileSystem.RemoveFile(oldModelFile)
	end

	response = newStepTrainEnd
	LogInfo("[RASA_APPS:TrainApp] 8 - END TRAIN. Success TRAINING app: '%s' vcc: '%s'. trainStatus: '%s'", info.appId, info.vcc, json.encode(response))

	-- retorno la app actualizada con la informacion publicado de los nodos en redis.
	local success, encodedApp, updatedApp = m_instanceOwner:GetApp(info.appId, info.vcc)

	return true, response, updatedApp
end

--[[
-----------------------------
TRAIN - GET LAST TRAIN APPLICATION STATUS
-----------------------------

]]--
function GetLastTrainAppStatus (info, keyspace)
	local success, allTrainSteps = GetAllTrainedAppStatus(info, keyspace)
	if not success then
		local error = allTrainSteps
		--LogError("[RASA_APPS:GetLastTrainAppStatus] Error GET Last TRAIN app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, error
	end

	local app = info.app
	local lastTrainStep = allTrainSteps[#app.trainInfo]
	--LogDebug("[RASA_APPS:GetLastTrainAppStatus] Last Train Step '%s' app '%s' vcc '%s'.", json.encode(lastTrainStep), info.appId, info.vcc)
	local lastTrainStepStatus = lastTrainStep[#lastTrainStep]
	--LogInfo("[RASA_APPS:GetLastTrainAppStatus] Last Train Step Status '%s' app '%s' vcc '%s'.", json.encode(lastTrainStepStatus), info.appId, info.vcc)

	return true, lastTrainStepStatus
end

--[[

## intent:IntentVacio

## intent:intent3
- Emma
- Emmita
- Ayelén

## intent:intent2
- Vallejo
- de Mello
- Aquino

## intent:intent1
- Estuve túbulo
- algo

## intent:Saludo
- Hola
- qué tal ?
- [buenos días](LaEntidadSaludo) como están todos ?
- Hola

## intent:None
- xxxxx
]]--

local function MakeContentNLUFile (info, keyspace)
	local entities -- Variable utilizada en (*)

	--[[
		Funcion encargada de Etiquetar los Utterances con las Entidades
	]]--
	local function MarkTextWithEntities (utterance)
		if not utterance or not utterance.text then
			-- Aca no debería pasar nunca, pero me cubro por las dudas, no vaya a ser que la export/import desde LUIS lleguen datos errones en el JSON !!!
			return ""
		end

		local text = utterance.text
		local entityLabels = utterance.entityLabels or {}
		local totalLabels = #entityLabels
		if totalLabels == 0 then
			-- Si el Utterance NO se ha etiquetado con ninguna Entidad
			return text
		end
		-- (*) A las Entidades de la BD, las voy a buscar una única vez
		entities = entities or rasaModels.VCCs.LoadEntities(info.vcc, info.appId, keyspace)

		local labels = {}
		local i = totalLabels
		local previousLbl

		-- 1 - Recorro desde la última a la primera de las "entityLabels" con las que se ha etiquetado el Utterance, y las agrego en un array que se procesara después (2).
		--
		while i > 0 do
			local lbl = entityLabels[i]
			lbl.entityId = lbl.entityId:lower()
			if entities[lbl.entityId] then
			-- Si la entidad con la que se ha etiquetado existe en la BD, procedo al marcado.
				if not previousLbl then
				-- Si es el primer label/entity que estmoa procesando, lo agrego
					table.insert(labels, lbl)
				else
				-- Si NO es el primer label/entity que estmoa procesando, tengo que validar que el que se ha procesado anterioremente NO este etiquetando a la misma Palabra
				--  Este es el caso de los HIERARCHICAL y los COMPOSITE de LUIS.
				--  Como en RASA NO los soporta, me quedo solo con un etiquetado (el del padre)
					if lbl.startCharIndex == previousLbl.startCharIndex and lbl.endCharIndex == previousLbl.endCharIndex then
					-- Elimino el etiquetado previo del array.
						table.remove(labels, #labels)
					end
					-- Ahora lo agrego.
					table.insert(labels, lbl)
				end
				previousLbl = lbl
			else
				LogWarning("[RASA_APPS:MakeContentNLUFile] Unable to MARK Utterance with Entity. Entity does not exist in DB.\nApp '%s' Vcc: '%s', Label Info: '%s'\nUtterance: '%s'", info.appId, info.vcc, json.encode(lbl), text)
			end
			i = i - 1
		end

		-- 2 - Ahora recorro el array de entity/labels que se creo en 1) y reemplazo las palabras por las entidades.
		--  
		local markedWordTemplate = "[%s](%s)" -- (**)
		local markedText = text

		for _, lbl in ipairs(labels) do
			-- Tengo que sumar 1 para obtener la palabra correctamente.
			local startCharIndex = lbl.startCharIndex + 1
			local endCharIndex = lbl.endCharIndex + 1
			-- Aca obtengo la palabra que se va a etiquetar
			local word = utf8.sub(markedText, startCharIndex, endCharIndex)
			-- Aca utilzo el template (**) y reemplazo la palabra por el formato que necesita RASA para el marcado. Ej:
			--   word
			--    [word](la_entidad)
			--
			local enitiy = entities[lbl.entityId]
			local entityName = enitiy.name or lbl.entityId
			--LogDebug("[RASA_APPS:MakeContentNLUFile] -----------------> lbl.entityId '%s' entityName: '%s', lbl Info: '%s'", lbl.entityId, entityName, json.encode(lbl))

			local markedWord = markedWordTemplate:format(word, entityName)
			-- Obtengo desde el inicio del Utterance, hasta donde comnienza la palabra que se esta etiquetando.
			local initialText = utf8.sub(markedText, 1, startCharIndex - 1)
			-- Obtengo desde el inicio del Utterance, hasta donde comnienza la palabra que se esta etiquetando.
			local endText = utf8.sub(markedText, endCharIndex + 1)

			--LogDebug(" --> %d --> word: '%s', entityId: '%s', markedWord: '%s'.\ninitialText: '%s',\nendText: '%s',", _, word, lbl.entityId, markedWord, initialText, endText)

			-- Ahora me quedo con el Utterance completo con la palabra YA etiquetada.
			--  concateno el inicio del utterance + la palabra etiquetada + el fin del utterance.
			markedText = initialText .. markedWord .. endText
		end

		--LogDebug(" ======> markedText: '%s'", markedText)

		return markedText
	end

	------------------------
	-- Comienzo la creación !!!
	------------------------
	local content = ""
	local intents = rasaModels.VCCs.LoadIntentsAndUtterances(info.vcc, info.appId, keyspace)

	local itentTemplate = "## intent:%s\n"
	local utteranceTemplate = "- %s\n"
	local contentUtterancesOfIntentNone

	-- Recorro la lista de intents
	for _, intent in pairs(intents) do
		if intent.name:upper() ~= "NONE" then
			-- (*) Proceso los Intents->Utterances que NO sean el NONE
			local contentIntent = itentTemplate:format(intent.name)
			content = content .. contentIntent
			for _, utterance in pairs(intent.utterances or {}) do
				-- Aca invoca a la funcion que realiza el etiquetado del Utterance con las Entidades.
				local markedText = MarkTextWithEntities(utterance)
				local contentUtterance = utteranceTemplate:format(markedText)
				content = content .. contentUtterance
			end
			if #content > 0 then
				content = content .. "\n"
			end
		else
			-- (**) Proceso los Intents->Utterances que SI sean el NONE.
			for _, utterance in pairs(intent.utterances or {}) do
				contentUtterancesOfIntentNone = contentUtterancesOfIntentNone or ""
				local contentUtterance = utteranceTemplate:format(utterance.text)
				contentUtterancesOfIntentNone = contentUtterancesOfIntentNone .. contentUtterance
			end
		end
	end

	local contentIntentNone = itentTemplate:format("None")
	local contentUtteranceNone
	if contentUtterancesOfIntentNone then
		-- (*) Si SI han llegando Utterances en el Intent NONE
		contentUtteranceNone = contentUtterancesOfIntentNone
	else
		-- (**) Si NO han llegando Utterances en el Intent NONE
		contentUtteranceNone = utteranceTemplate:format("xxxxx")
	end
	contentUtteranceNone = contentUtteranceNone .. "\n"

	content = content .. contentIntentNone
	content = content .. contentUtteranceNone

	return content
end

function MakeNLUFile (info, keyspace)
	assert(type(info) == "table")
	assert(info.appId)
	assert(info.vcc)

	local startTime = os.time()
	LogInfo("[RASA_APPS:MakeNLUFile] 1 - Start Making NLU File.")
	--LogDebug("[RASA_APPS:MakeNLUFile] --> - Start Making NLU File. Info: %s", json.encode(info))

	local directories = fileSystem.GetVccAppDirectories(info.vcc, info.appId)

	local content = MakeContentNLUFile(info, keyspace)
	LogInfo("[RASA_APPS:MakeNLUFile] 2 - End MAKING content file from cassandra.")
	local success, response = fileSystem.CreateFile(directories.fullNLUName, content)
	LogInfo("[RASA_APPS:MakeNLUFile] 3 - End CREATING file in file system.")
	if not success then
		LogError("[RASA_APPS:MakeNLUFile] Error Making NLU File '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

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

	LogInfo("[RASA_APPS:MakeNLUFile] Success Done NLU File app '%s'. ELAPSED Time '%s' seconds. Response: '%s'",  json.encode(info), elapsedTime, json.encode(response))

	return true, response
end

local function StarProcessCreateAppFileSystemStructure (info)
	LogInfo("[RASA_APPS:StarProcessCreateAppFileSystemStructure] Start Process Creating App File System Structure.", info.vcc, info.appId)

	local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, info.appId)
	--LogDebug("[RASA_APPS:StarProcessCreateAppFileSystemStructure] encodedApp %s", encodedApp)

	if not encodedApp then
		return false, {result = "UNABLE TO CREATE APP FILE SYSTEM STRUCTURE - APP DOES NOT EXIST", vcc = info.vcc, appId = info.appId}
	end
	local app = json.decode(encodedApp)
	local success, response = fileSystem.CreateAppFileSystemStructure(app)
	if not success then
		return false, response
	end

	-- Actualizo el lenguage/culture en el archivo de configuración "config.yml"
	UpdateLanguageConfigYmlFile(app)

	return true
end

--[[
-----------------------------
APPS - Create APP FileSystemStructure
-----------------------------
]]--
function CreateAppFileSystemStructure (info)
	assert(type(info) == "table")
	assert(type(info.vcc) == "string")
	assert(type(info.appId) == "string")

	local success, response = StarProcessCreateAppFileSystemStructure(info)
	if not success then
		LogError("[RASA_APPS:CreateAppFileSystemStructure] Unable to create App File System Structure. Error: '%s'", json.encode(response))
		return false, response
	end

	LogInfo("[RASA_APPS:CreateAppFileSystemStructure] Success Created appId %s File System Structure.", json.encode(info))

	return true
end

local function StarProcessDeleteAppFileSystemStructure (info)
	LogInfo("[RASA_APPS:StarProcessDeleteAppFileSystemStructure] Start Process Deleting App File System Structure. Info %s", json.encode(info))

	local app = info.appData
	-- 1 - Bajo la instancia del modelo de RASA de memoria (si es que la misma existe).
	runtime.DownRasaApp(info)

	-- 2 - Elimino el directorio de la app y toda su estructura.
	local success, response = RemoveAppDirectory(app)
	if not success then
		return false, response
	end

	return true
end

--[[
-----------------------------
APPS - Delete APP FileSystemStructure
-----------------------------
]]--
function DeleteAppFileSystemStructure (info)
	assert(type(info) == "table")
	assert(type(info.vcc) == "string")
	assert(type(info.appId) == "string")
	assert(type(info.appData) == "table")

	local success, response = StarProcessDeleteAppFileSystemStructure(info)
	if not success then
		LogError("[RASA_APPS:DeleteAppFileSystemStructure] Unable to Delete App File System Structure. Error: '%s'", json.encode(response))
		return false, response
	end

	LogInfo("[RASA_APPS:DeleteAppFileSystemStructure] Success Deleted appId %s File System Structure.", info.appId)

	return true
end

--[[
-----------------------------
TRAIN - CREATE TRAIN APPLICATION FILE SYSTEM STRUCTURE
-----------------------------
]]--
function CreateTrainAppFileSystem (info)
	assert(type(info) == "table")
	assert(info.appId)
	assert(info.vcc)

	local nodeIP = m_instanceOwner.GetWebHandlerServerIP()

	LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 1- START CREATING TRAIN FILES ON FILE SYSTEM app: '%s' vcc: '%s'. Node IP: '%s'", info.appId, info.vcc, nodeIP)

	-- 1 - Obtengo la App de Cassandra.
	local keyspace = utils.GetAgentKeySpace()
	local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, info.appId, keyspace)

	-- 1.1 - Valido que la App exista en Cassandra.
	if not encodedApp then
		keyspace:Close()
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP DOES NOT EXIST"}
		LogError("[RASA_APPS:CreateTrainAppFileSystem] Error TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		response.info = info
		return false, response
	end

	-- 2 - Obtengo el binario del model de la App de Cassandra.
	local withParts = true
	local appModelFile = rasaModels.VCCs.GetApplicationFile(info.vcc, info.appId, withParts, keyspace)
	-- LogDebug("[RASA_APPS:CreateTrainAppFileSystem] app: '%s' vcc: '%s'. appModelFile: %s", info.appId, info.vcc, appModelFile)
	-- for n, v in pairs(appModelFile or {}) do
	-- 	LogDebug("%s %s", n, v)
	-- end

	-- 2.1 - Valido que el archivo binario del Model de la App exista en Cassandra.
	if not appModelFile or not appModelFile.contentModelFile then
		keyspace:Close()
		local response = {result = "UNABLE TO CREATE TRAIN FILES - BINARY MODEL CONTENT FILE DOES NOT EXIST IN DATABASE"}
		LogError("[RASA_APPS:CreateTrainAppFileSystem] Error TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		response.info = info
		return false, response
	end

	-- 3 - Valido que la info de los directorios existan en la BD
	local app = json.decode(encodedApp)
	if not app.directories then
		keyspace:Close()
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP DIRECTORIES INFO DOES NOT EXIST IN DATABASE"}
		LogError("[RASA_APPS:CreateTrainAppFileSystem] Error TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		response.info = info
		return false, response
	end

	-- 4 - Valido que el directorio Base de la App en el VCC exista.
	local isDirectory = fileSystem.isDirectory(app.directories.appVccDir)
	if not isDirectory then
		-- 4.1 - Si no existe el directorio Base del App, intento crear la estructura de directorios.
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP BASE DIRECTORY DOES NOT EXIST", directory = app.directories.appVccDir}
		LogWarning("[RASA_APPS:CreateTrainAppFileSystem] Warning TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		local success, err = CreateAppFileSystemStructure(info)
		if not success then
			keyspace:Close()
			LogError("[RASA_APPS:CreateTrainAppFileSystem] Error TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
			response.info = info
			return false, response
		end
		-- Continuo ya que creo correctamente el Directorio Base de la App
	end

	-- 5 - Valido que el directorio del Model del Train de la App en el VCC exista.
	isDirectory = fileSystem.isDirectory(app.directories.modelDir)
	if not isDirectory then
		keyspace:Close()
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP MODEL DIRECTORY DOES NOT EXIST", directory = app.directories.appVccDir}
		LogError("[RASA_APPS:CreateTrainAppFileSystem] Error TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		response.info = info
		return false, response
	end

	-- 6 - Valido que el directorio del NLU Data de la App en el VCC exista.
	isDirectory = fileSystem.isDirectory(app.directories.dataDir)
	if not isDirectory then
		keyspace:Close()
		local response = {result = "UNABLE TO CREATE TRAIN FILES - APP NLU DATA DIRECTORY DOES NOT EXIST", directory = app.directories.appVccDir}
		LogError("[RASA_APPS:CreateTrainAppFileSystem] Error TRAINING app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		response.info = info
		return false, response
	end

	-- 7 - Creo el archivo del modelo "/model/elnombre.gz".
	LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 2- CREATING MODEL FILE ON FILE SYSTEM app: '%s' vcc: '%s''.", info.appId, info.vcc)
	local success, response = fileSystem.CreateFile(appModelFile.modelFileName, appModelFile.contentModelFile)
	--LogDebug("[RASA_APPS:CreateTrainAppFileSystem] 2.1 - End CREATING file in file system.")
	if not success then
		keyspace:Close()
		LogError("[RASA_APPS:CreateTrainAppFileSystem] Error Creating MODEL File '%s'. Error: '%s'", json.encode(info), json.encode(response))
		response.info = info
		return false, response
	end

	-- 8 - Obtengo el último Step del último Train.
	local success, lastTrainStepStatus = GetLastTrainAppStatus({
			vcc   = info.vcc,
			appId = info.appId,
			app   = app
		}, 
		keyspace
	)
	if success then
		-- 8.1 - Al último Step del último Train, le agrego la IP del Nodo que se ha actualizado.
		lastTrainStepStatus.updatedNodes = lastTrainStepStatus.updatedNodes or {}
		table.insert(lastTrainStepStatus.updatedNodes, nodeIP)
		local currentTrainSteps = table.remove(app.trainInfo, #app.trainInfo)
		table.insert(currentTrainSteps, errStepTimoutTrain)
		table.insert(app.trainInfo, currentTrainSteps)
		rasaModels.VCCs.UpdateApplication(info.vcc, info.appId, app, keyspace)
	end

	-- 9 - Creo el archivo NLU.
	LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 3 - CREATING NLU FILE ON FILE SYSTEM for app: '%s' vcc: '%s'.", info.appId, info.vcc)
	local success, response = MakeNLUFile(info, keyspace)
	keyspace:Close()

	if not success then
		LogWarning("[RASA_APPS:CreateTrainAppFileSystem] Unable to create NLU File for app '%s'. Error: '%s'", json.encode(info), json.encode(response))
	end

	-- 10 - Bajo la instancia de RASA para luego volver a subirla y que los cambios queden disponibles.
	-- Despues de realizar la copia del archivo del Model tengo que esperar unos segundos antes de realizar el Down del modelo de RASA, ya que sino las siguientes ejecuciones tiran el code error (30720) en el os.execute command line.
	LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 4 - Down RASA Model for app: '%s' vcc: '%s'.", info.appId, info.vcc)

	local waitSec = 10
	require("socket").sleep(waitSec)
	rasaCommandLine.DownRasaApp(info) -- TODO: Ver si realizo algo con las respuestas

	-- 11 - Subo nuevamente la instancia de RASA para que los cambios queden disponibles.
	-- Tengo que hacer una pausa para que haya bajado correctamente y luego volver a subirla.
	LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 5 - Up RASA Model for app: '%s' vcc: '%s'.", info.appId, info.vcc)

	info.app = app
	require("socket").sleep(2)
	rasaCommandLine.RunRasaApp(info)

	-- 12 - Si existia un Train previo debo eliminar su archivo model de HDD.
	if appModelFile.oldModelFileName  then
		LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 6 - Removing Old Model File '%s' app: '%s' vcc: '%s'.", appModelFile.oldModelFileName, info.appId, info.vcc)
		fileSystem.RemoveFile(appModelFile.oldModelFileName)
	end

	LogInfo("[RASA_APPS:CreateTrainAppFileSystem] 7 - END TRAIN. Success CREATED TRAIN FILES ON FILE SYSTEM. app: '%s' vcc: '%s'. Node IP: '%s'.", info.appId, info.vcc, nodeIP)

	return true, response
end

--[[
	Función encargada de validar el tiempo transcurrido del proceso de Train.
	Puede suceder de que por algún motivo el Train de Rasa no finalice, ya sea por caída del webhandler o cualquier otra eventualidad.
	Entonces hay que proceder a dejar el Step del Train cómo Fail.
    Esta validación la realiza según los steps del train, validando en el último Step por el atributo isRunning en true.
	Si se encuentra corriendo valida el tiempo transcurrido que no supere los 20 minutos (OVERFLOW_TIMES_SEC).
    En el caso de que haya superado el tiempo, inserta un último Step de Fail.
]]--
function ValidateRunningTrainTimeout (app, nowTime, keyspace)
	assert(app)
	nowTime = nowTime or LuaOrm.Date:time()

	local OVERFLOW_TIMES_SEC = 1200 -- 20 minutos en segundos
	--LogDebug("[RASA_APPS:ValidateRunningTrainTimeout] Validate Running Train Timeout By Last Step. app: '%s' vcc: '%s'. Now Time: %s. Maximum time allowed %s seconds.", app.id, app.vcc, nowTime, OVERFLOW_TIMES_SEC)

	-- 1 - Obtengo el último Step del último Train.
	local success, lastTrainStepStatus = GetLastTrainAppStatus({
			vcc   = app.vcc,
			appId = app.id,
			app   = app
		}, 
		keyspace
	)

	if not success then
		-- 2 -Nunca se ha entrenado la App.
		--
		--LogDebug("[RASA_APPS:ValidateRunningTrainTimeout] Never Trained app: '%s' vcc: '%s'.", app.id, app.vcc)
		return {success = true, result = "NEVER TRAINED"}
	end

	if not lastTrainStepStatus.isRunning then
		-- 3 - La App no se encuentra en un proceso de Train.
		--
		--LogDebug("[RASA_APPS:ValidateRunningTrainTimeout] Not Running: '%s' vcc: '%s'.", app.id, app.vcc)
		return {success = true, result = "NOT IS TRAINING"}
	end

	local startTrainDateTime = lastTrainStepStatus.startTrainDateTime
	local elapsedTimeSec = nowTime - startTrainDateTime

	--LogDebug("[RASA_APPS:ValidateRunningTrainTimeout] Running: '%s' vcc: '%s', startTrainDateTime: '%s', elapsedTime Seconds: '%s'.", app.id, app.vcc, startTrainDateTime, elapsedTimeSec)
	if elapsedTimeSec <= OVERFLOW_TIMES_SEC then
		-- 4 - La App SI se encuentra en un proceso de Train, pero dentro del tiempo permitido.
		--
		--LogDebug("[RASA_APPS:ValidateRunningTrainTimeout] Train process has not timed out. app: '%s' vcc: '%s'. Maximum time allowed %s seconds.", app.id, app.vcc, OVERFLOW_TIMES_SEC)
		return {success = true, result = "TRAINING BETWEEN ALLOWED TIME"}
	end

	-- 5 - La App SI se encuentra en un proceso de Train, y ha sobrepasado el tiempo permitido.
	--     Creo un Step de Fail y actualizo los Steps de último Train de la App.
	--
	local errStepTimoutTrain = utils.DeepCopy(lastTrainStepStatus)
	errStepTimoutTrain.message = "Rasa Rrain Timeout Err"
	errStepTimoutTrain.err = {err = "timemout"}
	errStepTimoutTrain.isRunning = false
	errStepTimoutTrain.statusId = "FAIL TRAIN"
	errStepTimoutTrain.step = errStepTimoutTrain.step + 1

	LogInfo("[RASA_APPS:ValidateRunningTrainTimeout] Abort Running Step Train for app '%s' vcc: '%s'. Has timed out in %s seconds.\nNew current Status: '%s'", app.id, app.vcc, elapsedTimeSec, json.encode(errStepTimoutTrain))

	local currentTrainSteps = table.remove(app.trainInfo, #app.trainInfo)
	table.insert(currentTrainSteps, errStepTimoutTrain)
	table.insert(app.trainInfo, currentTrainSteps)
	rasaModels.VCCs.UpdateApplication(app.vcc, app.id, app, keyspace)

	return
end
