require "LuaOrm.Date"
require "lfs"

local reporterFTP = require "reporter.ftp"
local reporterSFTP = require "reporter.sftp"
-- local ftp = require("socket.ftp")
-- local ltn12 = require("ltn12")

module(..., package.seeall)

-- Idem al directorio temporal del archivo "detail_query.lua"
local SOURCE_ROOT_PATH_TEMPORAL_FILES = "/tmp/"
-- Constante que define que el schedule se debe actualizar a running cada N achivos subidos al FTP.
local UPDATE_SCH_TO_RUNNING_EACH_N_UPLOADED_FILES_TO_FTP = 50
local MAX_PERCENTAGE_OF_FILES_NOT_UPLOAD_TO_FTP = 20
local m_counterUploadedFilesToFTP = 0 -- contador de arhivos subidos en el proceso actual

-- in case it's available
local LogInfo = LogInfo or function(msg, ...)
	print("Info", msg:format(...))
end
local LogWarning = LogWarning or function(msg, ...)
	print("Warning", msg:format(...))
end

local function GetSchedulerModule ()
	local scheduler = require("reporter.scheduler")
	return scheduler
end

local function GetSendTypeModule (schedule)
	assert(schedule)
	assert(schedule.SendType)
	local sendType = schedule.SendType:upper()
	if schedule.SendType == "ftp" then
		return reporterFTP
	elseif schedule.SendType == "sftp" then
		return reporterSFTP
	else
		assert(false, "Unrecognized sendType procotol: " .. schedule.SendType)
	end
end

local function GetProtocolConfig (schedule)
	assert(schedule)
	assert(schedule.SendType)
	local sendType = schedule.SendType:upper()
	if schedule.SendType == "ftp" then
		assert(schedule.ftp)
		return schedule.ftp
	elseif schedule.SendType == "sftp" then
		assert(schedule.sftp)
		return schedule.sftp
	else
		assert(false, "Unrecognized sendType procotol: " .. schedule.SendType)
	end
end

--[[
	Funcion que reemplaza las variables en el nombre de archivo destino.
]]--
local function ReplaceVars (name, scheduleToRun, utils)
	if not name or name == '' then
		return
	end

	local scheduler = GetSchedulerModule()
	-- Se obtiene la definicion timezone que utiliza el usuario.
	local timeZone = scheduler.get_user_timezone(utils, scheduleToRun.User, scheduleToRun.Vcc)

	--[[
		Esta funcion convierte una fecha/hora dada al timezone del usuario.
	]]--
	local MakeTimeZoneDate = function(currentDate, format)
		local stringDate = currentDate:toString("%Y-%m-%d")
		local stringHour = currentDate:toString("%H:%M:%S")
		local timezoneDate = utils.ConvertUTCDateToDate(stringDate, stringHour, format, nil, timeZone)

		--LogDebug("[FTPCONTROLLER:MakeTimeZoneDate] UTC Date: '%s', Timezone Date: '%s', Timezone: '%s'.", (stringDate .. " " .. stringHour), timezoneDate, json.encode(timeZone or {}))

		return timezoneDate
	end

	local now      = LuaOrm.Date()
	local dateFrom = nil
	local dateTo   = nil
	local params   = scheduleToRun.Params or {}

	local vars = {
		["{ACCOUNT}"] = function()
			return params.Account or "ALL_ACCOUNTS"
		end,
		["{AGENT}"] = function()
			return params.Agent or "ALL_USERS"
		end,
		["{ATTENTION_LEVEL}"] = function()
			return params.AttentionLevel or "ALL_ATT_LEVELS"
		end,
		["{CAMPAIGN}"] = function()
			return params.Campaign or "ALL_CAMPAIGNS"
		end,
		["{CHANNEL}"] = function()
			return params.Type or "ALL_CHANNELS"
		end,
		["{EXECUTION_DATE}"] = function()
			return MakeTimeZoneDate(now, "%Y_%m_%d")
		end,
		["{NOW}"] = function()
			return MakeTimeZoneDate(now, "%Y_%m_%d_%H_%M_%S")
		end,
		["{START_DATE}"] = function()
		-- TODO: Validar si aca tambien hay que convertirlo al timezone del usuario. Si es asi, solo hay que descomentar el codigo de abajo.
			-- local from = LuaOrm.Date(dateFrom)
			-- return MakeTimeZoneDate(from, "%Y_%m_%d")
			if not dateFrom then
				dateFrom, dateTo = scheduler.get_query_interval(scheduleToRun, "%Y_%m_%d")
			end

			return dateFrom
		end,
		["{END_DATE}"] = function()
		-- TODO: Validar si aca tambien hay que convertirlo al timezone del usuario. Si es asi, solo hay que descomentar el codigo de abajo.
			-- local to = LuaOrm.Date(dateTo)
			-- return MakeTimeZoneDate(to, "%Y_%m_%d")
			if not dateTo then
				dateFrom, dateTo = scheduler.get_query_interval(scheduleToRun, "%Y_%m_%d")
			end

			return dateTo
		end,
		["{VCC}"] = function()
			return scheduleToRun.Vcc or "ALL_VCCs"
		end
	}

	local newName = name
	for varId, replaceWithFn in pairs(vars) do
		if name:match(varId) then
			local replaceWith = replaceWithFn()
			--LogDebug("[FTPCONTROLLER:ReplaceVars] varId: '%s', replaceWith: '%s'.", varId, replaceWith)
			newName = newName:gsub(varId, replaceWith)
		end
	end

	--LogDebug("[FTPCONTROLLER:ReplaceVars] --> oldName: '%s', newName: '%s'.", name, newName)

	return newName
end

local function MakeFTPRootDestinationDirectoryName (protocolConfig, scheduleToRun, utils)
	--LogDebug("[FTPCONTROLLER:MakeFTPRootDestinationDirectoryName] --> protocolConfig: '%s'.", json.encode(protocolConfig))
	if protocolConfig and protocolConfig.Directory then
		local rootDirectory = ReplaceVars(protocolConfig.Directory, scheduleToRun, utils)
		--LogDebug("[FTPCONTROLLER:MakeFTPRootDestinationDirectoryName] --> oldDirectory: '%s', newDirectory: '%s'.", protocolConfig.Directory, rootDirectory)
		return rootDirectory
	end
	return ""
end

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

--[[
	Aca se realiza la creacion del directorio destino en FTP, en el cual se subiran los archivos.

]]--
local function FTPCreateDirectory (schedule, parentDirectory, directory, protocolConfig)
	LogInfo("[FTPCONTROLLER:FTPCreateDirectory] Create directory on FTP. Directory '%s', parent directory: '%s'.", directory, parentDirectory)
	local moduleFtpOrSftp = GetSendTypeModule(schedule)
	return moduleFtpOrSftp.CreateDirectory(parentDirectory, directory, protocolConfig)
end

--[[
	Aca se realiza el borrado de un directorio del FTP.
]]--
local function FTPRemoveDirectory (schedule, parentDirectory, directory, protocolConfig)
	LogInfo("[FTPCONTROLLER:FTPRemoveDirectory] Removing directory on FTP. Directory '%s', parent directory: '%s'.", directory, parentDirectory)
	local moduleFtpOrSftp = GetSendTypeModule(schedule)
	return moduleFtpOrSftp.RemoveDirectory(parentDirectory, directory, protocolConfig)
end

--[[
	Aca se realiza el UPLOAD del arhivo al FTP.
	El archivo se procesa en forma de BLOB o de string.
]]--
local function FTPUploadFile (fileName, content, fileType, protocolConfig, rootDirectory, scheduleToRun, utils, entry)
	local moduleFtpOrSftp = GetSendTypeModule(scheduleToRun)
	local success, err = moduleFtpOrSftp.UploadFile(fileName, content, fileType, protocolConfig, rootDirectory, scheduleToRun, utils, entry)

	m_counterUploadedFilesToFTP = (m_counterUploadedFilesToFTP or 0) + 1
	if m_counterUploadedFilesToFTP >= UPDATE_SCH_TO_RUNNING_EACH_N_UPLOADED_FILES_TO_FTP then
		-- Actualizo el estado del schedule a running cada N archivos subidos al FTP
		local scheduler = GetSchedulerModule()
		scheduler.update_state(utils, scheduleToRun, RUNNING)
		m_counterUploadedFilesToFTP = 0
	end

	return success, err
end

--[[
	Funcion encargada de eliminar los archivos temporales que se subieron o se iban a subir al FTP
]]--
local function DeleteTemporalFTPSourceContent (pathSourceContentDirectory, sourceRootPath)
	if not pathSourceContentDirectory then
		return
	end
	local sourcePath = sourceRootPath .. pathSourceContentDirectory
	if string.sub(sourcePath, -1) == "/" then
		sourcePath=string.sub(sourcePath, 1, -2)
	end
	-- Se elimina el directorio temporal raiz, donde se encuentra el contexto temporal a subir al FTP
	LogInfo("[FTPCONTROLLER:DeleteTemporalFTPSourceContent] Remove temporal directory and files. Path: '%s'.", sourcePath)
	os.execute("rm -rf " .. sourcePath)
end

--[[
Esta funcion es la encargada de Recorrer de forma recursiva el tree orgien con los archivos de contexto.
A cada uno de ellos los va creando al FTP destino, y cada archivo que exista en los directoris y subdirectorios,
los va subiendo al FTP destino en la misma estructura.

Despues de subir los archivos y directorios, se elimia el tree origen con sus archivos.

	@params:
		sourceContentDirectory: Contiene el path

Este directorio origen se ha creado en el archivo "detail_query.lua" funcion "getDetailsForSendToFTP"
]]--
local function FTPUploadRecursiveDirectories (pathSourceContentDirectory, suggestedPathTargetContentDirectory, sourceRootPath, protocolConfig, rootDirectory, scheduleToRun, utils)
	assert(pathSourceContentDirectory and pathSourceContentDirectory ~= "", "path source directory parameter is missing or empty")

	--LogDebug("1 FTPUploadRecursiveDirectories-> pathSourceContentDirectory: '%s'", pathSourceContentDirectory)
	--LogDebug("2 FTPUploadRecursiveDirectories-> suggestedPathTargetContentDirectory: '%s'", suggestedPathTargetContentDirectory)
	--LogDebug("3 FTPUploadRecursiveDirectories-> sourceRootPath: '%s'", sourceRootPath)

	local sourcePath = sourceRootPath .. pathSourceContentDirectory
	if string.sub(sourcePath, -1) == "/" then
		sourcePath=string.sub(sourcePath, 1, -2)
	end

	--LogDebug("4 FTPUploadRecursiveDirectories-> sourcePath: '%s'", sourcePath)
	local targetContentDirectory = (suggestedPathTargetContentDirectory or pathSourceContentDirectory) .. "_content"
	--LogDebug("5 FTPUploadRecursiveDirectories-> targetContentDirectory: '%s'", targetContentDirectory)

	local counterCreatedDirectories = 0
	-- Aca creo en el FTP el directorio destino, donde se creara la estructura en disco y se subiran los archivos del contexto al FTP
	local success, err = FTPCreateDirectory(scheduleToRun, rootDirectory, targetContentDirectory, protocolConfig)
	if success then
		counterCreatedDirectories = counterCreatedDirectories + 1
	end

	-- Esta funcion es la encargada de crear el nombre de los directorios y archivos destinos.
	-- Los crea a partir del nombre de los directorios y archivos origenes.
	--
	local ReplaceWithTargetDirectory = function(dir)
		-- 1 - Elimina sourceRootPath (Ej: /tmp/directorio_origen/ -> queda -> /directorio_origen/)
		dir = dir:gsub(sourceRootPath, "")

		-- 2 - Segundo reemplaza el directorio origen por el directorio destino.
		--  Ej: /directorio_origen/ -> queda -> /directorio_destino/
		local start, finish = dir:find(pathSourceContentDirectory, nil, true)
		if not start then
			return dir
		end
		local first = ""
		if start > 1 then
			first = dir:sub(1, start)
		end
		local last = dir:sub(finish + 1)
		dir = first .. targetContentDirectory .. last

		return dir
	end

	local counterUploadedFiles = 0
	local counterTotalFiles = 0
	local tableFilesNotUploaded
	-- Esta funcion recorre el tree origen de forma recursiva y va creando al FTP los directorios y subiendo los archivos destinos.
	local function dtree(dir)
		for entry in lfs.dir(dir) do
			if entry ~= "." and entry ~= ".." then
				entry=dir.."/"..entry
				local attr=lfs.attributes(entry)

				if attr.mode == "directory" then
					-- Obtengo el nombre del directorio destino
					local targetDir = ReplaceWithTargetDirectory(entry)
					-- Creo en el FTP el sub directorio destino
					local success = FTPCreateDirectory(scheduleToRun, rootDirectory, targetDir, protocolConfig)
					if success then
						counterCreatedDirectories = counterCreatedDirectories + 1
					end
					-- Invoco a la misma funcion de forma recursiva para continuar con el contenido del sub directorio
					dtree(entry)
					-- Elimino el sub directorio origen que ya se ha procesado.
					os.remove(entry)

				elseif attr.mode == "file" then
					-- Obtengo el nombre del archivo destino
					local targetFileName = ReplaceWithTargetDirectory(entry)
					-- Abro el archivo para luego subirlo al FTP
					--local file = assert(io.open(entry, "rb"), "file " .. entry .. " does not exist")
					local file = ""
					--Obtener el total de archivos a subir
					counterTotalFiles = counterTotalFiles  + 1
					-- Ahora subo el archivo al FTP en forma de BLOB
					local success, err = FTPUploadFile(targetFileName, file, "blob", protocolConfig, rootDirectory, scheduleToRun, utils, entry)
					if success then
						counterUploadedFiles = counterUploadedFiles + 1
					else
						if not tableFilesNotUploaded then
							tableFilesNotUploaded = {}
						end
						--Guardar info basica de archivos que fallaron al subir
						table.insert(tableFilesNotUploaded, {
							targetFileName = targetFileName,
							error = err
						})
					end
					-- Luego de subido, lo elimino al archivo origen.
					os.remove(entry)
				end
			end
		end
	end
	-- Aca comienza a recorer de forma recursiva el directorio origen
	dtree(sourcePath)
	LogInfo("[FTPCONTROLLER:FTPUploadRecursiveDirectories] Deleting temporal source directory: %s", sourcePath)
	-- Se elimina el directorio temporal raiz, donde se encuentra el contexto que se ha subido al FTP
	os.remove(sourcePath)

	return true, counterCreatedDirectories, counterUploadedFiles, counterTotalFiles, tableFilesNotUploaded
end

--[[
	Funcion que valida si el FTP esta lista para ser utilizado.
	Conexion basica contra el host, user y password.
]]--
local function FTPIsReady (schedule)
	local moduleFtpOrSftp = GetSendTypeModule(schedule)
	return moduleFtpOrSftp.IsReady(schedule)
end

--[[
	Funcion que valida si se puede escribir en el directorio destino del FTP.
]]--
local function FTPWriteValidation (scheduleToRun, utils)
	local moduleFtpOrSftp = GetSendTypeModule(scheduleToRun)
	local protocolConfig = GetProtocolConfig(scheduleToRun)
	rootDestinationDirectory = MakeFTPRootDestinationDirectoryName(protocolConfig, scheduleToRun, utils)

	return moduleFtpOrSftp.WriteValidation(scheduleToRun, rootDestinationDirectory)
end

--[[
Esta funcion comienza a realizar el proceso de UPLOAD al FTP del archivo exportado
 y de sus conversaciones si es que el mismo ha exportado las conversaciones.
]]--
local function FTPStartUpload (schedule, stringFileContent, fileName, sourceContentDirectory, scheduleToRun, utils)
	local protocolConfig = GetProtocolConfig(scheduleToRun)

	LogInfo("[FTPCONTROLLER:FTPStartUpload] START Upload to FTP, File name: '%s', Source content directory: '%s'. FTP host: '%s'.", fileName, sourceContentDirectory, protocolConfig.Host)

	if type(stringFileContent) ~= "string" then
		LogError("[FTPCONTROLLER:FTPStartUpload] String content file is required.")
		return false, "Incomplete definition. Content file destination is required."
	end
	if not fileName then
		LogError("[FTPCONTROLLER:FTPStartUpload] File name is required.")
		return false, "Incomplete definition. File name destination is required."
	end
	if not scheduleToRun then
		LogError("[FTPCONTROLLER:FTPStartUpload] Schedule definition is required.")
		return false, "Incomplete definition. Schedule definition is required."
	end

	local counterCreatedDirectories = 0
	local rootDestinationDirectory = MakeFTPRootDestinationDirectoryName(protocolConfig, scheduleToRun, utils)
	-- Creo el directorio raiz del FTP, donde se almacenaran todos los schedules
	if protocolConfig and protocolConfig.Directory and protocolConfig.Directory:len() > 0 then
		local rootDirectory = ""
		if FTPCreateDirectory(scheduleToRun, rootDirectory, rootDestinationDirectory, protocolConfig) then
			counterCreatedDirectories = counterCreatedDirectories + 1
		end
	end

	m_counterUploadedFilesToFTP = 0 -- Dejo en cero los archivos subidos para el proceso y la actualizacion a RUNNING del SCH

	local counterUploadedFiles = 0
	local dataFilesNotUpdated
	local totalFilesToUpload = 1

	-- PABLO PARA NO USAR EL socket.ftp -----------------
	local primaryFileReportPath = sourceContentDirectory or ""
	local localFile = SOURCE_ROOT_PATH_TEMPORAL_FILES .. primaryFileReportPath .. "/" .. fileName
	local localDir = SOURCE_ROOT_PATH_TEMPORAL_FILES .. primaryFileReportPath .. "/"
	local f = io.open(localFile, "w")
	if f then
		f:write(stringFileContent)
		f:close()
	end
	----------------
	
	-- Aca invoco a la funcion que realiza el UPLOAD al FTP del archivo exportado.
	local fileSize, err = FTPUploadFile(fileName, stringFileContent, "string", protocolConfig, rootDestinationDirectory, scheduleToRun, utils, localFile)

	if fileSize then
		counterUploadedFiles = counterUploadedFiles + 1
	else
		LogError("[FTPCONTROLLER:FTPStartUpload] Error uploading Export file '%s' to FTP. Err: '%s', FTP host: '%s'.", fileName, err, protocolConfig.Host)
		DeleteTemporalFTPSourceContent(primaryFileReportPath, SOURCE_ROOT_PATH_TEMPORAL_FILES)
		return false, err
	end

	LogInfo("[FTPCONTROLLER:FTPStartUpload] Export file '%s', successfully uploaded to FTP. File size '%s' bytes.", fileName, fileSize)

	if sourceContentDirectory then
		-- Funcion encargada de eliminar la extension del nombre del archivo al directorio destino.
		local function CleanUpDirectoryName(name)
			local extensionsToRemove = {"CSV", "PDF"} -- Lisa de extensiones a eliminar

			for i, extToRemove in ipairs(extensionsToRemove) do
				local extLength = extToRemove:len() -- obtengo el largo de la extension a eliminar
				local fileNameLength = name:len() -- obtengo el largo del archivo
				local fileExtension = name:sub(fileNameLength - extLength) -- obtengo la extension (tambien incluye el ".")

				-- Valido si la extension del archivo, coincide con la extension a eliminar.
				if fileExtension:upper() == ("." .. extToRemove) then
					-- Asigno el nombre del directorio sin la extension del archivo.
					local directory = name:sub(1, (fileNameLength - extLength) - 1)
					--LogDebug("[FTPCONTROLLER:FTPStartUpload] file name: '%s', directory name: '%s'", name, directory)
					return directory
				end
			end

			return name
		end
		local suggestedTargetContentDirectory = CleanUpDirectoryName(fileName)
		-- Aca realizo el UPLOAD al FTP de todos los archivos de contexto.
		local success, counterDirectories, counterFiles, counterTotalFiles, tableFilesNotUploaded = FTPUploadRecursiveDirectories(
			sourceContentDirectory,
			suggestedTargetContentDirectory,
			SOURCE_ROOT_PATH_TEMPORAL_FILES,
			protocolConfig,
			rootDestinationDirectory,
			scheduleToRun,
			utils
		)
		totalFilesToUpload = totalFilesToUpload + counterTotalFiles

		if success then
			counterUploadedFiles = counterUploadedFiles + counterFiles
			counterCreatedDirectories = counterCreatedDirectories + counterDirectories
		end

		if counterUploadedFiles ~= totalFilesToUpload then
			local counterNotUploaded = totalFilesToUpload - counterUploadedFiles
			local percentageNotUploaded = (counterNotUploaded * 100) / totalFilesToUpload

			dataFilesNotUpdated = {
				result = "NOT_UPLOAD_ALL_FILES",
				percentageNotUploaded = percentageNotUploaded,
				counterUploadedFiles = counterUploadedFiles,
				totalFilesToUpload =  totalFilesToUpload,
				filesNames = tableFilesNotUploaded
			}
		end
	end

	LogInfo("[FTPCONTROLLER:FTPStartUpload] END Upload to FTP, %d directories have been created and %d files uploaded of a total of %d.", counterCreatedDirectories, counterUploadedFiles, totalFilesToUpload)

	--Si la cantidad de archivos con fallo es demasiado alta (porciento mayor e igual a 'MAX_PERCENTAGE_OF_FILES_NOT_UPLOAD_TO_FTP') entonces se marcar toda la ejecucion como erronea.
		--Caso contrario pero existen igual archivos que no se subieron se notifica del lado del cliente.
	if dataFilesNotUpdated and dataFilesNotUpdated.percentageNotUploaded >= MAX_PERCENTAGE_OF_FILES_NOT_UPLOAD_TO_FTP then
		return false, dataFilesNotUpdated
	end

	return true, dataFilesNotUpdated
end

--[[
Funcion que crea la estructura base del directorio destino.
Ej:
	Recibe:
		/root/data/i6/grupoefe/SCHEDULER/sftp/campaigns/{CAMPAIGN}/date/{EXECUTION_DATE}
	Crea en HDD:
		/root/data/i6/grupoefe/SCHEDULER/sftp/campaigns/CAMPAIGN_1/date/2021_04_26
]]--
local function CreateFTPTreeDirectories (scheduleToRun, utils)
	local protocolConfig = GetProtocolConfig(scheduleToRun)
	local rootDestinationDirectory = MakeFTPRootDestinationDirectoryName(protocolConfig, scheduleToRun, utils)

	LogInfo("[FTPCONTROLLER:CreateFTPTreeDirectories] Creating FTP Tree destination directories '%s'.", rootDestinationDirectory)

	local tree = rootDestinationDirectory

	local treeDirToCreate = ""
	local isRoot = nil
	
	
	local firstChar = (tree or ""):sub(1, 1)
	if firstChar == "/" then
		isRoot = true
	else 
		isRoot = false
	end
	--isRoot = false
	--LogDebug("================> isRoot " .. tostring(isRoot))

	local success = false
	local function CreateDir (dir)
		dir = utils.alltrim(dir)
		if dir:len() > 0 then
			if treeDirToCreate:len() > 0 then
				treeDirToCreate = treeDirToCreate .. "/" .. dir
			else
				treeDirToCreate = dir
			end
			local completeDir = (isRoot and "/" or "") .. treeDirToCreate
			--LogDebug("1 - create dir: '" .. tostring(dir) .. "'")
			--LogDebug("1.1 - complete completeDir: '" .. tostring(completeDir) .. "'")
			local rootDirectory = ""
			success = FTPCreateDirectory(scheduleToRun, rootDirectory, completeDir, protocolConfig)
		end
	end

	while tree and tree:len() > 0 do
		local pos = tree:find("/")
		--LogDebug("pos " .. tostring(pos))
	
		if pos then
			if pos > 1 then
				local dir = tree:sub(1, pos - 1)
				CreateDir(dir)
				tree = tree:sub(pos + 1)
			else
				tree = tree:sub(pos + 1)
			end
		else
			if tree:len() > 0 then
				CreateDir(tree)
				tree = nil
			end
		end
	end

	if success then
		LogInfo("[FTPCONTROLLER:CreateFTPTreeDirectories] Success Created FTP Tree destination directories '%s'.", rootDestinationDirectory)
	else
		LogError("[FTPCONTROLLER:CreateFTPTreeDirectories] Unable to create FTP Tree destination directories '%s'.", rootDestinationDirectory)
	end

	return success
end

--[[
	Funcion encargada de ejecutar el reporte con upload hacia FTP
]]--
function ExecuteReport (utils, authFilter, prefix, scheduleToRun, executionType, protocolType)
	protocolType = (protocolType or ""):upper()
	LogInfo("[FTPCONTROLLER:ExecuteReport] %s execution schedule Id '%s', Execution Type: '%s'.", protocolType, scheduleToRun.Id, executionType)
	--LogDebug("[FTPCONTROLLER:ExecuteReport] ---> scheduleToRun '%s'.", json.encode(scheduleToRun))

	local allowedProtocols = {FTP = true, SFTP = true}
	assert(allowedProtocols[protocolType])

	local scheduler = GetSchedulerModule()

	-- 1 - Valida si se puede conectar contra el servidor FTP/SFTP
	local success, ftpErr = FTPIsReady(scheduleToRun)
	local errMsg = {subject = nil, body = nil, reason = nil}
	local now = LuaOrm.Date()
	local fileName
	local userId = utils.GetCurrentLoggedUser()

	local function AddSuccessHistory(filesNotUploaded)
		local data = {type = "text/csv", name = fileName, sendType = "FTP", executionType = executionType, userId = userId, filesNotUploaded = filesNotUploaded}
		scheduler.add_schedule_historical(utils, scheduleToRun.Id, now, true, data)
	end

	local function AddErrorHistory(data)
		data.sendType = protocolType -- "FTP" or "SFTP"
		data.executionType = executionType
		data.userId = userId
		scheduler.add_schedule_historical(utils, scheduleToRun.Id, now, false, data)
	end

	local OnFTPError = function(error, file)
		success = false
		errMsg.subject = "Failed to upload report '".. file .. "' to FTP."
		errMsg.body = "Some problem occurred during uploading files to FTP. Report: " .. file
		errMsg.reason = "Failed to upload report to FTP."

		if type(error) == "string" then
			errMsg.body = errMsg.body .. "\n\nError:\n" .. error
			errMsg.reason = error
		elseif type(error) == "table" and error.result == "NOT_UPLOAD_ALL_FILES" then
			errMsg.reason = error
		end

		LogWarning("[FTPCONTROLLER:ExecuteReport] Some problem occurred during uploading files to FTP. Report: %s", file)
		AddErrorHistory({reason = errMsg.reason})

		return success, errMsg
	end
	local protocolConfig = GetProtocolConfig(scheduleToRun)
	if not success then
		-- Si no se pude conectar en el punto 1) con el servidor FTP/SFTP
		
		local file = protocolConfig.FileName
		file = ReplaceVars(file, scheduleToRun, utils)
		return OnFTPError(ftpErr, file)
	end

	-- 2 - Valida si puede escribir contra el servidor FTP/SFTP
	success, ftpErr = FTPWriteValidation(scheduleToRun, utils)
	if not success then
		-- 3 - Si no puede escribir puede ser porque nunca se ha creado la estructura de directorios en el servidor FTP/SFTP.
		--     Entonces si es asi, pasa a crear la estructura destino.
		success =	CreateFTPTreeDirectories(scheduleToRun, utils)
		if not success then
			-- Definitivamente no puede escribir en servidor FTP/SFTP
			local file = protocolConfig.FileName
			file = ReplaceVars(file, scheduleToRun, utils)
			return OnFTPError(ftpErr, file)
		end
	end

	-- 4 - Ahora si, ejecuta el Reporte
	local result, code, headers, httpStatus, params, name, fileContent = scheduler.request_export_report(utils, authFilter, prefix, scheduleToRun)
	fileName = ReplaceVars(name, scheduleToRun, utils)

	if code == 200 then
		-- 5 - Comienza el Upload !!!
		local sourceContentDirectory = params.queryFields.saveContentTo
		local ok, err = FTPStartUpload(scheduleToRun, fileContent, fileName, sourceContentDirectory, scheduleToRun, utils)
		if not ok then
			success, errMsg = OnFTPError(err, fileName)
		else
			local filesNotUpdated = type(err) == "table" and err.result == "NOT_UPLOAD_ALL_FILES" and err or nil
			AddSuccessHistory(filesNotUpdated)
		end
	else
		success = false
		errMsg.subject = "Failed to execute report " .. fileName
		errMsg.body = "Some problem occurred during execution of your report " .. fileName
		errMsg.reason = "Error during execution report. " .. (httpStatus or code or "")

		LogWarning("[FTPCONTROLLER:ExecuteReport] Some problem occurred during execution of request: %s", httpStatus)

		AddErrorHistory({reason = errMsg.reason, result = result, code = code, headers = headers, httpStatus = httpStatus})
	end

	return success, errMsg
end
