pcall(require, "luarocks.require")

local utf8 = require "icu.utf8"
local json = require "cjson"
require("uuid")

local m_helpers = require "helpers"

module(..., package.seeall)
 
-- UTILS ----------------------------
LogInfo = LogInfo or function(s)
	print(s)
end
LogDebug = LogDebug or LogInfo
LogError = LogError or LogInfo
LogWarning = LogWarning or LogInfo

function ltrim (s) return (s or ""):gsub('^%s*','') end
function rtrim (s) return (s or ""):gsub('%s*$','') end
function alltrim (s) return rtrim(ltrim(s)) end

local http_request = nil
local function GetHttpRequest()
	if http_request == nil then
		http_request = require "shared.http.request"
	end
	return http_request
end

local function GetId (idAtVcc)
	local id = (idAtVcc or ""):match("(.+)@.+")
	return id or idAtVcc
end

local function AppendVcc (id_, vcc_)
	id_ = id_ or ""
	vcc_ = vcc_ or ""
	local id, vcc = id_:match("(.+)@(.+)")
	if id and vcc then
		return id.."@"..vcc_
	else
		return id_.."@"..vcc_
	end
end

function SeparateText (text, separator)
	assert(type(text) == "string")
	assert(type(separator) == "string")

	local prefix, suffix
	local separatorPosition = text:find(separator)
	if separatorPosition then
		prefix = text:sub(1, separatorPosition - 1)
		suffix = text:sub(separatorPosition + 1)
	end

	return prefix, suffix
end

local function SplitText(text, separator)
	local parts = {}
	string.gsub(text, string.format("([^%s]+)", separator), function(part) 
		table.insert(parts, part)
	end)
	return parts
end

local function GetUrlDomainProtocol(url)
	local parts = SplitText(url, "//")
	local protocol = parts[1]:gsub(":", "")
	parts = SplitText(parts[2], "/")
	local domain = parts[1]
	return domain, protocol
end

local function GetCookieValue(value, paramName)
	local params = SplitText(value, ";")
	for _, param in pairs(params) do
		local paramParts = SplitText(param, "=")
		if #paramParts == 2 and paramParts[1] == paramName then
			return paramParts[2]
		end
	end
end

--------------------------------------
-- helpers agentservice init options
local m_helpers_agentservice_options = nil
function InitHelpersAgentServiceOptions(options)
	if not(m_helpers_agentservice_options) then
		assert(options)
		assert(type(options) == "table")
		assert(options.host)
		assert(options.port)
		assert(options.cluster)
		m_helpers_agentservice_options = {
			host = options.host,
			port = options.port,
			cluster = options.cluster
		}
	end
end


--------------------------------------
-- REDIS
local m_redis = nil
function SetRedis (redis)
	m_redis = redis
end

--------------------------------------
-- AUTHFILTER
function InitAuthfilter (cassandra, redis)
	authFilter = (require "authfilter")("agent")
	authFilter:SetDatasource(cassandra, redis)
end

--------------------------------------
-- ASTERISK CLIENT
local GATEWAY_ASTERISK_ENABLED = true -- false to test it without a real gateway
local m_asteriskClient = nil
local function GetAsteriskClient ()
	if not m_asteriskClient then
		--LogDebug("[provisioningmanager:GetAsteriskClient] 1 - Creating Instance")
		require "lua_asteriskclient"
		m_asteriskClient = lua_asteriskclient.LuaAsteriskClient(
			inconcert.environment.apps.agent.cluster, "webhandler")
	end
	local tpAddresses = inconcert.environment.apps.provisioning.call or {}
	for _, address in pairs(tpAddresses) do
		m_asteriskClient:AddKnownTelephonyProxy(address.host, address.port)
	end
	return m_asteriskClient
end

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

-- SSL --(REQUESTs)---------------------------------------
--[[
	Ej: Utilizado para realizar request contra el API de M&S para obtener el Access Token para el render del Formulario (app)
]]--
local Synchronize = require "Synchronize"
local https = Synchronize("luasec_mutex", function()
	return require ("ssl.https")
end)
local cjson  = require "cjson"
local ltn12 = require "ltn12"
local m_request = {}

------------------------------------------------------
-- REQUEST (INICIO)
------------------------------------------------------

--[[
	Función encargada de agregar un timeout en segundos al Request.
	"ssl.https" tiene un bug al momento de agregar un timeout, el cual no funciona,
	para solucionar éste problema, se utiliza "socket.http", SOLO para asignar el timeout.

	Referencia:
		https://stackoverflow.com/questions/20193454/lua-https-timeout-is-not-working
		Ejemplos:
			1) NO funciona:
				local https = require "ssl.https"
				https.TIMEOUT = 0.01
				b, c, h = https.request("https://www.google.fr/")

			2) SI funciona:
				local http = require "socket.http"
				local https = require "ssl.https"
				http.TIMEOUT = 0.01
				b, c, h = https.request("https://www.google.fr/")
]]--
m_request.SetGetTimeout = function (self, options)
	local http = require "socket.http"
	local timeoutSec = nil
	if options then
		timeoutSec = tonumber(options.timeoutSec)
		if type(timeoutSec) ~= "number" then
			timeoutSec = nil
		end
	end
	http.TIMEOUT = timeoutSec
	return timeoutSec
end

m_request.Get = function (self, fullURI, headers, options)
	assert(fullURI)
	local startTime = os.time()

	headers = headers or {}
	local timeoutSec = self:SetGetTimeout(options)

	local log = "\n[PROVISIONING_MANAGER: REQUEST:Get] GET URI: '%s', headers: '%s', timeout: '%s' seconds. Please wait .....\n"
	LogDebug(log:format(fullURI, cjson.encode(headers), timeoutSec))

	local responseBody = {}
	local result, code, header = https.request{
		url = fullURI,
		method = "GET",
		headers = headers,
		sink = ltn12.sink.table(responseBody)
	}
	local encodedResponse = table.concat(responseBody, "")
	local endTime = os.time()
	local elapsedTimeSec = endTime - startTime

	if code == 200 then
		log = "[PROVISIONING_MANAGER: REQUEST:Get] GET Success. ELAPSED Time '%s' seconds,\nRESULT: '%s',\nCODE: '%s',\nHEADER: '%s',\nRESPONSE: '%s'.\n"
		LogDebug(log:format(tostring(elapsedTimeSec), tostring(result), tostring(code), cjson.encode(header), tostring(encodedResponse)))
		local decodedResponse = cjson.decode(encodedResponse)
		return true, encodedResponse, decodedResponse
	end
	log = "[PROVISIONING_MANAGER: REQUEST:Get] GET Error. ELAPSED Time '%s' seconds,\nRESULT: '%s',\nCODE: '%s',\nHEADER: '%s',\nRESPONSE: '%s'.\n"
	LogDebug(log:format(tostring(elapsedTimeSec), tostring(result), tostring(code), cjson.encode(header), tostring(encodedResponse)))

	return false, encodedResponse, code
end

------------------------------------------------------
-- REQUEST (FIN)
------------------------------------------------------


local function select (keyspace, column_family, first_key, last_key, first_col, last_col, reversed_cols)
	first_key = first_key or ""
	last_key = last_key or first_key
	first_col = first_col or ""
	last_col = last_col or first_col
	reversed_cols = reversed_cols or false

	local result = {}
	local keys = 100
	local colsPerKey = 100
	local allKeysObtained
	local nextFirstKey = first_key
	while not allKeysObtained do
		local keyRange = keyspace:GetRangeSlices(column_family,"",nextFirstKey,last_key,
			{
				slice_range = {
					start = first_col,
					finish = last_col,
					reversed = reversed_cols,
					count = colsPerKey + 1,
				},
			},
			keys + 1,
			CASSANDRA_CONSISTENCY_LEVEL_QUORUM,
			false
		)
		allKeysObtained = true
		if keyRange then
			local counter = 0
			for key, columns in keyRange:Iterator() do
				-- Iterate on the rows
				if counter < keys then
					local keyValues = {}
					-- Iterate on the columns
					while true do
						local allColsObtained = true
						local nextColumn
						for index, column in ipairs(columns) do
							if index < colsPerKey + 1 then
								--LogDebug("\tAdding "..key.."["..column.name.."] = "..column.value)
								keyValues[column.name] = column.value
							else
								--LogDebug("\tGetting more columns for "..key..", starting with "..column.name)
								allColsObtained = false
								nextColumn = column.name
							end
						end
						if allColsObtained then break end
						columns = keyspace:GetSliceRange(key, column_family, "", { slice_range = { start = nextColumn, finish = last_col, reversed = reversed_cols, count = colsPerKey+1 } })
					end
					result[key] = keyValues
					counter = counter + 1
				else
					allKeysObtained = false
					nextFirstKey = key
				end
			end
		end
	end
	return result
end

function cassSelect (keyspace, column_family, first_key, last_key, first_col, last_col, reversed_cols)
	return select(keyspace, column_family, first_key, last_key, first_col, last_col, reversed_cols)
end

local function superSelect (keyspace, column_family, first_superkey, last_superkey, first_supercol, last_supercol, reversed_supercols)
	first_superkey = first_superkey or ""
	last_superkey = last_superkey or first_superkey
	first_supercol = first_supercol or ""
	last_supercol = last_supercol or first_supercol
	reversed_supercols = reversed_supercols or false

	local result = {}
	local keys = 2
	local colsPerKey = 2
	local allKeysObtained
	local nextFirstKey = first_superkey
	while not allKeysObtained do
		local keyRange = keyspace:GetSuperRangeSlices(column_family,"",nextFirstKey,last_superkey,
			{
				slice_range = {
					start = first_supercol,
					finish = last_supercol,
					reversed = reversed_supercols,
					count = colsPerKey + 1,
				},
			},
			keys + 1,
			CASSANDRA_CONSISTENCY_LEVEL_QUORUM,
			false
		)
		allKeysObtained = true
		if keyRange then
			local counter = 0
			for key, supercolumns in keyRange:Iterator() do
				-- Iterate on the rows
				if counter < keys then
					local keyValues = {}
					-- Iterate on the columns
					while true do
						local allColsObtained = true
						local nextColumn
						for index, supercolumn in ipairs(supercolumns) do
							if index < colsPerKey + 1 then
								--LogDebug("\tAdding "..key.."["..column.name.."] = "..column.value)
								local scValues = {}
								for _, column in pairs(supercolumn.columns) do
									scValues[column.name] = column.value
								end
								keyValues[supercolumn.name] = scValues
							else
								--LogDebug("\tGetting more columns for "..key..", starting with "..column.name)
								allColsObtained = false
								nextColumn = supercolumn.name
							end
						end
						if allColsObtained then break end
						local thisKeyRange = keyspace:GetSuperRangeSlices(column_family,"",key,key,
							{
								slice_range = {
									start = nextColumn,
									finish = last_supercol,
									reversed = reversed_supercols,
									count = colsPerKey + 1,
								},
							},
							1,
							CASSANDRA_CONSISTENCY_LEVEL_QUORUM,
							false
						)
						if not thisKeyRange then break end
						allColsObtained = true
						for _, thisSupercolumns in thisKeyRange:Iterator() do
							allColsObtained = false
							supercolumns = thisSupercolumns
						end
						if allColsObtained then break end
					end
					result[key] = keyValues
					counter = counter + 1
				else
					allKeysObtained = false
					nextFirstKey = key
				end
			end
		end
	end
	return result
end

function cassSuperSelect (keyspace, column_family, first_superkey, last_superkey, first_supercol, last_supercol, reversed_supercols)
	return superSelect(keyspace, column_family, first_superkey, last_superkey, first_supercol, last_supercol, reversed_supercols)
end

local function split (str, sep)
	local result = {}
	while string.find(str, sep) do
		local thisStr
		thisStr, str = string.match(str, "([^"..sep.."]*)"..sep.."(.*)")
		if thisStr ~= "" then
			table.insert(result, thisStr)
		end
	end
	if str ~= "" then
		table.insert(result, str)
	end
	return result
end

local function isEmpty (t)
	for _,_ in pairs(t) do return false end
	return true
end

local function isEmptyString (s)
	return not s or s == ""
end

local function listKeys (t)
	local result = {}
	if t then
		for k, _ in pairs(t) do
			table.insert(result, k)
		end
	end
	return result;
end

local function cropEmpties (t, transformValue)
	transformValue = transformValue or function(v) return v end
	local result = {}
	for ID, data in pairs(t) do
		if not isEmpty(data) then
			result[ID] = transformValue(data)
		end
	end
	return result
end

local function listSubKeys (t)
	local result = {}
	for ID, data in pairs(t) do
		if not isEmpty(data) then
			result[ID] = listKeys(data)
		end
	end
	return result
end

local function newToken()
	return uuid.new():gsub('-', ''):upper()
end

local function valueToColumn(value)
	if (type(value) == "table") then
		return json.encode(value)
	end

	return tostring(value)
end

-----------------------------------------------------
-- INPUT
-----------------------------------------------------

--
-- Reads a line from stdin. If the line starts with # is ignored (a comment)
--
local function read_input ()
	while true do
		local input = io.read()
		if not input then error("User aborted.") end
		if not input:match("^#") then
			return input
		end
	end
end

function requestAlpha (prompt, params)
	params = params or {}
	while true do
		io.write(prompt..(params.previous and (" (previous value: '"..params.previous.."')") or "")..(params.default and (" ["..params.default.."]") or "")..": ")
		local input = read_input()
		if params.default and input == "" then
			input = params.default
		end
		if not params.allowEmpty and input == "" then
			LogDebug("An empty string is not allowed for this field.")
		else
			return input
		end
	end
end

function requestNumeric (prompt, params)
	params = params or {}
	while true do
		io.write(prompt..(params.previous and (" (previous value: '"..params.previous.."')") or "")..(params.default and (" ["..params.default.."]") or "")..": ")
		local input = read_input()
		if input == "" and params.default then
			input = tostring(params.default)
		end
		if not params.allowNull and input == "" then
			LogDebug("This field does not allow empty values.")
		elseif input == "" then
			return
		elseif not tonumber(input) then
			LogDebug("This field only allows numeric values.")
		else
			local numeric = tonumber(input)
			if not params.allowNegative and numeric < 0 then
				LogDebug("This field does not allow negative values.")
			elseif not params.allowFloat and numeric > math.floor(numeric) then
				LogDebug("This field only allows integer values.")
			else
				return input
			end
		end
	end
end

function requestBoolean (prompt, params)
	params = params or {}
	while true do
		io.write(prompt..(params.default and " [Y/n]" or " [y/N]")..": ")
		local input = string.lower(read_input())
		if input == "" then
			input = params.default and "y" or "n"
		end
		if input ~= "y" and input ~= "n" then
			LogDebug("Please enter 'y' or 'n' (or leave blank for the default value).")
		else
			return (input == "y")
		end
	end
end

------------------------------------------------------
-- VCCS
-- ********* This use ksAgent, NOT ksProv! ***********
------------------------------------------------------

local function SetVccAllegroIntegration (vcc)
	if not vcc then
		return
	end
	local hasIntegration = false
	if vcc.AllegroIntegration then
		if type(vcc.AllegroIntegration) == "string" then
			vcc.AllegroIntegration = json.decode(vcc.AllegroIntegration)
		end
		hasIntegration = vcc.AllegroIntegration.enabled
	end
	vcc.HasAllegroIntegration = hasIntegration
end

local function HasVccAllegroIntegration (vcc)
	local hasIntegration = false
	if not vcc then
		return hasIntegration
	end
	if vcc.AllegroIntegration then
		if type(vcc.AllegroIntegration) == "string" then
			vcc.AllegroIntegration = json.decode(vcc.AllegroIntegration)
		end
		hasIntegration = vcc.AllegroIntegration.enabled
	end
	return hasIntegration
end

--[[
	Esta función genera "custom fields" para el VCC, según las integraciones que se almacenan en el JSON de la Column "AppsIntegrations" en la CF "VCCs".
	En si genera un flag en true o false para saber si tiene o no integración.

	Ej:
		Recorre el json y crea el nombre del atributo de la siguiente forma:
		HasHelpDeskIntegration o HasMarketingAndSalesIntegration o HasWorkForceManagementIntegration
]]--
local function SetVccOthersIntegrations (vcc)
	if not vcc then
		return
	end
	if vcc.AppsIntegrations then
		if type(vcc.AppsIntegrations) == "string" then
			vcc.AppsIntegrations = json.decode(vcc.AppsIntegrations)
		end
		for name, integration in pairs(vcc.AppsIntegrations or {}) do
			local fieldName = string.format("Has%sIntegration", name)
			--LogDebug("[PROVISIONING_MANAGER:SetVccOthersIntegrations] Creating Custom Fields in VCC: " .. vcc.Id .. ", fieldName: " .. tostring(fieldName))
			vcc[fieldName] = tostring(integration.Enabled) == "1"
		end
	end
end

local function SetVccCustomFields (vcc)
	if not vcc then
		return
	end
	SetVccAllegroIntegration(vcc)
	SetVccOthersIntegrations(vcc)
end

-- ListVCCs
---------------------------
-- Returns a list of all the VCCs, with their associated data
function ListVCCs (ksAgent)
	local vccs = select(ksAgent, "VCCs")
	return cropEmpties(vccs, function(vcc)
		SetVccCustomFields(vcc)
		return vcc
	end)
end

--[[
	Funcion publica, invocada desde el mismo archivo y desde la app provisioning en el repo "agent"
]]--
function CreateESIndexesForVCC (vcc, indexes, ksAgent)
	assert(type(vcc) == "string")
	indexes = indexes or "contacts|interactions|posts|batches|contacts_dialer|blacklist_dialer|agent_logs_monitoring"
	LogInfo("[PROVISIONING_MANAGER:CreateESIndexForVCC] Creating ES indexes for VCC: " .. vcc .. ", indexes: " .. tostring(indexes))
	local temporalResultFileName = string.format([[/tmp/RESULT_ES_CREATION_FOR_VCC_%s.log]], vcc)
	local vccs = ListVCCs(ksAgent)
	local totalVCCs = m_helpers.table:GetRowCount(vccs)
	LogDebug("==> totalVCCs: " .. tostring(totalVCCs))
	--[[
		Cuando creamos los primeros VCCs (System o primer VCC del ambiente) se ejecta la accion "template|create-index".
		Cuando creamos mas VCCs solo se ejecuta la accion "create-index"
	]]--
	local action = (totalVCCs <= 2) and "template|create-index" or "create-index" -- creo el template y el creo el indice
	local command = string.format([[/usr/share/inconcert/helpers/reindex_elasticsearch.sh -a 0 -v '%s' -i '%s' -c '%s' > %s]], vcc, indexes, action, temporalResultFileName)
	LogDebug(command)
	os.execute(command)
	local success, contentResultFile = m_helpers.filesystem.FileRead(temporalResultFileName)
	LogInfo("")
	LogInfo("******************************************************************************************************************")
	LogInfo("  ELASTIC SEARCH TEMPLATES E INDEXES RESULT. FROM FILE: " .. temporalResultFileName)
	LogInfo("******************************************************************************************************************")
	if success then
		LogInfo(contentResultFile)
		local success, contentResultFile = m_helpers.filesystem.RemoveFile(temporalResultFileName)
	end
	LogInfo("******************************************************************************************************************")
	LogInfo("")
	LogInfo("[PROVISIONING_MANAGER:CreateESIndexForVCC] Created ES indexes for VCC: " .. vcc .. ", indexes: " .. tostring(indexes))
end

--[[
	enabled = true,
	key = "guid",

	urlsConfig = {
		resources = {
			high = {
				rateLimits = {
					timeLimitSec = 5,
					maxRequests = 1
				}
			},
			low = {
				rateLimits = {
					timeLimitSec = 5,
					maxRequests = 5
				}
			}
		}
	}
]]--
local function SetApiConfiguration (oldData, newData, ksProv)
	-- TODO: Realizar algo si es necesario con los valores del sistema !! queda como referencia.
	local apiSDKSystemConfig = GetSystemConfiguration(ksProv, "API_SDK")
	---------
	local vccCurrentEncodedConfig = oldData and oldData.ApiConfiguration

	--LogDebug("[PROVISIONING_MANAGER:SetApiConfiguration] vccCurrentEncodedConfig: " .. tostring(vccCurrentEncodedConfig))

	local config
	if vccCurrentEncodedConfig then
		-- actualizando el vcc
		config = json.decode(vccCurrentEncodedConfig)
	else
		-- nuevo VCC
		config = json.decode(apiSDKSystemConfig.JWTConfig)
		config.key = uuid.new()
	end
	if newData.ApiConfiguration then
		config.enabled = newData.ApiConfiguration.enabled or false
		config.urlsConfig = newData.ApiConfiguration.urlsConfig or {}
	end

	vccCurrentEncodedConfig = json.encode(config)
	newData.ApiConfiguration = vccCurrentEncodedConfig
end

--[[
	Los valores los obtiene de la CF del Sistema "SystemConfiguration", Id "RECORDING", Column "RecordingConfigurationDefaultValues"

	Ej:
		{
			CALL = {
				automatic = "all",
				format = "mp3",
				channels = "mono"
			},
			VIDEO_CALL = {
				automatic = "all",
				mode = "video_only",
				format = "webm"
			},
			VOICEMAIL = {
				format = "mp3",
				channels = "mono"
			}
		}
]]--
function LoadRecordingConfigurationDefaultValues (ksProv)
	local recordingSystemConfig = GetSystemConfiguration(ksProv, "RECORDING")
	local config = recordingSystemConfig.RecordingConfigurationDefaultValues
	--LogInfo("[PROVISIONING_MANAGER:LoadRecordingConfigurationDefaultValues] config:" .. tostring(config))
	return config
end

local function SetRecordingConfigurationForVCC (oldData, newData, ksProv)
	newData.RecordingConfiguration = (newData and newData.RecordingConfiguration) or
		(oldData and oldData.RecordingConfiguration) or
		LoadRecordingConfigurationDefaultValues(ksProv) -- lo cargo de los valores de la CF SystemConfiguration
end

local function AuthFilterCleanUpVccAvailableLicenses (VCCName)
	-- Actualiza las licencias en redis.
	if authFilter then
		LogInfo("[PROVISIONING_MANAGER:AuthFilterCleanUpVccAvailableLicenses] Execute AuthFilter - CleanUp Available Licenses in VCC:" .. VCCName)
		authFilter:CleanUpVccAvailableLicenses(VCCName)
	else
		LogWarning("[PROVISIONING_MANAGER:AuthFilterCleanUpVccAvailableLicenses] AuthFilter is undefined. Unable to CleanUp Available Licenses in VCC: " .. VCCName)
	end
end

local function ForceMustChangePassword(ksAgent, VCCName)
	LogDebug("[PROVISIONING_MANAGER:ForceMustChangePassword] VCC:'%s'", VCCName)
	local vccUsers = select(ksAgent, "VCCUsers", VCCName)[VCCName] or {}
	for username, vccUserString in pairs(vccUsers) do
		local userAtVcc = username .. "@" .. VCCName
		ksAgent:InsertColumn(userAtVcc, "Users", "MustChangePassword", "1")
		local vccUser = json.decode(vccUserString)
		vccUser.MustChangePassword = "1"
		ksAgent:InsertColumn(VCCName, "VCCUsers", username, json.encode(vccUser))
	end
end

-- Creates a campaign and account or type CALL, for internal calls use
-- This function has to be called ONLY when a new VCC is created
local function CreateInternalCallCampaingAndAccount(keyspace, vcc)
	local campaignId = "internal_calls"
	local accountName = "internalcallsaccount"		
	local campaignAtVCC = campaignId .. "@" .. vcc
	local dateString = LuaOrm.Date():toString()

	LogDebug("[PROVISIONING_MANAGER:CreateInternalCallCampaingAndAccount] Creating campaign '%s' and account '%s' for internal calls on vcc:'%s'", campaignId, accountName, vcc)

	--CampaignSchedulers
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "1_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "2_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "3_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "4_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "5_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "6_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "7_1970-01-01 00:00:00_1970-01-01 00:00:00", "")		

	--Campaigns
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "IsInternal", "1")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "AlwaysValid", "1")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "Description", "System Campaign to Internal Calls")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "Id", campaignAtVCC)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "LastUpdate", dateString)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "Name", campaignId)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "VCC", vcc)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "ValidationTimeZone", "UTC")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "campaignId", campaignId)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "defaultAgentCriteria", "CIRCULAR")
	local permisions = {
		members = {}
	}
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "permisions", json.encode(permisions))
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "updated_at", dateString)

	--VCCCampaigns
	keyspace:InsertColumn(vcc, 'VCCCampaigns', campaignId, campaignId)

	--CampaignThresholds
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ACDFirstMessageThreshold_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ACDRepeatedMessageThreshold_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ACDWaitThreshold_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "AverageCallDuration_OUTBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "Ghost_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "InactivityTimeout_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "LongInteraction_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "LongInteraction_OUTBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ServiceLevel_INBOUND", "30")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ServiceLevel_OUTBOUND", "30")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ShortInteraction_INBOUND", "-1")
	keyspace:InsertColumn("CALL:" .. campaignAtVCC, 'CampaignThresholds', "ShortInteraction_OUTBOUND", "-1")

	local accountId = campaignAtVCC .. "_" .. accountName
	local accountKey = "CALL_" .. accountId

	local account = {
		ActionOnAcdWaitTimeout = {},
		AfterInteractionWork = "0",
		AttentionLevels = {},
		BotsToTransferFromAgent = {},
		CCAlgorithm = "CIRCULAR",
		CallbackIsAllowed = "0",
		ContactManagement = {
			Enabled = "0",
			MergeCriteria = "",
			AutoAddOutgoingContact = "0",
			AutoAddIncomingContact = "0"
		},
		FlowInformation = {},
		GUI = {
			window_open_mode = "NORMAL"
		},
		LifeTimeAbandonedCalls = "",
		LifeTimeAppointmentCalls = "",
		Priority = "1",
		QnAMaker = {
			Enabled = "0"
		},
		ScreenRecording = "0",
		Skills = {},
		TicketEmitter = {},
		created_at = dateString,
		id = accountKey,
		incomingConfig = {
			enabled = true,
			ContactCenter = {
				LeaveVoicemailAtOutOfHours = false,
				CheckRedial = "0"
			}
		},
		language = "en",
		name = accountName,
		optimizeNodeDistribution = "1",
		outgoingConfig = {
			enabled = true,
			ContactCenter = {
				AllowManualCalls = "1",
				outboundPrefix = ""
			}
		},
		packages = {},
		proxyType = "CALL",
		state = "1",
		ttsDefaultVoice = "",
		uid = accountName,
		updated_at = dateString,
		webcallConfig = {
			enabled = false
		}
	}				

	--CampaignAccounts
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "ActionOnAcdWaitTimeout", json.encode(account.ActionOnAcdWaitTimeout));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "AfterInteractionWork", account.AfterInteractionWork);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "AttentionLevels", json.encode(account.AttentionLevels));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "BotsToTransferFromAgent", json.encode(account.BotsToTransferFromAgent));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "CCAlgorithm", account.CCAlgorithm);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "CallbackIsAllowed", account.CallbackIsAllowed);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "ContactManagement", json.encode(account.ContactManagement));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "FlowInformation", json.encode(account.FlowInformation));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "GUI", json.encode(account.GUI));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "LifeTimeAbandonedCalls", account.LifeTimeAbandonedCalls);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "LifeTimeAppointmentCalls", account.LifeTimeAppointmentCalls);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "Priority", account.Priority);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "QnAMaker", json.encode(account.QnAMaker));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "ScreenRecording", account.ScreenRecording);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "Skills", json.encode(account.Skills));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "TicketEmitter", json.encode(account.TicketEmitter));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "created_at", account.created_at);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "id", account.id);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "incomingConfig", json.encode(account.incomingConfig));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "language", account.language);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "name", account.name);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "optimizeNodeDistribution", account.optimizeNodeDistribution);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "outgoingConfig", json.encode(account.outgoingConfig));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "packages", json.encode(account.packages));
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "proxyType", account.proxyType);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "state", account.state);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "ttsDefaultVoice", account.ttsDefaultVoice);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "uid", account.uid);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "updated_at", account.updated_at);
	keyspace:InsertSuperColumn(campaignAtVCC, "CampaignAccounts", accountKey, "webcallConfig", json.encode(account.webcallConfig));
			
	--AccountsById
	keyspace:InsertColumn(accountKey, 'AccountsById', campaignAtVCC, json.encode(account))		

	--TODO: Crear key ActiveAccountsByChannel:CALL@VCC en redis
	local activeKey = "ActiveAccountsByChannel:CALL@" .. vcc
	m_redis:hset(activeKey, accountKey, campaignAtVCC)

	LogDebug("[PROVISIONING_MANAGER:CreateInternalCallCampaingAndAccount] Campaign '%s' and account '%s' created for internal calls on vcc:'%s'", campaignId, accountName, vcc)	
end

local function CreateSandboxChatAccount(params)
	local keyspace = params.keyspace
	local vcc = params.vcc
	local campaignId = params.campaignId
	local campaignAtVCC = campaignId .. "@" .. vcc
	local accountName = "sandbox_chat"

	LogDebug("[PROVISIONING_MANAGER:CreateSandboxChatAccount] Creating account '%s' " .. 
			 "for chat on vcc:'%s'",
			 accountName, vcc)

	--CampaignThresholds
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "ACDFirstMessageThreshold", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "ACDRepeatedMessageThreshold", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "ACDWaitThreshold", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "AnswerTimeout", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "AutoWrapupTimeout", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "Ghost", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "InactivityTimeout", "-1")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "ServiceLevel", "180")
	keyspace:InsertColumn("CHAT:" .. campaignAtVCC, 'CampaignThresholds', "ShortInteraction", "-1")

	local accountId = campaignAtVCC .. "_" .. accountName
	local accountKey = "CHAT_" .. accountId

	local account = {
		ActionOnAcdWaitTimeout = {},
		AfterInteractionWork = "0",
		AttentionLevels = {},
		BotsToTransferFromAgent = {},
		CCAlgorithm = "CIRCULAR",
		ContactManagement = {
			Enabled = "0",
			MergeCriteria = "",
			AutoAddIncomingContact = "0"
		},
		FlowInformation = {}, -- Process = token del bot asociado a la campaña
		GUI = {
			window_open_mode = "NORMAL"
		},
		GoChatIfTransfer = "0",
		Priority = "",
		QnAMaker = {},
		ScreenRecording = "0",
		Skills = {},
		TicketEmitter = {},
		Token = newToken(),
		Tracking = "0",
		TweetyAppName = "chatbottest{9CA752E9-3BA3-4FEB-931D-3E61734151D8}",
		checkActiveAgents = false,
		containerId = "",
		google_bySections = false,
		google_enabled = false,
		google_key = "",
		id = accountKey,
		language = "es",
		name = accountName,
		outHours_enabled = false,
		proxyType = "CHAT",
		snippet_parameters = {},
		start_button = "app",
		state = "1",
		triggers = {},
		uid = accountName,
		window_type = "embedded"
	}

	--CampaignAccounts
	for columnKey, columnValue in pairs(account) do
		keyspace:InsertSuperColumn(
			campaignAtVCC,
			"CampaignAccounts",
			accountKey,
			columnKey, 
			valueToColumn(columnValue)
		)
	end

	--AccountsById
	keyspace:InsertColumn(accountKey, 'AccountsById', campaignAtVCC, json.encode(account))		

	local activeKey = "ActiveAccountsByChannel:WEBCHAT@" .. vcc
	m_redis:hset(activeKey, accountKey, campaignAtVCC)
end

-- Creates a sandbox campaign
-- This function has to be called ONLY when a new VCC is created
function CreateSandboxCampaingAndAccount(keyspace, vcc)
	local campaignId = "sandbox"
	local campaignAtVCC = campaignId .. "@" .. vcc
	local dateString = LuaOrm.Date():toString()

	LogDebug("[PROVISIONING_MANAGER:CreateSandboxCampaingAndAccount] Creating campaign '%s' " .. 
			 "for chat on vcc:'%s'",
			 campaignId, vcc)

	--CampaignSchedulers
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "1_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "2_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "3_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "4_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "5_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "6_1970-01-01 00:00:00_1970-01-01 00:00:00", "")
	keyspace:InsertColumn(campaignAtVCC, 'CampaignSchedulers', "7_1970-01-01 00:00:00_1970-01-01 00:00:00", "")		

	--Campaigns
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "SystemType", "sandbox")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "AlwaysValid", "1")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "Description", "Sandbox campaign")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "Id", campaignAtVCC)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "LastUpdate", dateString)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "Name", campaignId)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "VCC", vcc)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "ValidationTimeZone", "UTC")
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "campaignId", campaignId)
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "defaultAgentCriteria", "CIRCULAR")
	local permisions = {
		members = {}
	}
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "permisions", json.encode(permisions))
	keyspace:InsertColumn(campaignAtVCC, 'Campaigns', "updated_at", dateString)

	--VCCCampaigns
	keyspace:InsertColumn(vcc, 'VCCCampaigns', campaignId, campaignId)

	CreateSandboxChatAccount({
		keyspace = keyspace,
		vcc = vcc,
		campaignId = campaignId,
	})

	LogDebug("[PROVISIONING_MANAGER:CreateSandboxCampaingAndAccount] Campaign '%s' created on vcc:'%s'",
			 campaignId, vcc)
end

-- UpdateVCC
---------------------------
-- Adds or modifies a VCC in provisioning
function UpdateVCC (ksAgent, VCCName, VCCData, ksProv)
	VCCName = utf8.lower(VCCName)
	VCCData.Id = VCCName
	VCCData.Description = VCCData.Description and string.len(VCCData.Description) and VCCData.Description or VCCName

	--LogDebug("[PROVISIONING_MANAGER:UpdateVCC] CF VCC:" .. VCCName .. " data: " .. json.encode(VCCData))

	local cleanedVccData = {}
	if m_helpers.utils.HasLicenseSystem() then
		-- #11602: Los datos licenciables no quedan mas en la CF VCCs, sino que se le pasan al AS
		local licensedAttr = {
			company = true,
			company_is_editable = true,
			OnlineUsers = true,
			CustomMetricsLimit = true,
			CustomFiltersLimit = true,
			ExportSpaceQuota = true,
			SharedFilesSpaceQuota = true
		}

		-- Los maximos asociados a licencia
		local licensedMaxAttr = {
			MAX_ONLINE_USERS = true,
			MAX_CustomMetricsLimit = true,
			MAX_CustomFiltersLimit = true,
			MAX_ExportSpaceQuota = true,
			MAX_SharedFilesSpaceQuota = true,
			MAX_AppsIntegrations = true
		}

		local licenseInfo = {}
		for name, v in pairs(VCCData) do
			if name == "company" then
				licenseInfo["company"] = v
			elseif name == "company_is_editable" then
				licenseInfo["company_is_editable"] = v and true or false
			elseif name == "OnlineUsers" then
				licenseInfo["ONLINE_USERS"] = tonumber(v)
			elseif string.find(name, "_licensed") then
				local channelLicenseName = name:gsub( "_licensed", "_ACCOUNTS")
				licenseInfo[channelLicenseName] = (v == true or v == "1") and true or false
			elseif licensedAttr[name] then
				licenseInfo[name] = tonumber(v)
			elseif name == "AppsIntegrations" then
				--Aqui dejamos el Licenses.User de cada App que va a ir a la CF VCCs a 0, y el valor que nos viene lo pasamos a licenseInfo
				local appsIntegrations
				if type(v) == "string" then
					appsIntegrations = json.decode(v)
				else
					appsIntegrations = json.decode(json.encode(v))
				end

				--cleanedVccData[name] = v
				for appId, appConfig in pairs(appsIntegrations) do
					if appConfig.Licenses and appConfig.Licenses.Users then
						licenseInfo.AppsIntegrations = licenseInfo.AppsIntegrations or {}
						licenseInfo.AppsIntegrations[appId] = appConfig.Licenses.Users
						appConfig.Licenses.Users = "0"
					end
				end
				cleanedVccData.AppsIntegrations = appsIntegrations
			elseif licensedMaxAttr[name] then
				licenseInfo[name] = v
			else
				cleanedVccData[name] = v
			end
		end
		
		--Allegro VCCs has "Company" instead "company". SetVccLicense requires "company"
		licenseInfo.company = licenseInfo.company or VCCData.Company

		local setVccLicenseResult = m_helpers.agentservice.SetVccLicense(m_helpers_agentservice_options, VCCName, json.encode(licenseInfo))
		if not setVccLicenseResult.success then
			-- EL error se debe de retornar en un JSON, con la siguiente estructura
			local err = {
				err         = setVccLicenseResult,
				errType     = "AGENT_SERVICE:SetVccLicence",
				licenseInfo = licenseInfo,
				vcc         = VCCName
			}
			error(json.encode(err), 0)
		end
	else
		cleanedVccData = VCCData
	end

	local oldData = GetVCC(ksAgent, VCCName)
	SetApiConfiguration(oldData, cleanedVccData, ksProv)
	SetRecordingConfigurationForVCC(oldData, cleanedVccData, ksProv)

	for k, _ in pairs(oldData) do
		if not cleanedVccData[k] then
			ksAgent:RemoveColumn(VCCName, "VCCs", k)
		end
	end

	for name, v in pairs(cleanedVccData) do
		local value = type(v) == "table" and json.encode(v) or v
		--LogDebug("[PROVISIONING_MANAGER:UpdateVCC] CF VCCs - key:" .. VCCName .. " name:" .. name .. " value:" .. tostring(value))
		-- Force users change password on next login
		if type(value) == "boolean" then
			value = value and "1" or "0"
		end
		if name == "Security" then
			local vccSecurity = json.decode(value)
			if (vccSecurity.authentication.type == "BASIC" or vccSecurity.authentication.type == "MFA") and vccSecurity.authentication.passwordChecks.changeOnFirstLogin then
				ForceMustChangePassword(ksAgent, VCCName)
				--No guardamos este flag en bd
				vccSecurity.authentication.passwordChecks.changeOnFirstLogin = nil
				value = json.encode(vccSecurity)
			end
		end
		ksAgent:InsertColumn(VCCName, "VCCs", name, value)
	end

	-- Si es un ADD VCC entonces inserto los channels en InteractionType.
	local isNewVCC = isEmpty(oldData)
	if isNewVCC then
		local types = select(ksAgent,"InteractionTypes")
		for id, data in pairs(types) do
			if not isEmpty(data) then
				local splitted = split(id,"@")
				if #splitted >= 2 and splitted[2] == "system" then
					--LogDebug ("[PROVISIONING_MANAGER:UpdateVCC] ID:" .. id .. " data: " .. json.encode(data))
					local channelType = splitted[1]
					local key = channelType .. "@" .. VCCName
					for name, value in pairs(data) do
						--LogDebug("[PROVISIONING_MANAGER:UpdateVCC] CF InteractionTypes - key:" .. key .. " name:" .. name .. " value:" .. value)
						ksAgent:InsertColumn(key, "InteractionTypes", name, value)
					end
				end
			end
		end

		-- Cuando se esta creando el VCC, también se crean en ES los indices y templates (solo el primer VCC que se crea, NO el system).
		--
		CreateESIndexesForVCC(VCCName, nil, ksAgent)

		-- Cuando se esta creando el VCC, también se crean la campaign y account internas para las llamadas entre agentes
		if not HasVccAllegroIntegration(VCCData) then
			CreateInternalCallCampaingAndAccount(ksAgent, VCCName)
			CreateSandboxCampaingAndAccount(ksAgent, VCCName)
		end
	end

	-- Actualiza las licencias en redis.
	AuthFilterCleanUpVccAvailableLicenses(VCCName)
end

-- DeleteVCC
---------------------------
-- Removes a VCC from provisioning
-- Pre: The VCC must exist
-- Post: If the VCC contains any package, they are free'd
-- Post: If the VCC contains any peers, they are free'd
function DeleteVCC (ksProv, ksAgent, VCC)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}
	VCC = utf8.lower(VCC)
	local VCCData = GetVCC(ksAgent, VCC)
	if isEmpty(VCCData) then
		result.success = false
		result.errType = "VCC DOES NOT EXIST"
		result.errMsg = "VCC "..VCC.." does not exist."
		return result
	end
	local packages = select(ksProv, "VCCPackages", "VCC:"..VCC)["VCC:"..VCC] or {}
	for package, _ in pairs(packages) do
		ksProv:InsertColumn('Free', 'VCCPackages', package, "")
		ksProv:RemoveColumn(package, "Packages", "VCC")
	end
	ksProv:RemoveColumn("VCC:"..VCC, 'VCCPackages')
	local peers = select(ksProv, "VCCPeers", "VCC:"..VCC)["VCC:"..VCC] or {}
	for peer, _ in pairs(peers) do
		ksProv:InsertColumn('Free', 'VCCPeers', peer, "")
		ksProv:RemoveColumn(peer, "Peers", "VCC")
	end
	local types = select(ksAgent,"InteractionTypes")
	for id, data in pairs(types) do
		if not isEmpty(data) then
			local splitted = split(id,"@")
			if #splitted >= 2 and splitted[2] == VCC then
				ksAgent:RemoveColumn(id, "InteractionTypes")
			end
		end
	end
	ksProv:RemoveColumn("VCC:"..VCC, 'VCCPeers')
	for k, _ in pairs(VCCData) do
		ksAgent:RemoveColumn(VCC, "VCCs", k)
	end

	-- Actualiza las licencias en redis.
	AuthFilterCleanUpVccAvailableLicenses(VCC)

	-- IS OK
	result.success = true
	return result
end

-- GetVCC
---------------------------
-- Returns data about a specific VCC, empty if it does not exist
function GetVCC (ksAgent, VCCName, withCustomFields)
	withCustomFields = withCustomFields or false
	VCCName = utf8.lower(VCCName)
	local vccInfo = select(ksAgent, "VCCs", VCCName)[VCCName] or {}
	if m_helpers.table:TableIsEmpty(vccInfo) then
		return {}
	end
	if m_helpers.utils.HasLicenseSystem() then
		vccInfo = m_helpers.agentservice.GetVccWithLicenseInfo(m_helpers_agentservice_options, vccInfo)
	end
	if withCustomFields then
		SetVccCustomFields(vccInfo)
	end
	return vccInfo
end

-- GetSystemConfiguration
---------------------------
-- Returns System configuration
function GetSystemConfiguration (ksProv, elementId)
	local info = select(ksProv, "SystemConfiguration", elementId)[elementId] or {}
	--LogDebug("[PROVISIONING_MANAGER:GetSystemConfiguration] elementId: '" .. tostring(elementId) .. "', info: " .. tostring(json.encode(info or {})))
	return info
end


------------------------------------------------------
-- HOSTS
------------------------------------------------------

-- ListHosts
---------------------------
-- Returns a list of all the hosts, with their associated data
function ListHosts (ksProv)
	return cropEmpties(select(ksProv, "Hosts"))
end

-- GetHost
---------------------------
-- Returns data of a specific host, or empty if non-existing
function GetHost (ksProv, hostId)
	hostId = utf8.lower(hostId or "")
	return select(ksProv, "Hosts", hostId)[hostId] or {}
end

-- UpdateHost
---------------------------
-- Adds or modifies a host in provisioning
function UpdateHost (ksProv, hostId, hostData)
	hostId = utf8.lower(hostId)
	hostData.id = hostId

	LogDebug("[PROVISIONING_MANAGER:UpdateHost] Updating Host: '" .. tostring(hostId) .. "', hostData: " .. tostring(json.encode(hostData or {})))

	local oldData = GetHost(ksProv, hostId)

	if oldData.sipServer and oldData.sipServer ~= hostData.sipServer then
		UpdateGatewaysNodeTrunksAttributeValue(ksProv, oldData.sipServer, hostData.sipServer)
	end

	for k, _ in pairs(oldData) do
		if not hostData[k] then
			ksProv:RemoveColumn(hostId, "Hosts", k)
		end
	end
	for k, v in pairs(hostData) do
		ksProv:InsertColumn(hostId, "Hosts", k, v)
	end

	-- OK
	return true
end

-- DeleteHost
------------------------------
-- Removes a host from provisioning
-- Pre: The host must exist
-- Pre: The host must not be associated with any peer
-- Pre: The host must not be associated with any trunk
function DeleteHost (ksProv, hostId)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	hostId = utf8.lower(hostId)

	LogDebug("[PROVISIONING_MANAGER:DeleteHost] Deleting Host: '" .. tostring(hostId) .. "'")

	-- EXIST validation
	local host = GetHost(ksProv, hostId)
	if isEmpty(host) then
		result.success = false
		result.errType = "HOST DOES NOT EXIST"
		result.errMsg = "Host "..tostring(hostId).." does not exist."
		return result
	end

	-- PEER validation
	local peers = GetPeersByHost(ksProv, hostId)
	for _, peerId in pairs(peers or {}) do
		result.success = false
		result.errType = "CANNOT DELETE HOST ASSOCIATED TO PEER"
		result.errMsg = "Cannot delete host "..hostId.." while PEER "..peerId.." is still associated with it."
		result.relatedId = peers
		return result
	end

	-- TRUNK validation
	local trunks = GetTrunksByHost(ksProv, hostId)
	for _, trunkId in pairs(trunks or {}) do
		result.success = false
		result.errType = "CANNOT DELETE HOST ASSOCIATED TO TRUNK"
		result.errMsg = "Cannot delete host "..hostId.." while trunk "..trunkId.." is still associated with it."
		result.relatedId = trunks
		return result
	end

	-- Ahora si lo elimino
	for k, _ in pairs(host) do
		ksProv:RemoveColumn(hostId, "Hosts", k)
	end

	-- IS OK
	result.success = true
	return result
end

------------------------------------------------------
-- ICE SERVERS
------------------------------------------------------

-- ListIceServers
---------------------------
-- Returns a list of all the ice servers, with their associated data
function GetIceServersCol(ksProv)

	local iceServers = ksProv:GetColumnValue ("ICE_SERVERS", "SystemConfiguration", "ServersConfiguration")

	if iceServers and type(iceServers) == "string" then
		return json.decode(iceServers)
	elseif iceServers and type(iceServers) == "table" then
		return iceServers
	else
		return {}
	end
end

-- IceServerExists
---------------------------
-- Check if exists or not an ice server by his serverId or by ice server information data
-- serverInfo can be a numeric ice serverId or a table with ice server info(user, password, domain and port)
-- Returns If exists true, index of the element and ice servers info table. If not exists returns false, -1 and ice servers info table or {} if not info
function IceServerExists (serverInfo, ksProv)
	local index      = -1
	local exists     = false
	local iceConfigs = GetIceServersCol(ksProv)

	if serverInfo and next(iceConfigs) ~= nil and iceConfigs.userConfigs and #iceConfigs.userConfigs > 0
	then
		if type(serverInfo) == "number"
		then
			for idx, server in ipairs(iceConfigs.userConfigs) do
				if server.serverId == serverInfo then
					exists = true
					index  = idx
					break
				end
			end
		elseif type(serverInfo) == "table" and next(serverInfo) ~= nil
		then
			for idx, server in ipairs(iceConfigs.userConfigs) do
				if server.user and serverInfo.user and server.user == serverInfo.user then
					if server.password and serverInfo.password and server.password == serverInfo.password then
						if server.domain and serverInfo.domain and server.domain == serverInfo.domain then
							if server.port and serverInfo.port and server.port == serverInfo.port then
								exists = true
								index  = idx
								break
							end
						end
					end
				end
			end
		end
	end

	return exists, index, iceConfigs
end

-- GetIceServer
---------------------------
-- Returns data of a specific ice server, or error if non-existing
function GetIceServer(serverId, ksProv)
	local result =  { success = false, errCode = "", data = nil }

	if not serverId or type(serverId) ~= "number" then
		result.errCode = "E_INVALID_PARAM"
		return result
	end

	local exists, index, iceData = IceServerExists(serverId, ksProv)

	if not exists then
		result.errCode = "E_NOT_EXISTS"
		return result
	end

	result.success = true
	result.data    = iceData["userConfigs"][index]

	return result
end

-- SaveIceServer
---------------------------
-- Adds an ice server in provisioning. If an ice server exists, an error is returned because duplicate ice servers are not allowed.
function SaveIceServer (serverInfo, ksProv)
	local result =  { success = false, errCode = "" }

	if type(serverInfo) ~= "table" or not serverInfo.serverId or type(serverInfo.serverId) ~= "number" then
		result.errCode = "E_INVALID_PARAM"
		return result 
	end

	local exists, index, iceData = IceServerExists(serverInfo, ksProv)
	if exists then
		if serverInfo.serverId ~= iceData["userConfigs"][index].serverId then
			result.errCode = "E_EXISTS"
			return result
		end

		result.success = true
		return result
	end

	if not iceData["userConfigs"] then
		iceData["userConfigs"] = {}
	else
		for idx, server in ipairs(iceData.userConfigs) do
			if server.serverId == serverInfo.serverId then
				table.remove(iceData["userConfigs"], idx)
				break
			end
		end
	end

	table.insert(iceData["userConfigs"], serverInfo)
	ksProv:InsertColumn("ICE_SERVERS", "SystemConfiguration", "ServersConfiguration", json.encode(iceData))

	result.success = true
	return result
end

-- DeleteIceServer
------------------------------
-- Removes an ice server from provisioning. If not exists returns error
function DeleteIceServer (serverId, ksProv)
	local result =  { success = false, errCode = "" }

	if not serverId or type(serverId) ~= "number" then
		result.errCode = "E_INVALID_PARAM"
		return result 
	end

	local exists, index, iceData = IceServerExists(serverId, ksProv)
	if not exists then
		result.errCode = "E_NOT_EXISTS"
		return result
	end

	table.remove(iceData["userConfigs"], index)
	ksProv:InsertColumn("ICE_SERVERS", "SystemConfiguration", "ServersConfiguration", json.encode(iceData))

	result.success = true
	return result
end

------------------------------------------------------
-- TTS/ASR DATA 
------------------------------------------------------

-- ListTtsAsrProviders
---------------------------
-- Returns a list of all the TTS/ASR Providers, with their associated data
function ListTtsAsrProviders (ksProv)
	local providersResult = {}
	local providers = cropEmpties(select(ksProv, "TtsAsrData", "Providers"))
	for _, provider in pairs(providers.Providers) do
		table.insert(providersResult, cjson.decode(provider))
	end
	return providersResult
end

-- ListTtsAsrProviderTypes
---------------------------
-- Returns a list of all the TTS/ASR Provider Types
function ListTtsAsrProviderTypes (ksProv)
	local providerTypesResult = {}
	local providerTypes = cropEmpties(select(ksProv, "TtsAsrData", "Types"))
	for _, providerType in pairs(providerTypes.Types) do
		table.insert(providerTypesResult, cjson.decode(providerType))
	end
	return providerTypesResult
end

------------------------------------------------------
-- TTS/ASR ENGINES
------------------------------------------------------

function ListTtsAsrVccEngines(vccId, ksAgent)
	return select(ksAgent, "VCCTtsAsrEngines", vccId)[vccId] or {}
end

function ListTtsAsrEnginesInUse(ksAgent, ksProv)
	local enginesInUse = {}
	local vccEngines = select(ksAgent, "VCCTtsAsrEngines") or {}
	local allEngines = cropEmpties(select(ksProv, "TtsAsrEngines"))
	for engineId, engine in pairs(allEngines) do
		for vccId, vcc in pairs(vccEngines) do
			if vcc.ttsEngine == engineId or vcc.asrEngine == engineId then
				enginesInUse[engineId] = true
				break
				--table.insert(enginesInUse, engineId)
			end
		end
	end
	return enginesInUse
end

-- ListTtsAsrProvisioningEngines
---------------------------
-- Returns a list of all the TTS/ASR Engines, with their associated data
function ListTtsAsrProvisioningEngines (ksProv)
	local providers = ListTtsAsrProviders(ksProv)
	local engines = cropEmpties(select(ksProv, "TtsAsrEngines"))
	for _, engine in pairs(engines) do
		engine.general = cjson.decode(engine.general)
		engine.profiles = cjson.decode(engine.profiles)
		for _, profile in pairs(engine.profiles) do
			--Replace provider by its corresponding data
			for _, provider in pairs(providers) do
				if profile.provider == provider.id then
					profile.provider = provider
				end
			end
			if type(profile.provider) == "string" then	--provider data not found: replace provider by a default table data
				local providerId = profile.provider
				profile.provider = {id = providerId, name = providerId, grammars = {}, voices = {}}
			end
		end
	end
	return engines
end

-- ListProvisionedTtsEngines
---------------------------
-- Returns a list of ENGINES with unused TTS PROFILES, including (if any) the engine and profile used by a specific vcc, with their associated data
function ListProvisionedTtsEngines (ksProv, ksAgent)
	local allEngines = ListTtsAsrProvisioningEngines(ksProv)
	local filteredTtsEngines = {}
	
	for k,v in pairs(allEngines) do
		if allEngines[k].enabled == nil or (allEngines[k].enabled and allEngines[k].enabled == "1") then
			local engineToAdd = {
				id = allEngines[k].id,
				description = allEngines[k].description,
				profiles = {}
			}
			local engineProfiles = allEngines[k].profiles
			for _, profile in pairs(engineProfiles) do
				if (profile.provider_type == "TTS" or profile.provider_type == "TTS-ASR") then
					local profileToAdd = {
						id = profile.id,
						description = profile.description,
						providerType = profile.provider_type
					}
					table.insert(engineToAdd.profiles, profileToAdd)
				end
			end
			
			if #engineToAdd.profiles > 0 then
				table.insert(filteredTtsEngines, engineToAdd)
			end
		end
	end
	
	return filteredTtsEngines
end

-- ListProvisionedAsrEngines
---------------------------
-- Returns a list of ENGINES with unused ASR PROFILES, including (if any) the engine and profile used by a specific vcc, with their associated data
function ListProvisionedAsrEngines (ksProv, ksAgent)
	local allEngines = ListTtsAsrProvisioningEngines(ksProv)
	local filteredAsrEngines = {}
	
	for k,v in pairs(allEngines) do
		if allEngines[k].enabled == nil or (allEngines[k].enabled and allEngines[k].enabled == "1") then
			local engineToAdd = {
				id = allEngines[k].id,
				description = allEngines[k].description,
				profiles = {}
			}
			local engineProfiles = allEngines[k].profiles
			for _, profile in pairs(engineProfiles) do
				if (profile.provider_type == "ASR" or profile.provider_type == "TTS-ASR") then
					local profileToAdd = {
						id = profile.id,
						description = profile.description,
						providerType = profile.provider_type
					}
					table.insert(engineToAdd.profiles, profileToAdd)
				end
			end
			
			if #engineToAdd.profiles > 0 then
				table.insert(filteredAsrEngines, engineToAdd)
			end
		end
	end
	
	return filteredAsrEngines
end

-- GetTtsAsrEngine
---------------------------
-- Returns data of a specific TTS/ASR Engine, or empty if non-existing
function GetTtsAsrEngine (ksProv, ttsasrEngineId)
	ttsasrEngineId = utf8.lower(ttsasrEngineId or "")
	return select(ksProv, "TtsAsrEngines", ttsasrEngineId)[ttsasrEngineId] or {}
end

-- UpdateTtsAsrEngine
---------------------------
-- Adds or modifies a TTS/ASR Engine in provisioning
function UpdateTtsAsrEngine (ksProv, ksAgent, ttsasrEngineId, ttsasrEngineData)
	ttsasrEngineId = utf8.lower(ttsasrEngineId)
	ttsasrEngineData.id = ttsasrEngineId

	LogDebug("[PROVISIONING_MANAGER:UpdateTtsAsrEngine] Updating TTS/ASR Engine: '" .. tostring(ttsasrEngineId) .. "', ttsasrEngineData: " .. tostring(json.encode(ttsasrEngineData or {})))

	local oldData = GetTtsAsrEngine(ksProv, ttsasrEngineId)
	for k, _ in pairs(oldData) do
		if not ttsasrEngineData[k] then
			ksProv:RemoveColumn(ttsasrEngineId, "TtsAsrEngines", k)
		end
	end
	for k, v in pairs(ttsasrEngineData) do
		ksProv:InsertColumn(ttsasrEngineId, "TtsAsrEngines", k, v)
	end

	-- OK
	return true
end

-- DeleteTtsAsrEngine
------------------------------
-- Removes a TTS/ASR Engine from provisioning
-- Pre: The TTS/ASR Engine must exist
-- Pre: The TTS/ASR Engine must not be associated with any vcc
function DeleteTtsAsrEngine (ksProv, ksAgent, ttsasrEngineId)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}
	ttsasrEngineId = utf8.lower(ttsasrEngineId)

	LogDebug("[PROVISIONING_MANAGER:DeleteTtsAsrEngine] Deleting TTS/ASR Engine: '" .. tostring(ttsasrEngineId) .. "'")

	-- EXIST validation
	local ttsasrEngine = GetTtsAsrEngine(ksProv, ttsasrEngineId)
	if isEmpty(ttsasrEngine) then
		result.success = false
		result.errType = "TTS/ASR ENGINE DOES NOT EXIST"
		result.errMsg = "TTS/ASR Engine "..tostring(ttsasrEngineId).." does not exist."
		return result
	end

	-- VCC validation
	local vcc = GetVCCsByTtsAsrEngine(ksAgent, ttsasrEngineId)
	if vcc then
		result.success = false
		result.errType = "CANNOT DELETE TTS/ASR ENGINE ASSOCIATED TO VCC"
		result.errMsg = "Cannot delete TTS/ASR Engine "..ttsasrEngineId.." while VCC "..vcc.." is still associated with it."
		result.relatedId = vcc
		return result
	end

	-- Ahora si lo elimino
	for k, _ in pairs(ttsasrEngine) do
		ksProv:RemoveColumn(ttsasrEngineId, "TtsAsrEngines", k)
	end

	-- IS OK
	result.success = true
	return result
end

function GetVCCsByTtsAsrEngine(ksAgent, ttsasrEngineId)
	local vccs = ListAllVCCTtsAsrEngines(ksAgent)
	for k, v in pairs(vccs) do
		if (v.ttsEngine and v.ttsEngine == ttsasrEngineId) or (v.asrEngine and v.asrEngine == ttsasrEngineId) then
			return k
		end
	end
end

function GetTtsAsrProfilesInUse(ksAgent, ttsasrEngineId)
	local ttsAsrProfilesInUse = {}
	local vccs = ListAllVCCTtsAsrEngines(ksAgent)
	for k, v in pairs(vccs) do
		if v.ttsProfile and v.ttsEngine and v.ttsEngine == ttsasrEngineId then
			ttsAsrProfilesInUse[v.ttsProfile] = v.Id
		end
		if v.asrProfile and v.asrEngine and v.asrEngine == ttsasrEngineId then
			ttsAsrProfilesInUse[v.asrProfile] = v.Id
		end
	end
	return ttsAsrProfilesInUse
end

------------------------------------------------------
-- CARRIERS
------------------------------------------------------

-- ListCarriers
---------------------------
-- Returns a list of all the carriers, with their associated data
function ListCarriers (ksProv)
	return cropEmpties(select(ksProv, "Carriers"))
end

-- ListCarrierIDs
---------------------------
-- Returns a list of all the carrier IDs, without data
function ListCarrierIDs (ksProv)
	return listKeys(ListCarriers(ksProv))
end

-- UpdateCarrier
---------------------------
-- Adds or modifies a carrier in provisioning
function UpdateCarrier (ksProv, carrierName, carrierData)
	carrierName = utf8.lower(carrierName)
	carrierData.id = carrierName

	LogDebug("[PROVISIONING_MANAGER:UpdateCarrier] Updating Carrier: '" .. tostring(carrierName) .. "', carrierData: " .. tostring(json.encode(carrierData or {})))

	local oldData = GetCarrier(ksProv, carrierName)
	for k, _ in pairs(oldData) do
		if not carrierData[k] then
			ksProv:RemoveColumn(carrierName, "Carriers", k)
		end
	end
	for k, v in pairs(carrierData) do
		ksProv:InsertColumn(carrierName, "Carriers", k, v)
	end
end

-- DeleteCarrier
------------------------------
-- Removes a carrier from provisioning
-- Pre: The carrier must exist
-- Pre: The carrier must not be associated with any DID
-- Pre: The carrier must not be associated with any trunk
-- Pre: The carrier must not be associated with any trunk group
function DeleteCarrier (ksProv, carrierName)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	carrierName = utf8.lower(carrierName)

	LogDebug("[PROVISIONING_MANAGER:DeleteCarrier] Deleting Carrier: '" .. tostring(carrierName) .. "'")

	-- EXIST validation
	local carrier = GetCarrier(ksProv, carrierName)
	if isEmpty(carrier) then
		result.success = false
		result.errType = "CARRIER DOES NOT EXIST"
		result.errMsg = "Carrier "..tostring(carrierName).." does not exist."
		return result
	end

	-- DID validation
	local DIDs = select(ksProv, "CarrierDIDs", carrierName)[carrierName] or {}
	for DID, _ in pairs(DIDs) do
		result.success = false
		result.errType = "CANNOT DELETE CARRIER ASSOCIATED TO DID"
		result.errMsg = "Cannot delete carrier "..carrierName.." while DID "..DID.." is still associated with it."
		result.relatedId = DID
		return result
	end

	-- TRUNK validation
	local trunks = select(ksProv, "CarrierTrunks", carrierName)[carrierName] or {}
	for trunk, _ in pairs(trunks) do
		result.success = false
		result.errType = "CANNOT DELETE CARRIER ASSOCIATED TO TRUNK"
		result.errMsg = "Cannot delete carrier "..carrierName.." while trunk "..trunk.." is still associated with it."
		result.relatedId = trunk
		return result
	end

	-- TRUNK GROUP validation
	local trunkGroups = select(ksProv, "CarrierTrunkGroups", carrierName)[carrierName] or {}
	for trunkGroup, _ in pairs(trunkGroups) do
		result.success = false
		result.errType = "CANNOT DELETE CARRIER ASSOCIATED TO TRUNK GROUP"
		result.errMsg = "Cannot delete carrier "..carrierName.." while trunk group "..trunkGroup.." is still associated with it."
		result.relatedId = trunkGroup
		return result
	end

	-- Ahora si lo elimino
	for k, _ in pairs(carrier) do
		ksProv:RemoveColumn(carrierName, "Carriers", k)
	end

	-- IS OK
	result.success = true
	return result
end

-- GetCarrier
---------------------------
-- Returns data of a specific carrier, or empty if non-existing
function GetCarrier (ksProv, carrierName)
	carrierName = utf8.lower(carrierName)
	return select(ksProv, "Carriers", carrierName)[carrierName] or {}
end

------------------------------------------------------
-- PEERS
------------------------------------------------------

-- UnpackPeer
---------------------------
-- Turns data of peer from Cassandra to Lua format
local function UnpackPeer (peerData)
	if peerData.codecs then
		peerData.codecs = table.concat(json.decode(peerData.codecs), ",")
	end
	if peerData.techSpecific then
		peerData.techSpecific = json.decode(peerData.techSpecific)
	end
	return peerData
end

-- PackPeer
---------------------------
-- Turns data of peer from Lua to Cassandra format
local function PackPeer (peerData)
	if peerData.codecs then
		peerData.codecs = json.encode(split(peerData.codecs, ","))
	end
	for k, v in pairs(peerData) do
		peerData[k] = (type(v) == "table" and json.encode(v) or tostring(v))
	end
	return peerData
end

-- ListPeers
---------------------------
-- Lists all the peers with their associated data
function ListPeers (ksProv)
	local peers = cropEmpties(select(ksProv, "Peers"))
	for peerID, peerData in pairs(peers) do
		peers[peerID] = UnpackPeer(peerData)
	end
	return peers
end

-- ListPeersByVCC
---------------------------
-- Returns all package ID's separated by VCC
function ListPeersByVCC (ksProv)
	return listSubKeys(select(ksProv, "VCCPeers"))
end

-- ListPeersByHost
---------------------------
-- Lists all the peers, separated by host ID
function ListPeersByHost(ksProv)
	return listSubKeys(select(ksProv, "PeersByHost"))
end

--
---------------------------
function GetPeersByHost(ksProv, hostId)
	local peersByHost = (listSubKeys(select(ksProv, "PeersByHost")) or {})[hostId]
	LogDebug("[PROVISIONING_MANAGER:GetPeersByHost] Host: '" .. tostring(hostId) .. "', peersByHost: " .. tostring(json.encode(peersByHost or {})))
	return peersByHost
end

-- UpdatePeer
---------------------------
-- Adds or modifies a peer in provisioning
function UpdatePeer (ksProv, tech, name, peerData, noLog)
	if noLog ~= true then
		LogDebug("[PROVISIONING_MANAGER:UpdatePeer] Updating PEER. Tech: '" .. tostring(tech) .. "', name: '" .. tostring(name) ..  "', peerData: " .. tostring(json.encode(peerData or {})))
	end

	-- HOST validation
	local hostInfo = GetHost(ksProv, peerData.hostId)
	if isEmpty(hostInfo) then
		return false, {
			errMsg = "Host "..tostring(peerData.hostId).." does not exist.",
			code = "HOST DOES NOT EXIST",
			rawData = {
				hostId = peerData.hostId
			}
		}
	end

	if not hostInfo.sipServer or not hostInfo.telProxyPort then
		return false, {
			errMsg = "Peer's resource_address is not in IP:Port format",
			code = "PEER RESOURCE_ADDRESS IS NOT IN IP:PORT FORMAT",
			rawData = {
				resource_address = peerData.resource_address or "NONE",
				hostId = peerData.hostId,
				sipServer = hostInfo.sipServer or "NULL",
				telProxyPort = hostInfo.telProxyPort or "NULL"
			}
		}
	end

	-- Ahora si, actualizo

	peerData.hostId = string.lower(peerData.hostId)
	peerData.resource_address = string.format("%s:%s", tostring(hostInfo.sipServer), tostring(hostInfo.telProxyPort))

	tech = utf8.upper(tech)
	name = utf8.lower(name)
	peerData.tech = tech
	peerData.name = name
	peerData.address_id = tech.."/"..name
	local peerID = tech.."_"..name
	local oldPeer = GetPeer(ksProv, tech, name)
	peerData.VCC = oldPeer.VCC
	peerData.assignationType = oldPeer.assignationType
	peerData = PackPeer(peerData)
	for k, _ in pairs(oldPeer) do
		if not peerData[k] then
			ksProv:RemoveColumn(peerID, "Peers", k)
		end
	end
	for k, v in pairs(peerData) do
		ksProv:InsertColumn(peerID, "Peers", k, v)
	end
	if not peerData.VCC then
		ksProv:InsertColumn('Free', 'VCCPeers', peerID, "")
	end
	if oldPeer.hostId ~= peerData.hostId then
		if oldPeer.hostId then
			ksProv:RemoveColumn(oldPeer.hostId, 'PeersByHost', peerID)
		end
		ksProv:InsertColumn(peerData.hostId, 'PeersByHost', peerID, "")
	end

	-- OK
	return true
end

-- AddPeerRange
---------------------------
-- Adds a range of peers, if they do not exist
function AddPeerRange (ksProv, tech, start, count, peerData)
	LogDebug("[PROVISIONING_MANAGER:AddPeerRange] Adding Peers Range. Tech: '" .. tostring(tech) .. "', start: " .. tostring(start) ..  "', count: " .. tostring(count) ..  "', peerData: " .. tostring(json.encode(peerData or {})))

	-- Ahora si, actualizo
	local noLog = true
	local notAdded = {}

	for i=start, start+count-1 do
		local peerName = tostring(i)
		if isEmpty(GetPeer(ksProv, tech, peerName)) then
			local safePeerData = { displayName = peerName }
			for k,v in pairs(peerData) do
				safePeerData[k] = v
			end
			UpdatePeer(ksProv, tech, peerName, safePeerData, noLog)
		else
			table.insert(notAdded, peerName)
		end
	end

	-- No se ha podido agregar ningun teléfono
	local totalNoAdded = #notAdded
	if count == totalNoAdded then
		return false, {
			errMsg = "No PEER could be added, they already exist.",
			code = "ALREADY EXIST ALL PEERS",
			rawData = {}
		}
	end

	-- OK
	if totalNoAdded == 0 then
		-- se agregaron todos
		return true, {code = "ALL PEERS ADDED", rawData = {}}
	end
	-- algunos no se agregaron
	return true, {
		errMsg = "Not all Peers have been added",
		code = "PARTIAL ADDED PEERS",
		rawData = {
			total = count,
			notAdded = notAdded
		}
	}
end

-- UpdatePeerRange
---------------------------
-- Updates a range of peers, add if they do not exist
function UpdatePeerRange (ksProv, tech, start, count, peerData)
	LogDebug("[PROVISIONING_MANAGER:UpdatePeerRange] Updating Peers Range. Tech: '" .. tostring(tech) .. "', start: " .. tostring(start) ..  "', count: " .. tostring(count) ..  "', peerData: " .. tostring(json.encode(peerData or {})))

	-- Ahora si, actualizo
	local notUpdated = {}

	for i=start, start+count-1 do
		local peerName = tostring(i)
		local peer = GetPeer(ksProv, tech, peerName)
		local isFound = not isEmpty(peer)

		--LogDebug("[PROVISIONING_MANAGER:UpdatePeerRange] peerName: " .. tostring(peerName) .. ", isFound: " .. tostring(isFound))

		if isFound then
			for k,v in pairs(peerData) do
				peer[k] = v
			end
			UpdatePeer(ksProv, tech, peerName, peer)
		else
			table.insert(notUpdated, peerName)
		end
	end

	-- No se ha podido actualizar ningun teléfono
	local totalNoUpdated = #notUpdated
	if count == totalNoUpdated then
		return false, {
			errMsg = "Could not update any peer. There are no specified peers",
			code = "THERE ARE NO SPECIFIED PEERS",
			rawData = {}
		}
	end

	-- OK
	if totalNoUpdated == 0 then
		-- se actualizaron todos
		return true, {code = "ALL PEERS UPDATED", rawData = {}}
	end

	-- algunos no se actualizaron
	return true, {
		errMsg = "Not all Peers have been updated",
		code = "PARTIAL UPDATED PEERS",
		rawData = {
			total = count,
			notUpdated = notUpdated
		}
	}
end

-- AddFreePeers
---------------------------
-- Adds as many peers as requested in the host which has less peers
function AddFreePeers (ksProv, ksAgent, count, tech, codecs, VCC)
	count = tonumber(count);

	-- defaults
	local tech = tech or "SIP";
	local codecs = codecs or "speex,g729,alaw,ulaw,h264,h263,h263p";
	local VCC = VCC or "";

	if VCC ~= "" and isEmpty(GetVCC(ksAgent, VCC)) then
		return 0, {}, "VCC "..VCC.." does not exist."
	end

	local result = {};

	-- returns the host with less peers in the cluster
	local getBestHost = function()
		local bestHost = nil;
		local hostList = {};

		local peersByHost = ListPeersByHost(ksProv);

		-- returns host-data stored on each peer
		local fillHostData = function(bestHost)
			for _,peerId in pairs(peersByHost[bestHost.hostId]) do
				local peer = UnpackPeer(select(ksProv, "Peers", peerId)[peerId] or {})
				if not isEmpty(peer) then
					bestHost.resource_address = peer.resource_address;
					bestHost.rtmpServer = peer.rtmpServer;
					bestHost.sipServer = peer.sipServer;
					bestHost.password = peer.password;
				end
			end
		end

		for hostId,peerList in pairs(peersByHost) do
			table.insert(hostList, hostId);

			local peerCount = 0;
			for _,_ in pairs(peerList) do
				peerCount = peerCount + 1;
			end
			if not bestHost or bestHost.peerCount > peerCount then
				bestHost = { hostId = hostId, peerCount = peerCount };
			end
		end

		local hostIndex = 1;
		if bestHost then
			fillHostData(bestHost)
			-- get the index of the best host assuming host names are ordered
			table.sort(hostList);
			while hostList[hostIndex] ~= bestHost.hostId do
				hostIndex = hostIndex + 1;
			end
		end

		return bestHost, hostIndex;
	end

	local pendingPeers = count;
	while pendingPeers > 0 do
		local bestHost, hostIndex = getBestHost();

		if not bestHost then
			return 0, result, "This function needs at least one peer in the DB, you should use 'add peer' and then call me again";
		end

		if bestHost.peerCount >= 9999 then
			return #result, result, "Cannot create more peers";
		end

		for startPeer = bestHost.peerCount+1,9999 do
			local peerName = hostIndex .. string.format("%04d", startPeer);

			if isEmpty(GetPeer(ksProv, tech, peerName)) then
				local peerData = {
					hostId = bestHost.hostId,
					displayName = peerName,
					codecs = codecs,
					resource_address = bestHost.resource_address,
					rtmpServer = bestHost.rtmpServer,
					sipServer = bestHost.sipServer,
					password = bestHost.password
				};
				UpdatePeer(ksProv, tech, peerName, peerData);

				if VCC ~= "" then
					AssignPeerToVCC(ksProv, ksAgent, tech, peerName, VCC)
				end

				table.insert(result, tech .."_".. peerName);
				pendingPeers = pendingPeers - 1;
				if pendingPeers == 0 then
					break;
				end
			end
		end
	end

	return #result, result;
end

-- DeletePeer
------------------------------
-- Removes a peer from provisioning
-- Pre: The peer must exist
-- Pre: Peer must not be assigned to a VCC
function DeletePeer (ksProv, tech, name)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	tech = utf8.upper(tech)
	name = utf8.lower(name)
	local peerID = tech.."_"..name

	-- EXIST validation
	local peer = GetPeer(ksProv, tech, name)
	if isEmpty(peer) then
		result.success = false
		result.errType = "PEER DOES NOT EXIST"
		result.errMsg = "Peer ".. tostring(peerID) .. " does not exist."
		return result
	end

	-- IN USE validation
	if peer.VCC then
		result.success = false
		result.errType = "PEER IN USE"
		result.errMsg = "Peer "..peerID.." belongs to VCC "..peer.VCC..". Free it before deleting it."
		result.relatedId = peer.VCC
		return result
	end

	-- Ahora si lo elimino
	ksProv:RemoveColumn('Free', 'VCCPeers', peerID)
	for k, _ in pairs(peer) do
		ksProv:RemoveColumn(peerID, "Peers", k)
	end
	ksProv:RemoveColumn(peer.hostId, 'PeersByHost', peerID)

	-- IS OK
	result.success = true
	return result
end

-- GetPeer
---------------------------
-- Returns data of a specific peer, or empty if non-existing
function GetPeer (ksProv, tech, name)
	tech = tech or "SIP" -- default
	tech = utf8.upper(tech)
	name = utf8.lower(name)
	local peerID = tech.."_"..name
	local peer = select(ksProv, "Peers", peerID)[peerID] or {}
	return UnpackPeer(peer)
end

-- GetPeer User
---------------------------
-- Returns data of a specific peer, or empty if non-existing
function GetPeerUser (ksAgent, tech, name)
	tech = utf8.upper(tech)
	name = utf8.lower(name)
	local peerID = tech.."_"..name
	local users = select(ksAgent, "UsersByPeer", peerID)[peerID] or {}
	for user, _ in pairs(users) do
		return user
	end
	return nil
end

-- GetPeerWithHostInfo
---------------------------
-- Esta funcion retorna el peer con la informacion de la realacion de la CF Hosts (rtmpServer, sipServer, telProxyPort)
--
function GetPeerWithHostInfo (ksProv, tech, name)
	local peer = GetPeer(ksProv, tech, name)
	local peerId = tostring(tech) .. "_" .. tostring(name)
	if not peer then
		-- el peer no existe
		LogError("[PROVISIONING_MANAGER:GetPeerWithHostInfo] Unable to retrive peer '" .. peerId .. "' with host info. Peer does not exist.")
		return
	end
	local hostInfo = GetHost(ksProv, peer.hostId)
	if not hostInfo then
		-- no encontro informacion host para el peer
		LogWarning("[PROVISIONING_MANAGER:GetPeerWithHostInfo] Unable to retrive peer '" .. peerId .. "' with host info. Host '" .. tostring(peer.hostId) .. "' does not exist.")
		return peer
	end
	-- ok
	peer.rtmpServer = hostInfo.rtmpServer
	peer.sipServer = hostInfo.sipServer
	peer.telProxyPort = hostInfo.telProxyPort
	return peer
end

-- AssignPeerToVCC
---------------------------
-- Assigns a free peer to a VCC
-- Pre: Peer must exist
-- Pre: VCC must exist
-- Pre: Peer must not be assigned to a VCC
function AssignPeerToVCC (ksProv, ksAgent, tech, name, VCC, assignationType)
	assert(tech)
	assert(name)
	assert(VCC)
	assignationType = assignationType or "agent"

	tech = utf8.upper(tech)
	name = utf8.lower(name)
	local peerID = tech.."_"..name
	VCC = utf8.lower(VCC)

	--LogDebug("[PROVISIONING_MANAGER:AssignPeerToVCC] Assigning Peer '" .. tostring(peerID) .. "' to VCC " .. tostring(VCC))

	if isEmpty(GetVCC(ksAgent, VCC)) then
		return false, {
			errMsg = "VCC "..VCC.." does not exist.",
			code = "VCC DOES NOT EXIST",
			rawData = {
				peerID = peerID,
				vcc = VCC
			}
		}
	end
	local peer = GetPeer(ksProv, tech, name)
	if isEmpty(peer) then
		return false, {
			errMsg = "Peer "..peerID.." does not exist.",
			code = "PEER DOES NOT EXIST",
			rawData = {
				peerID = peerID,
				vcc = VCC
			}
		}
	end
	if peer.VCC == VCC then
		return false, {
			errMsg = "Peer "..peerID.." already belongs to VCC "..VCC..".",
			code = "PEER ALREADY BELONGS TO VCC",
			rawData = {
				peerID = peerID,
				vcc = VCC
			}
		}
	end
	if peer.VCC then
		return false, {
			errMsg = "Peer "..tostring(peerID).." belongs to VCC "..tostring(peer.VCC)..". Free it before adding it to "..tostring(VCC),
			code = "PEER BELONGS TO ANOTHER VCC",
			rawData = {
				peerID = peerID,
				vcc = peer.VCC
			}
		}
	end

	-- Ahora si, actualizo
	ksProv:InsertColumn(peerID, "Peers", "VCC", VCC)
	ksProv:InsertColumn(peerID, "Peers", "assignationType", assignationType)
	ksProv:InsertColumn("VCC:"..VCC, 'VCCPeers', peerID, assignationType)
	ksProv:RemoveColumn("Free", 'VCCPeers', peerID)
	m_redis:sadd("VccFreePeers:" .. assignationType .. ":" .. VCC, peerID)

	-- OK
	return true
end


---------------------------
-- Assigns a range of peers to a VCC
-- Pre: Peer must exist
-- Pre: VCC must exist
-- Pre: Peer must not be assigned to a VCC
function AssignPeerRangeToVCC (ksProv, ksAgent, tech, start, count, vcc, assignationType)
	local errors = {}
	for i=start, start+count-1 do
		local peerName = tostring(i)
		local success, err = AssignPeerToVCC(ksProv, ksAgent, tech, peerName, vcc, assignationType)
		if not success then
			table.insert(errors, err)
		end
	end
	local success = #errors == 0
	return success, errors
end

-- FreePeerFromVCC
---------------------------
-- Frees a peer from its VCC
-- Pre: Peer must exist
-- Pre: Peer must be assigned to a VCC
-- Pre: Peer must be free in VCC
function FreePeerFromVCC (ksProv, ksAgent, tech, name, vcc)
	-- TODO: Check it's not associated with any agent!
	assert(tech)
	assert(name)
	assert(vcc)

	tech = utf8.upper(tech)
	name = utf8.lower(name)
	local peerID = tech.."_"..name

	--LogDebug("[PROVISIONING_MANAGER:FreePeerFromVCC] Release Peer '" .. tostring(peerID) .. "' from VCC " .. tostring(vcc))

	local peer = GetPeer(ksProv, tech, name)
	if isEmpty(peer) then
		return false, {
			errMsg = "Peer "..peerID.." does not exist.",
			code = "PEER DOES NOT EXIST",
			rawData = {
				peerID = peerID,
				vcc = vcc
			}
		}
	end
	if not peer.VCC then
		return false, {
			errMsg = "Peer "..peerID.." does not belong to any VCC.",
			code = "PEER DOES NOT BELONG TO ANY VCC",
			rawData = {
				peerID = peerID,
				vcc = vcc
			}
		}
	end
	if vcc and peer.VCC ~= vcc then
		return false, {
			errMsg = "Peer "..peerID.." belongs to another VCC "..vcc..".",
			code = "PEER BELONGS TO ANOTHER VCC",
			rawData = {
				peerID = peerID,
				vcc = vcc
			}
		}
	end
	local user = GetPeerUser(ksAgent, tech, name)
	if user then
		return false, {
			errMsg = "Peer "..peerID.." is in use by user: " .. user .. ".",
			code = "PEER IS IN USE BY USER",
			rawData = {
				peerID = peerID,
				user = user,
				vcc = vcc
			}
		}
	end

	-- Ahora si, actualizo
	ksProv:RemoveColumn(peerID, "Peers", "VCC")
	ksProv:RemoveColumn(peerID, "Peers", "assignationType")
	ksProv:RemoveColumn("VCC:"..peer.VCC, 'VCCPeers', peerID)
	ksProv:InsertColumn("Free", 'VCCPeers', peerID, "")
	m_redis:srem("VccFreePeers:" .. peer.assignationType .. ":" .. peer.VCC, peerID)

	-- OK
	return true
end


---------------------------
-- Frees a range of peers from a VCC
-- Pre: Peer must exist
-- Pre: Peer must be assigned to VCC
function FreePeerRangeFromVCC (ksProv, ksAgent, tech, start, count, vcc)
	local errors = {}
	for i=start, start+count-1 do
		local peerName = tostring(i)
		local success, err = FreePeerFromVCC(ksProv, ksAgent, tech, peerName, vcc)
		if not success then
			table.insert(errors, err)
		end
	end
	local success = #errors == 0
	return success, errors
end

-----------------------------
-- Función utilizada para cargar información del Peer en la relación VCC->Peer
local function LoadVCCPeersInfo (ksProv, vccPeers)
	assert(ksProv)

	for peerId in pairs(vccPeers or {}) do
		local tech, name = SeparateText(peerId, "_")
		local peerInfo = GetPeer(ksProv, tech, name)
		if peerInfo then
			vccPeers[peerId] = peerInfo
		end
	end
end

-- GetVCCPeers
---------------------------
function GetVCCPeers (ksProv, VCC, assignationType)
	assert(ksProv)
	assert(VCC)
	local key = "VCC:"..VCC
	local vccPeers = select(ksProv, "VCCPeers", key)[key] or {}
	if assignationType then
		local vccPeers_a = vccPeers
		vccPeers = {}
		for peerId, peerType in pairs(vccPeers_a) do
			if peerType == assignationType then
				vccPeers[peerId] = peerType
			end
		end
	end
	--LogDebug("[PROVISIONING_MANAGER:GetVCCPeers] VCC: '" .. tostring(key) .. "' Peers: " .. json.encode(vccPeers))
	LoadVCCPeersInfo(ksProv, vccPeers)
	return vccPeers
end

-- GetFreeVCCPeers
---------------------------
function GetFreeVCCPeers (ksProv)
	assert(ksProv)
	local key = "Free"
	local freeVccPeers = select(ksProv, "VCCPeers", key)[key] or {}
	--LogDebug("[PROVISIONING_MANAGER:GetFreeVCCPeers] Free VCC Peers: " .. json.encode(freeVccPeers))
	LoadVCCPeersInfo(ksProv, freeVccPeers)
	return freeVccPeers
end

-- ResetPeerToVCC
---------------------------

function ResetPeerToVCC (ksProv, ksAgent, VCC)
	assert(ksProv)
	assert(VCC)
	local key = "VCC:"..VCC
	local assignationType = {}
	local vccPeers = select(ksProv, "VCCPeers", key)[key] or {}
	for peerId, peerType in pairs(vccPeers) do
		if not assignationType[peerType] then
			assignationType[peerType] = true
		end
	end
	for assignationType in pairs(assignationType) do
		m_redis:del("VccFreePeers:" .. assignationType .. ":" .. VCC)
	end
	for peerId, peerType in pairs(vccPeers) do
		m_redis:sadd("VccFreePeers:" .. peerType .. ":" .. VCC, peerId)
	end
	return true
end

------------------------------------------------------
-- TRUNKS
------------------------------------------------------

-- UnpackTrunk
---------------------------
-- Turns data of trunk from Cassandra to Lua format
local function UnpackTrunk (trunkData)
	if trunkData.codecs then
		trunkData.codecs = table.concat(json.decode(trunkData.codecs), ",")
	end
	if trunkData.techSpecific then
		trunkData.techSpecific = json.decode(trunkData.techSpecific)
	end
	return trunkData
end

-- PackTrunk
---------------------------
-- Turns data of trunk from Lua to Cassandra format
local function PackTrunk (trunkData)
	if trunkData.codecs then
		trunkData.codecs = json.encode(split(trunkData.codecs, ","))
	end
	for k, v in pairs(trunkData) do
		trunkData[k] = (type(v) == "table" and json.encode(v) or tostring(v))
	end
	return trunkData
end

-- ListTrunks
---------------------------
-- Lists all the trunks with their associated data
function ListTrunks (ksProv)
	local trunks = cropEmpties(select(ksProv, "Trunks"))
	for trunkID, trunkData in pairs(trunks) do
		trunks[trunkID] = UnpackTrunk(trunkData)
	end
	return trunks
end

-- ListTrunksByHost
---------------------------
-- Lists all the trunks, separated by host ID
function ListTrunksByHost (ksProv)
	return listSubKeys(select(ksProv, "TrunksByHost"))
end

-- GetTrunksByHost
---------------------------
function GetTrunksByHost(ksProv, hostId)
	local trunksByHost = (ListTrunksByHost(ksProv) or {})[hostId]
	LogDebug("[PROVISIONING_MANAGER:GetTrunksByHost] Host: '" .. tostring(hostId) .. "', trunksByHost: " .. tostring(json.encode(trunksByHost or {})))
	return trunksByHost
end

-- ListTrunksByCarrier
---------------------------
-- Lists all the trunks, separated by carrier
function ListTrunksByCarrier (ksProv)
	return listSubKeys(select(ksProv, "CarrierTrunks"))
end

-- ListCarriersWithTrunks
---------------------------
-- Lists all the trunks, separated by carrier
function ListCarriersWithTrunks (ksProv)
	local carriersTrunks = select(ksProv, "CarrierTrunks")
	--LogDebug("[PROVISIONING_MANAGER:ListCarrierWithTrunks] List Carriers with Trunks: " .. json.encode(carriersTrunks or {}))
	return carriersTrunks
end

-- UpdateTrunk
---------------------------
-- Adds or modifies a trunk in provisioning
-- Pre: The trunk must specify an existing carrier
-- Pre: The trunk must specify a hostId
-- Pre: If the trunk is associated with any DID or belongs to a group, it must keep its carrier
function UpdateTrunk (ksProv, trunkID, trunkData)
	trunkID = utf8.lower(trunkID)
	trunkData.id = trunkID

	LogDebug("[PROVISIONING_MANAGER:UpdateTrunk] Updating TRUNK '" .. tostring(trunkID) .. "', trunkData: " .. tostring(json.encode(trunkData or {})))

	-- CARRIER validation
	if not trunkData.carrier then
		return false, {errMsg = "Trunk must specify an existing carrier.",
			code = "TRUNK MUST SPECIFY AN EXISTING CARRIER",
			rawData = {}
		}
	end
	trunkData.carrier = utf8.lower(trunkData.carrier)
	if isEmpty(GetCarrier(ksProv, trunkData.carrier)) then
		return false, {errMsg = "Carrier "..tostring(trunkData.carrier).." does not exist.",
			code = "CARRIER DOES NOT EXIST",
			rawData = {
				carrier = trunkData.carrier
			}
		}
	end

	-- HOST validation
	if not trunkData.hostId then
		return false, {errMsg = "Field 'hostId' must be specified.",
			code = "FIELD HOSTID MUST BE SPECIFIED",
			rawData = {}
		}
	end

	local hostInfo = GetHost(ksProv, trunkData.hostId)
	if isEmpty(hostInfo) then
		return false, {
			errMsg = "Host "..tostring(trunkData.hostId).." does not exist.",
			code = "HOST DOES NOT EXIST",
			rawData = {
				hostId = trunkData.hostId
			}
		}
	end
	trunkData.hostId = utf8.lower(trunkData.hostId)

	-- TRUNK validation
	local oldTrunk = GetTrunk(ksProv, trunkID)
	if oldTrunk.carrier and oldTrunk.carrier ~= trunkData.carrier then
		local DIDs = select(ksProv, "TrunkDIDs", trunkID)[trunkID] or {}
		for D, _ in pairs(DIDs) do
			return false, {errMsg = "Cannot change trunk "..trunkID.."'s carrier while DID "..D.." is still associated with it.",
				code = "CANNOT CHANGE TRUNK CARRIER WHILE DID IS STILL ASSOCIATED WITH IT",
				rawData = {
					trunk = trunkID,
					did = D
				}
			}
		end
		local currentGroup = select(ksProv, "CarrierTrunks", oldTrunk.carrier)[oldTrunk.carrier][trunkID]
		if currentGroup and currentGroup ~= "" then
			return false, {errMsg = "Cannot change trunk "..trunkID.."'s carrier while it belongs to group "..currentGroup..".",
				code = "CANNOT CHANGE TRUNK CARRIER WHILE IT BELONGS TO GROUP",
				rawData = {
					trunk = trunkID,
					currentGroup = currentGroup
				}
			}
		end
	end

	-- gateway
	local gatewayDialplan = trunkData.gatewayDialplan or ""
	trunkData.gatewayDialplan = nil
	if oldTrunk.gateway and trunkData.gateway ~= oldTrunk.gateway then
		local result = DeleteGatewayNodeTrunk(ksProv, oldTrunk.gateway, trunkID)
		if not result.success and result.errType ~= "GATEWAY TRUNK DOES NOT EXIST" then
			return false, { code = result.errType, errMsg = result.errMsg, rawData = { trunk = trunkID } }
		end
	end
	if trunkData.gateway then
		local result = UpdateGatewayNodeTrunkFromNode({
			ksProv = ksProv,
			gatewayId = trunkData.gateway,
			dialplanContext = gatewayDialplan,
			trunkId = trunkID,
			nodeData = trunkData,
			hostInfo = hostInfo
		})
		if not result.success then
			return false, { code = result.errType, errMsg = result.errMsg, rawData = { trunk = trunkID } }
		end
	end

	-- Ahora si, actualizo
	trunkData = PackTrunk(trunkData)
	for k, _ in pairs(oldTrunk) do
		if not trunkData[k] then
			ksProv:RemoveColumn(trunkID, "Trunks", k)
		end
	end
	for k, v in pairs(trunkData) do
		ksProv:InsertColumn(trunkID, "Trunks", k, v)
	end
	if oldTrunk.hostId ~= trunkData.hostId then
		if oldTrunk.hostId then
			ksProv:RemoveColumn(oldTrunk.hostId, 'TrunksByHost', trunkID)
		end
		ksProv:InsertColumn(trunkData.hostId, 'TrunksByHost', trunkID, "")
	end
	if oldTrunk.carrier ~= trunkData.carrier then
		if oldTrunk.carrier then
			ksProv:RemoveColumn(oldTrunk.carrier, 'CarrierTrunks', trunkID)
		end
		ksProv:InsertColumn(trunkData.carrier, 'CarrierTrunks', trunkID, "")
	end

	-- OK
	return true
end

-- DeleteTrunk
------------------------------
-- Removes a trunk from provisioning
-- Pre: The trunk must exist
-- Pre: The trunk must not be associated with any DIDs
-- Post: If the trunk belonged to a group, it is removed from the group
function DeleteTrunk (ksProv, trunkID)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	trunkID = utf8.lower(trunkID)

	-- EXIST validation
	local trunk = GetTrunk(ksProv, trunkID)
	if isEmpty(trunk) then
		result.success = false
		result.errType = "TRUNK DOES NOT EXIST"
		result.errMsg = "Trunk ".. tostring(trunkID) .. " does not exist."
		return result
	end

	-- DID validation
	local DIDs = select(ksProv, "TrunkDIDs", trunkID)[trunkID] or {}
	for DID, _ in pairs(DIDs) do
		result.success = false
		result.errType = "CANNOT DELETE TRUNK ASSOCIATED TO DID"
		result.errMsg = "Cannot delete trunk "..trunkID.." while DID "..DID.." is still associated with it."
		result.relatedId = DID
		return result
	end

	-- Elimino el troncal del gateway asociado
	if trunk.gateway then
		result = DeleteGatewayNodeTrunk(ksProv, trunk.gateway, trunkID)
		if not result.success then
			return result
		end
	end

	-- Ahora si lo Elimino
	local group = (select(ksProv, "CarrierTrunks", trunk.carrier)[trunk.carrier] or {})[trunkID]
	if group and group ~= "" then
		RemoveTrunkFromGroup(ksProv, trunkID)
	end
	for k, _ in pairs(trunk) do
		ksProv:RemoveColumn(trunkID, "Trunks", k)
	end
	ksProv:RemoveColumn(trunk.carrier, 'CarrierTrunks', trunkID)
	ksProv:RemoveColumn(trunk.hostId, 'TrunksByHost', trunkID)

	-- IS OK
	result.success = true
	return result
end

-- GetTrunk
---------------------------
-- Returns data of a specific trunk, or empty if non-existing
function GetTrunk (ksProv, trunkID)
	trunkID = utf8.lower(trunkID)
	return UnpackTrunk(select(ksProv, "Trunks", trunkID)[trunkID] or {})
end

------------------------------------------------------
-- TRUNK GROUPS
------------------------------------------------------

-- UnpackTrunkGroup
---------------------------
-- Turns data of trunk group from Cassandra to Lua format
local function UnpackTrunkGroup (trunkGroupData)
	if trunkGroupData.trunkList then
		trunkGroupData.trunkList = json.decode(trunkGroupData.trunkList)
	end
	return trunkGroupData
end

-- PackTrunkGroup
---------------------------
-- Turns data of trunk group from Lua to Cassandra format
local function PackTrunkGroup (trunkGroupData)
	trunkGroupData.trunkList = json.encode(trunkGroupData.trunkList)
	return trunkGroupData
end

-- ListTrunkGroups
---------------------------
-- Lists all the trunk groups with their associated data
function ListTrunkGroups (ksProv)
	local trunkGroups = cropEmpties(select(ksProv, "TrunkGroups"))
	for trunkGroupID, trunkGroupData in pairs(trunkGroups) do
		trunkGroups[trunkGroupID] = UnpackTrunkGroup(trunkGroupData)
	end
	return trunkGroups
end

-- ListTrunkGroupsByCarrier
---------------------------
-- Lists all the trunk groups, separated by carrier
function ListTrunkGroupsByCarrier (ksProv)
	return listSubKeys(select(ksProv, "CarrierTrunkGroups"))
end

-- UpdateTrunkGroup
---------------------------
-- Adds or modifies a trunk group in provisioning
-- Pre: The group must specify an existing carrier
-- Pre: If the trunk is associated with any DID or has any trunks, it must keep its carrier
-- Post: If new, the group is empty of trunks
function UpdateTrunkGroup (ksProv, trunkGroupID, trunkGroupData)
	trunkGroupID = utf8.lower(trunkGroupID)
	trunkGroupData.id = trunkGroupID

	LogDebug("[PROVISIONING_MANAGER:UpdateTrunkGroup] Updating TRUNK GROUP '" .. tostring(trunkGroupID) .. "', trunkGroupData: " .. tostring(json.encode(trunkGroupData or {})))

	if not trunkGroupData.carrier then
		return false, {
			errMsg = "Trunk group must specify an existing carrier.",
			code = "TRUNK GROUP MUST SPECIFY AN EXISTING CARRIER",
			rawData = {}
		}
	end
	trunkGroupData.carrier = utf8.lower(trunkGroupData.carrier)
	if isEmpty(GetCarrier(ksProv, trunkGroupData.carrier)) then
		return false, {
			errMsg = "Carrier "..tostring(trunkGroupData.carrier).." does not exist.",
			code = "CARRIER DOES NOT EXIST",
			rawData = {}
		}
	end

	local oldTrunkGroup = GetTrunkGroup(ksProv, trunkGroupID)
	if oldTrunkGroup.carrier and oldTrunkGroup.carrier ~= trunkGroupData.carrier then
		local DIDs = select(ksProv, "TrunkGroupDIDs", trunkGroupID)[trunkGroupID] or {}
		for D, _ in pairs(DIDs) do
			return false, {
				errMsg = "Cannot change trunk group " .. trunkGroupID .. "'s carrier while DID " .. D .. " is still associated with it.",
				code = "CANNOT CHANGE TRUNK GROUP CARRIER WHILE DID IS STILL ASSOCIATED WITH IT",
				rawData = {
					trunkGroup = trunkGroupID,
					did = D
				}
			}
		end
		for trunkID, _ in pairs(oldTrunkGroup.trunkList or {}) do
			return false, {
				errMsg = "Cannot change trunk group " .. trunkGroupID .. "'s carrier while trunk " .. trunkID .. " is still associated with it.",
				code = "CANNOT CHANGE TRUNK GROUP CARRIER WHILE TRUNK IS STILL ASSOCIATED WITH IT",
				rawData = {
					trunkGroup = trunkGroupID,
					trunkID = trunkID
				}
			}
		end
	end

	-- Ahora si, actualizo
	trunkGroupData.trunkList = oldTrunkGroup.trunkList or {}
	trunkGroupData = PackTrunkGroup(trunkGroupData)
	for k, _ in pairs(oldTrunkGroup) do
		if not trunkGroupData[k] then
			ksProv:RemoveColumn(trunkGroupID, "TrunkGroups", k)
		end
	end
	for k, v in pairs(trunkGroupData) do
		ksProv:InsertColumn(trunkGroupID, "TrunkGroups", k, v)
	end
	if oldTrunkGroup.carrier ~= trunkGroupData.carrier then
		if oldTrunkGroup.carrier then
			ksProv:RemoveColumn(oldTrunkGroup.carrier, 'CarrierTrunkGroups', trunkGroupID)
		end
		ksProv:InsertColumn(trunkGroupData.carrier, 'CarrierTrunkGroups', trunkGroupID, "")
	end

	-- OK
	return true
end

-- DeleteTrunkGroup
------------------------------
-- Removes a trunk group from provisioning
-- Pre: The trunk group must exist
-- Pre: The trunk group must not be associated with any DIDs
-- Post: If the trunk groups contained trunks, they are freed from their group
function DeleteTrunkGroup (ksProv, trunkGroupID)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	trunkGroupID = utf8.lower(trunkGroupID)

	-- EXIST validation
	local trunkGroup = GetTrunkGroup(ksProv, trunkGroupID)
	if isEmpty(trunkGroup) then
		result.success = false
		result.errType = "TRUNK GROUP DOES NOT EXIST"
		result.errMsg = "Trunk Group".. tostring(trunkGroupID) .. " does not exist."
		return result
	end

	-- DID validation
	local DIDs = select(ksProv, "TrunkGroupDIDs", trunkGroupID)[trunkGroupID] or {}
	for DID, _ in pairs(DIDs) do
		result.success = false
		result.errType = "CANNOT DELETE TRUNK GROUP ASSOCIATED TO DID"
		result.errMsg = "Cannot delete trunk Group "..trunkGroupID.." while DID "..DID.." is still associated with it."
		result.relatedId = DID
		return result
	end

	-- Ahora si lo Elimino
	for trunk, _ in pairs(trunkGroup.trunkList) do
		ksProv:InsertColumn(trunkGroup.carrier, 'CarrierTrunks', trunk, "")
	end
	for k, _ in pairs(trunkGroup) do
		ksProv:RemoveColumn(trunkGroupID, "TrunkGroups", k)
	end
	ksProv:RemoveColumn(trunkGroup.carrier, 'CarrierTrunkGroups', trunkGroupID)

	-- IS OK
	result.success = true
	return result
end

-- GetTrunkGroup
---------------------------
-- Returns data of a specific trunk group, or empty if non-existing
function GetTrunkGroup (ksProv, trunkGroupID)
	trunkGroupID = utf8.lower(trunkGroupID)
	return UnpackTrunkGroup(select(ksProv, "TrunkGroups", trunkGroupID)[trunkGroupID] or {})
end

-- AddTrunkToGroup
---------------------------
-- Adds a trunk to a trunk group
-- Pre: The trunk must exist
-- Pre: The trunk group must exist
-- Pre: The trunk must not belong to a group
-- Pre: The trunk must belong to the same carrier as the group
function AddTrunkToGroup (ksProv, trunkID, trunkGroupID)
	trunkID = utf8.lower(trunkID)
	trunkGroupID = utf8.lower(trunkGroupID)
	local trunk = GetTrunk(ksProv, trunkID)

	LogDebug("[PROVISIONING_MANAGER:AddTrunkToGroup] Add Trunk '" .. tostring(trunkID) .. "', To TRUNK Group '" .. tostring(trunkGroupID) .. "'")

	if isEmpty(trunk) then
		return false, {
			errMsg = "Trunk "..trunkID.." does not exist.",
			code = "TRUNK DOES NOT EXIST",
			rawData = {}
		}
	end
	local trunkGroup = GetTrunkGroup(ksProv, trunkGroupID)
	--LogDebug("[PROVISIONING_MANAGER:AddTrunkToGroup] Current Trunk Group '" .. json.encode(trunkGroup or {}) .. "'")

	if isEmpty(trunkGroup) then
		return false, {
			errMsg = "Trunk group "..trunkGroupID.." does not exist.",
			code = "TRUNK GROUP DOES NOT EXIST",
			rawData = {}
		}
	end

	if trunkGroup.trunkList[trunkID] then
		ksProv:InsertColumn(trunk.carrier, 'CarrierTrunks', trunkID, trunkGroupID)
		return false, {
			errMsg = "Trunk "..trunkID.." already belongs to trunk group "..trunkGroupID..".",
			code = "TRUNK ALREADY BELONGS TO TRUNK GROUP",
			rawData = {
				trunkGroup = trunkGroupID,
				trunkID = trunkID
			}
		}
	end
	if trunkGroup.carrier ~= trunk.carrier then
		return false, {
			errMsg = "Trunk "..trunkID.." belongs to carrier "..trunk.carrier.." while trunk group "..trunkGroupID.." belongs to carrier "..trunkGroup.carrier..".",
			code = "TRUNK BELONGS TO CARRIER WHILE TRUNK GROUP BELONGS TO CARRIER",
			rawData = {
				trunkGroup = trunkGroupID,
				trunkID = trunkID,
				trunkCarrier = trunk.carrier,
				trunkGroupCarrier = trunkGroup.carrier
			}
		}
	end
	local currentGroup = select(ksProv, "CarrierTrunks", trunk.carrier)[trunk.carrier][trunkID]
	if currentGroup and currentGroup ~= "" then
		return false, {
			errMsg = "Trunk "..trunkID.." belongs to trunk group "..currentGroup..". Free it before adding it to "..trunkGroupID..".",
			code = "TRUNK BELONGS TO TRUNK GROUP FREE IT BEFORE ADDING IT TO",
			rawData = {
				trunkGroup = trunkGroupID,
				trunkID = trunkID,
				currentGroup = currentGroup
			}
		}
	end

	-- Ahora si, actualizo
	trunkGroup.trunkList[trunkID] = ""
	ksProv:InsertColumn(trunkGroupID, "TrunkGroups", "trunkList", json.encode(trunkGroup.trunkList))
	ksProv:InsertColumn(trunk.carrier, 'CarrierTrunks', trunkID, trunkGroupID)

	-- OK
	return true
end

-- RemoveTrunkFromGroup
---------------------------
-- Removes a trunk from its associated group
-- Pre: The trunk must exist
-- Pre: The trunk must belong to a group
function RemoveTrunkFromGroup (ksProv, trunkID)
	trunkID = utf8.lower(trunkID)
	LogDebug("[PROVISIONING_MANAGER:RemoveTrunkFromGroup] Removing Trunk '" .. tostring(trunkID) .. "'")

	local trunk = GetTrunk(ksProv, trunkID)
	if isEmpty(trunk) then
		return false, {
			errMsg = "Trunk "..trunkID.." does not exist.",
			code = "TRUNK DOES NOT EXIST",
			rawData = {}
		}
	end
	local currentGroup = select(ksProv, "CarrierTrunks", trunk.carrier)[trunk.carrier][trunkID]
	if not currentGroup or currentGroup == "" then
		return false, {
			errMsg = "Trunk "..trunkID.." does not belong to any trunk group.",
			code = "TRUNK DOES NOT BELONG TO ANY TRUNK GROUP",
			rawData = {
				trunkID = trunkID
			}
		}
	end

	-- Ahora si, actualizo
	ksProv:InsertColumn(trunk.carrier, 'CarrierTrunks', trunkID, "")
	local trunkGroup = GetTrunkGroup(ksProv, currentGroup)
	if trunkGroup.trunkList then
		trunkGroup.trunkList[trunkID] = nil
		ksProv:InsertColumn(currentGroup, "TrunkGroups", "trunkList", json.encode(trunkGroup.trunkList))
	end

	-- OK
	return true
end

------------------------------------------------------
-- DIDS
------------------------------------------------------

-- ListDIDs
---------------------------
-- Lists all the DIDs with their associated data
function ListDIDs (ksProv)
	return cropEmpties(select(ksProv, "DIDs"))
end

-- ListDIDsByCarrier
---------------------------
-- Lists all the DIDs, separated by carrier
function ListDIDsByCarrier (ksProv)
	return listSubKeys(select(ksProv, "CarrierDIDs"))
end

-- GetDNISPackage
---------------------------
-- Returns the package associated with a DNIS, if any
local function GetDNISPackage (ksProv, DNIS)
	local packages = select(ksProv, "DNISPackage", DNIS)[DNIS] or {}
	for p, _ in pairs(packages) do
		return p
	end
end

-- UpdateDID
---------------------------
-- Adds or modifies a DID in provisioning.
-- Pre: The DID must specify an existing carrier
-- Pre: If the DID specifies a DNIS, it must specify a trunk or trunk group.
-- Pre: The DID may specify either a trunk or a trunk group, which must exist
--      and be of the same carrier as the DID.
-- Pre: If this is an update, if the DID was associated with an I or I/O package,
--      the new data must specify a DNIS not already associated with another
--      I or I/O package
-- Pre: If this is an update, if the DID was associated with an O or I/O package
--      with callerIDIsDID, the DID must not change carriers.
function UpdateDID (ksProv, DID, DIDData)
	DID = utf8.lower(DID)

	LogDebug("[PROVISIONING_MANAGER:UpdateDID] Updating DID '" .. tostring(DID) .. "', DIDData: " .. tostring(json.encode(DIDData or {})))

	-- Validaciones
	if not DIDData.carrier then
		return false, {errMsg = "DID must specify a carrier.", code = "DID MUST SPECIFY A CARRIER"}
	end
	DIDData.carrier = utf8.lower(DIDData.carrier)
	if isEmpty(GetCarrier(ksProv, DIDData.carrier)) then
		return false, {errMsg = "Carrier "..tostring(DIDData.carrier).." does not exist.", code = "CARRIER DOES NOT EXIST"}
	end
	if DIDData.DNIS and not (DIDData.trunk or DIDData.trunkGroup) then
		return false, {errMsg = "A DID with DNIS must be associated with a trunk or a trunk group.", code = "A DID WITH DNIS MUST BE ASSOCIATED WITH A TRUNK OR A TRUNK GROUP"}
	end
	if DIDData.trunk then
		DIDData.trunk = utf8.lower(DIDData.trunk)
		local trunk = GetTrunk(ksProv, DIDData.trunk)
		if isEmpty(trunk) then
			return false, {errMsg = "Trunk "..tostring(DIDData.trunk).." does not exist.", code = "TRUNK DOES NOT EXIST"}
		elseif trunk.carrier ~= DIDData.carrier then
			return false, {errMsg = "DID was associated with carrier "..DIDData.carrier..", while its trunk ("..DIDData.trunk..") is from carrier "..trunk.carrier..".",
				code = "DID WAS ASSOCIATED WITH CARRIER WHILE ITS TRUNK IS FROM CARRIER"
			}
		end
	end
	if DIDData.trunkGroup then
		if DIDData.trunk then
			return false, {errMsg = "A DID must be associated with either a trunk or a trunk group, but not both.", code = "A DID MUST BE ASSOCIATED WITH EITHER A TRUNK OR A TRUNK GROUP, BUT NOT BOTH"}
		end
		DIDData.trunkGroup = utf8.lower(DIDData.trunkGroup)
		local trunkGroup = GetTrunkGroup(ksProv, DIDData.trunkGroup)
		if isEmpty(trunkGroup) then
			return false, {errMsg = "Trunk group "..tostring(DIDData.trunkGroup).." does not exist.", code = "TRUNK GROUP DOES NOT EXIST"}
		elseif trunkGroup.carrier ~= DIDData.carrier then
			return false, {errMsg = "DID was associated with carrier "..DIDData.carrier..", while its trunk group ("..DIDData.trunkGroup..") is from carrier "..trunkGroup.carrier..".",
				code = "DID WAS ASSOCIATED WITH CARRIER WHILE ITS TRUNK GROUP IS FROM CARRIER"
			}
		end
	end
	local oldDID = GetDID(ksProv, DID)
	local oldPackage
	if oldDID.DNIS and oldDID.DNIS ~= DIDData.DNIS then
		oldPackage = GetDNISPackage(ksProv, oldDID.DNIS)
		if oldPackage then
			if not DIDData.DNIS then
				return false, {errMsg = "DID was associated with package "..oldPackage..", which needs a DNIS, so update without DNIS is not possible.",
					code = "DID WAS ASSOCIATED WITH PACKAGE WHICH NEEDS A DNIS, SO UPDATE WITHOUT DNIS IS NOT POSSIBLE"
				}
			else
				local package = GetDNISPackage(ksProv, DIDData.DNIS)
				if package then
					return false, {errMsg = "DID was associated with package "..oldPackage..", but new DNIS is already associated with package "..package..".",
						code = "DID WAS ASSOCIATED WITH PACKAGE BUT NEW DNIS IS ALREADY ASSOCIATED WITH PACKAGE"
					}
				end
			end
		end
	end
	if oldDID.carrier ~= DIDData.carrier then
		local packages = select(ksProv, "DIDPackages", DID)[DID] or {}
		for package, _ in pairs(packages) do
			local packageData = GetPackage(ksProv, package)
			if packageData.callerIDIsDID then
				return false, {errMsg = "DID cannot change carrier, because it's associated with package "..package..", which needs this DID to belong to carrier "..oldDID.carrier.." to set the right CallerID",
					code = "DID CANNOT CHANGE CARRIER, BECAUSE IT'S ASSOCIATED WITH PACKAGE WHICH NEEDS THIS DID TO BELONG TO CARRIER TO SET THE RIGHT CALLERID"
				}
			end
		end
	end

	-- Ahora si, actualizo
	for k, _ in pairs(oldDID) do
		if not DIDData[k] then
			ksProv:RemoveColumn(DID, "DIDs", k)
		end
	end
	for k, v in pairs(DIDData) do
		ksProv:InsertColumn(DID, "DIDs", k, v)
	end
	ksProv:InsertColumn(DID, "DIDs", "Id", DID) -- El ID

	if oldDID.carrier ~= DIDData.carrier then
		if oldDID.carrier then
			ksProv:RemoveColumn(oldDID.carrier, 'CarrierDIDs', DID)
		end
		ksProv:InsertColumn(DIDData.carrier, 'CarrierDIDs', DID, "")
	end
	if oldPackage then
		ksProv:RemoveColumn(oldDID.DNIS, 'DNISPackage', oldPackage)
		ksProv:InsertColumn(DIDData.DNIS, 'DNISPackage', oldPackage, "")
	end
	if oldDID.trunk and oldDID.trunk ~= DIDData.trunk then
		ksProv:RemoveColumn(oldDID.trunk, 'TrunkDIDs', DID)
	end
	if oldDID.trunkGroup and oldDID.trunkGroup ~= DIDData.trunkGroup then
		ksProv:RemoveColumn(oldDID.trunkGroup, 'TrunkGroupDIDs', DID)
	end
	if DIDData.trunk then
		ksProv:InsertColumn(DIDData.trunk, 'TrunkDIDs', DID, "")
	elseif DIDData.trunkGroup then
		ksProv:InsertColumn(DIDData.trunkGroup, 'TrunkGroupDIDs', DID, "")
	end

	-- OK
	return true
end

-- DeleteDID
---------------------------
-- Removes a DID from provisioning.
-- Pre: The DID must exist
-- Pre: The DID must not be assigned to any package
function DeleteDID (ksProv, DID)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	DID = utf8.lower(DID)

	-- EXIST validation
	local oldDID = GetDID(ksProv, DID)
	if isEmpty(oldDID) then
		result.success = false
		result.errType = "DID DOES NOT EXIST"
		result.errMsg = "DID ".. tostring(DID) .. " does not exist."
		return result
	end

	-- Package validation
	local packages = select(ksProv, "DIDPackages", DID)[DID] or {}
	for package, _ in pairs(packages) do
		result.success = false
		result.errType = "CANNOT DELETE DID ASSOCIATED TO PACKAGE"
		result.errMsg = "DID is associated with package "..package..", and so cannot be deleted."
		result.relatedId = package
		return result
	end

	-- Ahora si lo Elimino
	for k, _ in pairs(oldDID) do
		ksProv:RemoveColumn(DID, "DIDs", k)
	end
	ksProv:RemoveColumn(oldDID.carrier, 'CarrierDIDs', DID)
	if oldDID.trunk then
		ksProv:RemoveColumn(oldDID.trunk, 'TrunkDIDs', DID)
	end
	if oldDID.trunkGroup then
		ksProv:RemoveColumn(oldDID.trunkGroup, 'TrunkGroupDIDs', DID)
	end

	-- IS OK
	result.success = true
	return result
end

-- GetDID
---------------------------
-- Returns data about a specific DID, empty if it does not exist
function GetDID (ksProv, DID)
	DID = utf8.lower(DID)
	local DIDInfo = select(ksProv, "DIDs", DID)[DID] or {}
	return DIDInfo
end

------------------------------------------------------
-- ROUTES
------------------------------------------------------

local dayNames = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thurdsday", "Friday", "Saturday" }

-- DayOfWeekString
---------------------------
-- Turns number into day name ("Invalid" if invalid input)
function DayOfWeekString (dowNumber)
	return dowNumber and dayNames[dowNumber] or "Invalid"
end

-- DayOfWeekNumber
---------------------------
-- Turns day name into number (0 if invalid input)
function DayOfWeekNumber (dowString)
	dowString = string.lower(dowString)
	for number, name in pairs(dayNames) do
		if string.lower(name) == dowString then
			return number
		end
	end
	return 0
end

-- MilitaryTimeMilitaryString
---------------------------
-- Turns HH*100+MM into HH:MM.
function MilitaryTimeString (mt)
	local minutes = mt%100
	return tostring(math.floor(mt/100))..":"..(minutes < 10 and "0" or "")..tostring(minutes)
end

-- MilitaryTimeMilitaryString
---------------------------
-- Turns HH*100+MM into HHMM.
function MilitaryTimeMilitaryString (mt)
	local minutes = mt%100
	local hours = math.floor(mt/100)
	return (hours < 10 and "0" or "")..tostring(hours)..(minutes < 10 and "0" or "")..tostring(minutes)
end

-- MilitaryTimeFromString
---------------------------
-- Turns HH:MM into HH*100+MM. Return result if successful, nil if invalid input
function MilitaryTimeFromString (mt)
	if type(mt) ~= "string" then
		return
	end
	local hours, minutes = string.match(mt, "^([^:]*):(.*)$")
	if not hours or not minutes then
		return
	end
	hours = tonumber(hours)
	minutes = tonumber(minutes)
	if
		not hours or
		not minutes or
		hours < 0 or
		minutes < 0 or
		hours > 23 or
		minutes > 59
	then
		return
	end
	return hours*100+minutes
end

-- UnpackRouteSchedule
---------------------------
-- Changes the slotID -> trunkList structure into the unpacked structure:
--
-- trunks := { trunk -> trunkPriority }
-- from := <string in HH:MM format>
-- to := <string in HH:MM format>
-- militaryFrom := <int, hour*100+minutes>
-- militaryTo := <int, hour*100+minutes>
--
-- slot := { trunks, from, to, militaryTo }
-- day := { militaryFrom -> slot }
-- result := {
--               anyCountry = { dow -> day }
--               perCountry = {
--                               countryCode -> {
--                                                  anyArea = { dow-> day }
--                                                  perArea = {
--                                                                 areaCode -> { dow -> day}
--                                                            }
--                                              }
--                            }
--           }
local function UnpackRouteSchedule (schedule)
	local result = { anyCountry = {}, perCountry = {} }
	for slotID, trunks in pairs(schedule) do
		local country, area, dow, from, to = string.match(slotID, "^([^_]*)_([^_]*)_([^_]*)_([^_]*)_(.*)$")
		dow = tonumber(dow)
		from = tonumber(from)
		to = tonumber(to)
		local slot = {
			trunks = trunks,
			militaryTo = to,
			from = MilitaryTimeString(from),
			to = MilitaryTimeString(to)
		}
		local father = result.anyCountry
		if country ~= "ANY" then
			result.perCountry[country] = result.perCountry[country] or { anyArea = {}, perArea = { }}
			father = result.perCountry[country].anyArea
			if area ~= "ANY" then
				result.perCountry[country].perArea[area] = result.perCountry[country].perArea[area] or {}
				father = result.perCountry[country].perArea[area]
			end
		end
		father[dow] = father[dow] or {}
		father[dow][from] = slot
	end
	return result
end

-- LoadRawRoutesSchedulers
---------------------------
-- Returns RAW all the outgoing routes on provisioning, with schedule data.
-- as found in the database!!
function LoadRawRoutesSchedulers (ksProv)
	local schedules = cropEmpties(superSelect(ksProv, "RouteSchedules"))
	--LogDebug("[PROVISIONING_MANAGER:LoadRawRoutesSchedulers] All Route Schedules: '" .. tostring(json.encode(schedules or {})))
	return schedules
end

-- GetRawRouteSchedulers
---------------------------
function GetRawRouteSchedulers (ksProv, routeID)
	local schedule = superSelect(ksProv, "RouteSchedules", routeID)[routeID] or {}
	--LogDebug("[PROVISIONING_MANAGER:GetRawRouteSchedulers] Route  Schedules: '" .. tostring(json.encode(schedule or {})))
	return schedule
end

-- InsertRawRouteSchedulers (utilizado por el rollback)
---------------------------
function InsertRawRouteSchedulers (ksProv, routeID, schedulers)
	local encodedSchedulers = json.encode(schedule or {})
	--LogDebug("[PROVISIONING_MANAGER:InsertRawRouteSchedulers] Insert Ras Route " .. tostring(routeID) .. " Schedules: " .. encodedSchedulers)
	for slotID, info in pairs(schedulers) do
		for trunk, trunkPrio in pairs(info) do
			ksProv:InsertSuperColumn(routeID, "RouteSchedules", slotID, utf8.lower(trunk), trunkPrio)
		end
	end
end

-- ListRoutes
---------------------------
-- Returns all the outgoing routes on provisioning, with schedule data.
function ListRoutes (ksProv)
	local result = cropEmpties(select(ksProv, "Routes"))
	local schedules = LoadRawRoutesSchedulers(ksProv)
	for routeID, schedule in pairs(schedules) do
		result[routeID] = result[routeID] or {}
		result[routeID].schedule = UnpackRouteSchedule(schedule)
	end
	--LogDebug("[PROVISIONING_MANAGER:ListRoutes] All Route Schedules: '" .. tostring(json.encode(schedules or {})))
	return result
end

-- GetRoute
---------------------------
-- Returns data about an specific outgoing route, empty if it does not exist
function GetRoute (ksProv, routeID)
	routeID = utf8.lower(routeID)
	local result = select(ksProv, "Routes", routeID)[routeID] or {}
	local schedule = superSelect(ksProv, "RouteSchedules", routeID)[routeID] or {}
	if not isEmpty(schedule) then
		result.schedule = UnpackRouteSchedule(schedule)
	end
	--LogDebug("[PROVISIONING_MANAGER:ListRoutes] Route '" .. tostring(routeID) .. "' -> Schedules: '" .. tostring(json.encode(result or {})))
	return result
end

-- GetRouteMonoCarrier
---------------------------
-- If all the trunks on the route belong to the same carrier, returns the carrier. Otherwise returns nil.
local function GetRouteMonoCarrier (ksProv, routeID)
	routeID = utf8.lower(routeID)
	local result
	local schedule = superSelect(ksProv, "RouteSchedules", routeID)[routeID]
	if schedule then
		for _, trunks in pairs(schedule) do
			for trunk, _ in pairs(trunks) do
				local trunkData = GetTrunk(ksProv, trunk)
				if not result then
					result = trunkData.carrier
				elseif result ~= trunkData.carrier then
					return
				end
			end
		end
	end
	return result
end

-- UpdateRoute
---------------------------
-- Modifies all route data which is not schedule
function UpdateRoute (ksProv, routeID, routeData)
	LogDebug("[PROVISIONING_MANAGER:UpdateRoute] Updating Route. routeID: '" .. tostring(routeID) .. "', routeData: " .. tostring(json.encode(routeData or {})))

	routeID = utf8.lower(routeID)
	for key, value in pairs(routeData) do
		ksProv:InsertColumn(routeID, 'Routes', key, value)
	end

	-- OK
	return true
end

-- DeleteRoute
---------------------------
-- Removes all data and schedule from a route. It must not be in use.
-- Pre: Route is not being used in any package
function DeleteRoute (ksProv, routeID)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	routeID = utf8.lower(routeID)

	-- Route Package validation
	local packages = select(ksProv, "RoutePackages", routeID)[routeID] or {}
	for p, _ in pairs(packages) do
		result.success = false
		result.errType = "CANNOT DELETE ROUTE ASSOCIATED TO PACKAGE"
		result.errMsg = "Route " .. routeID .. " is being used at package ".. p
		result.relatedId = p
		return result
	end

	-- Ahora si lo Elimino
	ksProv:RemoveColumn(routeID, "Routes")
	ksProv:RemoveColumn(routeID, "RouteSchedules")

	-- IS OK
	result.success = true
	return result
end

-- DeleteRouteSchedules
---------------------------
function DeleteRouteAllSchedules (ksProv, routeID)
	assert(routeID)
	ksProv:RemoveColumn(routeID, "RouteSchedules")
	return true
end

-- CheckSlotOverlap
---------------------------
-- Returns an error message if the newSlot overlaps with one of the current slots
local function CheckSlotOverlap (newSlot, currentSlots)
	for dow, _ in pairs(newSlot.dowKeys) do
		if currentSlots[dow] then
			local newTo = newSlot.militaryTo
			if newTo == 0 then
				newTo = 10000
			end
			for militaryFrom, currentSlot in pairs(currentSlots[dow]) do
				if militaryFrom >= newTo then
					break
				end
				local currentTo = currentSlot.militaryTo
				if currentTo == 0 then
					currentTo = 10000
				end
				if currentTo > newSlot.militaryFrom then
					return "Slot overlaps with another slot going from "..currentSlot.from.." to "..currentSlot.to.." on "..DayOfWeekString(dow).."."
				end
			end
		end
	end
end

-- CheckSlotOverlap
---------------------------
-- Adds to result the string definition of all slots overlapping with newSlot
local function AddOverlappingSlots (window, currentSlots, result)
	for dow, _ in pairs(window.dowKeys) do
		if currentSlots[dow] then
			local newTo = window.militaryTo
			if newTo == 0 then
				newTo = 10000
			end
			for militaryFrom, currentSlot in pairs(currentSlots[dow]) do
				if militaryFrom >= newTo then
					break
				end
				local currentTo = currentSlot.militaryTo
				if currentTo == 0 then
					currentTo = 10000
				end
				if currentTo > window.militaryFrom then
					local slotID = (window.country or "ANY").."_"..
								   (window.country and window.area or "ANY").."_"..
								   tostring(dow).."_"..
								   MilitaryTimeMilitaryString(militaryFrom).."_"..
								   MilitaryTimeMilitaryString(currentSlot.militaryTo)
					table.insert(result, slotID)
				end
			end
		end
	end
end

-- AddRouteScheduleSlot
---------------------------
-- Adds a slot to a route schedule
-- Pre: Input must include 'dow', an array of dayOfWeek (integers from 1 to 7)
-- Pre: Input must include 'from', a start time in HH:MM format
-- Pre: Input must include 'to', an end time in HH:MM format
-- Pre: 'from' must be previous to 'to'
-- Pre: No slots intersect with the new one for the same country and area code
function AddRouteScheduleSlot (ksProv, routeID, slotData)
	LogDebug("[PROVISIONING_MANAGER:AddRouteScheduleSlot] Add Route Schedule Slot. routeID: '" .. tostring(routeID) .. "', slotData: " .. tostring(json.encode(slotData or {})))

	assert(slotData)

	routeID = utf8.lower(routeID)
	-- Check correct data in new slot
	slotData.militaryFrom = MilitaryTimeFromString(slotData.from)
	if not slotData.militaryFrom then
		return false, {
			errMsg = "Slot beginning ("..tostring(slotData.from)..") is not in time format HH:MM.",
			code = "SLOT FROM DATE IS NO IN TME FORMAT HH:MM",
			rawData = {
				fromDate = slotData.from
			}
		}
	end
	slotData.militaryTo = MilitaryTimeFromString(slotData.to)
	if not slotData.militaryTo then
		return false, {
			errMsg = "Slot end ("..tostring(slotData.to)..") is not in time format HH:MM.",
			code = "SLOT END DATE IS NO IN TME FORMAT HH:MM",
			rawData = {
				endDate = slotData.to
			}
		}
	end
	if slotData.militaryTo > 0 and slotData.militaryFrom > slotData.militaryTo then
		return false, {
			errMsg = "Slot ends at "..slotData.to.." before it begins at "..slotData.from..".",
			code = "SLOT END IS MINOR FROM DATE",
			rawData = {
				fromDate = slotData.from,
				endDate = slotData.to
			}
		}
	end
	if type(slotData.dow) ~= "table" or isEmpty(slotData.dow) then
		return false, {
			errMsg = "Parameter 'dow' must be a table with day values (numbers from 1 to 7)",
			code = "PARAMETER DOW MUST BE A TABLE WITH DAY VALUES NUMBERS FROM 1 TO 7",
			rawData = {
				dow = slotData.dow
			}
		}
	end
	slotData.dowKeys = {}
	for _, dow in pairs(slotData.dow) do
		if not dow or DayOfWeekString(dow) == "Invalid" then
			return false, {
				errMsg = tostring(dow).." is not a valid day of the week.",
				code = "DOW IS NOT A VALID DAY OF THE WEEK",
				rawData = {
					dow = dow
				}
			}
		end
		slotData.dowKeys[dow] = true
	end
	-- Check it doesn't overlap with an existing slot
	local oldRoute = GetRoute(ksProv, routeID)
	if oldRoute.schedule then
		local overlap

		if not slotData.country then
			overlap = CheckSlotOverlap(slotData, oldRoute.schedule.anyCountry)
		elseif oldRoute.schedule.perCountry[slotData.country] then
			if not slotData.area then
				overlap = CheckSlotOverlap(slotData, oldRoute.schedule.perCountry[slotData.country].anyArea)
			elseif oldRoute.schedule.perCountry[slotData.country].perArea[slotData.area] then
				overlap = CheckSlotOverlap(slotData, oldRoute.schedule.perCountry[slotData.country].perArea[slotData.area])
			end
		end

		if overlap then
			return false, {
				errMsg = "overlap with an existing slot",
				code = "OVERLAP WITH AN EXISTING SLOT",
				rawData = {
					overlap = overlap
				}
			}
		end
	end

	-- Valida que los dias tenga asignados los trunks priority list
	for dow, _ in pairs(slotData.dowKeys) do
		if not slotData.trunks then
			return false, {
				errMsg = tostring(dow).." trunks priority list is required.",
				code = "TRUNKS PRIORITY LIST IS REQUIRED",
				rawData = {
					dow = dow,
					slotData = slotData
				}
			}
		end
	end

	-- Ahora si, actualizo

	-- Insert the slots
	for dow, _ in pairs(slotData.dowKeys) do
		local slotID = (slotData.country or "ANY").."_"..
						(slotData.country and slotData.area or "ANY").."_"..
						tostring(dow).."_"..
						MilitaryTimeMilitaryString(slotData.militaryFrom).."_"..
						MilitaryTimeMilitaryString(slotData.militaryTo)

		for trunk, trunkPrio in pairs(slotData.trunks) do
			ksProv:InsertSuperColumn(routeID, "RouteSchedules", slotID, utf8.lower(trunk), trunkPrio)
		end
	end

	-- OK
	return true
end

-- RemoveRouteScheduleSlot
---------------------------
-- Removes all slots of a route schedule that intersects the input window
-- Pre: Input may include 'dow', an array of dayOfWeek (integers from 1 to 7). If not,
--      all days are assumed
-- Pre: Input may include 'from', a start time in HH:MM format. If not, 0:00 is assumed
-- Pre: Input may include 'to', an end time in HH:MM format. If not, the end of day is assumed
-- Pre: 'from' must be previous to 'to'.
-- Returns the number of removed slots
function RemoveRouteScheduleSlot (ksProv, routeID, window)
	LogDebug("[PROVISIONING_MANAGER:RemoveRouteScheduleSlot] Removing Route Schedule Slot. routeID: '" .. tostring(routeID) .. "', window: " .. tostring(json.encode(window or {})))

	routeID = utf8.lower(routeID)
	window.dow = window.dow or { 1, 2, 3, 4, 5, 6, 7 }
	window.from = window.from or "0:00"
	window.to = window.to or "0:00"

	-- Check correct data in window
	window.militaryFrom = MilitaryTimeFromString(window.from)
	if not window.militaryFrom then
		return false, {
			errMsg = "Window beginning ("..tostring(window.from)..") is not in time format HH:MM.",
			code = "WINDOW FROM DATE IS NO IN TME FORMAT HH:MM",
			rawData = {
				fromDate = window.from
			}
		}
	end

	window.militaryTo = MilitaryTimeFromString(window.to)
	if not window.militaryTo then
		return false, {
			errMsg = "Window end ("..tostring(window.to)..") is not in time format HH:MM.",
			code = "WINDOW END DATE IS NO IN TME FORMAT HH:MM",
			rawData = {
				endDate = window.to
			}
		}
	end

	if window.militaryTo > 0 and window.militaryFrom > window.militaryTo then
		return false, {
			errMsg = "Window ends at "..window.to.." before it begins at "..window.from..".",
			code = "SLOT END IS MINOR FROM DATE",
			rawData = {
				fromDate = window.militaryFrom,
				endDate = window.militaryTo
			}
		}
	end

	if type(window.dow) ~= "table" or isEmpty(window.dow) then
		return false, {
			errMsg = "Parameter 'dow' must be a table with day values (numbers from 1 to 7)",
			code = "PARAMETER 'DOW' MUST BE A TABLE WITH DAY VALUES (NUMBERS FROM 1 TO 7)",
			rawData = {
				dow = window.dow
			}
		}
	end

	window.dowKeys = {}
	for _, dow in pairs(window.dow) do
		if not dow or DayOfWeekString(dow) == "Invalid" then
			return false, {
				errMsg = tostring(dow).." is not a valid day of the week.",
				code = "'DOW' IS NOT A VALID DAY OF THE WEEK",
				rawData = {
					dow = dow
				}
			}
		end
		window.dowKeys[dow] = true
	end

	-- Ahora si, actualizo

	-- Collect all slots overlapping with the window
	local overlappingSlots = {}
	local oldRoute = GetRoute(ksProv, routeID)
	if oldRoute.schedule then
		if not window.country then
			AddOverlappingSlots(window, oldRoute.schedule.anyCountry, overlappingSlots)
		elseif oldRoute.schedule.perCountry[window.country] then
			if not window.area then
				AddOverlappingSlots(window, oldRoute.schedule.perCountry[window.country].anyArea, overlappingSlots)
			elseif oldRoute.schedule.perCountry[window.country].perArea[window.area] then
				AddOverlappingSlots(window, oldRoute.schedule.perCountry[window.country].perArea[window.area], overlappingSlots)
			end
		end
	end
	-- Removing the slots
	for _, slot in pairs(overlappingSlots) do
		ksProv:RemoveSuperColumn(routeID, "RouteSchedules", slot)
	end

	-- OK
	local totalOverlappingSlots = #overlappingSlots

	return true, totalOverlappingSlots
end

------------------------------------------------------
-- DIALPLAN PROCESSORS
------------------------------------------------------

-- ListDialplanProcessors
---------------------------
-- Returns all the dialplan processors on provisioning.
function ListDialplanProcessors (ksProv)
	local result = cropEmpties(select(ksProv, "DialplanProcessors"))
	--LogDebug("[PROVISIONING_MANAGER:ListDialplanProcessors] All Processors: '" .. tostring(json.encode(result or {})))
	return result
end

-- GetDialplanProcessor
---------------------------
-- Returns data about an specific dialplan processor, empty if it does not exist
function GetDialplanProcessor (ksProv, processorID)
	processorID = utf8.lower(processorID)
	local result = select(ksProv, "DialplanProcessors", processorID)[processorID] or {}
	--LogDebug("[PROVISIONING_MANAGER:GetDialplanProcessor] Processor '" .. processorID .. "': '" .. tostring(json.encode(result or {})))
	return result
end

-- UpdateDialplanProcessor
---------------------------
-- Modifies dialplan processor data
function UpdateDialplanProcessor (ksProv, processorID, processor)
	LogDebug("[PROVISIONING_MANAGER:UpdateDialplanProcessor] Updating Processor. processorID: '%s' processor: '%s'",
		processorID, json.encode(processor or {}))

	processorID = utf8.lower(processorID)
	for key, value in pairs(processor) do
		ksProv:InsertColumn(processorID, 'DialplanProcessors', key, value)
	end
	CleanDialplanProcessorTrace(ksProv, processorID)

	-- OK
	return true
end

-- DeleteDialplanProcessor
---------------------------
-- Removes a processor. It must not be in use.
-- Pre: Processor is not being used in any package
function DeleteDialplanProcessor (ksProv, processorID)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	processorID = utf8.lower(processorID)

	-- Route Package validation
	local packages = GetDialplanProcessorPackages(ksProv, processorID)
	for p, _ in pairs(packages) do
		result.success = false
		result.errType = "CANNOT DELETE DIALPLAN PROCESSOR ASSOCIATED TO PACKAGE"
		result.errMsg = "Processor " .. processorID .. " is being used at package ".. p
		result.relatedId = p
		return result
	end

	-- Ahora si lo Elimino
	ksProv:RemoveColumn(processorID, "DialplanProcessors")
	CleanDialplanProcessorTrace(ksProv, processorID)

	-- IS OK
	result.success = true
	return result
end

-- GetPackageDialplanProcessors
---------------------------
-- Gets package dialplan processors list
local function GetPackageDialplanProcessors(packageData)
	local processors = packageData and packageData.dialplanProcessors or {}
	if type(processors) == "string" then
		processors = json.decode(processors)
	end
	return processors
end

-- GetDialplanProcessorPackages
---------------------------
-- Returns all package ID's associated to a processor
function GetDialplanProcessorPackages (ksProv, processorID)
	processorID = utf8.lower(processorID)
	return select(ksProv, "DialplanProcessorPackages", processorID)[processorID] or {}
end

-- UpdatePackageDialplanProcessors
---------------------------
-- Updates list of processors used by a package
function UpdatePackageDialplanProcessors(ksProv, packageID, oldPackage, newPackage)
	local added = {}
	local removed = {}
	for _, data in pairs(GetPackageDialplanProcessors(newPackage)) do
		local processorID = utf8.lower(data.processor)
		added[processorID] = true
	end
	for _, data in pairs(GetPackageDialplanProcessors(oldPackage)) do
		local processorID = utf8.lower(data.processor)
		if not added[processorID] then
			removed[processorID] = true
		end
	end
	
	for processorID, _ in pairs(added) do
		ksProv:InsertColumn(processorID, 'DialplanProcessorPackages', packageID, "")
	end
	for processorID, _ in pairs(removed) do
		ksProv:RemoveColumn(processorID, 'DialplanProcessorPackages', packageID)
	end
end

-- DeletePackageDialplanProcessors
---------------------------
-- Deletes list of processors used by a package
function DeletePackageDialplanProcessors(ksProv, packageID, packageData)
	for _, data in pairs(GetPackageDialplanProcessors(packageData)) do
		local processorID = utf8.lower(data.processor)
		ksProv:RemoveColumn(processorID, 'DialplanProcessorPackages', packageID)
	end
end

-- CleanDialplanProcessorTrace
---------------------------
-- Removes a processor trace.
function CleanDialplanProcessorTrace (ksProv, processorID)
	processorID = utf8.lower(processorID)
	
	local packages = GetDialplanProcessorPackages(ksProv, processorID)
	for packageId, _ in pairs(packages) do
		m_redis:del("DialplanProcessorTrace:" .. processorID .. ":" .. packageId)
	end
end

-- GetDialplanProcessorTrace
---------------------------
-- Get dialplan processor trace
function GetDialplanProcessorTrace (ksProv, processorID)
	local result = {}

	processorID = utf8.lower(processorID)
	
	local packages = GetDialplanProcessorPackages(ksProv, processorID)
	for packageId, _ in pairs(packages) do
		result[packageId] = m_redis:hgetall("DialplanProcessorTrace:" .. processorID .. ":" .. packageId) or {}
	end
	
	return result
end

------------------------------------------------------
-- PACKAGES
------------------------------------------------------


-- ProcessPackageData
---------------------------
-- Process package data when is get
local function GetPackageData(pack)
	if not next(pack) then
		-- si no viene una tabla o es vacia
		return pack
	end
	pack.dialplanProcessors = GetPackageDialplanProcessors(pack)
	return pack
end

-- ListPackages
---------------------------
-- Returns all packages in provisioning
function ListPackages (ksProv)
	local packages = cropEmpties(select(ksProv, "Packages"))
	for _, pack in pairs(packages) do
		pack = GetPackageData(pack)
	end
	return packages
end

-- ListPackagesByVCC
---------------------------
-- Returns all package ID's separated by VCC
function ListPackagesByVCC (ksProv)
	return listSubKeys(select(ksProv, "VCCPackages"))
end

-- ListPackagesByDNIS
---------------------------
-- Returns all package ID's with a DNIS associated
function ListPackagesByDNIS (ksProv)
	local pbd = cropEmpties(select(ksProv, "DNISPackage"))
	local result = {}
	for DNIS, packageList in pairs(pbd) do
		for package, _ in pairs(packageList) do
			if not result[DNIS] then
				result[DNIS] = package
			else
				LogDebug("Warning: DNIS "..DNIS.." has an extra package "..package.."!")
			end
		end
	end
	return result
end

-- variable que se accede tambien desde el pm-cli.lua (aunque en el pm-cli.lua se comento todo el codigo) En algun momento se podria mover hacia adentro de la funcion "UpdatePackage"
allowedIOModes = { I = true, O = true, ["I/O"] = true }

-- UpdatePackage
---------------------------
-- Adds a package to provisioning
-- Pre: Package data must include an existing DID
-- Pre: Package data must include an IO mode from this options: I, O, I/O
-- Pre: If IO mode is O or I/O, package data must include an existing route.
--      If callerIDIsDID is set, this route must be comprised only by trunks
--      of the same carrier as the DID
-- Pre: If IO mode is I or I/O, DID must hava a DNIS which is not already in
--      use by another I or I/O package
-- Post: Package will not be assigned to any VCC
function UpdatePackage (ksProv, packageID, packageData)
	assert(ksProv)

	packageID = utf8.lower(packageID)

	LogDebug("[PROVISIONING_MANAGER:UpdatePackage] Updating PACKAGE '" .. tostring(packageID) .. "', packageData: " .. tostring(json.encode(packageData or {})))

	if not packageData.DID then
		return false, {errMsg = "Package must specify an existing DID.", code = "PACKAGE MUST SPECIFY A DID"}
	end
	packageData.DID = utf8.lower(packageData.DID)
	local DID = GetDID(ksProv, packageData.DID) or {}
	if isEmpty(DID) then
		return false, {errMsg = "DID "..tostring(packageData.DID).." does not exist.",
			code = "DID DOES NOT EXIST"
		}
	end

	if not packageData.ioMode or not allowedIOModes[packageData.ioMode] then
		local validValues = table.concat(listKeys(allowedIOModes), ", ")
		return false, {errMsg = "Invalid ioMode "..tostring(packageData.ioMode).." (valid values: ".. validValues ..")",
			code = "INVALID IOMODE VALUES",
			validValues = allowedIOModes
		}
	end
	if packageData.ioMode ~= "I" then
		-- Outbound checks
		if not packageData.outgoingRoute then
			return false, {errMsg = "Outbound package must specify an existing route.",
				code = "OUTBOUND ERROR - OUTBOUND PACKAGE MUST SPECIFY AN EXISTING ROUTE"
			}
		end
		packageData.outgoingRoute = utf8.lower(packageData.outgoingRoute)
		if isEmpty(GetRoute(ksProv, packageData.outgoingRoute)) then
			return false, {errMsg ="Route "..tostring(packageData.outgoingRoute).." does not exist.",
				code = "OUTBOUND ERROR - ROUTE DOES NOT EXIST"
			}
		end
		if packageData.callerIDIsDID then
			local monoCarrier = GetRouteMonoCarrier(ksProv, packageData.outgoingRoute)
			if not monoCarrier then
				return false, {errMsg = "Cannot set 'callerIDIsDID', because route "..packageData.outgoingRoute.." uses trunks from more than one carrier.",
					code = "OUTBOUND ERROR - CANNOT SET 'callerIDIsDID' BECAUSE ROUTE USES TRUNKS FROM MORE THAN ONE CARRIER",
					rawData = {
						outgoingRoute = packageData.outgoingRoute
					}
				}
			end
			if monoCarrier ~= DID.carrier then
				return false, {errMsg = "Cannot set 'callerIDIsDID', because route "..packageData.outgoingRoute.." uses trunks from carrier "..monoCarrier..", while DID "..packageData.DID.." belongs to carrier "..tostring(DID.carrier)..".",
					code = "OUTBOUND ERROR - CANNOT SET 'callerIDIsDID' BECAUSE ROUTE USES TRUNKS FROM CARRIER WHILE DID BELONGS TO CARRIER",
					rawData = {
						did           = packageData.DID,
						outgoingRoute = packageData.outgoingRoute,
						monoCarrier   = monoCarrier,
						didCarrier    = DID.carrier
					}
				}
			end
		end
	end

	-- El paquete existente
	local oldPackage = GetPackage(ksProv, packageID)

	if packageData.ioMode ~= "O" then
		-- Inbound checks
		if not DID.DNIS then
			return false, {errMsg = "DID "..packageData.DID.." needs a DNIS to be used as an inbound number.",
				code = "INBOUND ERROR - DID NEEDS A DNIS TO BE USED AS AN INBOUND NUMBER",
				rawData = {
					did = packageData.DID
				}
			}
		end
		local package = GetDNISPackage(ksProv, DID.DNIS)
		if package and package ~= packageID then
			return false, {errMsg = "DID "..packageData.DID.." has DNIS "..DID.DNIS..", which is already in use on package "..package..".",
				code = "INBOUND ERROR - DID HAS DNIS WHICH IS ALREADY IN USE ON PACKAGE",
				rawData = {
					did     = packageData.DID,
					dnis    = DID.DNIS,
					package = package
				}
			}
		end

		-- Si estoy actualizando obtengo el viejo package
		if not isEmpty(oldPackage) and oldPackage.ioMode == "O" then

		-- Acá se valida que si el paquete cambio de Modo Saliente a Entrante, el mismo NO se encuentre utilizado en más de una campaña/cuenta.
		--  Esto porque un paquete entrante o I/O NO pueden estar en más de una campaña en el mismo vcc.
		--
			local vccPackages = GetVCCPackages(ksProv, oldPackage.VCC) or {}
			--LogDebug("-1---->vccPackages:" .. json.encode(vccPackages))

			local campaigns = {}
			for vccPackageId, vccPackage in pairs(vccPackages) do
				if packageID:lower() == vccPackageId:lower() then
					for campaignId, accounts in pairs(vccPackage.campaigns or {}) do
						table.insert(campaigns, campaignId)
					end
				end
			end
			--LogDebug("-2---->campaigns:" .. json.encode(campaigns))

			if #campaigns > 1 then
				return false, {
					errMsg = "Unable to change Package "..packageID.." from outgoing to inbound mode. It´s associated to " .. tostring(#campaigns) .. " campaigns of " .. oldPackage.VCC .. " Contact Center.",
					code = "I/O ERROR - UNABLE TO CHANGE PACKAGE FROM OUTGOING TO INBOUND MODE. IT ASSOCIATED TO CAMPAIGNS OF VCC",
					rawData = {
						packageID = packageID,
						vcc = oldPackage.VCC,
						campaigns = campaigns
					}
				}
			end
		end
	end

	-- Ahora si, actualizo
	packageData.VCC = oldPackage.VCC
	for k, _ in pairs(oldPackage) do
		if not packageData[k] then
			ksProv:RemoveColumn(packageID, "Packages", k)
		end
	end
	for k, v in pairs(packageData) do
		local v_s = type(v) == "table" and json.encode(v) or tostring(v)
		ksProv:InsertColumn(packageID, "Packages", k, v_s)
	end
	local oldDID = oldPackage.DID and GetDID(ksProv, oldPackage.DID) or {}
	if oldDID.DNIS then
		ksProv:RemoveColumn(oldDID.DNIS, 'DNISPackage', packageID)
	end
	if packageData.ioMode ~= "O" then
		ksProv:InsertColumn(DID.DNIS, 'DNISPackage', packageID, "")
	end
	if not packageData.VCC then
		ksProv:InsertColumn('Free', 'VCCPackages', packageID, "")
	end
	if oldPackage.DID ~= packageData.DID then
		if oldPackage.DID then
			ksProv:RemoveColumn(oldPackage.DID, 'DIDPackages', packageID)
		end
		ksProv:InsertColumn(packageData.DID, 'DIDPackages', packageID, "")
	end
	if oldPackage.outgoingRoute ~= packageData.outgoingRoute then
		if oldPackage.outgoingRoute then
			ksProv:RemoveColumn(oldPackage.outgoingRoute, 'RoutePackages', packageID)
		end
		if packageData.ioMode ~= "I" then
			ksProv:InsertColumn(packageData.outgoingRoute, 'RoutePackages', packageID, "")
		end
	end
	UpdatePackageDialplanProcessors(ksProv, packageID, oldPackage, packageData)

	-- OK
	return true
end

-- GetPackage
---------------------------
-- Returns data about a specific package, empty if it does not exist
function GetPackage (ksProv, packageID)
	packageID = utf8.lower(packageID)
	local pack = select(ksProv, "Packages", packageID)[packageID] or {}
	return GetPackageData(pack)
end

-- DeletePackage
---------------------------
-- Removes a package from provisioning
-- Pre: Package must exist
-- Pre: Package must not be assigned to a VCC
function DeletePackage (ksProv, packageID)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	packageID = utf8.lower(packageID)

	-- EXIST validation
	local package = GetPackage(ksProv, packageID)
	if isEmpty(package) then
		result.success = false
		result.errType = "PACKAGE DOES NOT EXIST"
		result.errMsg = "Package "..packageID.." does not exist."
		return result
	end

	-- VCC validation
	if package.VCC then
		result.success = false
		result.errType = "CANNOT DELETE PACKAGE ASSOCIATED TO VCC"
		result.errMsg = "Package "..packageID.." belongs to VCC "..package.VCC..". Free it before deleting it."
		result.relatedId = package.VCC
		return result
	end

	-- Ahora si lo Elimino
	ksProv:RemoveColumn('Free', 'VCCPackages', packageID)
	local DID = package.DID and GetDID(ksProv, package.DID) or {}
	if DID.DNIS then
		ksProv:RemoveColumn(DID.DNIS, 'DNISPackage', packageID)
	end
	ksProv:RemoveColumn(package.DID, 'DIDPackages', packageID)
	if package.outgoingRoute then
		ksProv:RemoveColumn(package.outgoingRoute, 'RoutePackages', packageID)
	end
	DeletePackageDialplanProcessors(ksProv, packageID, package)
	for k, _ in pairs(package) do
		ksProv:RemoveColumn(packageID, "Packages", k)
	end

	-- IS OK
	result.success = true
	return result
end

-- AssignPackageToVCC
---------------------------
-- Assigns a free package to a VCC
-- Pre: Package must exist
-- Pre: VCC must exist
-- Pre: Package must not be assigned to a VCC
function AssignPackageToVCC (ksProv, ksAgent, packageID, VCC)
	assert(packageID)
	assert(VCC)

	packageID = utf8.lower(packageID)
	VCC = utf8.lower(VCC)

	--LogDebug("[PROVISIONING_MANAGER:AssignPackageToVCC] Assigning Package '" .. tostring(packageID) .. "' to VCC " .. tostring(VCC))

	if isEmpty(GetVCC(ksAgent, VCC)) then
		return false, {
			errMsg = "VCC "..VCC.." does not exist.",
			code = "VCC DOES NOT EXIST",
			rawData = {
				packageID = packageID
			}
		}
	end

	local package = GetPackage(ksProv, packageID)
	if isEmpty(package) then
		return false, {
			errMsg = "Package "..packageID.." does not exist.",
			code = "PACKAGE DOES NOT EXIST",
			rawData = {
				packageID = packageID
			}
		}
	end

	if package.VCC == VCC then
		return false, {
			errMsg = "Package "..packageID.." already belongs to VCC "..VCC..".",
			code = "PACKAGE ALREADY BELONGS TO VCC",
			rawData = {
				packageID = packageID
			}
		}
	end

	if package.VCC then
		return false, {
			errMsg = "Package "..packageID.." belongs to VCC "..package.VCC..". Free it before adding it to "..VCC..".",
			code = "PACKAGE BELONGS TO ANOTHER VCC",
			rawData = {
				packageID = packageID,
				vcc = package.VCC
			}
		}
	end

	-- Ahora si, actualizo
	ksProv:InsertColumn(packageID, "Packages", "VCC", VCC)
	ksProv:InsertColumn("VCC:"..VCC, 'VCCPackages', packageID, json.encode({}))
	ksProv:RemoveColumn("Free", 'VCCPackages', packageID)

	-- OK
	return true
end

-- FreePackageFromVCC
---------------------------
-- Frees a package from its VCC
-- Pre: Package must exist
-- Pre: Package must be assigned to a VCC
function FreePackageFromVCC (ksProv, packageID)
	-- TODO: Check it's not associated with any campaign!
	assert(packageID)

	packageID = utf8.lower(packageID)

	--LogDebug("[PROVISIONING_MANAGER:FreePackageFromVCC] Free Package " .. tostring(packageID))

	local package = GetPackage(ksProv, packageID)
	if isEmpty(package) then
		return false, {
			errMsg = "Package "..packageID.." does not exist.",
			code = "PACKAGE DOES NOT EXIST",
			rawData = {
				packageID = packageID
			}
		}
	end

	if not package.VCC then
		return false, {
			errMsg = "Package "..packageID.." belongs to VCC "..package.VCC,
			code = "PACKAGE BELONGS TO ANOTHER VCC",
			rawData = {
				packageID = packageID,
				vcc = package.VCC
			}
		}
	end

	-- Acá se valida que antes de remover el Package del VCC, el mismo no se encuentre asociado a ninguna campaña de este VCC.
	local vccPackages = GetVCCPackages(ksProv, package.VCC) or {}
	--LogDebug("----->vccPackages:" .. json.encode(vccPackages))

	for vccPackageId, vccPackage in pairs(vccPackages) do
		if packageID == vccPackageId:lower() then
			for campaign, accounts in pairs(vccPackage.campaigns or {}) do
				return false, {
					errMsg = "Package "..packageID.." is assinged to som campaigns of VCC "..package.VCC,
					code = "PACKAGE IS ASSIGNED TO SOME VCC CAMPAIGNS",
					rawData = {
						packageID = packageID,
						vcc = package.VCC,
						campaigns = vccPackage.campaigns
					}
				}
			end
		end
	end

	-- Ahora si, actualizo
	ksProv:RemoveColumn(packageID, "Packages", "VCC")
	ksProv:RemoveColumn("VCC:"..package.VCC, 'VCCPackages', packageID)
	ksProv:InsertColumn("Free", 'VCCPackages', packageID, "")

	-- OK
	return true
end

------------------------------------------------------
-- UpdateCampaignAccountPackage
--   Funcion que Actualiza un paquete asociado a la campaña/cuenta.
--
---------------------------
local function UpdateCampaignAccountPackage(ksProv, VCC, campaignId, accountId, packageId, package, assigned)
	campaignId = campaignId:lower()
	local campaigns = package.campaigns or {}

	--apply the change
	if assigned then
		if campaigns[campaignId] and campaigns[campaignId][accountId] then return end

		campaigns[campaignId] = campaigns[campaignId] or {}
		campaigns[campaignId][accountId] = true
	else
		if not campaigns[campaignId] then return end

		campaigns[campaignId] = nil
	end

	--store it
	LogDebug("[PROVISIONING_MANAGER:UpdateCampaignAccountPackage] campaign %s@%s account %s package %s %s",
			 campaignId, VCC, accountId, packageId, assigned and "assigned" or "removed")
	ksProv:InsertColumn("VCC:"..VCC, 'VCCPackages', packageId, json.encode(campaigns))
end

------------------------------------------------------
-- UpdateCampaignAccountPackages
--   Funcion que Actualiza la lista de paquetes asociados a la campaña/cuenta.
--
---------------------------
function UpdateCampaignAccountPackages (ksProv, ksAgent, VCC, campaignId, accountId, accountPackages)
	assert(VCC)
	assert(campaignId)
	assert(accountId)
	accountPackages = accountPackages or {}

	LogDebug("[PROVISIONING_MANAGER:UpdateCampaignAccountPackages] campaign %s@%s account %s packages %s",
			 campaignId, VCC, accountId, json.encode(accountPackages))

	--validate vcc
	if isEmpty(GetVCC(ksAgent, VCC)) then
		return false, {
			errMsg = "VCC "..VCC.." does not exist.",
			code = "VCC DOES NOT EXIST",
			rawData = { VCC = VCC, campaignId = campaignId, accountId  = accountId }
		}
	end

	--get and validate packages
	local vccPackages = GetVCCPackages(ksProv, VCC) or {}
	for packageId, _ in pairs(accountPackages) do
		if not vccPackages[packageId] then
			return false, {
				errMsg = "Package "..packageId.." does not exist.",
				code = "PACKAGE DOES NOT EXIST",
				rawData = {
					VCC        = VCC,
					packageID  = packageID,
					campaignId = campaignId,
					accountId  = accountId
				}
			}
		end
	end

	for packageId, package in pairs(vccPackages) do
		local assigned = accountPackages[packageId] ~= nil
		UpdateCampaignAccountPackage(ksProv, VCC, campaignId, accountId, packageId, package, assigned)
	end
	return true
end

-----------------------------
-- Función utilizada para cargar información del Package en la relación VCC->Package y las campañas que se encuentren asociadas.
local function LoadVCCPackagesInfo (ksProv, vccPackages)
	assert(ksProv)

	for packageId, encodedCampaigns in pairs(vccPackages or {}) do
		local packageInfo = GetPackage(ksProv, packageId) or {}
		local success, campaigns = pcall(json.decode, encodedCampaigns)
		if success and campaigns then
			packageInfo.campaigns = campaigns
		end
		vccPackages[packageId] = packageInfo
	end
end

-- GetVCCPackages
---------------------------
function GetVCCPackages (ksProv, VCC)
	assert(ksProv)
	assert(VCC)
	local key = "VCC:"..VCC
	local vccPackages = select(ksProv, "VCCPackages", key)[key] or {}
	--LogDebug("[PROVISIONING_MANAGER:GetVCCPackages] VCC: '" .. tostring(key) .. "' Packages: " .. json.encode(vccPackages))
	LoadVCCPackagesInfo(ksProv, vccPackages)
	return vccPackages
end

-- GetFreeVCCPackages
---------------------------
function GetFreeVCCPackages (ksProv)
	assert(ksProv)
	local key = "Free"
	local freeVccPackages = select(ksProv, "VCCPackages", key)[key] or {}
	--LogDebug("[PROVISIONING_MANAGER:GetFreeVCCPackages] 1 Free VCC Packages: " .. json.encode(freeVccPackages))
	LoadVCCPackagesInfo(ksProv, freeVccPackages)
	--LogDebug("[PROVISIONING_MANAGER:GetFreeVCCPackages] 2 Free VCC Packages: " .. json.encode(freeVccPackages))
	return freeVccPackages
end

-----------------------------------------------------
-- SECURITY CAPTCHA
-----------------------------------------------------

--[[
	reCAPTCHA V2:
	-------------
	Las claves pública y privada se crearon en el link de abajo,
	y se crearon con el usuario de gmail de inconcert:

		El usuario y pwd de gmail es una cuenta de Julio Guridi.

		Clave del sitio:
		----------------
		(client side) Usada en el código HTML que se muestra a los usuarios.
			6Lf-J0UUAAAAAGmR_IKYvcsFhjsu9v_AXeTnsg87

		Clave secreta  :
		----------------
		(server side) Usada para las comunicaciones entre tu sitio y Google. No revelársela a nadie.
			6Lf-J0UUAAAAAFFGy0pjfQgF0tZRtaGYOc2NuWdw
]]--
function LoadInconcertCaptchaInfo ()
	local captchaInfo = {
		keys = {
			publicKey = "6Lf-J0UUAAAAAGmR_IKYvcsFhjsu9v_AXeTnsg87",
			secretKey = "6Lf-J0UUAAAAAFFGy0pjfQgF0tZRtaGYOc2NuWdw"
		}
	}
	return captchaInfo
end

function MakeCaptchaConfig (security, captchaInfo)
	assert(type(security) == "table")
	assert(type(captchaInfo) == "table")

	security.captcha = {
		enabled   = captchaInfo.enabled,
		publicKey = captchaInfo.publicKey,
		secretKey = captchaInfo.secretKey
	}
end
-----------------

-----------------------------------------------------
-- SESSION TIMEOUTS
-----------------------------------------------------

--[[
]]--
function LoadDefaultSessionTimeouts ()
	local sessionTimeouts = {
		MOBILE = (3 * 60 * 60) , -- 3 horas a segundos
		WEB    = 40              -- segundos
	}
	return sessionTimeouts
end
-----------------

--[[---------------------------------------------------
	VCC NOTIFICATIONS
		El atributo "mustAsk" es utilizado para saber si debe de realizar la consulta (prompt)
		Eso es así, ya que esta función es invocada tanto:
			- command line (si tiene que hacer la consulta)
			- administration web (no tiene que hacer la consulta, ya que la data viene dada en Post)
-----------------------------------------------------]]--
function LoadVCCInteractionNotifications (vcc, mustAsk)
	assert(type(vcc) == "table")
	mustAsk = mustAsk ~= false

	local interactionNotifications = vcc.InteractionNotifications or {}
	if  type(interactionNotifications) == "string" then
		interactionNotifications = json.decode(interactionNotifications)
	end
	interactionNotifications.Enabled = "1" -- (Ref #8749) Siempre tiene que estar Enabled las notificaciones y NO se debe preguntar nunca !!!

	--[[
		local interactionNotifications = vcc.InteractionNotifications or {Enabled = "0"}
		if  type(interactionNotifications) == "string" then
			interactionNotifications = json.decode(interactionNotifications)
		end

		if mustAsk then
			interactionNotifications.Enabled = requestBoolean("Support Notifications?", {default = interactionNotifications.Enabled ~= "0"}) and "1" or "0"
		else
			if interactionNotifications.Enabled ~= "1" then
				interactionNotifications.Enabled = "0"
			end
		end
	]]--
	vcc.InteractionNotifications = json.encode(interactionNotifications)

	local enabled = interactionNotifications.Enabled == "1"
	return enabled
end

-----------------------------------------------------
-- APPS INTEGRATIONS
-----------------------------------------------------

local GetSample = function(sample, default)
	if default then
		return ""
	end
	if not sample then
		return ""
	end
	return string.format(" - SAMPLE: (%s)", sample)
end

local m_isEnabledNotifications = false
local GetAppNotificationsUrl = function(self, name, default)
	if not m_isEnabledNotifications then
		return ""
	end
	local enabled = requestBoolean("Are " .. name .. " notifications enabled?", {default = (default or "") ~= ""})
	if enabled then
		return requestAlpha("Enter " .. name .. " Notifications URL" .. GetSample(self.Sample, default), {default = default, allowEmpty = false})
	end
	return ""
end

--[[-----------------------------------------
	HelpDesk Integration
--------------------------------------------]]
local GetHelpDeskHost = function(self, default)
	local name = "Enter HelpDesk HOST" .. GetSample(self.Sample, default)
	return requestAlpha(name, {default = default, allowEmpty = false})
end
local GetHelpDeskUser = function(self, default)
	return requestAlpha("Enter HelpDesk Login USER", {default = default, allowEmpty = false})
end
local GetHelpDeskPassword = function(self, default)
	return requestAlpha("Enter HelpDesk Login PASSWORD", {default = default, allowEmpty = false})
end
local GetHelpDeskClientId = function(self, default)
	return requestAlpha("Enter HelpDesk Login Client Id", {default = default, allowEmpty = false})
end
local GetHelpDeskGrantType = function(self, default)
	return requestAlpha("Enter HelpDesk Login Grant Type", {default = default, allowEmpty = false})
end
local GetHelpDeskScope = function(self, default)
	return requestAlpha("Enter HelpDesk Login Scope", {default = default, allowEmpty = false})
end
local GetHelpDeskDefaultUsersLicense = function()
	return 0
end
local GetHelpDeskUsersLicense = function(self, default)
	local msg = "Enter HelpDesk Users Licenses"
	return requestNumeric(msg, {default = default or GetHelpDeskDefaultUsersLicense()})
end
-- local GeHelpDeskApptLicense = function(self, default, appId)
	-- local msg = string.format("Enter HelpDesk Licenses for %s APP", (appId or ""):upper())
	-- return requestNumeric(msg, {default = default or 0})
-- end
local GetHelpDeskNotificationsUrl = function(self, default, currentConfig)
	local notificationsURL = GetAppNotificationsUrl(self, "HelpDesk", default)
	return notificationsURL
end

--[[-----------------------------------------
	Marketing And Sales Integration
--------------------------------------------]]
local GetMarketingAndSalesNotificationsUrl = function(self, default, currentConfig)
	local notificationsURL = GetAppNotificationsUrl(self, "Marketing and Sales", default)
	return notificationsURL
end

local GetMarketingAndSalesTokenUrl = function(self, default, currentConfig)
	local name = "Enter Marketing And Sales Access Token URL" .. GetSample(self.Sample, default)
	local url = requestAlpha(name, {default = default, allowEmpty = false})
	return url
end

local GetHTTPMarketingAndSalesAccessToken = function(self, default, currentConfig)
	if not currentConfig then
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Error 1")
		return
	end
	if not currentConfig.AccessToken then
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Error 2")
		return
	end
	local accessTokenURL = currentConfig.AccessToken.Url
	if not accessTokenURL then
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Error 3")
		return
	end

	-- Si en 5 segundos NO realizó el Logout, me voy por timeout. Esto para NO quedar colgados esperando.
	local options = {timeoutSec = 10}
	local success, encodedResponse, response = m_request:Get(accessTokenURL, nil, options)
	if not success then
		local err = encodedResponse
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Response HTTP Err." .. tostring(err))
		return
	end
	if not response then
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Error 4")
		return
	end
	if not response.status then
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Error 5")
		return
	end
	local token = response.data
	if not token then
		LogDebug("[PROVISIONING_MANAGER:GetHTTPMarketingAndSalesAccessToken] Error 6")
		return
	end
	return token
end

local GetMarketingAndSalesAppRenderUrl = function(self, default, currentConfig)
	local name = "Enter Marketing And Sales App Render URL" .. GetSample(self.Sample, default)
	local url = requestAlpha(name, {default = default, allowEmpty = false})
	return url
end

local GetMarketingAndSalesApiUrl = function(self, default, currentConfig)
	local name = "Enter Marketing And Sales API URL Base" .. GetSample(self.Sample, default)
	local url = requestAlpha(name, {default = default, allowEmpty = false})
	return url
end

--[[-----------------------------------------
	Work Force Management Integration
--------------------------------------------]]
local GetWorkForceManagementNotificationsUrl = function(self, default, currentConfig)
	local notificationsURL =  GetAppNotificationsUrl(self, "Work Force Management", default)
	return notificationsURL
end

--[[-----------------------------------------
	Integrations Template

	Esta función es invocada desde este mismo archivo o de forma externa.
	Ej: desde el frm de provisioning "frm_vccs_content.basic.html"
--------------------------------------------]]

function GetSupportedIntegrationsTemplate (widgetsIntegrations)
	local integrations = {
		{
			--[[
				HELPDESK Domain de testing.
					https://helpdesk.domain --> https://helpdeskic.inconcertdesk.com/ICHelpDesk16
			]]--
			Name = "HelpDesk",
			Description = "HelpDesk",
			Config = {
				-- BASE URL (HOST)
				{
					Id = "Server",
					Items = {
						{
							Field  = "HOST",
							Value  = GetHelpDeskHost,
							Sample = "https://helpdesk.domain/testsubdomain"
						}
					}
				},
				-- NOTIFICATIONS URL
				{
					Id = "Notifications",
					Items = {
						{
							Field  = "Url",
							Value  = GetHelpDeskNotificationsUrl,
							Sample = "https://helpdesk.domain/testsubdomain/notifications"
						}
					}
				},
				{ -- API Utilizada desde Administration de OCC para el ABM de Users y Roles en Helpdesk
					Id = "UserAuthentication",
					Items = {
						{Field = "User"     , Value = GetHelpDeskUser     , Default = nil                               },
						{Field = "Password" , Value = GetHelpDeskPassword , Default = nil                               },
						{Field = "ClientId" , Value = GetHelpDeskClientId , Default = "51507d0c6a714f54bb23375c5eb4cabc"},
						{Field = "GrantType", Value = GetHelpDeskGrantType, Default = "password"                        },
						{Field = "Scope"    , Value = GetHelpDeskScope    , Default = "FullControl"                     }
					}
				},
				{  -- API Utilizada desde los flujos de OCC para la Actividad de Helpdesk
					Id = "ApiUserAuthentication",
					Items = {
						{Field = "User"     , Value = GetHelpDeskUser     , Default = nil                               },
						{Field = "Password" , Value = GetHelpDeskPassword , Default = nil                               },
						{Field = "ClientId" , Value = GetHelpDeskClientId , Default = "51507d0c6a714f54bb23375c5eb4cabc"},
						{Field = "GrantType", Value = GetHelpDeskGrantType, Default = "password"                        },
						{Field = "Scope"    , Value = GetHelpDeskScope    , Default = "FullControl"                     }
					}
				},
				{
					Id = "Licenses",
					Items = {
						{Field = "Users"   , Value = GetHelpDeskUsersLicense}
						-- {Field = "Administration", Value = GeHelpDeskApptLicense },
						-- {Field = "Interaction"   , Value = GeHelpDeskApptLicense },
						-- {Field = "Supervisor"    , Value = GeHelpDeskApptLicense }
					}
				}
			}
		},
		{
			--[[
				M&S Domain de testing.
					https://mas.domain --> https://mas-testing.inconcertcc.com
			]]--
			Name = "MarketingAndSales",
			Description = "Marketing and Sales",
			Config = {
				-- NOTIFICATIONS URL
				{
					Id = "Notifications",
					Items = {
						{
							Field  = "Url",
							Value  = GetMarketingAndSalesNotificationsUrl,
							Sample = "https://mas.domain/public/occ/process"
						}
					}
				},
				{
					Id = "AccessToken",
					Items = {
						-- AUTORIZATION ACCESS TOKEN URL
						{
							Field  = "Url",
							Value  = GetMarketingAndSalesTokenUrl,
							Sample = "https://mas.domain/public/occ/get_session_token"
						},
						-- AUTORIZATION ACCESS TOKEN CODE (Realiza un request para obtener el token)
						{
							Field  = "Token",
							Value  = GetHTTPMarketingAndSalesAccessToken,
							--[[
								Atributo utilizado para forzar la ejecución de la función que obtiene el value.
								Sirve cuando el save se realiza desde la WEB y los valores vienen dados en el POST,
								y no se piden los valores en el Prompt como sí se utliza en el pm-cli.lua.
								En este caso específico, se realiza un request para obtener el Token.
							]]--
							MustForceValue = true
						}
					}
				},
				-- APP Form Render
				{
					Id = "AppRender",
					Items = {
						{
							Field  = "Url",
							Value  = GetMarketingAndSalesAppRenderUrl,
							--[[
								Client side tienen que añadir los parámetros que se necesiten.
								https://mas.domain/form/render?interaction_id=[INTERACTION_ID]
							]]
							Sample = "https://mas.domain/form/render"
						}
					}
				},
				-- API URL Base
				{
					Id = "Api",
					Items = {
						{
							Field  = "Url",
							Value  = GetMarketingAndSalesApiUrl,
							Sample = "https://mas.domain/api"
						}
					}
				}
			}
		},
		{
		--[[
			WFM Domain de testing.
				https://wfm.domain --> https:///wfm.domain
		]]--
			Name = "WorkForceManagement",
			Description = "Work Force Management",
			Config = {
				-- NOTIFICATIONS URL
				{
					Id = "Notifications",
					Items = {
						{
							Field = "Url",
							Value = GetWorkForceManagementNotificationsUrl,
							Sample = "https://wfm.domain/notifications"
						}
					}
				}
			}
		}
	}

	if not widgetsIntegrations or #widgetsIntegrations == 0 then
		--LogDebug("[PROVISIONING_MANAGER:GetSupportedIntegrationsTemplate] 1 - Only Integrations, without Widgets.")
		return integrations
	end

	local allIntegrations = {}
	for _, integration in pairs(integrations) do
		table.insert(allIntegrations, integration)
	end
	for _, widget in pairs(widgetsIntegrations) do
		widget.IsWidget = "1"
		widget.Config = widget.Config or {}
		table.insert(allIntegrations, widget)
	end

	--LogDebug("[PROVISIONING_MANAGER:GetSupportedIntegrationsTemplate] 2 - Integrations & Widgets.")

	return allIntegrations
end

--[[---------------------------------------------------
	VCC APPS INTEGRATIONS
		El atributo "mustAsk" es utilizado para saber si debe de realizar la consulta (prompt)
		Eso es así, ya que esta función es invocada tanto:
			- command line (si tiene que hacer la consulta)
			- administration web (no tiene que hacer la consulta, ya que la data viene dada en el Post)
-----------------------------------------------------]]--
function LoadVCCAppsIntegrations (vcc, isEnabledNotifications, mustAsk, widgetsIntegrations)
	assert(type(vcc) == "table")
	mustAsk = mustAsk ~= false
	m_isEnabledNotifications = isEnabledNotifications

	local supportedIntegrations = GetSupportedIntegrationsTemplate(widgetsIntegrations)

	local vccCurrentAppsIntegrations = vcc.AppsIntegrations or {}
	if type(vccCurrentAppsIntegrations) == "string" then
		local ok, info = pcall(json.decode, vccCurrentAppsIntegrations)
		if ok then
			vccCurrentAppsIntegrations = info
		end
	end

	local appsIntegrations = {}
	for _, supportedIntegration in ipairs(supportedIntegrations) do
		local currentConfigData = vccCurrentAppsIntegrations[supportedIntegration.Name] or {}

		LogDebug("[PROVISIONING_MANAGER:LoadVCCAppsIntegrations] Integration Config: " .. tostring(supportedIntegration.Description) ..  " -------------")

		if mustAsk then
			local description = string.format("Support '%s' App Integration ?", supportedIntegration.Description)
			currentConfigData.Enabled = requestBoolean(description, {default = currentConfigData.Enabled ~= "0"}) and "1" or "0"
		end

		---------
		-- Informacion relevante utilizada en los Widgets.
		currentConfigData.Id = supportedIntegration.Name
		currentConfigData.Name = supportedIntegration.Name
		currentConfigData.IsWidget = supportedIntegration.IsWidget or "0"
		currentConfigData.DisplayName = supportedIntegration.displayName or supportedIntegration.Description
		---------

		if currentConfigData.Enabled == "1" then
			for _, entryConfig in ipairs(supportedIntegration.Config or {}) do
				if type(entryConfig) == "table" then
					local entry = entryConfig.Id
					currentConfigData[entry] = currentConfigData[entry] or {}

					for _, values in ipairs(entryConfig.Items or {}) do
						local default = currentConfigData[entry][values.Field]
						if not default and values.Default then
							default = values.Default
						end
						if mustAsk or values.MustForceValue then
							currentConfigData[entry][values.Field] = values:Value(default, currentConfigData)
						else
							currentConfigData[entry][values.Field] = default
						end
					end
				end
			end
		else
			currentConfigData.Enabled = "0"
		end

		appsIntegrations[supportedIntegration.Name] = currentConfigData
	end

	vcc.AppsIntegrations = json.encode(appsIntegrations)
end

-----------------------------------------------------
-- USERS
-----------------------------------------------------
function CreateAdminUser (userInfo, ksAgent)
	LogDebug("[PROVISIONING MANAGER] CreateAdminUser Keys. userInfo: %s " .. json.encode(userInfo or {}))

	assert(ksAgent)
	assert(type(userInfo) == "table")
	assert(type(userInfo.vccId) == "string")
	assert(type(userInfo.userId) == "string")

	local userId = alltrim(userInfo.userId)
	assert(userId:len() > 0, "User Id is required")

	local vccId = userInfo.vccId
	local currentVCC = GetVCC(ksAgent, vccId)
	assert(not isEmpty(currentVCC), "VCC " .. tostring(vccId) .. " does not exist")

	--LogDebug(" A - currentVCC.AppsIntegrations " .. tostring(currentVCC.AppsIntegrations))
	local vccCurrentAppsIntegrations = type(currentVCC.AppsIntegrations) == "string" and json.decode(currentVCC.AppsIntegrations) or nil
	--LogDebug(" B - currentVCC.AppsIntegrations.HelpDesk " .. tostring(vccCurrentAppsIntegrations.HelpDesk))
	--LogDebug(" C - currentVCC.AppsIntegrations.HelpDesk.Enabled " .. tostring(vccCurrentAppsIntegrations.HelpDesk.Enabled))

	local email
	if vccCurrentAppsIntegrations.HelpDesk and tonumber(vccCurrentAppsIntegrations.HelpDesk.Enabled) == 1 then
		email = userInfo.email and alltrim(userInfo.email)
		assert(email and email:len() > 0, "Email Id is required for VCC With HelpDesk Integration")
	end

	local fullUserId = AppendVcc(GetId(userId), vccId):lower()
	local pathName = "/usr/share/inconcert/helpers/provisioning/"
	local fileName = "new_user.lua"
	local cmdLine = string.format("lua %s%s -u %s", pathName, fileName, fullUserId)

	if email then
		cmdLine = string.format("%s -e %s", cmdLine, email)
	end

	LogDebug("")
	LogDebug("[PROVISIONING MANAGER] Execute:\n" .. cmdLine)
	LogDebug("")

	os.execute(cmdLine)
end

-- RASA -----------------------
--
local apiRasa = require("rasa"):new() -- rocka
rasa = {}

--[[
]]--
rasa.EnableLogger = function (enabled)
	--LogDebug("[PROVISIONING MANAGER] rasa EnableLogger", tostring(enabled))
	apiRasa.EnableLogger(enabled)
end

--[[
   Importa una App de LUIS a RASA
]]--
rasa.ImportLuisApp = function(info)
	assert(type(info) == "table")
	--LogDebug("[PROVISIONING MANAGER:RASA->ImportLuisApp] Importing LUIS App to RASA. Info: ." .. json.encode(info))
	assert(info.vcc)
	assert(info.luisAppId)
	local success, response = apiRasa:ImportLuisApp(info.luisAppId, info.vcc)
	return success, response
end

--[[
	Obtiene la lista Apps de LUIS que no han sido importadas a  RASA
]]--
rasa.LoadUnimportedLuisApps = function(info)
	assert(type(info) == "table")
	--LogDebug("[PROVISIONING MANAGER:RASA->ImportLuisApp] Importing LUIS App to RASA. Info: ." .. json.encode(info))
	assert(info.vcc)
	return apiRasa:LoadUnimportedLuisApps(info.vcc)
end

-- LUIS -----------------------
--
-- TODO: Hay que realizar una Rocka para poder utilizar esto tanto desde el webhandler (administration), como desde provisioningmanager.
--       Por ahora lo dejo, pero deberia cambiarlo lo antes posible.

luis = {}

----------------------------------------------------------------------------
--[[
    Con el FLAG "m_luisApiVersion" indicamos la versión de LUIS a utilizar.
    El mismo puede ser modificdo únicamente por Fábrica o TQA para validar pruebas entre las diferentes versiones.
]]--

--ya no se define la versión aquí
--local m_luisApiVersion = "2.0" -- VERSION CON LA CUAL HEMOS INTEGRADO LUIS POR PRIMERA VEZ CON I6.
--local m_luisApiVersion = "3.0-PREVIEW" -- VERSION ACTUAL (2020/11/23)

local api_luis = require("luis"):new()
----------------------------------------------------------------------------


--[[
	Load Subscriptions key desde LUIS
]]--
luis.EnableLogger = function (enabled)
	--LogDebug("[PROVISIONING MANAGER] EnableLogger", tostring(enabled))
	api_luis.EnableLogger(enabled)
end

--[[
	Load VCC Subscriptions key desde LUIS
]]--
luis.LoadVCCSubscriptions = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] Load LoadVCCSubscriptions Keys. VCC " .. tostring(vcc))
	local list, total = api_luis.LoadVCCSubscriptions(vcc)
	local encodedList = json.encode(list or {})
	return encodedList, total, list
end

--[[
	Load VCC Subscriptions key WITHOUT DEMO desde LUIS
]]--
luis.LoadVCCSubscriptionsWithoutDemo = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] Load LoadVCCSubscriptionsWithoutDemo Keys. VCC " .. tostring(vcc))
	local list, total = api_luis.LoadVCCSubscriptionsWithoutDemo(vcc)
	local encodedList = json.encode(list or {})
	return encodedList, total, list
end

--[[
	Valida si un VCC tiene configurada una Key de DEMO
]]--
luis.HasConfiguredDemoSubscriptionsInVcc = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] HasConfiguredDemoSubscriptionsInVcc. VCC " .. tostring(vcc))
	local isDemo, demoSubscription = api_luis.HasConfiguredDemoSubscriptionsInVcc(vcc)
	return isDemo, demoSubscription
end

--[[
	Load VCCs By Subscription key desde LUIS
]]--
luis.LoadVCCsBySubscription = function (key)
	--LogDebug("[PROVISIONING MANAGER] Load LoadVCCsBySubscription Keys. key " .. tostring(key))
	local list, total = api_luis.LoadVCCsBySubscription(key)
	local encodedList = json.encode(list or {})
	return encodedList, total
end

--[[
	Load VCCs By DEMO Subscription key desde LUIS
]]--
luis.LoadVCCsByDemoSubscription = function (key)
	--LogDebug("[PROVISIONING MANAGER] Load LoadVCCsByDemoSubscription Keys. key " .. tostring(key))
	return luis.LoadVCCsBySubscription(key)
end

--[[
	Asignar una Subscriptions key a un VCC
]]--
luis.AssignSubscriptionKeyToVCC = function (info)
	LogDebug("[PROVISIONING MANAGER] Assign Subscription Key To VCC: " .. json.encode(info))
	local success, response = api_luis.AssignSubscriptionKeyToVCC(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Quitar una Subscriptions key de un VCC
]]--
luis.UnAssignSubscriptionKeyOfVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Unassign Subscription Key Of VCC: " .. json.encode(info))
	local success, response = api_luis:UnAssignSubscriptionKeyOfVCC(info.vcc, info.subscriptionKey)
	--LogDebug("[PROVISIONING MANAGER:UnAssignSubscriptionKeyOfVCC] success " .. tostring(success) .. ", response: " .. json.encode(response))
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Asignar una DEMO Subscriptions key a un VCC
]]--
luis.AssignDemoSubscriptionKeyToVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Assign Demo Subscription Key To VCC: " .. json.encode(info))
	local success, response = api_luis.AssignDemoSubscriptionKeyToVCC(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Quitar una Demo Subscriptions key de un VCC
	Esta funcion hace lo mismo que la funcion "UnAssignSubscriptionKeyOfVCC", por lo que invoco desde aca por si hay que hacer alguna otra cosa.
]]--
luis.UnAssignDemoSubscriptionKeyOfVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Unassign Demo Subscription Key Of VCC: " .. json.encode(info))
	info.isDemo = true
	local success, response = luis.UnAssignSubscriptionKeyOfVCC(info)
	--LogDebug("[PROVISIONING MANAGER:UnAssignDemoSubscriptionKeyOfVCC] success " .. tostring(success) .. ", response: " .. json.encode(response))
	return success, response
end

--[[
	Sample info:
		subscriptionKey: (requerido) las de Tito:  d24518b086a248c4ae64afd6e3eb475c   O   f5c41ac5c06b46d6a697c076f4f3923c
		vcc            : (requerido) inconcert
]]--
luis.ExecuteUnAssignSubscriptionKeyOfVCC = function (data, isDemo, noQuestions)
	if not data then
		LogDebug("[PROVISIONING MANAGER.LUIS] Unassigning Subscription Key Of VCC")
	end

	local info
	if data then
		info = data
	else
		info = {
			subscriptionKey = requestAlpha("Enter the Subscription Key Value (GUID 32 characters)"),
			vcc             = requestAlpha("Enter the VCC")
		}
	end
	local demoString = isDemo and "DEMO" or ""

	local function UnAssign ()
		local fn = isDemo and luis.UnAssignDemoSubscriptionKeyOfVCC or luis.UnAssignSubscriptionKeyOfVCC
		local success, response = fn(info)
		--LogDebug("[PROVISIONING MANAGER.LUIS] luis.UnAssignSubscriptionKeyOfVCC response: " .. json.encode(response))
		if not success then
			local err = luis.ConvertErrorToString(response)
			LogDebug("[PROVISIONING MANAGER.LUIS] Error Unassigning " .. demoString .. " Subscription Key: '" .. info.subscriptionKey .. "' of VCC: '" .. info.vcc .. "' " .. err)
			return
		end
		response = response or {}
		local updatedApps = response.updatedApps or {}
		LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Unassigned " .. demoString .. " Subscription Key: '" .. info.subscriptionKey .. "' of VCC: '" .. info.vcc .. "'. Updated '" .. tostring(#updatedApps) .. "' Apps.")
	end

	local _, totalVCCSubscriptionsKeys = luis.LoadVCCSubscriptions(info.vcc)

	if totalVCCSubscriptionsKeys == 0 then
	-- Si no tenemos subscriptions keys asociadas al VCC
	LogDebug("VCC " .. info.vcc .. " has not configured any Subscriptions Keys.")

	elseif totalVCCSubscriptionsKeys == 1 then
	-- Cuando tenemos una unica subscription key asociada al VCC
		if not noQuestions then
			local canContinue = requestAlpha("The VCC " .. info.vcc .. " has single Subscription Key. Do you want to Update Publish Info for All Applications of the VCC ?",
				{default = "N"}
			)
			if canContinue ~= "N" then
				UnAssign()
			else
				LogDebug("Cancelled")
			end
		else
			local isAssigned, apps = luis.IsAssignedSubscriptionKeyInSomeApp(info)
			if isAssigned then
				LogDebug("[PROVISIONING MANAGER.LUIS] The Subscription key " .. info.subscriptionKey .. " is assigned in " .. tostring(#apps) .. " applications. Have to publish again")
			end
			UnAssign()
		end
	else
	-- Cuando tenemos mas de una subscriptions keys asociadas al VCC
		local isAssigned, apps = luis.IsAssignedSubscriptionKeyInSomeApp(info)
		if isAssigned then
			if not noQuestions then
				local canContinue = requestAlpha("The Subscription key is assigned in " .. tostring(#apps) .. " applications. Do you want unassign Subscription keys and Update Publish Info in this Applications ?",
					{default = "N"}
				)
				if canContinue ~= "N" then
					UnAssign()
				else
					LogDebug("Cancelled")
				end
			else
				LogDebug("[PROVISIONING MANAGER.LUIS] The Subscription key " .. info.subscriptionKey .. " is assigned in " .. tostring(#apps) .. " applications. Have to publish again")
				UnAssign()
			end
		else
			UnAssign()
		end
	end

	return true
end

luis.GetDefaultDemoSubscriptionKey = function ()
	--LogDebug("[PROVISIONING MANAGER] GetDefaultDemoSubscriptionKey")
	return api_luis.GetDefaultDemoSubscriptionKey()
end

--[[
	Funcion que pide la info del Tenant.

	tenantInfo = {
		tenant_id = "359e9065-2b08-4aae-9504-38e2a3b4b356",
		client_id = "d336fabc-9804-4186-8f8f-b40c52f39c6e",
		client_secret = "J]Z/1@x/4PJXG9K1+SIj96GaIQ:iLS1s",
		resource = "https://management.azure.com/"
	}
]]--
luis.GetTenantInfo = function (vcc, customTenantInfo, mustAsk)
	local defaultTenantInfo = luis.LoadDefaultTenantAzureAccount()
	local tenantInfo = customTenantInfo or {}

	--LogDebug("[PROVISIONING MANAGER.LUIS] GetTenantInfo - Current VCC '" .. tostring(vcc) .. "' mustAsk " .. tostring(mustAsk))

	if vcc and not customTenantInfo then
		local vccTenantAzureAccounts, total = luis.LoadVCCTenantAzureAccount(vcc)
		--LogDebug("--> total " .. tostring(total))
		--LogDebug("--> vccTenantAzureAccounts " .. json.encode(vccTenantAzureAccounts))
		if total > 0 then
			local _, info = next(vccTenantAzureAccounts)
			tenantInfo = info
		end
		LogDebug("[PROVISIONING MANAGER.LUIS] GetTenantInfo - Current VCC '" .. tostring(vcc) .. "' tenant Info " .. json.encode(tenantInfo))
	end

	if not mustAsk then
		-- desde la web
		return tenantInfo
	end

	-- command line
	return {
		tenant_id = requestAlpha("Enter the Tenant Id (GUID 36 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
			{default = tenantInfo.tenant_id or defaultTenantInfo.tenant_id}
		),
		client_id = requestAlpha("Enter the Secret Client Id (GUID 36 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
			{default = tenantInfo.client_id or defaultTenantInfo.client_id}
		),
		client_secret = requestAlpha("Enter the Secret Name",
			{default = tenantInfo.client_secret or defaultTenantInfo.client_secret}
		),
		resource = requestAlpha("Enter the Resource Name", {
			default = tenantInfo.resource or defaultTenantInfo.resource}
		)
	}
end

--[[
	Funcion que pide la info del Language Studio Account.

	languageStudioInfo = {
		resource_name = "inconcert" // (https://{{resourcename}}.cognitiveservices.azure.com/),
		azure_subscription_id = "32cea6dd-b332-4b44-90e4-5204db991dc0",
		subscription_key_1 = "497d2d1c1a364e4ab54063d6d3148a62",
		subscription_key_2 = "497d2d1c1a364e4ab54063d6d3148a63"
	}
]]--
luis.GetLanguageStudioInfo = function (vcc, languageStudioInfo)
	
	assert(type(vcc) == "string")

	if not languageStudioInfo then
		local vccAzureLanguageStudioAccounts, total = luis.LoadVCCAzureLanguageStudioAccount(vcc)
		--LogDebug("--> total " .. tostring(total))
		--LogDebug("--> vccAzureLanguageStudioAccounts " .. json.encode(vccAzureLanguageStudioAccounts))
		if total > 0 then
			local _, info = next(vccAzureLanguageStudioAccounts)
			languageStudioInfo = info
		end
	end
	
	--LogDebug("[PROVISIONING MANAGER.LUIS] GetLanguageStudioInfo - Current VCC '" .. tostring(vcc) .. "' Azure Language Studio Info " .. json.encode(languageStudioInfo))

	return languageStudioInfo
end

--[[
	START Asignar un Tenant Account al VCC
]]--

luis.StartAssignTenantAzureAccountToVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("[PROVISIONING MANAGER.LUIS] Start Assigning Tenant AZURE Account To VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))

	local info

	if mustAsk == false then
		assert(vccInfo.vcc)
		local tenantInfo = luis.GetTenantInfo(vccInfo.vcc, vccInfo.tenantInfo, mustAsk)
		info = {
			vcc = vccInfo.vcc,
			tenantInfo = tenantInfo
		}
	else
		local vcc = requestAlpha("Enter the VCC")
		local tenantInfo = luis.GetTenantInfo(vcc, nil, mustAsk)
		info = {
			vcc = vcc,
			tenantInfo = tenantInfo
		}
	end

	local noLog = true
	local success, response = luis.AssignTenantAzureAccountToVCC(info, noLog)
	if not success then
		local err = luis.ConvertErrorToString(response)
		LogDebug("[PROVISIONING MANAGER.LUIS] Error Assigning Tenant AZURE Account '" .. json.encode(info.tenantInfo) .. "' to VCC: '" .. info.vcc .. "' " .. err)
		return false, response
	end

	LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Assigned Tenant AZURE Account: '" .. json.encode(info.tenantInfo) .. "' to VCC: '" .. info.vcc .. "'")
	return true
end

--[[
	START assign an azure language studio account to the vcc
]]--

luis.StartAssignLanguageStudioAccountToVCC = function (vccInfo)

	LogDebug("[PROVISIONING MANAGER.LUIS] Start Assigning AZURE Language Studio Account To VCC - vccInfo: " .. json.encode(vccInfo))

	local info

	assert(vccInfo.vcc)
	local languageStudioInfo = luis.GetLanguageStudioInfo(vccInfo.vcc, vccInfo.languageStudioInfo)

	LogDebug("[PROVISIONING MANAGER.LUIS] languageStudioInfo result: " .. json.encode(languageStudioInfo))
	
	info = {
		vcc = vccInfo.vcc,
		languageStudioInfo = languageStudioInfo
	}

	local noLog = true
	local success, response = luis.AssignAzureLanguageStudioAccountToVCC(info, noLog)
	if not success then
		local err = luis.ConvertErrorToString(response)
		LogDebug("[PROVISIONING MANAGER.LUIS] Error Assigning AZURE Language Studio Account '" .. json.encode(info.languageStudioInfo) .. "' to VCC: '" .. info.vcc .. "' " .. err)
		return false, response
	end

	LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Assigned AZURE Language Studio Account: '" .. json.encode(info.languageStudioInfo) .. "' to VCC: '" .. info.vcc .. "'")
	return true
end

--[[
	START Asignar una DEMO Subscriptions key a un VCC
]]--
luis.StartAssignDemoSubscriptionKeyToVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("[PROVISIONING MANAGER] LUIS StartAssignDemoSubscriptionKeyToVCC - Start Assigning DEMO Subscription Key To VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))

	local defaultDemoSubscriptionKey = luis.GetDefaultDemoSubscriptionKey()
	local vcc

	if mustAsk == false then
		assert(vccInfo)
		assert(vccInfo.vcc)
		vccInfo.subscriptionKey = vccInfo.subscriptionKey or defaultDemoSubscriptionKey
		vcc = vccInfo.vcc
	else
		vcc = requestAlpha("Enter the VCC")
	end

	-- Asigno los Default Tenant Accounts
	luis.AssignDefaultTenantAzureAccountToVCC(vcc)

	local isDemo, demoSubscription = luis.HasConfiguredDemoSubscriptionsInVcc(vcc)
	if isDemo then
		LogDebug("[PROVISIONING MANAGER] LUIS StartAssignDemoSubscriptionKeyToVCC - VCC: '" .. vcc .. "' has already configured DEMO Subscription Key. Current DEMO Subscription: " .. demoSubscription.subscriptionKey)
		return false, "ALREADY ASSIGNED - DEMO SUBSCRIPTION KEY"
	end

	local info = {
		vcc = vcc,
		subscriptionKey = mustAsk == false and vccInfo.subscriptionKey or
									requestAlpha("Enter the DEMO Subscription Key Value (GUID 32 characters) SAMPLE: " .. tostring(defaultDemoSubscriptionKey))
	}

	if info.subscriptionKey:len() == 0 then
		LogDebug("[PROVISIONING MANAGER] LUIS StartAssignDemoSubscriptionKeyToVCC - Cancel Assigning DEMO Subscription Key to VCC: '" .. vcc .. "' ")
		return false, "NO ALLOWED EMPTY DEMO SUBSCRIPTION KEY"
	end

	local noLog = true
	local success, response = luis.AssignDemoSubscriptionKeyToVCC(info, noLog)
	if not success then
		local err = response
		local stringErr = luis.ConvertErrorToString(err)
		LogDebug("[PROVISIONING MANAGER] LUIS StartAssignDemoSubscriptionKeyToVCC - Error Assigning DEMO Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "' " .. stringErr)
		return false, err
	end

	LogDebug("[PROVISIONING MANAGER] LUIS StartAssignDemoSubscriptionKeyToVCC - Successfully Assigned DEMO Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "'")
	return true
end

luis.AssignDefaultTenantAzureAccountToVCC = function (vcc)
	LogDebug("[PROVISIONING MANAGER] LUIS AssignDefaultTenantAzureAccountToVCC - Assigning Default Tenant AZURE Account To VCC " .. tostring(vcc))

	vcc = vcc or requestAlpha("Enter the VCC")

	local vccTenantAzureAccounts, total = luis.LoadVCCTenantAzureAccount(vcc)
	if total > 0 then
		LogDebug("[PROVISIONING MANAGER] LUIS AssignDefaultTenantAzureAccountToVCC - Warning Assigning Default Tenant AZURE Account to VCC: '" .. vcc .. "' VCC already Tenant AZURE Account")
		return false, "ALREADY ASSIGNED - TENANT AZURE ACCOUNT"
	end

	local defaultTenantInfo = luis.LoadDefaultTenantAzureAccount()
	local info = {
		vcc = vcc,
		tenantInfo = defaultTenantInfo
	}
	local noLog = true
	local success, response = luis.AssignTenantAzureAccountToVCC(info, noLog)
	if not success then
		local err = response
		local stringErr = luis.ConvertErrorToString(err)
		LogDebug("[PROVISIONING MANAGER] LUIS AssignDefaultTenantAzureAccountToVCC - Warning Assigning Default Tenant AZURE Account '" .. json.encode(defaultTenantInfo) .. "' to VCC: '" .. info.vcc .. "' " .. stringErr)
		return false, err
	end

	LogDebug("[PROVISIONING MANAGER] LUIS AssignDefaultTenantAzureAccountToVCC - Successfully Assigned Default Tenant AZURE Account: '" .. json.encode(defaultTenantInfo) .. "' to VCC: '" .. info.vcc .. "'")
	return true
end

--[[
	Funcion encargada de convertir a string un error generado x Inconcert.
	Ya que el mismo se debe de visualizar en el LOG.
	Los errores de los request contra el API de LUIS, no se deben de visualizar en el LOG.
]]--
luis.ConvertErrorToString = function (err)
	local stringErr = ""
	if err and err.errType == "inconcert" then
		stringErr = json.encode(err)
	end
	return stringErr
end

--[[
]]--
luis.IsAssignedSubscriptionKeyInSomeApp = function (info)
	--LogDebug("[PROVISIONING MANAGER] Is Assigned Subscription Key In Some App: " .. json.encode(info))
	return api_luis.IsAssignedSubscriptionKeyInSomeApp(info.vcc, info.subscriptionKey)
end

-- --[[
-- 	Load VCC Azure Accounts desde LUIS
-- ]]--
-- luis.LoadVCCAzureAccounts = function (vcc)
-- 	--LogDebug("[PROVISIONING MANAGER] Load LoadVCCAzureAccounts Azure accounts. VCC " .. tostring(vcc))
-- 	local list, total = api_luis.LoadVCCAzureAccounts(vcc)
-- 	local encodedList = json.encode(list or {})
-- 	return encodedList, total
-- end

--[[
	Load VCCs By Azure Account desde LUIS
]]--
luis.LoadVCCsByAzureAccount = function (key)
	--LogDebug("[PROVISIONING MANAGER] Load LoadVCCsByAzureAccount . key " .. tostring(key))
	local list, total = api_luis:LoadVCCsByAzureAccount(key)
	local encodedList = json.encode(list or {})
	return encodedList, total
end

--[[
	Valida si los datos del Azure Account son validos.
]]--
luis.IsValidAzureAccount = function (info)
	--LogDebug("[PROVISIONING MANAGER] IsValidAzureAccount To VCC: " .. json.encode(info))
	local success, response = api_luis:IsValidAzureAccount(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Asignar una Subscriptions key a un VCC
]]--
luis.AssignAzureAccountToVCC = function (info)
	LogDebug("[PROVISIONING MANAGER] AssignAzureAccountToVCC To VCC: " .. json.encode(info))
	local success, response = api_luis:AssignAzureAccountToVCC(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Quitar una Subscriptions key de un VCC
]]--
luis.UnAssignAzureAccountOfVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Unassign Subscription Key Of VCC: " .. json.encode(info))
	local success, response = api_luis.UnAssignAzureAccountOfVCC(info.vcc, info.AzureSubscriptionId)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
]]--
local StartAssignAzureAccountToVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("[PROVISIONING MANAGER.LUIS] Start Assigning Azure Subscription To VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))
	LogDebug("")

	local action = "ADD"
	local vccAzureSubscriptionId, currentAzureAccount

	local info = vccInfo

	-- Asigno los Default Tenant Accounts
	luis.AssignDefaultTenantAzureAccountToVCC(info.vcc)

	local vccAzureAccounts, totalVCCAzureAccounts = luis.LoadVCCAzureAccounts(info.vcc)
-- LogDebug(tostring(totalVCCAzureAccounts))
-- LogDebug(tostring(vccAzureAccounts))

	if totalVCCAzureAccounts > 0 then
		if mustAsk ~= false then
			LogDebug([[

**************************************************************************************************************
ATTENTION:
----------
If you modify any value, you must publish the VCC applications again.
**************************************************************************************************************
			]])
		end
		action = "UPDATE"
		vccAzureSubscriptionId, currentAzureAccount = next(vccAzureAccounts)
		--LogDebug(vccAzureSubscriptionId)
		--print(currentAzureAccount)
		LogDebug("")
		LogDebug("[PROVISIONING MANAGER.LUIS] Current Azure Subscription Info in VCC: '" .. info.vcc .. "'. Info: " .. json.encode(currentAzureAccount))
		LogDebug("")
		local encodedSubscriptionsKeysWithoutDemo, total = luis.LoadVCCSubscriptionsWithoutDemo(info.vcc)
		LogDebug("[PROVISIONING MANAGER.LUIS] " .. tostring(total) .. " Subscriptions Keys in VCC: '" .. info.vcc .. "'. Info: " .. encodedSubscriptionsKeysWithoutDemo)
		LogDebug("**************************************************************************************************************")
		LogDebug("")
	end

--LogDebug("------------------> action: " .. tostring(action))

	if action == "ADD" then
		if mustAsk ~= false then
			-- Cuando la función se ha invocado desde el command line
			info.AzureSubscriptionId = requestAlpha("Enter the Azure Subscription Id (GUID 36 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)")
			info.ResourceGroup = requestAlpha("Enter the Azure Resource Group Name")
			info.AccountName = requestAlpha("Enter the Azure Account Name")
		end

		LogDebug("[PROVISIONING MANAGER.LUIS] Assigning Azure Info: '" .. json.encode(info))
	else
		if mustAsk ~= false then
			-- Cuando la función se ha invocado desde el command line
			info.AzureSubscriptionId = requestAlpha("Update the Azure Subscription Id", {default = currentAzureAccount.AzureSubscriptionId})
			info.ResourceGroup = requestAlpha("Update the Azure Resource Group Name", {default = currentAzureAccount.ResourceGroup})
			info.AccountName = requestAlpha("Update the Azure Account Name", {default = currentAzureAccount.AccountName})
		end

		if info.AzureSubscriptionId == currentAzureAccount.AzureSubscriptionId and
			info.ResourceGroup == currentAzureAccount.ResourceGroup and
			info.AccountName == currentAzureAccount.AccountName then
			LogDebug("[PROVISIONING MANAGER.LUIS] The Azure Subscriptions information has not changed.")
			return true, info -- retorno true, para que continúe con la actualización de las SUBSCRIPTIONS KEYS
		end

		local success, response = luis.IsValidAzureAccount(info)
		if not success then
			local err = luis.ConvertErrorToString(response)
			LogDebug("[PROVISIONING MANAGER.LUIS] Error Updating New Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' of VCC: '" .. info.vcc .. "' Err: " .. err)
			return false, response
		end

		if info.AzureSubscriptionId ~= currentAzureAccount.AzureSubscriptionId then
			-- Como el AzureSubscriptionId ha cambiado, elimino el mismo para que luego agregue nuevamente con el nuevo AzureSubscriptionId
			LogDebug("[PROVISIONING MANAGER.LUIS] A. Updating Azure Subscriptions information with the DIFFERENT Azure Subscription Id: '" .. info.AzureSubscriptionId)
			local success, response = luis.UnAssignAzureAccountOfVCC(currentAzureAccount)
			if not success then
				local err = luis.ConvertErrorToString(response)
				LogDebug("[PROVISIONING MANAGER.LUIS] Error Updating New Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' of VCC: '" .. info.vcc .. "' Err: " .. err)
				return false, response
			end
		else
			LogDebug("[PROVISIONING MANAGER.LUIS] B. Updating Azure Subscriptions information with the SAME Azure Subscription Id: '" .. info.AzureSubscriptionId)
		end

		LogDebug("[PROVISIONING MANAGER.LUIS] Updating New Azure Info: '" .. json.encode(info))
	end

	local success, response = luis.AssignAzureAccountToVCC(info)
	if not success then
		local err = luis.ConvertErrorToString(response)
		local actionDesc = action == "ADD" and "Assigning" or "Updating"
		LogDebug("[PROVISIONING MANAGER.LUIS] Error " .. actionDesc .. " Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' to VCC: '" .. info.vcc .. "' " .. err)
		return false, response
	end

	local actionDesc = action == "ADD" and "Assigned" or "Updated"
	LogDebug("[PROVISIONING MANAGER.LUIS] Successfully " .. actionDesc .. " Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' to VCC: '" .. info.vcc .. "'")
	return true, info, action
end

luis.ExecuteAssignSubscriptionKeyToVCC = function (data, noLog)
	if not noLog then
		LogDebug("[PROVISIONING MANAGER.LUIS] Execute Assigning Subscription Key To VCC")
	end

	local info = data or {}

	if not info.vcc then
		info.vcc = requestAlpha("Enter the VCC")
	end

	if not info.subscriptionKey then
		local aditionalInfo = {allowEmpty = info.allowEmpty}
		info.subscriptionKey = requestAlpha("Enter the Subscription Key Value (GUID 32 characters)", aditionalInfo)
	end

	if info.subscriptionKey:len() == 0 then
		if not noLog then
			if info.allowEmpty then
				LogDebug("[PROVISIONING MANAGER.LUIS] Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'.")
			else
				LogDebug("[PROVISIONING MANAGER.LUIS] Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'. Subscription Key is Required.")
			end
		end
		return false, {errType = "inconcert", code = "SUBSCRIPTION KEY IS EMPTY"}
	end

	if not info.subscriptionName then
		info.subscriptionName = requestAlpha("Enter the Subscription Name")
	end

	if info.subscriptionName:len() == 0 then
		if not noLog then
			LogDebug("[PROVISIONING MANAGER.LUIS] Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'. Subscription Name is Required.")
		end
		return false, {errType = "inconcert", code = "SUBSCRIPTION NAME IS EMPTY"}
	end

	local success, response = luis.AssignSubscriptionKeyToVCC(info)
	if not success then
		if not noLog then
			local err = luis.ConvertErrorToString(response)
			LogDebug("[PROVISIONING MANAGER.LUIS] Error Assigning Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "' " .. err)
		end
		return false, response
	end
	if not noLog then
		LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Assigned Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "'")
	end

	return true
end

--[[
]]--
local ExecuteUnAssignSubscriptionKeyOfVCCNoQuestion = function (subscriptionKey, vcc, noLog)
	if not noLog then
		LogDebug("")
		LogDebug("[PROVISIONING MANAGER.LUIS] Unassign " .. subscriptionKey .. " subscription Key of VCC: " .. vcc)
	end
	local noQuestions = true
	luis.ExecuteUnAssignSubscriptionKeyOfVCC(
		{
			subscriptionKey = subscriptionKey,
			vcc = vcc
		},
		isDemo,
		noQuestions
	)
	if not noLog then LogDebug("") end
end

--[[
]]--
luis.StartProcessAssignSubscriptionToVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("[PROVISIONING MANAGER.LUIS] START Assigning Subscriptions To VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. json.encode(vccInfo or {}))
	LogDebug("")

	local info
	local subscriptions = vccInfo and vccInfo.subscriptions or {}

	if mustAsk == false then
		-- Cuando la función se ha invocado desde la WEB
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc,
			AzureSubscriptionId = vccInfo.AzureSubscriptionId,
			ResourceGroup = vccInfo.ResourceGroup,
			AccountName = vccInfo.AccountName
		}
	else
		-- Cuando la función se ha invocado desde el command line
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	-- 1 - Aca agrego o actualizo la Info de la Azure Account Subscription Id
	local success, azureInfo, action = StartAssignAzureAccountToVCC(info, mustAsk)
	if not success then
		local err = azureInfo
		return false, err
	end

	LogDebug("")
	local hasConfiguredDemo, demoSubscription = luis.HasConfiguredDemoSubscriptionsInVcc(azureInfo.vcc)

	-- 2 - Ahora agrego o actualizo las Subscriptions KEYS (máximo 2)
	local maxSubscripionsKeys = 2

	-- Funcion para Agregar
	local function AssignSubscriptionKeyToVCC (isRequired, added)
		local requiredMsg, allowEmpty
		if isRequired then
			requiredMsg = "(REQUIRED)"
			allowEmpty = false
		else
			requiredMsg = "(OPTIONAL)"
			allowEmpty = true
		end
		local itemNumber = added + 1

		-- Aca están subscriptions que llegan desde la Web - en el caso del command line esto es nil
		local subscription = subscriptions[itemNumber]

		if mustAsk == false then
			-- Cuando la función se ha invocado desde la WEB
			if not subscription then
				return false
			end
		end

		LogDebug("")
		LogDebug("[PROVISIONING MANAGER.LUIS] " .. tostring(itemNumber) .. " Assigning " .. requiredMsg .. " Subscription Key To VCC. Subcription: " .. tostring(subscription and json.encode(subscription)))

		local info = {
			vcc = azureInfo.vcc,
			allowEmpty = allowEmpty
		}
		if subscription then
			info.subscriptionKey = subscription.subscriptionKey
			info.subscriptionName = subscription.subscriptionName
			info.subscriptionRegion = subscription.subscriptionRegion
		end

		local noLog = true
		local success, response = luis.ExecuteAssignSubscriptionKeyToVCC(info, noLog)
		if not success then
			if response and response.errType == "inconcert" and response.code == "SUBSCRIPTION KEY IS EMPTY" then
				LogDebug("[PROVISIONING MANAGER.LUIS] Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'.")
			else
				local err = luis.ConvertErrorToString(response)
				LogDebug("[PROVISIONING MANAGER.LUIS] Error Updated Subscription Key to VCC: '" .. azureInfo.vcc .. "'. Err: " .. err)
			end
		end
		return success, response
	end

	-- Funcion que itera sobre la lista a agregar
	local function AddSubscriptionsKeys ()
		local added = 0
		local errors = 0
		while added < maxSubscripionsKeys do
			local isRequired = added == 0
			local success, response = AssignSubscriptionKeyToVCC(isRequired, added)
			if not success then
				errors = errors + 1
			end
			added = added + 1
		end
		LogDebug("")
		LogDebug("[PROVISIONING MANAGER.LUIS] END Assigned Subscriptions To VCC '" .. azureInfo.vcc .. "'. Total added Subscriptions Keys: " .. tostring(added - errors))
	end

	-- Funcion para Actualizar
	local function UpdateSubscriptionsKeys (subscriptionsKeysWithoutDemo, currentTotal)
		local removed = 0
		local updated = 0
		local listToRemove = {}
		local listToUpdate = {}

		for key, currentSubscriptionInfo in pairs(subscriptionsKeysWithoutDemo) do
			local subscriptionKey = currentSubscriptionInfo.subscriptionKey
			if mustAsk ~= false then
				-- Cuando la función se ha invocado desde el command line
				subscriptionKey = requestAlpha("Update the Subscription Key Value (ENTER the char D to REMOVE)", {default = subscriptionKey, allowEmpty = true})
			end
--LogDebug("subscriptionKey " .. subscriptionKey).

			if subscriptionKey:upper() == "D" then
				listToRemove[currentSubscriptionInfo.subscriptionKey] = currentSubscriptionInfo
				removed = removed + 1
				LogDebug("[PROVISIONING MANAGER.LUIS] Will be removed Subscription Key '" .. currentSubscriptionInfo.subscriptionKey .. "' of VCC '" .. azureInfo.vcc .. "'.")
			else
				if mustAsk == false then
					-- Cuando la función se ha invocado desde la WEB
					listToRemove[currentSubscriptionInfo.subscriptionKey] = currentSubscriptionInfo
				else
					local subscriptionName = currentSubscriptionInfo.subscriptionName
					if mustAsk ~= false then
						-- Cuando la función se ha invocado desde el command line
						subscriptionName = requestAlpha("Update the Subscription Name", {default = subscriptionName})
					end
					local newSubscriptionInfo = {
						vcc = azureInfo.vcc,
						subscriptionKey  = currentSubscriptionInfo.subscriptionKey,
						subscriptionName = subscriptionName--,
						--isDemo           = false
					}
					if newSubscriptionInfo.subscriptionKey ~= currentSubscriptionInfo.subscriptionKey or newSubscriptionInfo.subscriptionName ~= currentSubscriptionInfo.subscriptionName then
						listToUpdate[currentSubscriptionInfo.subscriptionKey] = newSubscriptionInfo
						updated = updated + 1
						LogDebug("[PROVISIONING MANAGER.LUIS] Will be updated Subscription Key '" .. currentSubscriptionInfo.subscriptionKey .. "' of VCC '" .. azureInfo.vcc .. "'.")
					else
						LogDebug("[PROVISIONING MANAGER.LUIS] The Subscription Key '" .. currentSubscriptionInfo.subscriptionKey .. "' information has not changed.")
					end
				end
			end
		end

		--LogDebug("1 listToRemove: " .. json.encode(listToRemove))
		--LogDebug("2 listToUpdate: " .. json.encode(listToUpdate))

	--Actualizo
		local totalUpdated = removed + updated
		-- 1 - Removiendo
		for subscriptionKey in pairs(listToRemove) do
			ExecuteUnAssignSubscriptionKeyOfVCCNoQuestion(subscriptionKey, azureInfo.vcc)
		end

		-- 2 - Actualizando
		local errors = 0

		for oldSubscriptionKey, newSubscriptionInfo in pairs(listToUpdate) do
			local noLog = true
			local success, response = luis.ExecuteAssignSubscriptionKeyToVCC(newSubscriptionInfo, noLog) -- Agrega sin PREGUNTAR
			if not success then
				local err = luis.ConvertErrorToString(response)
				LogDebug("[PROVISIONING MANAGER.LUIS] Error Updated Subscription Key: '" .. newSubscriptionInfo.subscriptionKey .. "' to VCC: '" .. azureInfo.vcc .. "' " .. err)
				errors = errors + 1
			else
				LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Updated Subscription Key: '" .. newSubscriptionInfo.subscriptionKey .. "' to VCC: '" .. azureInfo.vcc .. "'")
				if oldSubscriptionKey ~= newSubscriptionInfo.subscriptionKey then
					local noLog = true
					ExecuteUnAssignSubscriptionKeyOfVCCNoQuestion(oldSubscriptionKey, azureInfo.vcc, noLog)
				end
			end
		end

		if currentTotal == maxSubscripionsKeys then
			-- regreso ya que no puedo agregar más subscriptions de las permitidas
			LogDebug("")
			LogDebug("[PROVISIONING MANAGER.LUIS] END Updated Subscriptions To VCC '" .. azureInfo.vcc .. "'. Total updated Subscriptions Keys: " .. tostring(totalUpdated - errors))
			return
		end

		local added = 0

		-- Agrego
		while currentTotal < maxSubscripionsKeys do
			local isRequired = false
			local success, response = AssignSubscriptionKeyToVCC(isRequired, added)
			if not success then
				-- local err = luis.ConvertErrorToString(response)
				-- LogDebug("[PROVISIONING MANAGER.LUIS] Error Assigning Subscription Key to VCC: '" .. azureInfo.vcc .. "' " .. err)
			else
				added = added + 1
			end
			currentTotal = currentTotal + 1
		end

		totalUpdated = totalUpdated + added
		LogDebug("")
		LogDebug("[PROVISIONING MANAGER.LUIS] END Updated Subscriptions To VCC '" .. azureInfo.vcc .. "'. Total updated Subscriptions Keys: " .. tostring(totalUpdated))
	end

	if action == "ADD" then
	-- ADD
		AddSubscriptionsKeys()
	else
	-- UPDATED
		local encodedSubscriptionsKeysWithoutDemo, total = luis.LoadVCCSubscriptionsWithoutDemo(azureInfo.vcc)
		if total == 0 then
			AddSubscriptionsKeys()
		else
			local subscriptionsKeysWithoutDemo = json.decode(encodedSubscriptionsKeysWithoutDemo)
			UpdateSubscriptionsKeys(subscriptionsKeysWithoutDemo, total)
			if mustAsk == false then
				-- Cuando la función se ha invocado desde la WEB
				AddSubscriptionsKeys()
			end
		end
	end
	-- ok
	return true
end

--[[
]]--
luis.StartProcessUnAssignSubscriptionOfVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("[PROVISIONING MANAGER.LUIS] Start Unassigning Subscription Of VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))

--[[
Si elimina las suscripciones VCC, se asigna con la Demostración de la clave de suscripción.
A continuación, debe publicar las aplicaciones de nuevo.
]]--
LogDebug([[

	**************************************************************************************************************
	ATTENTION:
	----------
	If by remove the VCC Subscriptions, it is assigned with the Demonstration of the Subscription Key.
	Then, you must publish the Applications again.
	
	**************************************************************************************************************
	
			]])

	local info

	if mustAsk == false then
		-- Cuando la función se ha invocado desde la WEB
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc
		}
	else
		-- Cuando la función se ha invocado desde el command line
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	local vccAzureAccounts, totalVCCAzureAccounts = luis.LoadVCCAzureAccounts(info.vcc)
	if totalVCCAzureAccounts == 0 then
	-- Si no tenemos Azure Accounts asociadas al VCC
		LogDebug("VCC " .. info.vcc .. " has not configured any Azure Accounts.")
		return
	end

	info.AzureSubscriptionId = next(vccAzureAccounts)
	LogDebug("[PROVISIONING MANAGER.LUIS] Have Assigned Azure Subscription Subscription Id '" .. info.AzureSubscriptionId .. "' Of VCC " .. info.vcc)

	-- Funcion que ejecuta el proceso de Remover una Azure Account del VCC
	local function UnAssignAzureAccount ()
		LogDebug("")
		LogDebug("[PROVISIONING MANAGER.LUIS] Unassigning Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' of VCC: '" .. info.vcc)
		local success, response = luis.UnAssignAzureAccountOfVCC(info)
		if not success then
			local err = luis.ConvertErrorToString(response)
			LogDebug("[PROVISIONING MANAGER.LUIS] Error Unassigning Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' of VCC: '" .. info.vcc .. "' Err: " .. err)
			return false
		end
		LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Unassigned Azure Subscription Id: '" .. info.AzureSubscriptionId .. "' of VCC: '" .. info.vcc .. "'")
		return true
	end

	-- Funcion que ejecuta el proceso de Remover todas las Subscriptions Keys asociadas al VCC (Menos la Subscription Key DEMO)
	local subscriptionsKeys, totalSubscriptionsKeys

	local function UnAssignSubscriptionsKeys ()
		local hasConfiguredDemo, demoSubscription = luis.HasConfiguredDemoSubscriptionsInVcc(info.vcc)
		LogDebug("")
		local totalSubscritionsWithoutDemo = hasConfiguredDemo and (totalSubscriptionsKeys - 1) or totalSubscriptionsKeys
		LogDebug("[PROVISIONING MANAGER.LUIS] Validating Unassign  " .. tostring(totalSubscritionsWithoutDemo) ..  " Subscriptions Keys on VCC: '" .. info.vcc)
		if totalSubscritionsWithoutDemo > 0 then
			if totalSubscriptionsKeys == 1 and hasConfiguredDemo then
				LogDebug("[PROVISIONING MANAGER.LUIS] Unable to Unassign Demo Subscriptions Keys in VCC: " .. info.vcc)
				return true
			end

			local subscriptionsToUnassign = {}
			local totalAssignedApps = 0
			for subscriptionKey in pairs(subscriptionsKeys or {}) do
				if not demoSubscription or demoSubscription.subscriptionKey ~= subscriptionKey then
					-- Aca Removes la Subscription Key del VCC, y al invocar la funcion "...NoQuestion", NO nos realizara ninguna consulta si es que
					--   se encuentran Apps que se hayan publicado con esta key.
					--   Es decir, eliminara la key de las Apps.
					table.insert(subscriptionsToUnassign, subscriptionKey)
					local subscriptionKeyInfo = {
						subscriptionKey = subscriptionKey,
						vcc             = info.vcc
					}
					local isAssigned, apps = luis.IsAssignedSubscriptionKeyInSomeApp(subscriptionKeyInfo)
					if isAssigned then
						totalAssignedApps = totalAssignedApps + #apps
					end
				end
			end

			if totalAssignedApps > 0 then
				if mustAsk ~= false then
				-- Cuando la función se ha invocado desde el command line
					local canContinue = requestAlpha("The Subscription is assigned in " .. tostring(totalAssignedApps) .. " applications. Do you want unassign Subscription and Update Publish Info in this Applications ?",
						{default = "N"}
					)
					--LogDebug("canContinue: " ..tostring(canContinue))
					if canContinue:upper() == "N" then
						LogDebug("Cancelled")
						return false
					end
				end
			end

			for _, subscriptionKey in ipairs(subscriptionsToUnassign) do
				ExecuteUnAssignSubscriptionKeyOfVCCNoQuestion(subscriptionKey, info.vcc)
			end
			return true
		end
	end

-- Cuando tenemos UNA Azure Account asociada al VCC
	local encodedSubscriptionsKeys, total = luis.LoadVCCSubscriptions(info.vcc)
	subscriptionsKeys = json.decode(encodedSubscriptionsKeys) -- Esta tabla se utiliza en la funcion "UnAssignSubscriptionsKeys"
	totalSubscriptionsKeys = total

	local mustUnassignAzureAccount = false
	if totalSubscriptionsKeys > 0 then
		-- Primero si al menos tenemos una Azure Subscriptions Keys, removemos las mismas para después proceder a remover la Azure Account
		mustUnassignAzureAccount = UnAssignSubscriptionsKeys()
	else
		mustUnassignAzureAccount = true
	end
	-- Ahora Remuevo las Azure Accounts
	if mustUnassignAzureAccount then
		UnAssignAzureAccount()
	end

	LogDebug("")
	LogDebug("[PROVISIONING MANAGER.LUIS] END Unassigned Subscriptions To VCC '" .. info.vcc)
end

--[[
]]--
luis.LoadAzureAccounts = function (tenantInfo)
	--LogDebug("[PROVISIONING MANAGER] LoadAzureAccounts")
	return api_luis:LoadAzureAccounts(tenantInfo)
end

--[[
]]--
luis.LoadVCCTenantAzureAccount = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] LoadVCCTenantAzureAccount")
	return api_luis:LoadVCCTenantAzureAccount(vcc)
end

--[[
]]--
luis.LoadVCCAzureLanguageStudioAccount = function (vcc)
	assert(type(vcc) == "string")
	local vcc_id = tostring(vcc);
	return api_luis:LoadVCCAzureLanguageStudioAccount(vcc_id)
end

--[[
]]--
luis.LoadDefaultTenantAzureAccount = function ()
	--LogDebug("[PROVISIONING MANAGER] LoadDefaultTenantAzureAccount")
	return api_luis:LoadDefaultTenantAzureAccount()
end

--[[
]]--
luis.RestoreDefaultTenantAzureAccountInVCC = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] RestoreDefaultTenantAzureAccountInVCC")
	return api_luis:RestoreDefaultTenantAzureAccountInVCC(vcc)
end

--[[
]]--
luis.RestoreDefaultLanguageStudioAccountInVCC = function (vcc) 
	--LogDebug("[PROVISIONING MANAGER] RestoreDefaultLanguageStudioAccountInVCC")
	return api_luis:RestoreDefaultLanguageStudioAccountInVCC(vcc)
end

--[[
	Asignar una Tenant Azure Account a un VCC
]]--
luis.AssignTenantAzureAccountToVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Assign Tenant Azure Account To VCC: " .. json.encode(info))
	local success, response = api_luis.AssignTenantAzureAccountToVCC(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Asignar una Azure Language Studio Account a un VCC
]]--
luis.AssignAzureLanguageStudioAccountToVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Assign Azure Language Studio Account To VCC: " .. json.encode(info))
	local success, response = api_luis.AssignAzureLanguageStudioAccountToVCC(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Quitar una Tenant Azure Account de un VCC
]]--
luis.UnAssignTenantAzureAccountOfVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Unassign Tenant Azure Account Of VCC: " .. json.encode(info))
	local success, response = api_luis.UnAssignTenantAzureAccountOfVCC(info.vcc)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Quitar una Azure Language Studio Account de un VCC
]]--
luis.UnAssignAzureLanguageStudioAccountOfVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER] Unassign Azure Language Studio Account Of VCC: " .. json.encode(info))
	local success, response = api_luis.UnAssignAzureLanguageStudioAccountOfVCC(info.vcc)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	START Quitar un Tenant Account del VCC
]]--
luis.StartUnAssignTenantAzureAccountOfVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("[PROVISIONING MANAGER.LUIS] Start Unassigning Tenant AZURE Account Of VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))

	local info

	if mustAsk == false then
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc
		}
	else
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	local noLog = true
	local success, response = luis.UnAssignTenantAzureAccountOfVCC(info, noLog)
	if not success then
		local err = luis.ConvertErrorToString(response)
		LogDebug("[PROVISIONING MANAGER.LUIS] Error Unassigning Tenant AZURE Account of VCC: '" .. info.vcc .. "' " .. err)
		return false
	end

	LogDebug("[PROVISIONING MANAGER.LUIS] Successfully Unassigning Tenant AZURE Account of VCC: '" .. info.vcc .. "'")
	return true
end

--[[
]]--
luis.LoadVCCAzureAccounts = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] LoadVCCAzureAccounts")
	return api_luis.LoadVCCAzureAccounts(vcc)
end

--[[
]]--
-- luis.IsAssignedAzureAccountInSomeApp = function (info)
-- 	--LogDebug("[PROVISIONING MANAGER] IsAssignedAzureAccountInSomeApp: " .. json.encode(info))
-- 	return api_luis.IsAssignedAzureAccountInSomeApp(info.vcc, info.AzureSubscriptionId)
-- end

--[[
]]--
luis.LoadApps = function (vcc)
	--LogDebug("[PROVISIONING MANAGER] LoadApps")
	return api_luis:LoadApps(vcc)
end

-- QNA MAKER -----------------------
--
-- TODO: Hay que realizar una Rocka para poder utilizar esto tanto desde el webhandler (administration), como desde provisioningmanager.
--       Por ahora lo dejo, pero deberia cambiarlo lo antes posible.

qnaMaker = {}

local apiQnaMaker = require "qna_maker" -- rocka

--[[
	Load Subscriptions key desde Qna Maker
]]--
qnaMaker.EnableLogger = function (enabled)
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] EnableLogger", tostring(enabled))
	apiQnaMaker.EnableLogger(enabled)
end

--[[
	Load VCC Subscriptions key desde Qna Maker
]]--
qnaMaker.LoadVCCSubscriptions = function (vcc)
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] Load LoadVCCSubscriptions Keys. VCC " .. tostring(vcc))
	local list, total = apiQnaMaker.LoadVCCSubscriptions(vcc)
	local encodedList = json.encode(list or {})
	return encodedList, total, list
end

qnaMaker.LoadVCCSubscriptionsV2 = function (vcc) -- fede
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] Load LoadVCCSubscriptionsV2 Keys. VCC " .. tostring(vcc))
	local list, total = apiQnaMaker.LoadVCCSubscriptionsV2(vcc)
	local encodedList = json.encode(list or {})
	return encodedList, total, list
end

--[[
	Load VCCs By Subscription key desde Qna Maker
]]--
qnaMaker.LoadVCCsBySubscription = function (key)
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] Load LoadVCCsBySubscription Keys. key " .. tostring(key))
	local list, total = apiQnaMaker.LoadVCCsBySubscription(key)
	local encodedList = json.encode(list or {})
	return encodedList, total
end

qnaMaker.LoadVCCsBySubscriptionV2 = function (key) -- fede
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] Load LoadVCCsBySubscriptionV2 Keys. key " .. tostring(key))
	local list, total = apiQnaMaker.LoadVCCsBySubscriptionV2(key)
	local encodedList = json.encode(list or {})
	return encodedList, total
end

--[[
	Asignar una Subscriptions key a un VCC
]]--
qnaMaker.AssignSubscriptionKeyToVCC = function (info)
	LogDebug("[PROVISIONING MANAGER.qnaMaker] Assign Subscription Key To VCC: " .. json.encode(info or {}))
	local success, response = apiQnaMaker.AssignSubscriptionKeyToVCC(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

qnaMaker.AssignSubscriptionKeyToVCCV2 = function (info) -- fede
	LogDebug("[PROVISIONING MANAGER.qnaMaker] Assign Subscription Key To VCC: " .. json.encode(info or {}))
	local success, response = apiQnaMaker.AssignSubscriptionKeyToVCCV2(info)
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Quitar una Subscriptions key de un VCC
]]--
qnaMaker.UnAssignSubscriptionKeyOfVCC = function (info)
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] Unassign Subscription Key Of VCC: " .. json.encode(info))
	local success, response = apiQnaMaker.UnAssignSubscriptionKeyOfVCC(info.vcc, info.subscriptionKey)
	--LogDebug("[PROVISIONING MANAGER.qnaMaker:UnAssignSubscriptionKeyOfVCC] success " .. tostring(success) .. ", response: " .. json.encode(response))
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

qnaMaker.UnAssignSubscriptionKeyOfVCCV2 = function (info) -- fede
	--LogDebug("[PROVISIONING MANAGER.qnaMaker] Unassign Subscription Key Of VCC: " .. json.encode(info))
	local success, response = apiQnaMaker.UnAssignSubscriptionKeyOfVCCV2(info.vcc, info.subscriptionKey)
	--LogDebug("[PROVISIONING MANAGER.qnaMaker:UnAssignSubscriptionKeyOfVCCV2] success " .. tostring(success) .. ", response: " .. json.encode(response))
	if not success then
		local err = response
		return false, err
	end
	return true, response
end

--[[
	Sample info:
		subscriptionKey: (requerido) las de Tito:  d24518b086a248c4ae64afd6e3eb475c   O   f5c41ac5c06b46d6a697c076f4f3923c
		vcc            : (requerido) inconcert
]]--
qnaMaker.ExecuteAssignSubscriptionKeyToVCC = function (data, noLog)
	if not noLog then
		LogDebug("(QNA MAKER) Assigning Subscription Key To VCC")
	end

	local info = data or {}

	if not info.vcc then
		info.vcc = requestAlpha("Enter the VCC")
	end

	if not info.subscriptionKey then
		local aditionalInfo = {allowEmpty = info.allowEmpty}
		info.subscriptionKey = requestAlpha("Enter the Subscription Key Value (GUID 32 characters)", aditionalInfo)
	end

	if info.subscriptionKey:len() == 0 then
		if not noLog then
			if info.allowEmpty then
				LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'.")
			else
				LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'. Subscription Key is Required.")
			end
		end
		return false, {errType = "inconcert", code = "SUBSCRIPTION KEY IS EMPTY"}
	end

	if not info.subscriptionName then
		info.subscriptionName = requestAlpha("Enter the Subscription Name")
	end

	if info.subscriptionName:len() == 0 then
		if not noLog then
			LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'. Subscription Name is Required.")
		end
		return false, {errType = "inconcert", code = "SUBSCRIPTION NAME IS EMPTY"}
	end

	local success, response = qnaMaker.AssignSubscriptionKeyToVCC(info)
	if not success then
		if not noLog then
			local err = qnaMaker.ConvertErrorToString(response)
			LogDebug("(QNA MAKER) Error Assigning Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "' " .. err)
		end
		return false, response
	end
	if not noLog then
		LogDebug("(QNA MAKER) Successfully Assigned Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "'")
	end

	return true
end

qnaMaker.ExecuteAssignSubscriptionKeyToVCCV2 = function (data, noLog) -- fede
	if not noLog then
		LogDebug("(QNA MAKER) Assigning Subscription Key To VCC")
	end

	local info = data or {}

	if not info.vcc then
		info.vcc = requestAlpha("Enter the VCC")
	end

	if not info.subscriptionKey then
		local aditionalInfo = {allowEmpty = info.allowEmpty}
		info.subscriptionKey = requestAlpha("Enter the Subscription Key Value (GUID 32 characters)", aditionalInfo)
	end

	if info.subscriptionKey:len() == 0 then
		if not noLog then
			if info.allowEmpty then
				LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'.")
			else
				LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'. Subscription Key is Required.")
			end
		end
		return false, {errType = "inconcert", code = "SUBSCRIPTION KEY IS EMPTY"}
	end

	if not info.subscriptionName then
		info.subscriptionName = requestAlpha("Enter the Subscription Name")
	end

	if info.subscriptionName:len() == 0 then
		if not noLog then
			LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'. Subscription Name is Required.")
		end
		return false, {errType = "inconcert", code = "SUBSCRIPTION NAME IS EMPTY"}
	end

	local success, response = qnaMaker.AssignSubscriptionKeyToVCCV2(info)
	if not success then
		if not noLog then
			local err = qnaMaker.ConvertErrorToString(response)
			LogDebug("(QNA MAKER) Error Assigning Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "' " .. err)
		end
		return false, response
	end
	if not noLog then
		LogDebug("(QNA MAKER) Successfully Assigned Subscription Key: '" .. info.subscriptionKey .. "' to VCC: '" .. info.vcc .. "'")
	end

	return true
end

--[[

]]--
qnaMaker.StartProcessAssignSubscriptionToVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("(QNA MAKER) START Assigning Subscriptions To VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))

	local info
	local subscriptions = vccInfo and vccInfo.subscriptions or {}

	if mustAsk == false then
		-- Cuando la función se ha invocado desde la WEB
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc
		}
	else
		-- Cuando la función se ha invocado desde el command line
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	-- 1 - Ahora agrego o actualizo las Subscriptions KEYS (máximo 1)
	local maxSubscripionsKeys = 1

	local _, totalCurrentAssigned = qnaMaker.LoadVCCSubscriptions(info.vcc)

	LogDebug("(QNA MAKER) VCC '" .. info.vcc .. "', Total Current Assigned: '" .. totalCurrentAssigned)
	if totalCurrentAssigned >= maxSubscripionsKeys then
		LogDebug("(QNA MAKER) VCC '" .. info.vcc .. "', has reached the maximum number of Subscriptions allowed. MAX: '" .. tostring(maxSubscripionsKeys) .. "', Total in VCC: " .. tostring(totalCurrentAssigned))
		return false, "VCC HAS REACHED THE MAXIMUM SUBSCRITIONS ALLOWED"
	end

	local function AssignSubscriptionKeyToVCC (isRequired, added)
		local requiredMsg, allowEmpty
		if isRequired then
			requiredMsg = "(REQUIRED)"
			allowEmpty = false
		else
			requiredMsg = "(OPTIONAL)"
			allowEmpty = true
		end

		local itemNumber = added + 1

		-- Aca están subscriptions que llegan desde la Web - en el caso del command line esto es nil
		local subscription = subscriptions[itemNumber]

		if mustAsk == false then
			-- Cuando la función se ha invocado desde la WEB
			if not subscription then
				return false
			end
		end

		LogDebug("")
		LogDebug("(QNA MAKER) " .. tostring(itemNumber) .. " Assigning " .. requiredMsg .. " Subscription Key To VCC. Subcription: " .. tostring(subscription and json.encode(subscription)))

		if subscription then
			info.subscriptionKey = subscription.subscriptionKey
			info.subscriptionName = subscription.subscriptionName
			info.subscriptionRegion = subscription.subscriptionRegion
		end

		info.allowEmpty = allowEmpty

		local noLog = true
		local success, response = qnaMaker.ExecuteAssignSubscriptionKeyToVCC(info, noLog)
		if not success then
			if response and response.errType == "inconcert" and response.code == "SUBSCRIPTION KEY IS EMPTY" then
				LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'.")
			else
				local err = qnaMaker.ConvertErrorToString(response)
				LogDebug("(QNA MAKER) Error Updated Subscription Key to VCC: '" .. info.vcc .. "'. Err: " .. err)
			end
		end
		return success, response
	end

	local function AddSubscriptionsKeys ()
		local added = 0
		local errors = 0
		while added < maxSubscripionsKeys do
			local isRequired = added == 0
			local success, response = AssignSubscriptionKeyToVCC(isRequired, added)
			if not success then
				errors = errors + 1
			end
			added = added + 1
		end
		LogDebug("")
		LogDebug("(QNA MAKER) END Assigned Subscriptions To VCC '" .. info.vcc .. "'. Total added Subscriptions Keys: " .. tostring(added - errors))
	end

	-- ADD
	AddSubscriptionsKeys()
	return true
end

qnaMaker.StartProcessAssignSubscriptionToVCCV2 = function (vccInfo, mustAsk) -- fede
	mustAsk = mustAsk ~= false
	LogDebug("(QNA MAKER) START Assigning Subscriptions To VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. tostring(vccInfo))

	local info
	local subscriptions = vccInfo and vccInfo.subscriptionsV2 or {}

	if mustAsk == false then
		-- Cuando la función se ha invocado desde la WEB
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc
		}
	else
		-- Cuando la función se ha invocado desde el command line
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	-- 1 - Ahora agrego o actualizo las Subscriptions KEYS (máximo 1)
	local maxSubscripionsKeys = 1

	local _, totalCurrentAssigned = qnaMaker.LoadVCCSubscriptionsV2(info.vcc)

	LogDebug("(QNA MAKER) VCC '" .. info.vcc .. "', Total Current Assigned: '" .. totalCurrentAssigned)
	if totalCurrentAssigned >= maxSubscripionsKeys then
		LogDebug("(QNA MAKER) VCC '" .. info.vcc .. "', has reached the maximum number of Subscriptions allowed. MAX: '" .. tostring(maxSubscripionsKeys) .. "', Total in VCC: " .. tostring(totalCurrentAssigned))
		return false, "VCC HAS REACHED THE MAXIMUM SUBSCRITIONS ALLOWED"
	end

	local function AssignSubscriptionKeyToVCCV2 (isRequired, added)
		local requiredMsg, allowEmpty
		if isRequired then
			requiredMsg = "(REQUIRED)"
			allowEmpty = false
		else
			requiredMsg = "(OPTIONAL)"
			allowEmpty = true
		end

		local itemNumber = added + 1

		-- Aca están subscriptions que llegan desde la Web - en el caso del command line esto es nil
		local subscription = subscriptions[itemNumber]

		if mustAsk == false then
			-- Cuando la función se ha invocado desde la WEB
			if not subscription then
				return false
			end
		end

		LogDebug("")
		LogDebug("(QNA MAKER) " .. tostring(itemNumber) .. " Assigning " .. requiredMsg .. " Subscription Key To VCC. Subcription: " .. tostring(subscription and json.encode(subscription)))

		if subscription then
			info.subscriptionKey = subscription.subscriptionKey
			info.subscriptionName = subscription.subscriptionName
			info.subscriptionRegion = subscription.subscriptionRegion
		end

		info.allowEmpty = allowEmpty

		local noLog = true
		local success, response = qnaMaker.ExecuteAssignSubscriptionKeyToVCCV2(info, noLog)
		if not success then
			if response and response.errType == "inconcert" and response.code == "SUBSCRIPTION KEY IS EMPTY" then
				LogDebug("(QNA MAKER) Cancel Assigning Subscription Key to VCC: '" .. info.vcc .. "'.")
			else
				local err = qnaMaker.ConvertErrorToString(response)
				LogDebug("(QNA MAKER) Error Updated Subscription Key to VCC: '" .. info.vcc .. "'. Err: " .. err)
			end
		end
		return success, response
	end

	local function AddSubscriptionsKeysV2 ()
		local added = 0
		local errors = 0
		while added < maxSubscripionsKeys do
			local isRequired = added == 0
			local success, response = AssignSubscriptionKeyToVCCV2(isRequired, added)
			if not success then
				errors = errors + 1
			end
			added = added + 1
		end
		LogDebug("")
		LogDebug("(QNA MAKER) END Assigned Subscriptions To VCC '" .. info.vcc .. "'. Total added Subscriptions Keys: " .. tostring(added - errors))
	end

	-- ADD
	AddSubscriptionsKeysV2()
	return true
end

--[[

]]--
qnaMaker.StartProcessUnAssignSubscriptionOfVCC = function (vccInfo, mustAsk)
	mustAsk = mustAsk ~= false
	LogDebug("(QNA MAKER) START Unassigning Subscription Of VCC - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. json.encode(vccInfo))

	local info

	if mustAsk == false then
		-- Cuando la función se ha invocado desde la WEB
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc
		}
	else
		-- Cuando la función se ha invocado desde el command line
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	local encodedSubscriptionsKeys, totalSubscriptionsKeys = qnaMaker.LoadVCCSubscriptions(info.vcc)
	if totalSubscriptionsKeys == 0 then
		LogDebug("(QNA MAKER) VCC " .. info.vcc .. " has not configured Subscriptions.")
		return false, "VCC HAS NOT CONFIGURED SUBSCRIPTIONS"
	end

	LogDebug("(QNA MAKER) Have Assigned " .. tostring(totalSubscriptionsKeys) .. " in VCC " .. tostring(info.vcc) .. ". Subscriptions '" .. encodedSubscriptionsKeys)

	-- Funcion que ejecuta el proceso de Remover todas las Subscriptions Keys asociadas al VCC (Menos la Subscription Key DEMO)
	local subscriptionsKeys = json.decode(encodedSubscriptionsKeys) -- Esta tabla se utiliza en la funcion "UnAssignSubscriptionsKeys"
	totalSubscriptionsKeys = total

	local function UnAssignSubscriptionsKeys ()
		for subscriptionKey, subscriptionInfo in pairs(subscriptionsKeys) do
			info.subscriptionKey = subscriptionKey
			local success, response = qnaMaker.UnAssignSubscriptionKeyOfVCC(info)
			LogDebug("(QNA MAKER) qnaMaker.UnAssignSubscriptionKeyOfVCC Unassigning subscriptionKey " .. subscriptionKey .. ", success: ".. tostring(success) .. ", response: " .. json.encode(response))
		end
	end

	UnAssignSubscriptionsKeys()

	LogDebug("")
	LogDebug("(QNA MAKER) END Unassigned Subscriptions To VCC '" .. info.vcc)

	return true
end

qnaMaker.StartProcessUnAssignSubscriptionOfVCCV2 = function (vccInfo, mustAsk) -- fede
	mustAsk = mustAsk ~= false
	LogDebug("(QNA MAKER) START Unassigning Subscription Of VCC V2 - mustAsk: " .. tostring(mustAsk) .. ", vccInfo: " .. json.encode(vccInfo))

	local info

	if mustAsk == false then
		-- Cuando la función se ha invocado desde la WEB
		assert(vccInfo.vcc)
		info = {
			vcc = vccInfo.vcc
		}
	else
		-- Cuando la función se ha invocado desde el command line
		info = {
			vcc = requestAlpha("Enter the VCC")
		}
	end

	local encodedSubscriptionsKeys, totalSubscriptionsKeys = qnaMaker.LoadVCCSubscriptionsV2(info.vcc)
	if totalSubscriptionsKeys == 0 then
		LogDebug("(QNA MAKER) VCC " .. info.vcc .. " has not configured Subscriptions.")
		return false, "VCC HAS NOT CONFIGURED SUBSCRIPTIONS"
	end

	LogDebug("(QNA MAKER) Have Assigned " .. tostring(totalSubscriptionsKeys) .. " in VCC " .. tostring(info.vcc) .. ". Subscriptions '" .. encodedSubscriptionsKeys)

	-- Funcion que ejecuta el proceso de Remover todas las Subscriptions Keys asociadas al VCC (Menos la Subscription Key DEMO)
	local subscriptionsKeys = json.decode(encodedSubscriptionsKeys) -- Esta tabla se utiliza en la funcion "UnAssignSubscriptionsKeys"
	totalSubscriptionsKeys = total

	local function UnAssignSubscriptionsKeysV2 ()
		for subscriptionKey, subscriptionInfo in pairs(subscriptionsKeys) do
			info.subscriptionKey = subscriptionKey
			local success, response = qnaMaker.UnAssignSubscriptionKeyOfVCCV2(info)
			LogDebug("(QNA MAKER) qnaMaker.UnAssignSubscriptionKeyOfVCCV2 Unassigning subscriptionKey " .. subscriptionKey .. ", success: ".. tostring(success) .. ", response: " .. json.encode(response))
		end
	end

	UnAssignSubscriptionsKeysV2()

	LogDebug("")
	LogDebug("(QNA MAKER) END Unassigned Subscriptions To VCC '" .. info.vcc)

	return true
end

-----------------------------------------------------
-- LICENSES
-----------------------------------------------------

--[[
	Online Users, Metrics, Filters, Quotas, HelpDesk
]]--
function LoadDefaultLicensesValues ()
	local licenses = {
		ONLINE_USERS             = 1,
		CUSTOM_METRICS_LIMIT     = 0,
		CUSTOM_FILTERS_LIMIT     = 0,
		EXPORT_SPACE_QUOTA       = 0,
		SHARED_FILES_SPACE_QUOTA = 0,
		HELPDESK_USERS_LICENSES  = GetHelpDeskDefaultUsersLicense()
	}
	return licenses
end
-----------------

-----------------------------------------------------
-- SCREEN RECORDING
-----------------------------------------------------

--[[
	Port, fps, bitrate
]]--
function LoadDefaultScreenRecordingValues ()
	local licenses = {
		PORT        = 8998,
		FPS         = 5,
		BITRATE     = 96000
	}
	return licenses
end
-----------------

-----------------------------------------------------
-- STORAGE LIFECYCLE
-----------------------------------------------------

--[[
	S3Files, S3MetaData, RepoCall, RepoMail, RepOther
]]--
function LoadDefaultStorageLifecycleValues ()
	local lifecycle = {
		S3FILES     = 90,
		S3METADATA  = 90,
		REPOCALL    = 90,
		REPOMAIL    = 90,
		REPOTHER    = 90
	}
	return lifecycle
end
-----------------

-----------------------------------------------------
-- VCCTtsAsrEngines
-----------------------------------------------------

-- ListAllVCCTtsAsrEngines
---------------------------
-- Returns all existing VCCTtsAsrEngines
function ListAllVCCTtsAsrEngines (ksAgent)
	local ttsasrEngines = select(ksAgent, "VCCTtsAsrEngines") or {}
	return ttsasrEngines
end

-- GetVCCTtsAsrEngines
---------------------------
-- Returns data about a specific VCC, empty if it does not exist
function GetVCCTtsAsrEngines (ksAgent, VCCName)
	VCCName = utf8.lower(VCCName)
	local ttsasrEngines = select(ksAgent, "VCCTtsAsrEngines", VCCName)[VCCName] or {}
	return ttsasrEngines
end

-- UpdateVCCTtsAsrEngines
---------------------------
-- Adds or modifies a VCC TtsAsrEngines configuration in provisioning
function UpdateVCCTtsAsrEngines (ksAgent, VCCName, VCCData)
	VCCName = utf8.lower(VCCName)
	VCCData.Id = VCCName

	--LogDebug("[PROVISIONING_MANAGER:UpdateVCCTtsAsrEngines] CF VCCTtsAsrEngines:" .. VCCName .. " data: " .. json.encode(VCCData))

	local oldData = GetVCCTtsAsrEngines(ksAgent, VCCName)
	for k, _ in pairs(oldData) do
		if not VCCData[k] then
			ksAgent:RemoveColumn(VCCName, "VCCTtsAsrEngines", k)
		end
	end
	for name, v in pairs(VCCData) do
		local value = type(v) == "table" and json.encode(v) or v
		--LogDebug("[PROVISIONING_MANAGER:UpdateVCCTtsAsrEngines] CF VCCTtsAsrEngines - key:" .. VCCName .. " name:" .. name .. " value:" .. tostring(value))
		ksAgent:InsertColumn(VCCName, "VCCTtsAsrEngines", name, value)
	end
end


-----------------------------------------------------
-- LDAP
-----------------------------------------------------

function GetLdapGroups(server, baseDN, adminUserRDN, adminPassword, useTLS)
	if not authFilter then
		authFilter = (require "authfilter")("agent")
	end
	return authFilter:GetLdapGroups(server, baseDN, adminUserRDN, adminPassword, useTLS)
end

-----------------------------------------------------
-- Azure AD
-----------------------------------------------------

function GetAzureADGroups(openid_configuration_url, client_id, client_secret)
	if not authFilter then
		authFilter = (require "authfilter")("agent")
	end
	return authFilter:GetAzureADGroups(openid_configuration_url, client_id, client_secret)
end

function GetOpenIdConfiguration(openid_configuration_url)
	if not authFilter then
		authFilter = (require "authfilter")("agent")
	end
	return authFilter:GetOpenIdConfiguration(openid_configuration_url)
end

-----------------------------------------------------
-- Whatsapp inConcert Cloud API Activation Codes
-----------------------------------------------------

function GetWhatsappInconcertCloudApiCode(ksProv, activationCode)
	local success = true
	local err = ""
	local result
	if m_helpers.utils.HasLicenseSystem() then
		result = m_helpers.agentservice.ValidateWhatsappActivationCode(m_helpers_agentservice_options, activationCode)
	else
		result = { code_is_valid = false }
		local codes = select(ksProv, "SystemConfiguration", "WhatsappInconcertCloudApiCodes")["WhatsappInconcertCloudApiCodes"] or {}
		for existingCode, assignedAccount in pairs(codes) do
			if existingCode == activationCode then
				result.code_is_valid = true
				result.accountId = assignedAccount
				break
			end
		end		
	end
	return result and result.code_is_valid or false, result and result.accountId or nil
end

function SetWhatsappInconcertCloudApiCode(ksProv, activationCode, vcc, accountId, user)
	local success = true
	local err = ""

	local result
	if m_helpers.utils.HasLicenseSystem() then
		result = m_helpers.agentservice.EnableWhatsappActivationCode(m_helpers_agentservice_options, activationCode, vcc, accountId, user)
	else
		result = { success = false }
		local isValid, assignedAccount = GetWhatsappInconcertCloudApiCode(ksProv, activationCode)
		if not isValid then
			LogWarning("[PROVISIONING_MANAGER:SetWhatsappInconcertCloudApiCode] ActivationCode '%s' does not exists", activationCode)
		else
			if assignedAccount and assignedAccount ~= "" and assignedAccount ~= accountId then
				LogWarning("[PROVISIONING_MANAGER:SetWhatsappInconcertCloudApiCode] ActivationCode '%s' is already in use by account '%s'", activationCode, assignedAccount)
			else
				ksProv:InsertColumn("WhatsappInconcertCloudApiCodes", 'SystemConfiguration', activationCode, accountId)
				result.success = true
			end			
		end		
	end
	return result and result.success or false 
end


------------------------------------------------------
-- GATEWAYS (asterisk integration)
------------------------------------------------------
local function GetGatewayCredentials(configuration)
	return {
		Domain = configuration.Domain,
		AmiPort = configuration.AmiPort,
		AmiUser = configuration.AmiUser,
		AmiPassword = configuration.AmiPassword,
	}
end

local function GetGatewayAttributesUpdates(existingAttributes, newAttributes)
	if type(existingAttributes) == "string" then existingAttributes = json.decode(existingAttributes) end
	local attributes = {}
	local attributesMap = {}

	for _, field in pairs(newAttributes) do
		table.insert(attributes, field)
		attributesMap[field.name] = true
	end
	for _, field in pairs(existingAttributes) do
		if not attributesMap[field.name] then
			table.insert(attributes, { name = field.name, deleted = true })
		end
	end
	return attributes
end

local function GetGatewaySectionsUpdates(existingSections, newSections)
	local sections = {}
	local removedSections = {}

	for sectionId, existingSection in pairs(existingSections) do
		if newSections[sectionId] then
			sections[sectionId] = GetGatewayAttributesUpdates(
				existingSection.Attributes or {}, newSections[sectionId] and newSections[sectionId].Attributes or {})
		else
			table.insert(removedSections, sectionId)
		end
	end
	for sectionId, newSection in pairs(newSections) do
		if not existingSections[sectionId] then
			sections[sectionId] = GetGatewayAttributesUpdates({}, newSection.Attributes)
		end
	end
	return sections, removedSections
end

local function UpdateAsteriskGatewayGlobalConfiguration(ksProv, gatewayId, configuration)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true } end

	local credentials = GetGatewayCredentials(configuration)
	local existingSections = GetGatewaySections(ksProv, gatewayId)
	local sections, removedSections = GetGatewaySectionsUpdates(existingSections, configuration.Sections)

	local client = GetAsteriskClient()
	local result = client:AmiUpdateGlobalConfiguration(json.encode({
		credentials = credentials,
		sections = sections,
		removedSections = removedSections,
		pjsip = configuration.Pjsip == "1"
	}))
	if result.result ~= "OK" then
		LogError(
			"[PROVISIONING_MANAGER:UpdateAsteriskGatewayGlobalConfiguration] error updating configuration: %s",
			result.reason)
		return { success = false, errType = result.result, errMsg = result.reason }
	end

	local asteriskSections = result.sections
	for sectionId, attributes in pairs(asteriskSections) do
		if configuration.Sections[sectionId] then
			configuration.Sections[sectionId].Attributes = attributes
		end
	end
	return { success = true }
end

local function UpdateAsteriskGatewayTrunk(configuration, trunkId, existingTrunk, newTrunk)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	if type(existingTrunk) == "string" then existingTrunk = json.decode(existingTrunk) end
	local credentials = GetGatewayCredentials(configuration)
	table.insert(newTrunk.Attributes, { name = "context", value = newTrunk.DialplanContext })
	local attributes = GetGatewayAttributesUpdates(existingTrunk.Attributes or {}, newTrunk.Attributes)

	local client = GetAsteriskClient()
	local result = client:AmiStoreTrunk(json.encode({
		credentials = credentials,
		trunkId = trunkId,
		attributes = attributes,
		oldRegistrationPath = existingTrunk.RegistrationPath or "",
		newRegistrationPath = newTrunk.RegistrationPath or "",
		pjsip = configuration.Pjsip == "1"
	}))
	if result.result ~= "OK" then
		LogError(
			"[PROVISIONING_MANAGER:UpdateAsteriskGatewayTrunk] error updating configuration: %s",
			result.reason)
		return { success = false, errType = result.result, errMsg = result.reason }
	end

	local asteriskAttributes = result.attributes
	newTrunk.Attributes = {}
	for _, attribute in pairs(asteriskAttributes) do
		if attribute.name ~= "context" then
			table.insert(newTrunk.Attributes, attribute)
		end
	end
	return { success = true }
end

local function DeleteAsteriskGatewayTrunk(configuration, trunkId, trunk)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	if type(trunk) == "string" then trunk = json.decode(trunk) end
	local credentials = GetGatewayCredentials(configuration)

	local client = GetAsteriskClient()
	local result = client:AmiDeleteTrunk(json.encode({
		credentials = credentials,
		trunkId = trunkId,
		registrationPath = trunk.RegistrationPath or "",
		pjsip = configuration.Pjsip == "1"
	}))
	if result.result ~= "OK" then
		LogError(
			"[PROVISIONING_MANAGER:DeleteAsteriskGatewayTrunk] error removing configuration: %s",
			result.reason)
		return { success = false, errType = result.result, errMsg = result.reason }
	end

	return { success = true }
end

local function ValidateAsteriskGatewayDialplanContext(configuration, contextId, context)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	local credentials = GetGatewayCredentials(configuration)

	local client = GetAsteriskClient()
	local result = client:AmiValidateDialplanContext(json.encode({
		credentials = credentials,
		contextId = contextId,
		code = context.Code
	}))
	if result.result ~= "OK" then
		LogError(
			"[PROVISIONING_MANAGER:ValidateAsteriskGatewayDialplanContext] error updating configuration: %s",
			result.reason)
		return { success = false, errType = result.result, errMsg = result.reason, context = result }
	end

	return { success = true }
end

local function UpdateAsteriskGatewayDialplanContext(configuration, contextId, context)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	local credentials = GetGatewayCredentials(configuration)

	local client = GetAsteriskClient()
	local result = client:AmiStoreDialplanContext(json.encode({
		credentials = credentials,
		contextId = contextId,
		code = context.Code
	}))
	if result.result ~= "OK" then
		LogError(
			"[PROVISIONING_MANAGER:UpdateAsteriskGatewayDialplanContext] error updating configuration: %s",
			result.reason)
		return { success = false, errType = result.result, errMsg = result.reason, context = result }
	end

	return { success = true }
end

local function DeleteAsteriskGatewayDialplanContext(configuration, contextId)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	local credentials = GetGatewayCredentials(configuration)

	local client = GetAsteriskClient()
	local result = client:AmiDeleteDialplanContext(json.encode({
		credentials = credentials,
		contextId = contextId
	}))
	if result.result ~= "OK" then
		LogError(
			"[PROVISIONING_MANAGER:DeleteAsteriskGatewayDialplanContext] error deleting configuration: %s",
			result.reason)
		return { success = false, errType = result.result, errMsg = result.reason }
	end

	return { success = true }
end

local function SendAsteriskGatewayConsoleCommand(configuration, command)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true, content = "dummy response" } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	local credentials = GetGatewayCredentials(configuration)

	local client = GetAsteriskClient()
	local result = client:AmiSendConsoleCommand(json.encode({
		credentials = credentials,
		command = command
	}))
	if result.result ~= "OK" then
		LogError("[PROVISIONING_MANAGER:SendAsteriskGatewayConsoleCommand] error: %s", result.reason)
		return { success = false, errType = result.result, errMsg = result.reason }
	end

	return { success = true, content = result.content }
end

local function GetAsteriskGatewayConfigurationFile(configuration, fileName)
	if not GATEWAY_ASTERISK_ENABLED then return { success = true, content = "test" } end

	if type(configuration) == "string" then configuration = json.decode(configuration) end
	local credentials = GetGatewayCredentials(configuration)

	local client = GetAsteriskClient()
	local result = client:AmiGetConfigurationFile(json.encode({
		credentials = credentials,
		fileName = fileName,
	}))
	if result.result ~= "OK" then
		LogError("[PROVISIONING_MANAGER:GetAsteriskGatewayConfigurationFile] error: %s", result.reason)
		return { success = false, errType = result.result, errMsg = result.reason }
	end

	return { success = true, content = result.content }
end

------------------------------------------------------
-- GATEWAYS
------------------------------------------------------

-- ListGateways
---------------------------
-- Returns a list of all the gateways, with their associated data
function ListGateways (ksProv)
	return cropEmpties(superSelect(ksProv, "Gateways"))
end

-- Returns a list of all the gateways, with their associated data
function GetGateway (ksProv, gatewayId)
	gatewayId = utf8.lower(gatewayId)
	return superSelect(ksProv, "Gateways", gatewayId)[gatewayId] or {}
end

-- Returns gateway configuration sections
function GetGatewaySections (ksProv, gatewayId)
	local gateway = GetGateway(ksProv, gatewayId)
	local sections = gateway.configuration and gateway.configuration.Sections or {}
	if type(sections) == "string" then sections = json.decode(sections) end
	return sections
end

-- UpdateGateway
---------------------------
-- Adds or modifies a gateway in provisioning
function UpdateGateway (ksProv, gatewayId, configuration)
	gatewayId = utf8.lower(gatewayId)

	local configurationStr = json.encode(configuration)
	configurationStr = string.gsub(configurationStr, configuration.AmiPassword, "***")
	LogDebug("[PROVISIONING_MANAGER:UpdateGateway] Updating Gateway: '%s' configuration: '%s'",
		     gatewayId, configurationStr)

	local result = UpdateAsteriskGatewayGlobalConfiguration(ksProv, gatewayId, configuration)
	if not result.success then return result end

	for column, value in pairs(configuration) do
		local strValue =  type(value) ~= "table" and tostring(value) or json.encode(value)
		ksProv:InsertSuperColumn(gatewayId, "Gateways", "configuration", column, strValue)
	end
	return { success = true, configuration = configuration }
end

-- DeleteGateway
------------------------------
-- Removes a gateway from provisioning
-- Pre: The gateway must exist
-- Pre: The gateway must not be associated with any trunk
function DeleteGateway (ksProv, gatewayId)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)

	LogDebug("[PROVISIONING_MANAGER:DeleteGateway] Deleting Gateway: '%s'", gatewayId)

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- TRUNK validation
	local trunks = ListTrunks(ksProv)
	for trunkId, trunk in pairs(trunks) do
		if trunk.gateway == gatewayId then
			result.success = false
			result.errType = "CANNOT DELETE GATEWAY ASSOCIATED TO TRUNK"
			result.errMsg = "Cannot delete gateway "..gatewayId.." while trunk "..trunkId.." is still associated with it."
			result.relatedId = trunkId
			return result
		end
	end

	--remove dialplan contexts
	for contextId, context in pairs(gateway.dialplanContexts or {}) do
		DeleteAsteriskGatewayDialplanContext(gateway.configuration, contextId)
	end

	--remove carrier trunks
	for trunkId, trunk in pairs(gateway.carrierTrunks or {}) do
		if type(trunk) == "string" then trunk = json.decode(trunk) end
		DeleteAsteriskGatewayTrunk(gateway.configuration, trunkId, trunk)
	end

	-- Ahora si lo elimino
	for k, _ in pairs(gateway) do
		ksProv:RemoveSuperColumn(gatewayId, "Gateways", k)
	end

	-- IS OK
	result.success = true
	return result
end


-- UpdateGatewayNodeTrunk
---------------------------
-- Adds or modifies a gateway node trunk from the source node

-- register => user[:secret[:authuser]]@host[:port][/extension]
local function MakeNodeTrunkRegistrationPath(trunkId, nodeData, hostInfo)
	local userId = trunkId
	if nodeData.registrable == "yes" and not isEmptyString(nodeData.registrationUser) then
		userId = nodeData.registrationUser
	end
	local path = string.format("%s:%s:%s@%s",
		userId,
		nodeData.password or "",
		userId,
		hostInfo.sipServer)
	if nodeData.registrable == "yes" and not isEmptyString(nodeData.registrationExten) then
		path = string.format("%s/%s", path, nodeData.registrationExten)
	end
	return path
end

local function AddNodeTrunkRegistrationAttributes(nodeData, hostInfo, result)
	result.removed.secret = true
	result.removed.username = true
	result.removed.host = true

	if not isEmptyString(nodeData.password) then
		table.insert(result.added, { name = "secret", value = nodeData.password })
	end
	if nodeData.registrable == "yes" and not isEmptyString(nodeData.registrationUser) then
		table.insert(result.added, { name = "username", value = nodeData.registrationUser })
	end
	table.insert(result.added,{ name = "host", value = hostInfo.sipServer })
end

local function AddNodeTrunkCodecsAttributes(nodeData, result)
	result.removed.disallow = true
	result.removed.allow = true
	table.insert(result.added, { name = "disallow", value = "all" })
	if nodeData.codecs == "" then return end

	local codecs = split(nodeData.codecs, ",")
	for _, codec in pairs(codecs) do
		table.insert(result.added, { name = "allow", value = codec })
	end
end

local function MakeNodeTrunkAttributes(nodeData, hostInfo)
	local result = { added = {}, removed = {} }
	AddNodeTrunkRegistrationAttributes(nodeData, hostInfo, result)
	AddNodeTrunkCodecsAttributes(nodeData, result)
	return result
end

local function MixGatewayAttributes(currentAttributes, mix)
	local attributes = {}
	local attributesDict = {}

	local function HasToAddAttribute(attribute)
		local removed = mix.removed[attribute.name]
		if not removed then return true end
		if type(removed) == "boolean" then return false end
		return not removed[attribute.value]
	end

	for _, attribute in pairs(currentAttributes) do
		if HasToAddAttribute(attribute) then
			table.insert(attributes, attribute)
			attributesDict[attribute.name] = attribute.value
		end
	end

	for _, attribute in pairs(mix.added) do
		if attributesDict[attribute.name] ~= attribute.value then
			table.insert(attributes, attribute)
		end
	end
	return attributes
end

function UpdateGatewayNodeTrunkFromNode(params)
	local ksProv = params.ksProv
	local gatewayId = params.gatewayId
	local dialplanContext = params.dialplanContext
	local trunkId = params.trunkId
	local nodeData = params.nodeData
	local hostInfo = params.hostInfo
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	local trunk = gateway.nodeTrunks and gateway.nodeTrunks[trunkId] or {
		Id = trunkId,
		Name = trunkId,
		Attributes = {
			{ name = "type", value = "peer" },
			{ name = "dtmfmode", value = "rfc2833" },
			{ name = "nat", value = "no" },
			{ name = "port", value = "5060" },
			{ name = "canreinvite", value = "yes" },
			{ name = "qualify", value = "yes" },
			{ name = "insecure", value = "port,invite" },
		}
	}
	if type(trunk) == "string" then trunk = json.decode(trunk) end
	trunk.DialplanContext = dialplanContext
	trunk.RegistrationPath = MakeNodeTrunkRegistrationPath(trunkId, nodeData, hostInfo)
	trunk.Attributes = MixGatewayAttributes(trunk.Attributes, MakeNodeTrunkAttributes(nodeData, hostInfo))
	return UpdateGatewayNodeTrunk (ksProv, gatewayId, trunkId, trunk)
end

-- UpdateGatewayNodeTrunk
---------------------------
-- Adds or modifies a gateway node trunk in provisioning
-- Pre: The gateway must exist
-- Pre: The gateway must not have a carrierTrunk with same id
function UpdateGatewayNodeTrunk (ksProv, gatewayId, trunkId, data)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	trunkId = utf8.lower(trunkId)
	local dataStr = json.encode(data)

	LogDebug(
		"[PROVISIONING_MANAGER:UpdateGatewayNodeTrunk] Updating Gateway: '%s'  Trunk: '%s' configuration: '%s'",
		gatewayId, trunkId, dataStr)

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- carrier trunk validation
	if gateway.carrierTrunks and gateway.carrierTrunks[trunkId] then
		result.success = false
		result.errType = "GATEWAY CARRIER TRUNK EXISTS"
		result.errMsg = "Gateway "..tostring(gatewayId).." has a carrier trunk with same name."
		return result
	end

	if data.Attributes then
		local existingTrunk = gateway.nodeTrunks and gateway.nodeTrunks[trunkId] or {}
		local result = UpdateAsteriskGatewayTrunk(gateway.configuration, trunkId, existingTrunk, data)
		if not result.success then return result end
	end

	ksProv:InsertSuperColumn(gatewayId, "Gateways", "nodeTrunks", trunkId, json.encode(data))

	-- IS OK
	result.success = true
	result.trunk = data
	return result
end

function UpdateGatewayNodeTrunkDialplanContext(ksProv, gatewayId, trunkId, dialplanContext)
	local result = {
		success = true,
		errType = "",
		errMsg  = ""
	}

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- EXIST GATEWAY TRUNK validation
	local trunk = gateway.nodeTrunks and gateway.nodeTrunks[trunkId]
	if not trunk then
		result.success = false
		result.errType = "GATEWAY TRUNK DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." trunk does not exist."
		return result
	end

	if type(trunk) == "string" then trunk = json.decode(trunk) end
	if trunk.DialplanContext == dialplanContext then
		return result
	end

	trunk.DialplanContext = dialplanContext
	return UpdateGatewayNodeTrunk (ksProv, gatewayId, trunkId, trunk)
end

function UpdateGatewayNodeTrunkAttributeValue(ksProv, gatewayId, trunkId, trunk, oldValue, newValue)
	local changes = false
	for _, attribute in pairs(trunk.Attributes) do
		if string.find(attribute.value, oldValue) then
			attribute.value = string.gsub(attribute.value, oldValue, newValue)
			changes = true
		end
	end
	if not changes then
		return { success = true }
	end

	return UpdateGatewayNodeTrunk (ksProv, gatewayId, trunkId, trunk)
end

function UpdateGatewayNodeTrunksAttributeValue(ksProv, gatewayId, gateway, oldValue, newValue, abortOnError)
	local trunks = gateway.nodeTrunks or {}
	for trunkId, trunk in pairs(trunks) do
		if type(trunk) == "string" then trunk = json.decode(trunk) end
		local result = UpdateGatewayNodeTrunkAttributeValue(
			ksProv, gatewayId, trunkId, trunk, oldValue, newValue)
		if abortOnError and not result.success then
			return result
		end
	end
	return { success = true }
end

function UpdateGatewaysNodeTrunksAttributeValue(ksProv, oldValue, newValue, abortOnError)
	local gateways = ListGateways(ksProv)
	for gatewayId, gateway in pairs(gateways) do
		local result = UpdateGatewayNodeTrunksAttributeValue(
			ksProv, gatewayId, gateway, oldValue, newValue, true)
		if abortOnError and not result.success then
			return result
		end
	end
	return { success = true }
end

-- DeleteGatewayNodeTrunk
------------------------------
-- Removes a gateway node trunk from provisioning
-- Pre: The gateway must exist
-- Pre: The gateway node trunk must exist
-- Pre: The gateway node trunk must not be associated with any trunk
function DeleteGatewayNodeTrunk (ksProv, gatewayId, trunkId)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	trunkId = utf8.lower(trunkId)

	LogDebug("[PROVISIONING_MANAGER:DeleteGatewayNodeTrunk] Deleting Gateway: '%s' Trunk: '%s'",
			 gatewayId, trunkId)

	-- EXIST GATEWAY validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- EXIST GATEWAY TRUNK validation
	local existingTrunk = gateway.nodeTrunks and gateway.nodeTrunks[trunkId]
	if not existingTrunk then
		result.success = false
		result.errType = "GATEWAY TRUNK DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." trunk does not exist."
		return result
	end

	local result = DeleteAsteriskGatewayTrunk(gateway.configuration, trunkId, existingTrunk)
	if not result.success then return result end

	-- Ahora si lo elimino
	ksProv:RemoveColumn(gatewayId, "Gateways", trunkId, "nodeTrunks")

	-- IS OK
	result.success = true
	return result
end

-- UpdateGatewayNodeTrunk
---------------------------
-- Adds or modifies a gateway carrieer trunk in provisioning
-- Pre: The gateway must exist
-- Pre: The gateway must not have a nodeTrunk with same id
function UpdateGatewayCarrierTrunk (ksProv, gatewayId, trunkId, data)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	trunkId = utf8.lower(trunkId)
	local dataStr = json.encode(data)

	LogDebug(
		"[PROVISIONING_MANAGER:UpdateGatewayCarrierTrunk] Updating Gateway: '%s'  Trunk: '%s' configuration: '%s'",
		gatewayId, trunkId, dataStr)

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- carrier trunk validation
	if gateway.nodeTrunks and gateway.nodeTrunks[trunkId] then
		result.success = false
		result.errType = "GATEWAY NODE TRUNK EXISTS"
		result.errMsg = "Gateway "..tostring(gatewayId).." has a node trunk with same name."
		return result
	end

	local existingTrunk = gateway.carrierTrunks and gateway.carrierTrunks[trunkId] or {}
	local result = UpdateAsteriskGatewayTrunk(gateway.configuration, trunkId, existingTrunk, data)
	if not result.success then return result end

	ksProv:InsertSuperColumn(gatewayId, "Gateways", "carrierTrunks", trunkId, json.encode(data))

	-- IS OK
	result.success = true
	result.trunk = data
	return result
end

-- DeleteGatewayCarrierTrunk
------------------------------
-- Removes a gateway carrier trunk from provisioning
-- Pre: The gateway must exist
-- Pre: The gateway carrier trunk must exist
function DeleteGatewayCarrierTrunk (ksProv, gatewayId, trunkId)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	trunkId = utf8.lower(trunkId)

	LogDebug("[PROVISIONING_MANAGER:DeleteGatewayCarrierTrunk] Deleting Gateway: '%s' Trunk: '%s'",
			 gatewayId, trunkId)

	-- EXIST GATEWAY validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- EXIST GATEWAY TRUNK validation
	local existingTrunk = gateway.carrierTrunks and gateway.carrierTrunks[trunkId]
	if not existingTrunk then
		result.success = false
		result.errType = "GATEWAY TRUNK DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." trunk does not exist."
		return result
	end

	local result = DeleteAsteriskGatewayTrunk(gateway.configuration, trunkId, existingTrunk)
	if not result.success then return result end

	-- Ahora si lo elimino
	ksProv:RemoveColumn(gatewayId, "Gateways", trunkId, "carrierTrunks")

	-- IS OK
	result.success = true
	return result
end

-- ValidateGatewayDialplanContext
---------------------------
-- Validates a gateway carrieer dialplan context in provisioning
-- Pre: The gateway must exist
function ValidateGatewayDialplanContext (ksProv, gatewayId, contextId, data)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	contextId = utf8.lower(contextId)
	local dataStr = json.encode(data)

	LogDebug(
		"[PROVISIONING_MANAGER:ValidateGatewayDialplanContext] Validating Gateway: '%s'  Context: '%s' configuration: '%s'",
		gatewayId, contextId, dataStr)

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	local result = ValidateAsteriskGatewayDialplanContext(gateway.configuration, contextId, data)
	if not result.success then return result end

	-- IS OK
	result.success = true
	return result
end

-- UpdateGatewayDialplanContext
---------------------------
-- Adds or modifies a gateway carrieer dialplan context in provisioning
-- Pre: The gateway must exist
function UpdateGatewayDialplanContext (ksProv, gatewayId, contextId, data)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	contextId = utf8.lower(contextId)
	local dataStr = json.encode(data)

	LogDebug(
		"[PROVISIONING_MANAGER:UpdateGatewayDialplanContext] Updating Gateway: '%s'  Context: '%s' configuration: '%s'",
		gatewayId, contextId, dataStr)

	-- EXIST validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	local result = UpdateAsteriskGatewayDialplanContext(gateway.configuration, contextId, data)
	if not result.success then return result end

	ksProv:InsertSuperColumn(gatewayId, "Gateways", "dialplanContexts", contextId, dataStr)

	-- IS OK
	result.success = true
	return result
end

-- DeleteGatewayDialplanContext
------------------------------
-- Removes a gateway dialplan context from provisioning
-- Pre: The gateway must exist
-- Pre: The gateway dialplan must exist
-- Pre: The gateway dialplan must not be assigned to a trunk
function DeleteGatewayDialplanContext (ksProv, gatewayId, contextId)
	local result = {
		success = false,
		errType = "",
		errMsg  = ""
	}

	gatewayId = utf8.lower(gatewayId)
	contextId = utf8.lower(contextId)

	LogDebug("[PROVISIONING_MANAGER:DeleteGatewayDialplanContext] Deleting Gateway: '%s' Context: '%s'",
			 gatewayId, contextId)

	-- EXIST GATEWAY validation
	local gateway = GetGateway(ksProv, gatewayId)
	if isEmpty(gateway) then
		result.success = false
		result.errType = "GATEWAY DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." does not exist."
		return result
	end

	-- EXIST GATEWAY CONTEXT validation
	if not gateway.dialplanContexts or not gateway.dialplanContexts[contextId] then
		result.success = false
		result.errType = "GATEWAY CONTEXT DOES NOT EXIST"
		result.errMsg = "Gateway "..tostring(gatewayId).." context does not exist."
		return result
	end

	-- ASSIGNED TO TRUNK validation
	for trunkId, trunk in pairs(gateway.nodeTrunks or {}) do
		if type(trunk) == "string" then trunk = json.decode(trunk) end
		if trunk.DialplanContext == contextId then
			result.success = false
			result.relatedId = trunkId
			result.errType = "GATEWAY CONTEXT ASSIGNED TO NODE TRUNK"
			result.errMsg = "Gateway "..tostring(gatewayId).." context assigned to node trunk."
			return result
		end
	end
	for trunkId, trunk in pairs(gateway.carrierTrunks or {}) do
		if type(trunk) == "string" then trunk = json.decode(trunk) end
		if trunk.DialplanContext == contextId then
			result.success = false
			result.relatedId = trunkId
			result.errType = "GATEWAY CONTEXT ASSIGNED TO CARRIER TRUNK"
			result.errMsg = "Gateway "..tostring(gatewayId).." context assigned to carrier trunk."
			return result
		end
	end

	local result = DeleteAsteriskGatewayDialplanContext(gateway.configuration, contextId)
	if not result.success then return result end

	-- Ahora si lo elimino
	ksProv:RemoveColumn(gatewayId, "Gateways", contextId, "dialplanContexts")

	-- IS OK
	result.success = true
	return result
end

-- UpdateGatewayMonitoring
---------------------------
-- Adds or modifies a gateway monitoring configuration in provisioning
function UpdateGatewayMonitoring (ksProv, gatewayId, configuration)
	gatewayId = utf8.lower(gatewayId)

	local configurationStr = json.encode(configuration)
	if not isEmptyString(configuration.GrafanaPassword) then
		configurationStr = string.gsub(configurationStr, configuration.GrafanaPassword, "***")
	end
	if not isEmptyString(configuration.SipCapturePassword) then
		configurationStr = string.gsub(configurationStr, configuration.SipCapturePassword, "***")
	end
	LogDebug("[PROVISIONING_MANAGER:UpdateGateway] Updating Gateway: '%s' monitoring configuration: '%s'",
		     gatewayId, configurationStr)

	for column, value in pairs(configuration) do
		local strValue =  type(value) ~= "table" and tostring(value) or json.encode(value)
		ksProv:InsertSuperColumn(gatewayId, "Gateways", "monitoring", column, strValue)
	end
	return { success = true }
end

-- GatewayMonitoringGrafanaWidgets
---------------------------
-- Open grafana widgets

local function GrafanaLogin(monitoring)
	local domain, protocol = GetUrlDomainProtocol(monitoring.GrafanaOverviewUrl)
	local user = monitoring.GrafanaUser
	local password = monitoring.GrafanaPassword
	local url = string.format("%s://%s/login", protocol, domain)
	local request = { user = user, password = password }
	local code, response, headers = GetHttpRequest().postObject(url, nil, "json", request, nil, "json")
	if code ~= 200 then
		return false, {
			success = false,
			errType = "LOGIN ERROR",
			errMsg = response.message or "Login error"
		}
	end

	local session = GetCookieValue(headers["set-cookie"] or "", "grafana_session")
	if isEmptyString(session) then
		return false, {
			success = false,
			errType = "LOGIN ERROR",
			errMsg = "Session not found"
		}
	end

	return session
end

local function MakeGrafanaWidgetUrl(url, session)
	local domain, protocol = GetUrlDomainProtocol(url)
	local params = GetHttpRequest().encodeUrlEncodedRequest({
		widget = url,
		session = session
	})
	return string.format("%s://%s/open_widget?%s", protocol, domain, params)
end

function GatewayMonitoringGrafanaWidgets (ksProv, gatewayId)
	local gateway = GetGateway(ksProv, gatewayId) or {}
	local monitoring = gateway.monitoring or {}
	if type(monitoring) == "string" then monitoring = json.decode(monitoring) end
	if not monitoring.GrafanaEnabled or monitoring.GrafanaEnabled ~= "1" then
		return {
			success = false,
			errType = "NOT CONFIGURED",
			errMsg = "Grafana widgets not configured yet"
		}
	end

	if not GATEWAY_ASTERISK_ENABLED then
		return {
			success = true,
			overview = monitoring.GrafanaOverviewUrl,
			logs = monitoring.GrafanaLogsUrl
		}
	end

	local session, err = GrafanaLogin(monitoring)
	if not session then return err end

	return {
		success = true,
		overview = MakeGrafanaWidgetUrl(monitoring.GrafanaOverviewUrl, session),
		logs = MakeGrafanaWidgetUrl(monitoring.GrafanaLogsUrl, session),
	}
end

-- GatewayMonitoringSipCaptureWidgets
---------------------------
-- Open sip capture widgets

local function SipCaptureLogin(monitoring)
	local domain, protocol = GetUrlDomainProtocol(monitoring.SipCaptureUrl)
	local user = monitoring.SipCaptureUser
	local password = monitoring.SipCapturePassword
	local url = string.format("%s://%s/api/v3/auth", protocol, domain)
	local request = { username = user, password = password, type = "internal" }
	local code, response = GetHttpRequest().postObject(url, nil, "json", request, nil, "json")
	if type(code) ~= "number" or code < 200 or code >= 300 then
		return false, {
			success = false,
			errType = "LOGIN ERROR",
			errMsg = response.message or "Login error"
		}
	end

	return json.encode(response)
end

local function MakeSipCaptureWidgetUrl(url, session)
	local domain, protocol = GetUrlDomainProtocol(url)
	local params = GetHttpRequest().encodeUrlEncodedRequest({
		widget = url,
		session = session
	})
	return string.format("%s://%s/open_widget?%s", protocol, domain, params)
end

function GatewayMonitoringSipCaptureWidgets (ksProv, gatewayId)
	local gateway = GetGateway(ksProv, gatewayId) or {}
	local monitoring = gateway.monitoring or {}
	if type(monitoring) == "string" then monitoring = json.decode(monitoring) end
	if not monitoring.SipCaptureEnabled or monitoring.SipCaptureEnabled ~= "1" then
		return {
			success = false,
			errType = "NOT CONFIGURED",
			errMsg = "Sip Capture widgets not configured yet"
		}
	end

	if not GATEWAY_ASTERISK_ENABLED then
		return {
			success = true,
			home = monitoring.SipCaptureUrl
		}
	end

	local session, err = SipCaptureLogin(monitoring)
	if not session then return err end

	return {
		success = true,
		home = MakeSipCaptureWidgetUrl(monitoring.SipCaptureUrl, session),
	}
end

function GatewayMonitoringStopSipCaptureWidgets (ksProv, gatewayId)
	local gateway = GetGateway(ksProv, gatewayId) or {}
	local monitoring = gateway.monitoring or {}
	if type(monitoring) == "string" then monitoring = json.decode(monitoring) end
	if not monitoring.SipCaptureEnabled == "1" then
		return { success = true }
	end

	local domain, protocol = GetUrlDomainProtocol(monitoring.SipCaptureUrl)
	local user = monitoring.SipCaptureUser
	local password = monitoring.SipCapturePassword
	local url = string.format("%s://%s/close_widget", protocol, domain)
	local request = {}
	local code = GetHttpRequest().postObject(url, nil, "json", request)
	if code ~= 200 then
		return false, {
			success = false,
			errType = "STOP ERROR",
			errMsg = "error stopping sip capture"
		}
	end

	return { success = true }
end

function GatewayMonitoringSendConsoleCommand (ksProv, gatewayId, data)
	local gateway = GetGateway(ksProv, gatewayId) or {}
	return SendAsteriskGatewayConsoleCommand(gateway.configuration or {}, data.command)
end

function GatewayMonitoringConfigurationFile (ksProv, gatewayId, fileName)
	local gateway = GetGateway(ksProv, gatewayId) or {}
	return GetAsteriskGatewayConfigurationFile(gateway.configuration or {}, fileName)
end
