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

WFCatDeniedListHelper.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.logger
local WFLogger = require 'WFLogger'

	-- WF.core.utils
local KSData = require 'KSData'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFRxLowFidelityTimer = require 'WFRxLowFidelityTimer'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'

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

local check = assert( WFArguments.check )
KSData.registerArgumentChecker()

local runUntilDone = WFRxRunUntilDone

local derefOpt = assert( KSTableUtils.derefOpt )

local logger = WFLogger 'WFCatDeniedListHelper'
--:setLevel( 'debug' )

local removeLocalIdFromDeniedList -- forward declaration

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

local function isDeniedForKey( _, docValue, deniedListKey )

	local isDenied = derefOpt( docValue, '_localOnly', 'deniedBy', deniedListKey )
			or derefOpt( docValue, '_localOnly', 'blacklistedBy', deniedListKey )
		-- "blacklistedBy" is no longer the term we use, as it's not inclusive.
		-- To avoid risky migrations on persisted data, we write new docs using
		-- "deniedBy" but have decided to accept "blacklistedBy" in legacy docs.
			
	return isDenied

end

--============================================================================--

--[[!

	\class WFCatDeniedListHelper
	
	\brief This class can be used to temporarily block certain documents. You can choose
	to ether deny the documents in a local table, or by annotating the localOnly\
	section of the docstore.
		
]]--

-- @mark WFCatDeniedListHelper class

local WFCatDeniedListHelper = {

	_docStore = false,
		-- (WFRxDocStore)
	
	_deniedListName = false,
		-- (string) Unique name given to this deniedList instance. Optionally
		-- used in doc store annotations.
	
	_numberOfFailuresBeforeDenying = false,
		-- (number) of failures before denying an Id.

	_failedBlackoutWindowInSeconds = false,
		-- (number) of seconds an Id remains denied.
	
	_annotateDocStore = false,
		-- (boolean) if true, we annotate the doc store with deny status
		-- so we can check if an asset is denied when indexing.
	
	_failureCountById = false,
		-- (table) Lookup table used for tracking failures.
		-- key: ID
		-- value: Failure Count
		
	_localDeniedList = false,
		-- (table) Lookup table for denied Ids.
		-- key: ID
		-- value: true
		
	class = {
		isDeniedForKey = isDeniedForKey,
	},
		
	WFCloseSupport.addCloseSupportMethods,

}

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

--[[!

	\brief Creates a new WFCatDeniedListHelper instance.

	\param catalog (WFRxDocStore) A valid WFRxDocStore object.
	
	\param deniedListName (string) Unique name given to this deniedList instance.
	
	\param numberOfFailuresBeforeDenying (number) of failures before denying an id.
	
	\param failedBlackoutWindowInSeconds (number) of seconds an Id remains denied.
	
	\param annotateDocStore (boolean) if true, we annotate the doc store with deniedList status
		so we can check if an asset is denied when indexing.
	
	\return WFCatDeniedListHelper object
	
--]]

function WFCatDeniedListHelper:__init( docStore, deniedListName, numberOfFailuresBeforeDenying, failedBlackoutWindowInSeconds, annotateDocStore )

	check( 'WFCatDeniedListHelper', 'WFRxDocStore', 'string', '?number', '?number', '?boolean' )

	self._docStore = docStore

	self._deniedListName = deniedListName

	self._numberOfFailuresBeforeDenying = numberOfFailuresBeforeDenying or 3

	self._failedBlackoutWindowInSeconds = failedBlackoutWindowInSeconds or 30

	self._annotateDocStore = annotateDocStore or false

	self._failureCountById = {}

	self._localDeniedList = {}

end

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

--[[!

	\brief Close this istance.

	\return (void)
	
--]]

function WFCatDeniedListHelper:close()

	check( 'WFCatDeniedListHelper' )
	
	-- Clear all deniedList annotations
	
	for id, _ in pairs( self._localDeniedList ) do
		
		removeLocalIdFromDeniedList( self, id )
	
	end

end
	
--------------------------------------------------------------------------------

removeLocalIdFromDeniedList = function( self, localId, forceClear )

	if self._annotateDocStore or forceClear then
	
		local deniedList = {}
		deniedList[ self._deniedListName ] = { _delete = true }

		if not WFCloseSupport.hasBeenOrWillBeClosed( self._docStore  ) then
		
			self._docStore:applyOverlay(
				{	
					_id = localId,
					_localOnly = { deniedBy = deniedList }
				} )
			:tap( function()

				-- Clear _localDeniedList entry after the annotation has been
				-- applied to avoid having maybeResetDeniedListStatus get
				-- called unneccessarily and multiple times because the
				-- annotation is applied asynchronously.

				self._localDeniedList[ localId ] = nil
				self._failureCountById[ localId ] = nil

			end )
			:call( runUntilDone )

		end

	else

		self._localDeniedList[ localId ] = nil
		self._failureCountById[ localId ] = nil

	end
		
end

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

local function deniedListLocalId( self, localId, blackoutTime )

	local result = Observable.just( localId )
	
	if not self._localDeniedList[ localId ] then
		
		logger:infof( "Denying Id: %s", localId )

		self._localDeniedList[ localId ] = true
		
		if self._annotateDocStore then

			local deniedList = {}
			deniedList[ self._deniedListName ] = true

			result = self._docStore:applyOverlay(
				{	
					_id = localId,
					_localOnly = { deniedBy = deniedList }
				} )

		end
		
		-- Clear from the deniedList after a set amount of time.
			
		WFRxLowFidelityTimer.timer( self._failedBlackoutWindowInSeconds )
			:flatMapLatest( function() 
				logger:debugf( "Un-denying Id: %s", localId )
				return removeLocalIdFromDeniedList( self, localId ) 
			end )
			:call( runUntilDone )
			
	else
		
		logger:infof( "Id %s is already denied", localId )
	
	end

	return result:call( runUntilDone )

end

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

function WFCatDeniedListHelper:maybeDeny( localId )

	check( 'WFCatDeniedListHelper', 'number|string' )

	local maybeFailureCount = self._failureCountById[ localId ]
	local newFailureCount = maybeFailureCount and maybeFailureCount + 1 or 1
	local isDenied = false
	
	if newFailureCount >= self._numberOfFailuresBeforeDenying then
	
		deniedListLocalId( self, localId )
		isDenied = true
		
	else
	
		self._failureCountById[ localId ] = newFailureCount
		
	end
		
	return isDenied

end

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

function WFCatDeniedListHelper:addToDeniedList( localId )

	check( 'WFCatDeniedListHelper', 'number|string' )
	return deniedListLocalId( self, localId )

end

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

function WFCatDeniedListHelper:isDenied( localId )

	check( 'WFCatDeniedListHelper', 'number|string' )

	return not not self._localDeniedList[ localId ]
	
end

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

function WFCatDeniedListHelper:maybeResetDeniedListStatus( localId )

	check( 'WFCatDeniedListHelper', 'number|string' )

	local denied = true

	if not self._localDeniedList[ localId ] then
	
		-- If the asset is annotated as denied but for some reason we're not tracking it,
		-- then something went wrong. This could potentially happen if the asset is denied
		-- and then the app restarts. The annotation will remain in the doc store, but the
		-- _localDeniedList table will be reset. In this case, we simply remove the id from the
		-- deniedList.
		
		removeLocalIdFromDeniedList( self, localId, true ) 
	
		denied = false
	
	end
	
	return denied
	
end

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

return Rx.Class 'WFCatDeniedListHelper'( WFCatDeniedListHelper )
