local json = require "cjson"
local request = require "rasa.request"
local rasaCommandLine =  require "rasa.rasaCommandLine"
local utils = require "rasa.utils"
local rasaModels = require("rasa.models"):GetModels()
require "socket"

module(..., package.seeall)

local m_instanceOwner
function SetOwner (instanceOwner)
	m_instanceOwner = instanceOwner
end

function GetDefaultProtocol ()
	return "http"
end

function GetDefaultDomain ()
	return "localhost"
end

function GetDefaultPort ()
	return 5005
end

local function MakeCompleteURI (runtime, urlSuffix)
	assert(urlSuffix)
	local templateUrl = "%s://%s:%s/%s" -- sample: http://localhost:5005/model/parse
	local protocol = GetDefaultProtocol()
	local domain = GetDefaultDomain()
	local port = GetDefaultPort()
	if runtime then
		protocol = runtime.protocol or protocol
		domain = runtime.domain or domain
		port = runtime.port or port
	end
	local uri = templateUrl:format(protocol, domain, port, urlSuffix)
	--LogDebug("[RASA_RUNTIME:MakeCompleteURI] Make Complete URI '%s' - runtime: '%s'", uri, json.encode(runtime or {}))
	return uri
end


-- *******************************************************************************
-- API RUNTIME
-- *******************************************************************************

--[[
-----------------------------
RUN - APP STATUS
-----------------------------

	curl -XGET 127.0.0.1:5005/status

	Response:
		{
			"fingerprint": {
				"core-config": "977455427a0873dad8c7696e8b802567",
				"stories": 751098818652390000,
				"domain": 283583868163720000,
				"trained_at": 1613507662.9978,
				"config": "99914b932bd37a50b983c5e7c90ae93b",
				"version": "2.0.2",
				"nlg": "3e5947fe9ca08efc448ddf93ce745798",
				"messages": 423839203592270000,
				"nlu-config": "ff66761eed347a3dc96c2532afef9d0f"
			},
			"num_active_training_jobs": 0,
			"model_file": "/root/my_rasa/vccs/vcc1/botatlaslocalpablo_vccomni___clusterinconcertcc/models/nlu-20210216-203422.tar.gz"
		}

	Sample URI:
		curl -XGET 127.0.0.1:5005/status
]]--

function AppStatus (info)
	assert(type(info) == "table")

	------------------------------
	-- http://localhost:5005/status
	--
	local uriSuffix = "status"
	local uri = MakeCompleteURI(info.runtime, uriSuffix)

	local success, response = request.Get(uri)
	if not success then
		LogError("[RASA_RUNTIME:AppStatus] Error Executing App Status Info '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local encodedResponse = response
	LogInfo("[RASA_RUNTIME:AppStatus] Success Retrieving App Status information '%s'.", json.encode(info))
	--LogDebug("[RASA_RUNTIME:AppStatus] AppStatus - encodedResponse: '%s'", encodedResponse)

	local queryResponse = json.decode(encodedResponse)
	return true, queryResponse
end

--[[
-----------------------------
RUN - QUERY
-----------------------------

	curl -d '{"text":"Rocio"}' -H "Content-Type: application/json" -X POST http://localhost:5005/model/parse

	Response:
		{
		"text": "rocio",
		"intent": {
			"id": -7360103040756765000,
			"name": "intent3",
			"confidence": 0.9999941587
		},
		"entities": [],
		"intent_ranking": [
			{
			"id": -7360103040756765000,
			"name": "intent3",
			"confidence": 0.9999941587
			},
			{
			"id": 8788948334749256000,
			"name": "intent1",
			"confidence": 0.0000055969
			},
			{
			"id": -7412281749461472000,
			"name": "None",
			"confidence": 1.892e-7
			},
			{
			"id": 1862904215177507600,
			"name": "Saludo",
			"confidence": 9.96e-8
			}
		],
		"response_selector": {
			"all_retrieval_intents": [],
			"default": {
			"response": {
				"id": null,
				"response_templates": null,
				"confidence": 0,
				"intent_response_key": null,
				"template_name": "utter_None"
			},
			"ranking": []
			}
		}
		}

	Sample URI:
		http://localhost:5005/model/parse
]]--

--[[
	Funcion que instancia RASA (rasa run ...) con retry validando en el loop el status de la misma para saber si quedo ready.
]]--
local function StartRunAppWithRetries (info)
	assert(type(info) == "table")

	local maxRetries = 12
	local retry = 0
	local waitSec = 5
	local success, statusResponse
	local isRunning = false
	local runningResponse

	while true do
		retry = retry + 1
		LogInfo("[RASA_RUNTIME:StartRunAppWithRetries] Starting App '%s', Vcc '%s', Retry %d of %d every %d seconds.", info.appId, info.vcc, retry, maxRetries, waitSec)

		success, statusResponse = AppStatus(info)
		if success then
			LogDebug("[RASA_RUNTIME:StartRunAppWithRetries] Is Running & Ready App'%s', Vcc '%s', status %s.", info.appId, info.vcc, json.encode(statusResponse))
			break
		end

		if not isRunning then
			LogDebug("[RASA_RUNTIME:StartRunAppWithRetries] Not is Running  App'%s', Vcc '%s'. Starting Up ....", info.appId, info.vcc)
			isRunning, runningResponse = RunRasaApp({
				appId = info.appId,
				vcc = info.vcc
			})
			if not isRunning then
				LogDebug("[RASA_RUNTIME:StartRunAppWithRetries] Not is Running  App'%s', Vcc '%s'. Err: %s", info.appId, info.vcc, json.encode(runningResponse))
				statusResponse = runningResponse
				break
			end
		else
			LogDebug("[RASA_RUNTIME:StartRunAppWithRetries] Is Running App'%s', Vcc '%s'. Waiting for READY ...", info.appId, info.vcc)
		end

		if retry >= maxRetries then
			break
		end

		LogDebug("[RASA_RUNTIME:StartRunAppWithRetries] Waiting %d seconds.", waitSec)
		socket.sleep(waitSec)
	end

	if not success then
		local err = statusResponse
		LogWarning("[RASA_RUNTIME:StartRunAppWithRetries] Unable to Run App'%s', Vcc '%s'. Err: %s", info.appId, info.vcc, json.encode(err))
		return false, statusResponse
	end

	return true, statusResponse
end

--[[
	Funcion que valida el status de Model de RASA de una App/VCC, con retry validando en el loop el status de la misma para saber si quedo ready.
]]--
function StartAppStatusWithRetries (info)
	assert(type(info) == "table")

	local maxRetries = info.maxRetries or 12
	local retry = 0
	local waitSec = info.waitSec or 5
	local success, statusResponse
	local isRunning = false
	local runningResponse

	while true do
		retry = retry + 1
		LogInfo("[RASA_RUNTIME:StartAppStatusWithRetries] Starting App '%s', Vcc '%s', Retry %d of %d every %d seconds.", info.appId, info.vcc, retry, maxRetries, waitSec)

		success, statusResponse = AppStatus(info)
		if success then
			LogDebug("[RASA_RUNTIME:StartAppStatusWithRetries] Is Running & Ready App'%s', Vcc '%s', status %s.", info.appId, info.vcc, json.encode(statusResponse))
			break
		end

		if retry >= maxRetries then
			break
		end

		LogDebug("[RASA_RUNTIME:StartAppStatusWithRetries] Waiting %d seconds.", waitSec)
		socket.sleep(waitSec)
	end

	if not success then
		local err = statusResponse
		LogWarning("[RASA_RUNTIME:StartAppStatusWithRetries] Unable to Load Staus of App'%s', Vcc '%s'. Err: %s", info.appId, info.vcc, json.encode(err))
		return false, statusResponse
	end

	return true, statusResponse
end


--[[
-----------------------------
RUN - QUERY
-----------------------------

]]--

function Query (info)
	assert(type(info) == "table")
	assert(type(info.query) == "table")
	assert(type(info.query.text) == "string")

	------------------------------
	-- Valido si se encuentra una instancia de RASA para el modelo/app.
	--
	local success, statusResponse = AppStatus(info)
	if not success then
		LogWarning("[RASA_RUNTIME:Query] Is Not Ready RASA Models of App'%s', Vcc '%s', status %s.", info.appId, info.vcc, json.encode(statusResponse))
		return false, statusResponse
	end

	------------------------------
	-- http://localhost:5005/model/parse
	--
	local uriSuffix = "model/parse"
	local uri = MakeCompleteURI(info.runtime, uriSuffix)

	local queryInfo = {
		text = info.query.text
	}

	local success, response = request.Post(uri, queryInfo)
	if not success then
		LogError("[RASA_RUNTIME:Query] Error Executing Query Info '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local encodedResponse = response
	LogInfo("[RASA_RUNTIME:Query] Success Retrieving Query information '%s'.", json.encode(info))
	--LogDebug("[RASA_RUNTIME:Query] ----> Query - encodedResponse: '%s'", encodedResponse)

	local queryResponse = json.decode(encodedResponse)
	return true, queryResponse
end

--[[
-----------------------------
RUN - RASA APP (MODEL)
-----------------------------
]]--
function RunRasaApp (info)
	assert(type(info) == "table")
	assert(info.appId)
	assert(info.vcc)

	LogInfo("[RASA_RUNTIME:RunRasaApp] START RUNNING RASA app: '%s' vcc: '%s'.", info.appId, info.vcc)
	--LogDebug("[RASA_RUNTIME:RunRasaApp] START RUNNING RASA appInfo: '%s'.", json.encode(info))

	local isAlreadyRunning = rasaCommandLine.IsRunningRasaApp(info)
	if isAlreadyRunning then
		info.app = nil
		LogInfo("[RASA_RUNTIME:RunRasaApp] UNABLE TO RUN RASA APP '%s'. Already exist another instance of App.", json.encode(info))
		return true, {result = "ALREADY RUNNING INSTANCE"}
	end

	local appId = utils.SanitizeAppId(info.appId):lower()
	local app = info.app

	if not app then
		local encodedApp = rasaModels.VCCs.GetApplication(info.vcc, appId)
		if not encodedApp then
			info.app = nil
			local response = {result = "UNABLE TO RUN RASA APP - APP DOES NOT EXIST", info = info}
			LogError("[RASA_RUNTIME:RunRasaApp] Error RUNNING RASA app '%s'. Error: '%s'", json.encode(info), json.encode(response))
			return false, response
		end
		app = json.decode(encodedApp)
	end

	local appsModule = require "rasa.v2_0.apps"
	info.app = app

	local success, lastTrainStepStatus = appsModule.GetLastTrainAppStatus(info)
	if not success then
		info.app = nil
		local err = lastTrainStepStatus
		LogError("[RASA_RUNTIME:RunRasaApp] Error RUNNING RASA app '%s'. Error: '%s'", json.encode(info), json.encode(err))
		return false, err
	end

	if lastTrainStepStatus.statusId ~= "COMPLETED TRAIN" then
		info.app = nil
		local err = {result = "UNABLE TO RUN RASA APP - APP MUST BE IN STATE COMPLETED TRAIN", info = info, status = lastTrainStepStatus}
		LogError("[RASA_RUNTIME:RunRasaApp] Error RUNNING RASA app '%s'. Error: '%s'", json.encode(info), json.encode(err))
		return false, err
	end

	info.app = app
	local success, response = rasaCommandLine.RunRasaApp(info)
	info.app = nil
	if not success then
		LogError("[RASA_RUNTIME:RunRasaApp] Error RUNNING RASA app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_RUNTIME:RunRasaApp] END RUN RASA. Success RUNNING app '%s', vcc '%s'.", appId, info.vcc)

	return true, response
end

--[[
-----------------------------
RUN - RASA APP (MODEL)
-----------------------------
]]--
function DownRasaApp (info)
	assert(type(info) == "table")
	assert(info.appId)
	assert(info.vcc)

	LogInfo("[RASA_RUNTIME:DownRasaApp] START DOWN RASA app: '%s'.", json.encode(info))

	local isAlreadyRunning = rasaCommandLine.IsRunningRasaApp(info)
	if not isAlreadyRunning then
		info.app = nil
		LogInfo("[RASA_RUNTIME:DownRasaApp] UNABLE TO GOING DOWN RASA APP '%s'. Does not exist running instance of App.", json.encode(info))
		return false, err
	end

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

	if not encodedApp then
		local response = {result = "UNABLE TO GOING DOWN APP - APP DOES NOT EXIST", info = info}
		LogError("[RASA_RUNTIME:DownRasaApp] Error GOING DOWN RASA app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local app = json.decode(encodedApp)
	local appsModule = require "rasa.v2_0.apps"
	info.app = app

	local success, lastTrainStepStatus = appsModule.GetLastTrainAppStatus(info)
	if success then
		if lastTrainStepStatus.statusId ~= "COMPLETED TRAIN" then
			local err = {result = "UNABLE TO GOING DOWN RASA APP -  THERE IS A PROCESS OF RUNNING THE APP TRAIN", info = info, status = lastTrainStepStatus}
			LogError("[RASA_RUNTIME:DownRasaApp] Error GOING DOWN RASA app '%s'. Error: '%s'", json.encode(info), json.encode(err))
			return false, err
		end
	end

	local success, response = rasaCommandLine.DownRasaApp(info)
	if not success then
		LogError("[RASA_RUNTIME:DownRasaApp] Error GOING DOWN RASA app '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	LogInfo("[RASA_RUNTIME:DownRasaApp] END DOWN RASA. Success DOWN app '%s', vcc '%s'.", appId, info.vcc)

	return true, response
end

--[[
-----------------------------
RUN - RASA ALL APPs of VCC (MODELS)
-----------------------------
]]--
function RunRasaVccApps (info)
	assert(type(info) == "table")
	assert(info.vcc)

	--LogInfo("[RASA_RUNTIME:RunRasaVccApps] START RUNNING RASA ALL APPs of VCC: '%s'.", json.encode(info))

	local vccExist = rasaModels.VCCs.VCCExists(info.vcc)
	if not vccExist then
		local response = {result = "UNABLE TO RUN ALL APPs OF VCC - VCC DOES NOT EXIST", info = info}
		LogError("[RASA_RUNTIME:RunRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local encodedBotInfo = rasaModels.VCCs.GetVccColumn(info.vcc, "BotInfo")
	--LogDebug("encodedBotInfo %s", encodedBotInfo)
	if not encodedBotInfo then
		local response = {result = "UNABLE TO RUN ALL APPs OF VCC - VCC NOT CONFIGURED BOT", info = info}
		--LogInfo("[RASA_RUNTIME:RunRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local botInfo = json.decode(encodedBotInfo)
	if botInfo.Type ~= "RASA" then
		local response = {result = "UNABLE TO RUN ALL APPs OF VCC - VCC NOT CONFIGURED RASA BOT", info = info}
		--LogInfo("[RASA_RUNTIME:RunRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	if not botInfo.Enabled then
		local response = {result = "UNABLE TO RUN ALL APPs OF VCC - VCC HAS DISABLED RASA BOT", info = info}
		--LogInfo("[RASA_RUNTIME:RunRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local apps = rasaModels.VCCs.GetApplications(info.vcc)
	if not next(apps) then
		local response = {result = "UNABLE TO RUN ALL APPs OF VCC - VCC NOT HAS APPS", info = info}
		LogInfo("[RASA_RUNTIME:RunRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local totalApps = utils.GetRowCount(apps)

	LogInfo("[RASA_RUNTIME:RunRasaVccApps] RUNNING %d Apps of VCC '%s'.", totalApps, info.vcc)

	local response = {vcc = info.vcc, totalApps = totalApps, runnedApps = {}, noRunnedApps = {}}
	for appId, app in pairs(apps) do
		LogDebug("")
		local appInfo = {
			vcc = info.vcc,
			appId = appId,
			app = app
		}
		local success, result = RunRasaApp(appInfo)
		if success then
			table.insert(response.runnedApps, {appId = appId})
		else
			table.insert(response.noRunnedApps, {appId = appId, error = result})
		end
	end
	LogDebug("")

	if response.totalApps == #response.runnedApps then
		LogInfo("[RASA_RUNTIME:RunRasaVccApps] RAN ALL APPs of VCC '%s'.", json.encode(info))
		return true, response
	elseif response.totalApps == #response.noRunnedApps then
		local result = {result = "UNABLE TO RUN ALL APPs OF VCC - NO VCC APP HAS RUN", info = response}
		LogWarning("[RASA_RUNTIME:RunRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(result))
		return false, result
	else
		local result = {result = "SOME VCC APPS HAVE BEEN RUN", info = response}
		LogInfo("[RASA_RUNTIME:RunRasaVccApps] %d RASA apps have been run of VCC %s.\nRunned Apps: %s", #response.runnedApps, info.vcc, json.encode(response.runnedApps))
		LogInfo("[RASA_RUNTIME:RunRasaVccApps] %d RASA apps have NOT been run of VCC %s.\nApps Without Run: %s", #response.noRunnedApps, info.vcc, json.encode(response.noRunnedApps))
		return true, result
	end
end

--[[
-----------------------------
DOWN - RASA ALL APPs of VCC (MODELS)
-----------------------------
]]--
function DownRasaVccApps (info)
	assert(type(info) == "table")
	assert(info.vcc)

	LogInfo("[RASA_RUNTIME:DownRasaVccApps] START DOWN RASA ALL APPs of VCC: '%s'.", json.encode(info))

	local vccExist = rasaModels.VCCs.VCCExists(info.vcc)
	if not vccExist then
		local response = {result = "UNABLE TO GOING DOWN ALL APPs OF VCC - VCC DOES NOT EXIST", info = info}
		LogError("[RASA_RUNTIME:DownRasaVccApps] Error GOING DOWN ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local encodedBotInfo = rasaModels.VCCs.GetVccColumn(info.vcc, "BotInfo")
	--LogDebug("encodedBotInfo %s", encodedBotInfo)
	if not encodedBotInfo then
		local response = {result = "UNABLE TO GOING DOWN ALL APPs OF VCC - VCC NOT CONFIGURED BOT", info = info}
		--LogInfo("[RASA_RUNTIME:DownRasaVccApps] Error GOING DOWN ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local botInfo = json.decode(encodedBotInfo)
	if botInfo.Type ~= "RASA" then
		local response = {result = "UNABLE TO GOING DOWN ALL APPs OF VCC - VCC NOT CONFIGURED RASA BOT", info = info}
		--LogInfo("[RASA_RUNTIME:DownRasaVccApps] Error GOING DOWN ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	if not botInfo.Enabled then
		local response = {result = "UNABLE TO GOING DOWN ALL APPs OF VCC - VCC HAS DISABLED RASA BOT", info = info}
		--LogInfo("[RASA_RUNTIME:DownRasaVccApps] Error GOING DOWN ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local apps = rasaModels.VCCs.GetApplications(info.vcc)
	if not next(apps) then
		local response = {result = "UNABLE TO GOING DOWN ALL APPs OF VCC - VCC NOT HAS APPS", info = info}
		LogInfo("[RASA_RUNTIME:DownRasaVccApps] Error GOING DOWN ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(response))
		return false, response
	end

	local totalApps = utils.GetRowCount(apps)

	LogInfo("[RASA_RUNTIME:DownRasaVccApps] DOWN %d Apps of VCC '%s'.", totalApps, info.vcc)

	local response = {vcc = info.vcc, totalApps = totalApps, goingDownApps = {}, notBeenDownApps = {}}
	for appId, app in pairs(apps) do
		LogDebug("")
		local appInfo = {
			vcc = info.vcc,
			appId = appId,
			app = app
		}
		local success, result = DownRasaApp(appInfo)
		if success then
			table.insert(response.goingDownApps, {appId = appId})
		else
			table.insert(response. notBeenDownApps, {appId = appId, error = result})
		end
	end
	LogDebug("")

	if response.totalApps == #response.goingDownApps then
		LogInfo("[RASA_RUNTIME:DownRasaVccApps] GOING DOWN ALL APPs of VCC '%s'.", json.encode(info))
		return true, response
	elseif response.totalApps == #response. notBeenDownApps then
		local result = {result = "UNABLE TO GOING DOWN ALL APPs OF VCC - NO VCC APP HAS RUN", info = response}
		LogWarning("[RASA_RUNTIME:DownRasaVccApps] Error RUNNING ALL APPs of VCC '%s'. Error: '%s'", json.encode(info), json.encode(result))
		return false, result
	else
		local result = {result = "SOME VCC APPS HAVE BEEN GOING DOWN", info = response}
		LogInfo("[RASA_RUNTIME:DownRasaVccApps] %d RASA apps have been going down.\nDowned Apps: %s", #response.goingDownApps, json.encode(response.goingDownApps))
		LogInfo("[RASA_RUNTIME:DownRasaVccApps] %d RASA apps have NOT been down.\nApps Without Down: %s", #response. notBeenDownApps, json.encode(response. notBeenDownApps))
		return true, result
	end
end

--[[
-----------------------------
RUN - RASA ALL APPs of ALL VCC (MODELS)
-----------------------------
]]--
function RunRasaAllVccsApps ()
	LogInfo("[RASA_RUNTIME:RunRasaAllVccsApps] START RUNNING RASA ALL APPs of ALL VCC.")

	local columns = {"Id"}
	local vccs = rasaModels.VCCs.LoadVCCs(columns)
	--LogDebug("[RASA_RUNTIME:RunRasaAllVccsApps] vccs: %s.", json.encode(vccs or {}))
	for vccId in pairs(vccs or {}) do
		RunRasaVccApps({vcc = vccId})
	end
end

--[[
-----------------------------
DOWN - RASA ALL APPs of ALL VCC (MODELS)
-----------------------------
]]--
function DownRasaAllVccsApps ()
	LogInfo("[RASA_RUNTIME:DownRasaAllVccsApps] START DOWN RASA ALL APPs of ALL VCC.")

	local columns = {"Id"}
	local vccs = rasaModels.VCCs.LoadVCCs(columns)
	--LogDebug("[RASA_RUNTIME:DownRasaAllVccsApps] vccs: %s.", json.encode(vccs or {}))
	for vccId in pairs(vccs or {}) do
		DownRasaVccApps({vcc = vccId})
	end
end

--[[
-----------------------------
STOP - RASA
-----------------------------
]]--
function StopRasa ()
	LogInfo("[RASA_RUNTIME:StopRasa] STOPING RASA.")

	local success, response = rasaCommandLine.StopRasa()
	if not success then
		LogError("[RASA_RUNTIME:StopRasa] Error STOPING RASA. Error: '%s'", json.encode(response))
		return false, response
	end

	LogInfo("[RASA_RUNTIME:DownRasaApp] STOPPED RASA.")

	return true, response
end
