require "LuaOrm.Date"
local json = require "cjson"
local utils = require "rasa.utils"
local rasa_models = require("rasa.models"):GetModels()
require "uuid"
local utf8 = require "icu.utf8"

module(..., package.seeall)

local m_instanceOwner
function SetOwner (instanceOwner)
	m_instanceOwner = instanceOwner
end

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

--[[
-----------------------------
MODELS - GET INTENT INFO
-----------------------------
	Gets information about the intent model.

	Referencia:
]]--
function GetAppIntent (info)
	assert(info)
	assert(info.id)
	assert(info.appId)
	assert(info.vcc)

	local encodedIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.id)
	if not encodedIntent then
		local response = {result = "INTENT DOES NOT EXIST", info = info}
		LogError("[RASA_INTENTS:Get] Error getting APP INTENTS '%s' Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_INTENTS:GetAppIntent] Success get APP INTENTS '%s.", json.encode(info))

	--LogDebug("[RASA_INTENTS:GetAppIntent] info: '%s', data: '%s'", json.encode(info), encodedIntent)
	return true, encodedIntent
end

local function StarProcessCreateIntentInApp (info)
	LogInfo("[RASA_INTENTS:StarProcessCreateIntentInApp] Start Process Creating Intent in app %s.", json.encode(info))

	local keyspace = utils.GetAgentKeySpace()
	info.id = info.name:lower()

	local encodedApp = rasa_models.VCCs.GetApplication(info.vcc, info.appId, keyspace)
	if not encodedApp then
		keyspace:Close()
		return false, {result = "UNABLE TO CREATE INTENT IN APP - APP DOES NOT EXIST", info = info}
	end

	local encodedOldIntent = rasa_models.VCCs.GetIntentByName(info.vcc, info.appId, info.name, keyspace)
	if encodedOldIntent then
		keyspace:Close()
		return false, {result = "UNABLE TO CREATE INTENT - INTENT ALREADY EXIST", info = info, alreadyIntent = json.decode(encodedOldIntent)}
	end

	local intentId = info.id
	local encodedCurrentIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.id, keyspace)
	if encodedCurrentIntent then
		-- Esto se puede dar si el intent que se esta creando, tiene el mimsmo id que un intent que exite con el mismo id, pero se ha cambiado su nombre.
		intentId = ("%s_%s"):format(intentId, uuid.new())
		LogInfo("[RASA_INTENTS:StarProcessCreateIntentInApp] Making random intentId '%s' in app %s.", intentId, json.encode(info))
	end

	info.id = intentId
	info.fullId = utils.MakeIntentId(info.vcc, info.appId, intentId)
	info.createdDateTime = LuaOrm.Date:time()
	info.creatorUserId = info.userId or "ANONYMOUS"
	info.userId = nil

	rasa_models.VCCs.UpdateIntent(info.vcc, info.appId, intentId, info, keyspace)
	keyspace:Close()


	return true, info
end

--[[
-----------------------------
MODELS - CREATE INTENT CLASSIFIER
-----------------------------
	Adds an intent classifier to the application.
]]--
function CreateAppIntent (info)
	assert(type(info) == "table")
	assert(info.vcc)
	assert(info.appId)
	assert(info.name)

	local success, response = StarProcessCreateIntentInApp(info)
	if not success then
		LogError("[RASA_INTENTS:CreateAppIntent] Error creating APP INTENTS on info '%s' vcc: '%s'. Response: '%s'", info.appId, info.vcc, json.encode(response))
		return false, response, info
	end

	local intentId = response.id
	LogInfo("[RASA_INTENTS:CreateAppIntent] Success created APP INTENT '%s' vcc: '%s', intentId: '%s'.", info.appId, info.vcc, intentId)

	return true, intentId, info
end

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

	local encodedApp = rasa_models.VCCs.GetApplication(info.vcc, info.appId)
	if not encodedApp then
		return false, {result = "UNABLE TO RENAME INTENT - APP DOES NOT EXIST", info = info}
	end

	local encodedOldIntent = rasa_models.VCCs.GetIntentByName(info.vcc, info.appId, info.name, keyspace)
	if encodedOldIntent then
		return false, {result = "UNABLE TO RENAME INTENT - INTENT ALREADY EXIST", info = info, alreadyIntent = json.decode(encodedOldIntent)}
	end

	if info.intentId:lower() == "none" then
		return false, {result = "UNABLE TO RENAME INTENT - INTENT NONE IS THE SYSTEM", info = info}
	end

	local encodedIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.intentId)
	local currentIntent = json.decode(encodedIntent)
	currentIntent.oldName = currentIntent.name
	currentIntent.name = info.name
	currentIntent.lastModifiedDateTime = LuaOrm.Date:time()
	currentIntent.lastModifiedUserId = info.userId or "ANONYMOUS"
	info.userId = nil

	rasa_models.VCCs.UpdateIntent(info.vcc, info.appId, info.intentId, currentIntent)

	return true, info
end

--[[
-----------------------------
MODELS - RENAME INTENT MODEL
-----------------------------
	Updates the name of an intent classifier.

]]--
function RenameAppIntent (info)
	assert(type(info) == "table")
	assert(type(info.intentId) == "string")
	assert(type(info.appId) == "string")
	assert(type(info.vcc) == "string")
	assert(type(info.name) == "string")

	local success, response = StarProcessRenameAppIntent(info)
	if not success then
		LogError("[RASA_INTENTS:RenameAppIntent] Unable to Renaming APP INTENTS. Error: '%s'", json.encode(response))
		return false, response
	end

	LogInfo("[RASA_INTENTS:RenameAppIntent] Success Renamed APP INTENTS %s.", json.encode(info))

	return true, response
end

local function StarProcessDeleteIntent (info)
	LogInfo("[RASA_APPS:StarProcessDeleteIntent] Start Process Deleting Intent %s.", json.encode(info))

	local encodedApp = rasa_models.VCCs.GetApplication(info.vcc, info.appId)
	if not encodedApp then
		return false, {result = "UNABLE TO DELETE INTENT - APP DOES NOT EXIST", info = info}
	end

	local encodedIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.id)
	if not encodedIntent then
		return false, {result = "UNABLE TO DELETE INTENT - INTENT DOES NOT EXIST", info = info}
	end

	if info.id:lower() == "none" then
		return false, {result = "UNABLE TO DELETE INTENT - INTENT NONE IS THE SYSTEM", info = info}
	end

	local intent = json.decode(encodedIntent)
	rasa_models.VCCs.RemoveIntent(info.vcc, info.appId, info.id, intent.name)

	return true, info
end

--[[
-----------------------------
MODELS - DELETE INTENT MODEL
------------------------------
	Deletes an intent classifier from the application.
]]--
function DeleteAppIntent (info)
	assert(type(info) == "table")
	assert(info.vcc)
	assert(info.appId)
	assert(info.id)

	----
	local success, response = StarProcessDeleteIntent(info)
	if not success then
		LogError("[RASA_INTENTS:DeleteAppIntent] Deleting APP INTENT '%s' Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_INTENTS:DeleteAppIntent] Success deleted APP INTENT %s.", json.encode(info))

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

--[[
-----------------------------
API - ADD NEW UTTERANCE 
-----------------------------
]]--
local function StarProcessAddUtteranceToIntent (info)
	LogInfo("[RASA_INTENTS:StarProcessAddUtteranceToIntent] Start Process Adding Utterance to Intent: %s.", json.encode(info))

	info.utterance = utils.SanitizeUtterance(info.utterance)
	-- El Id del utterance
	info.id = info.utterance

	local encodedApp = rasa_models.VCCs.GetApplication(info.vcc, info.appId)
	if not encodedApp then
		return false, {result = "UNABLE TO ADD UTTERANCE TO INTENT - APP DOES NOT EXIST", info = info}
	end

	local encodedIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.intentId)
	if not encodedIntent then
		return false, {result = "UNABLE TO ADD UTTERANCE TO INTENT - INTENT DOES NOT EXIST", info = info}
	end

	--LogDebug("1- info.id: '%s'", info.id)
	info.id = utils.alltrim(utf8.lower(info.id))
	--LogDebug("2- info.id: '%s'", info.id)

	local encodedUtterance = rasa_models.VCCs.GetUtterance(info.vcc, info.appId, info.intentId, info.id)
	--LogDebug("--> encodedUtterance %s", encodedUtterance)
	if encodedUtterance then
		return false, {result = "UNABLE TO ADD UTTERANCE TO INTENT - UTTERANCE ALREADY EXIST", info = info, alreadyUtterance = json.decode(encodedUtterance)}
	end

	-- Esto se realiza para mantener la compatibiliad con la estructura de LUIS
	info.text = info.utterance
	info.utterance = nil

	info.fullId = utils.MakeUtteranceId(info.vcc, info.appId, info.intentId, info.id)
	info.createdDateTime = LuaOrm.Date:time()
	info.creatorUserId = info.userId or "ANONYMOUS"
	info.userId = nil

	-- START - Tokenized ----------------
	local text = info.text
	local startPosition = 1
	local endPosition = nil
	local separator = " "
	local index = 0
	--
	info.tokenizedText = {} -- array con los tokenized text utilizados del lado del cliente.

	local function Insert (word)
		if #word > 0 then
			-- inserto la palabra en el array de tokenized text
			word = utf8.lower(word)
			table.insert(info.tokenizedText, word)
			index = index + 1
			--LogDebug(" %s --> word: '%s' length: '%s'.", index, word, utf8.len(word))
		end
	end

	-- Recorro las palabras existentes y las parseo.
	----LogDebug(" text: %s", text)
	while true do
		endPosition = utf8.find(text, separator, startPosition, endPosition, true)
		if not endPosition then
			local value = utf8.sub(text, startPosition)
			endPosition = (startPosition - 1) + utf8.len(value)
			Insert(value)
			break
		end
		local value = utf8.sub(text, startPosition, endPosition - 1)
		Insert(value)
		startPosition = endPosition + 1
		endPosition = nil
	end
	--LogDebug("1- tokenizedText: %s", json.encode(info.tokenizedText))
	--
	-- END Tokenized ------------------

	-- Actualizo en el modelo de Cassandra
	rasa_models.VCCs.UpdateUtterance(info.vcc, info.appId, info.intentId, info.id, info)

	return true, info
end

--[[
-----------------------------
API - ADD NEW UTTERANCE
-----------------------------
]]--
function AddUtteranceAppIntent (info)
	assert(type(info) == "table")
	assert(info.vcc)
	assert(info.appId)
	assert(info.intentId)
	assert(info.utterance)

	local success, response = StarProcessAddUtteranceToIntent(info)
	if not success then
		LogError("[RASA_INTENTS:AddUtteranceAppIntent] Error Adding UTTERANCE TO INTENT info '%s'. Response: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local newUtteranceId = response.id
	LogInfo("[RASA_INTENTS:AddUtteranceAppIntent] Success Added UTTERANCE TO INTENT info: '%s'. New Utterance Id: '%s'.", json.encode(info), newUtteranceId)

	return true, newUtteranceId, response
end

--[[
-----------------------------
ADD or REMOVE LABELS to UTTERANCE
-----------------------------
]]--

local function StarProcessUpdateLabelsInUtterance (info)
	LogInfo("[RASA_INTENTS:StarProcessUpdateLabelsInUtterance] Start Process Updating Labels in Utterance to Intent: %s.", json.encode(info))

	info.utterance = utils.SanitizeUtterance(info.utterance)
	-- El Id del utterance
	info.id = info.utterance

	local encodedApp = rasa_models.VCCs.GetApplication(info.vcc, info.appId)
	if not encodedApp then
		return false, {result = "UNABLE TO UPDATE LABELS IN UTTERANCE TO INTENT - APP DOES NOT EXIST", info = info}
	end

	local encodedIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.intentId)
	if not encodedIntent then
		return false, {result = "UNABLE TO UPDATE LABELS IN UTTERANCE TO INTENT - INTENT DOES NOT EXIST", info = info}
	end

	--LogDebug("1- info.id: '%s'", info.id)
	info.id = utils.alltrim(utf8.lower(info.id))
	--LogDebug("2- info.id: '%s'", info.id)

	local oldUtterance
	local encodedUtterance = rasa_models.VCCs.GetUtterance(info.vcc, info.appId, info.intentId, info.id)
	--LogDebug("--> encodedUtterance %s", encodedUtterance)
	if not encodedUtterance then
		return false, {result = "UNABLE TO UPDATE LABELS IN UTTERANCE TO INTENT - UTTERANCE DOES NOT EXIST", info = info}
	end

	oldUtterance = json.decode(encodedUtterance)

	-- Esto se realiza para mantener la compatibiliad con la estructura de LUIS
	info.text = info.utterance
	info.utterance = nil

	info.fullId = utils.MakeUtteranceId(info.vcc, info.appId, info.intentId, info.id)
	info.lastUpdateDateTime = LuaOrm.Date:time()
	info.userId = nil

	-- START - Tokenized ----------------
	local text = info.text
	local startPosition = 1
	local endPosition = nil
	local separator = " "
	local index = 0
	local isLastWord = false
	--
--	info.tokenizedText = {} -- array con los tokenized text utilizados del lado del cliente.
	local tokenizedStartCharsPos = {} -- info que nos sirve para saber en que "charindex" comienza una palabra y en que posición ocupa la palabra dentro del utterance.
	local tokenizedEndCharsPos = {} -- info que nos sirve para saber en que "charindex" termina una palabra y en que posición ocupa la palabra dentro del utterance.

	local function Insert (word)
		if #word > 0 then
--			-- inserto la palabra en el array de tokenized text
			word = utf8.lower(word)
--			table.insert(info.tokenizedText, word)

			-- Actualizo las listas con las posiciones "start" y "end" de los "charindex"
			-- Siempre tengo que restar 1 (ultima palabra) o 2 (otras palabras), ya que del lado del cliente en JS los array comienzan en 0 (cero)
			local myEndPosition = endPosition - (isLastWord and 1 or 2)
			local myStartPosition = startPosition - 1

			local info = {word = word, index = index, startCharIndex = myStartPosition, endCharIndex = myEndPosition}
			tokenizedStartCharsPos[tostring(myStartPosition)] = info
			tokenizedEndCharsPos[tostring(myEndPosition)] = info

			index = index + 1

			--LogDebug(" %s --> word: '%s' length: '%s', startCharIndex: '%s', endCharIndex: '%s'.", index, word, utf8.len(word),  myStartPosition, myEndPosition)
		end
	end

	-- Recorro las palabras existentes y las parseo.
	----LogDebug(" text: %s", text)
	while true do
		endPosition = utf8.find(text, separator, startPosition, endPosition, true)
		if not endPosition then
			isLastWord = true
			local value = utf8.sub(text, startPosition)
			endPosition = (startPosition - 1) + utf8.len(value)
			Insert(value)
			break
		end
		local value = utf8.sub(text, startPosition, endPosition - 1)
		Insert(value)
		startPosition = endPosition + 1
		endPosition = nil
	end
	--LogDebug("1- tokenizedText: %s", json.encode(info.tokenizedText))
	--LogDebug("2- tokenizedStartCharsPos: %s", json.encode(tokenizedStartCharsPos))
	--LogDebug("3- tokenizedEndCharsPos: %s", json.encode(tokenizedEndCharsPos))

	--
	-- END Tokenized ------------------

	-- Opcional
	-----------
	--   Labels/Utterances: Inserto en el array el etiquetado de Utterances.
	if type(info.entityLabels) == "table" then
		local entityLabels = utils.DeepCopy(info.entityLabels)
		info.entityLabels = {}
		for _, lbl in pairs(entityLabels) do
			local entityLabel = utils.DeepCopy(lbl)
			assert(lbl.entityId)
			local entityId = lbl.entityId
			entityLabel.entityId    = entityId
			entityLabel.intentId    = info.intentId:lower()
			entityLabel.utteranceId = info.id

			local startWord = tokenizedStartCharsPos[tostring(entityLabel.startCharIndex or 0)]
			local endWord = tokenizedEndCharsPos[tostring(entityLabel.endCharIndex or 0)]

			-- LogDebug("4- startCharIndex: '%s' startWord: %s", entityLabel.startCharIndex, json.encode(startWord or {}))
			-- LogDebug("5- endCharIndex: '%s' endWord: %s", entityLabel.endCharIndex, json.encode(endWord or {}))

			if startWord and endWord then
				-- El tokenized index (start y end) utilzado del lado del cliente para marcar visualmente las palabras etiquetadas con las entidades.
				entityLabel.startTokenIndex = startWord.index
				entityLabel.endTokenIndex = endWord.index
			end

			table.insert(info.entityLabels, entityLabel)
		end
	end

	-- Actualizo en el modelo de Cassandra
	rasa_models.VCCs.UpdateUtterance(info.vcc, info.appId, info.intentId, info.id, info, nil, nil, oldUtterance)

	return true, info
end

--[[
-----------------------------
API - ADD or REMOVE LABELS to UTTERANCE
-----------------------------
]]--

function UpdateUtteranceLabels (info)
	assert(type(info) == "table")
	assert(info.vcc)
	assert(info.appId)
	assert(info.intentId)
	assert(info.utterance)

	local success, response = StarProcessUpdateLabelsInUtterance(info)
	if not success then
		LogError("[RASA_INTENTS:UpdateUtteranceLabels] Error Uptading Labels in UTTERANCE TO INTENT info '%s'. Response: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local utteranceId = response.id
	LogInfo("[RASA_INTENTS:UpdateUtteranceLabels] Success Updated Labels in UTTERANCE TO INTENT info: '%s'. Utterance Id: '%s'.", json.encode(info), newUtteranceId)

	return true, utteranceId, response
end

local function StarProcessDeleteUtterance (info)
	LogInfo("[RASA_APPS:StarProcessDeleteUtterance] Start Process Deleting Utterance %s.", json.encode(info))

	local encodedApp = rasa_models.VCCs.GetApplication(info.vcc, info.appId)
	if not encodedApp then
		return false, {result = "UNABLE TO DELETE UTTERANCE - APP DOES NOT EXIST", info = info}
	end

	local encodedIntent = rasa_models.VCCs.GetIntent(info.vcc, info.appId, info.intentId)
	if not encodedIntent then
		return false, {result = "UNABLE TO DELETE UTTERANCE - INTENT DOES NOT EXIST", info = info}
	end

	local encodedUtterance = rasa_models.VCCs.GetUtterance(info.vcc, info.appId, info.intentId, info.id)
	if not encodedUtterance then
		return false, {result = "UNABLE TO DELETE UTTERANCE - UTTERANCE DOES NOT EXIST", info = info}
	end

	local mustUpdateAppInfo = true
	rasa_models.VCCs.RemoveUtterance(info.vcc, info.appId, info.intentId, info.id, mustUpdateAppInfo)

	return true, info
end


--[[
-----------------------------
EXAMPLES - DELETE EXAMPLE LABELS
-----------------------------

]]--
function DeleteAppUtterance (info)
	assert(type(info) == "table")
	assert(info.vcc)
	assert(info.appId)
	assert(info.intentId)
	assert(info.id)

	----
	local success, response = StarProcessDeleteUtterance(info)
	if not success then
		LogError("[RASA_INTENTS:DeleteAppUtterance] Deleting APP UTTERANCE '%s' Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_INTENTS:DeleteAppUtterance] Success deleted APP UTTERANCE %s.", json.encode(info))

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