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

WFCatSyncClientBase.lua

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

ADOBE CONFIDENTIAL
___________________

Copyright 2019 Adobe

All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

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

	-- RxLua
local Rx = require 'Rx'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCapabilities = require 'WFCapabilities'
local WFCloseSupport = require 'WFCloseSupport'
local WFConfiguration = require 'WFConfiguration'
local WFDiagnosticLog = require 'WFDiagnosticLog'
local WFFunctionUtils = require 'WFFunctionUtils'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxScheduler = require 'WFRxScheduler'

	-- WF.oz_client.managed_catalog
local WFCatUser = require 'WFCatUser'
local WFCatSpec = require 'WFCatSpec'
local WFCatStuckSyncUtils = require 'WFCatStuckSyncUtils'
local WFCatalog = require 'WFCatalog'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'
local WFCatSpaceFeedsManager = require 'WFCatSpaceFeedsManager'
local WFCatNotificationSync = require 'WFCatNotificationSync'
local NmbCatalog
local LrClassicCatalog

if WFPlatformInfo.isDesktop then
	NmbCatalog = require 'NmbCatalog'
	LrClassicCatalog = require 'LrClassicCatalog'
end

local WFLogger = require 'WFLogger'
local logger = WFLogger( 'WFCatSyncClientBase' )

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

local check = assert( WFArguments.check )
local Observable = Rx.Observable
local runUntilDone = WFRxRunUntilDone

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

local pusherDocumentBatchCount = WFConfiguration.Sync.pusherDocumentBatchCount
local pullerDocumentBatchCount = WFConfiguration.Sync.pullerDocumentBatchCount

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

-- @mark WFCatSyncClientBase namespace

-- WFCatSyncClientBase contains much of the sync infrastructure that is shared
-- between all WF clients. All of the class methods defined within WFCatSyncClientBase
-- are automatically exposed within the client-facing sync client objects;
-- WFCatSyncClient on Desktop, and LrmSyncClient on Mobile.

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

local WFCatSyncClientBase = {

	_user = false,
		-- (WFCatUser)
		
	_catSpec = false,
		-- (WFCatSpec)
		
	_catalog = false,
		-- (WFCatalog)
		
	_catalogPuller = false,
		-- (WFCatMetadataPuller)
		
	_catalogPusher = false,
		-- (WFCatMetadataPusher)
		
	_freemiumPusher = false,
		-- (WFCatMetadataPusher)
		
	_spaceFeedsManager = false,
		-- (WFCatSpaceFeedsManager)
		
	_notificationSync = false,
		-- (WFCatNotificationSync)
		
	_checkOzForCatalogUpdatesSubscription = false,
		-- (Rx Disposable)
		
	_spacePrioritizationTimeoutSubscription = false,
		-- (Rx Disposable) Timer subscription for space prioritization timeout

	_originalCatalogWasRunning = false,
		-- (boolean) Tracks the original catalog running state before any space prioritization started

	WFCloseSupport.addCloseSupportMethods,
	
}

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

function WFCatSyncClientBase:__init( options )

	check( 'WFCatSyncClientBase', 'table' )
	
	-- Required settings
	
	local serverBase = assert( options.serverBase )
	local userId = assert( options.userId )
	local userInfoCacheFolder = assert( options.userInfoCacheFolder )
	local catalogId = assert( options.catalogId )
	local rootCatalogPath = assert( options.rootCatalogPath )
	local docStorePath = assert( options.docStorePath )
	local indexSpecScript = assert( options.indexSpecScript )
			
	-- Optional settings
	
	local isSyncClientForProfileCatalog = options.isSyncClientForProfileCatalog
	local pullerTaskDelays = options.metadataPullerArgs
	local noCatalogSearch = options.noCatalogSearch
	local pullerPrefetch = options.pullerPrefetch
	local pusherDelayBeforeSendingUpdates = options.pusherDelayBeforeSendingUpdates
	local albumAssetsForFilteredAssetsQueryFn = options.albumAssetsForFilteredAssetsQueryFn
	local locale = options.locale or "en_US"
	
	local dataCache
	if WFPlatformInfo.isAndroid then
		dataCache = options.dataCache
	end
	
	-- Set up sync infrastructure
	
	local user = options.wfCatUser or WFCatUser( serverBase, userId, userInfoCacheFolder )
	
	local catSpec = options.wfCatSpec or WFCatSpec( user, options.catalogDoc or catalogId )

	local catArgs = {
		docStorePath = docStorePath,
		indexSpecScript = indexSpecScript,
		noCatalogSearch = noCatalogSearch,
		indexStoreReadThreads = options.indexStoreReadThreads,
		syncClientBaseInstance = true,
		isSyncClientForProfileCatalog = isSyncClientForProfileCatalog
	}
	
	local wfCatalog = options.wfCatalog 
	
	if not wfCatalog then
	
		if WFPlatformInfo.isMobile() then
			wfCatalog = WFCatalog( catSpec, rootCatalogPath, catArgs )
		elseif WFPlatformInfo.isClassic() then
			wfCatalog = LrClassicCatalog( catSpec, rootCatalogPath, catArgs )
		else
			wfCatalog = NmbCatalog( catSpec, rootCatalogPath, catArgs )
		end
		
	end
	
	self._catSpec = catSpec
	
	-- Sync Down
	
	local pullerOptions = {
	
		prefetch = pullerPrefetch,
		documentBatchCount = pullerDocumentBatchCount,
		continuousTaskDelays = pullerTaskDelays,
		isCatalogPuller = true
		
	}
	
	local pullerDescription
	
	if isSyncClientForProfileCatalog then
		pullerDescription = "Profile Catalog Puller"
	else
	 	pullerDescription = "Catalog Puller"
	end
	
	local puller = WFCatMetadataPuller( wfCatalog, pullerDescription, pullerOptions )
	
	self:setOwned {
		_user = user,
		_catalog = wfCatalog,
		_catalogPuller = puller,
	}
	
	if not isSyncClientForProfileCatalog then
		
		WFCatStuckSyncUtils.addDiagnosticLogSectionFunc( wfCatalog )
			-- Track Stuck Sync info in the diagnostic log.

		-- LrmUserSubModel:setUserAuthenticated's deviceToken parameter is optional. But WFCatNotificationSync 
		-- requires one for communicating with https://notify.adobe.io/ans/v1/notifications . 
		-- If we allow WFCatNotificationSync.startNotificationSync() then queryNotificationOnce will fail,
		-- and Oz triggers a forced logout. In order to avoid this clients and unit tests may disable
		-- WFCatNotificationSync by setting the `noNotificationSync` capability.
		local noNotificationSync = WFCapabilities.getCapability( 'WFCatSyncClient.noNotificationSync' )
		if	noNotificationSync and noNotificationSync == "true" then 
			logger:warnf("WFCatNotificationSync is disabled" )
		elseif WFPlatformInfo.isClassic() then
			logger:warnf("WFCatNotificationSync is disabled on Classic platform" )
		else
			logger:infof("WFCatNotificationSync is enabled" )
			local notificationSync = WFCatNotificationSync( wfCatalog, locale )

			self:setOwned {
				_notificationSync = notificationSync,
			}						
		end
		
	 	-- Sync Up
		
		local catalogPusherOptions = {
			delayBeforeSendingUpdates = pusherDelayBeforeSendingUpdates or 2,
			documentBatchCount = pusherDocumentBatchCount or 100,
				-- Prevents the pusher from being overly aggressive at sending updates.
			albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn
		}
		
		local freemiumPusherOptions = {
			delayBeforeSendingUpdates = pusherDelayBeforeSendingUpdates or 2,
			documentBatchCount = pusherDocumentBatchCount or 100,
				-- Prevents the pusher from being overly aggressive at sending updates.
			albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
			isFreemiumPusher = true
		}
		
		local freemiumPusher = nil
		
		-- Currently only expose freemiumPusher in unit test.
		if WFCapabilities.getCapability( "enableFreemiumSharing" ) or WFPlatformInfo.isUnitTest() then
			 freemiumPusher = WFCatMetadataPusher( wfCatalog, freemiumPusherOptions )
		end

		local catalogPusher = WFCatMetadataPusher( wfCatalog, catalogPusherOptions )
		
	
		self:setOwned {
			_catalogPusher = catalogPusher,
			_freemiumPusher = freemiumPusher,
		}
	
		-- WF-256: Reset Catalog MetadataPuller polling frequency after sending data to Oz.
	
		self._checkOzForCatalogUpdatesSubscription = self._catalogPusher:workHasCompletedStream()
			:subscribe( function()
				self._catalogPuller:checkOzForUpdates()
			end )
	
		-- Space-based Metadata Sync
	
		if WFPlatformInfo.isClassic() then
			logger:warnf("WFCatSpaceFeedsManager is disabled on Classic platform" )
		else
		
			local spaceFeedsManagerArgs = {
				taskDelays = pullerTaskDelays,
				pullerDocumentBatchCount = pullerDocumentBatchCount,
				pusherDocumentBatchCount = pusherDocumentBatchCount or 100,
				albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
				dataCache = dataCache, -- Only used by Android.
				testMode = options.testMode -- Only used by unit tests.
			}
		
			local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	
			self:setOwned {
				_spaceFeedsManager = spaceFeedsManager
			}
	
			-- WF-398: Pause space-based sync during initial sync.
	
			self._catalogPuller:onceInitialSyncIsComplete()
				
				:map( function()
				
					self._spaceFeedsManager:reportCatalogInitialSyncIsComplete()
					
					return true
					
				end )
				
				:call( runUntilDone )
				
		end
		
		-- the catalog puller should report sync issues to the diagnostic log
		WFDiagnosticLog.addLogProvider( self )

	end
	
end

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

function WFCatSyncClientBase:close()

	check( 'WFCatSyncClientBase' )
	
	if self._checkOzForCatalogUpdatesSubscription then
	
		self._checkOzForCatalogUpdatesSubscription:dispose()
		self._checkOzForCatalogUpdatesSubscription = nil
	
	end
	
	-- Cancel space prioritization timeout if active
	if self._spacePrioritizationTimeoutSubscription then
		self._spacePrioritizationTimeoutSubscription:dispose()
		self._spacePrioritizationTimeoutSubscription = nil
	end

end

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

function WFCatSyncClientBase:getCatalog()

	check( 'WFCatSyncClientBase' )
	
	return self._catalog
	
end

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

function WFCatSyncClientBase:getCatSpec()

	check( 'WFCatSyncClientBase' )
	
	return self._catSpec
	
end

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

function WFCatSyncClientBase:getCatalogPuller()

	check( 'WFCatSyncClientBase' )

	return self._catalogPuller
	
end

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

function WFCatSyncClientBase:getSpaceFeedsManager()

	check( 'WFCatSyncClientBase' )

	return self._spaceFeedsManager

end

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

function WFCatSyncClientBase:getCatalogPusher()

	check( 'WFCatSyncClientBase' )
	
	return self._catalogPusher
	
end

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

function WFCatSyncClientBase:getFreemiumPusher()

	check( 'WFCatSyncClientBase' )

	return self._freemiumPusher

end

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

function WFCatSyncClientBase:getNotificationSync()

	check( 'WFCatSyncClientBase' )
	
	return self._notificationSync
	
end

--------------------------------------------------------------------------------
--@mark Start and Stop Sync Methods
--------------------------------------------------------------------------------

local function maybeCancelSpacePrioritization( self )

	-- Check if we're monitoring for sync calls during space prioritization

	if self._spacePrioritizationTimeoutSubscription then

		logger:info( "Sync call detected during space prioritization - stopping space prioritization" )

		-- Cancel the timeout subscription

		self._spacePrioritizationTimeoutSubscription:dispose()
		self._spacePrioritizationTimeoutSubscription = nil

		-- Stop space prioritization without resuming normal sync behavior

		if self._spaceFeedsManager then
			self._spaceFeedsManager:stopPrioritizeSpaceSync()
		end

	end

end

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

function WFCatSyncClientBase:startSync()

	check( 'WFCatSyncClientBase' )
	
	maybeCancelSpacePrioritization( self )
	
	self._catalogPuller:getUpdatesContinuously()

	if self._freemiumPusher then
		self._freemiumPusher:stopContinuousUpdates()
	end

	if self._catalogPusher then
		self._catalogPusher:sendUpdatesContinuously()
	end

	if self._spaceFeedsManager then
		self._spaceFeedsManager:startSendingUpdates()
		self._spaceFeedsManager:startFetchingUpdates()
	end
	
	if self._notificationSync then
		self._notificationSync:startNotificationSync()
	end
	
	return Observable.empty()
	
end

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

function WFCatSyncClientBase:stopSync()

	check( 'WFCatSyncClientBase' )
	
	maybeCancelSpacePrioritization( self )
	
	self._catalogPuller:stopContinuousUpdates()
	
	if self._catalogPusher then
		self._catalogPusher:stopContinuousUpdates()
	end

	if self._freemiumPusher then
		self._freemiumPusher: stopContinuousUpdates()
	end

	if self._spaceFeedsManager then
		self._spaceFeedsManager:stopSendingUpdates()
		self._spaceFeedsManager:stopFetchingUpdates()
	end
	
	if self._notificationSync then
		self._notificationSync:stopNotificationSync()
	end
	
	return Observable.empty()
	
end

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

function WFCatSyncClientBase:startSyncInResidualMode()

	check( 'WFCatSyncClientBase' )

	maybeCancelSpacePrioritization( self )

	self._catalogPuller:getUpdatesContinuously()

	if self._spaceFeedsManager then
		self._spaceFeedsManager:startSendingUpdates()
		self._spaceFeedsManager:startFetchingUpdates()
	end

	if self._notificationSync then
		self._notificationSync:startNotificationSync()
	end

	return Observable.empty()
	
end

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

function WFCatSyncClientBase:stopSyncInResidualMode()

	check( 'WFCatSyncClientBase' )

	maybeCancelSpacePrioritization( self )

	self._catalogPuller:stopContinuousUpdates()

	if self._spaceFeedsManager then
		self._spaceFeedsManager:stopSendingUpdates()
		self._spaceFeedsManager:stopFetchingUpdates()
	end
	
	if self._notificationSync then
		self._notificationSync:stopNotificationSync()
	end

	return Observable.empty()

end

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

function WFCatSyncClientBase:startSpaceMetadataSyncInFreemiumMode()

	check( 'WFCatSyncClientBase' )

	maybeCancelSpacePrioritization( self )

	if self._spaceFeedsManager then

		self._spaceFeedsManager:reportCatalogInitialSyncIsComplete()
			-- WF-749: In Freemium mode, the catalog does not sync. In other modes, we wait
			-- for initial sync to complete before syncing the spaces; initial catalog
			-- sync will never complete in freemium mode so we must activate this trigger
			-- manually. This allows space-feed sync to work.

		self._spaceFeedsManager:startSendingUpdates()
		self._spaceFeedsManager:startFetchingUpdates()	

	end
	
	if self._notificationSync then
		self._notificationSync:startNotificationSync()
	end
	
	return Observable.empty()

end

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

function WFCatSyncClientBase:stopSpaceMetadataSyncInFreemiumMode()

	check( 'WFCatSyncClientBase' )

	maybeCancelSpacePrioritization( self )

	if self._spaceFeedsManager then
		self._spaceFeedsManager:stopSendingUpdates()
		self._spaceFeedsManager:stopFetchingUpdates()
	end
	
	if self._notificationSync then
		self._notificationSync:stopNotificationSync()
	end

	return Observable.empty()

end

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

-- Currently clients are not yet picking up freemium sharing, but eventually we need to do some clean up 
-- with WFCatSyncClientBase:startSpaceMetadataSyncInFreemiumMode() and stopSpaceMetadataSyncInFreemiumMode
function WFCatSyncClientBase:startSharedMetadataSyncInFreemiumMode()

	check( 'WFCatSyncClientBase' )

	maybeCancelSpacePrioritization( self )
	
	self._catalogPuller:getUpdatesContinuously()
	
	if self._catalogPusher then
		self._catalogPusher:stopContinuousUpdates()
    end

	if self._freemiumPusher then
		self._freemiumPusher:sendUpdatesContinuously()
	end

	if self._spaceFeedsManager then

		self._spaceFeedsManager:reportCatalogInitialSyncIsComplete()
			-- WF-749: In Freemium mode, the catalog does not sync. In other modes, we wait
			-- for initial sync to complete before syncing the spaces; initial catalog
			-- sync will never complete in freemium mode so we must activate this trigger
			-- manually. This allows space-feed sync to work.

		self._spaceFeedsManager:startSendingUpdates()
		self._spaceFeedsManager:startFetchingUpdates()	

	end
	
	if self._notificationSync then
		self._notificationSync:startNotificationSync()
	end
	
	return Observable.empty()

end

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

function WFCatSyncClientBase:stopSharedMetadataSyncInFreemiumMode()

	check( 'WFCatSyncClientBase' )

	maybeCancelSpacePrioritization( self )

	self._catalogPuller:stopContinuousUpdates()

	if self._freemiumPusher then
		self._freemiumPusher:stopContinuousUpdates()
	end
	
	if self._catalogPusher then
		self._catalogPusher:stopContinuousUpdates()
	end

	if self._spaceFeedsManager then
		self._spaceFeedsManager:stopSendingUpdates()
		self._spaceFeedsManager:stopFetchingUpdates()
	end

	if self._notificationSync then
		self._notificationSync:stopNotificationSync()
	end

	return Observable.empty()

end

--------------------------------------------------------------------------------
--@mark Other APIs
--------------------------------------------------------------------------------

--[[!
	\brief Get a stream that fires with updates when a shared-with-you space's 
		availability changes due to Oz maintenance on the owner's account.

	\return Observable(spaceId, status) Status is 'true' in the event
		that the owner's account is under maintenance or down, otherwise
		it will be 'false'.
--]]

function WFCatSyncClientBase:spaceFeedsMaintenanceNotificationStream()

	check( 'WFCatSyncClientBase' )

	if self._spaceFeedsManager then
		return self._spaceFeedsManager:spaceFeedsMaintenanceNotificationStream()
	end

	return Observable.empty()

end

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

function WFCatSyncClientBase:pausePusherReturningCounter()

	-- Used for synchronizing trash operations with the pusher.
	-- Since the space-feed assets don't end up in the trash, we
	-- only have to do this for the catalog pusher.
	
	check( 'WFCatSyncClientBase' )

	local pauseCatalogPusherReturningCounter = Observable.just( 0 )

	if self._catalogPusher then
		pauseCatalogPusherReturningCounter = self._catalogPusher:pauseReturningCounter()
	end
	
	return pauseCatalogPusherReturningCounter
	
end

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

function WFCatSyncClientBase:resumePusher()

	-- Used for synchronizing trash operations with the pusher.
	-- Since the space-feed assets don't end up in the trash, we
	-- only have to do this for the catalog pusher.
	
	check( 'WFCatSyncClientBase' )

	if self._catalogPusher then
		self._catalogPusher:resume()
	end
	
	return Observable.empty()

end

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

function WFCatSyncClientBase:_DEBUG_startMetadataPusher()

	check( 'WFCatSyncClientBase' )
	
	if self._catalogPusher then
		self._catalogPusher:sendUpdatesContinuously()
	end
	
	if self._spaceFeedsManager then
		self._spaceFeedsManager:startSendingUpdates()
	end

	return Observable.empty()
	
end

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

function WFCatSyncClientBase:_DEBUG_stopMetadataPusher()

	check( 'WFCatSyncClientBase' )
	
	if self._catalogPusher then
		self._catalogPusher:stopContinuousUpdates()
	end
	
	if self._spaceFeedsManager then
		self._spaceFeedsManager:stopSendingUpdates()
	end

	return Observable.empty()

end

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

function WFCatSyncClientBase:setUserIsOverQuota( ... )

	check( 'WFCatSyncClientBase' )
	
	if self._catalogPusher then
		self._catalogPusher:setUserIsOverQuota( ... )
	end
	
	return Observable.empty()
	
end

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

function WFCatSyncClientBase:setUserIsExpired( ... )

	check( 'WFCatSyncClientBase' )
	
	if self._catalogPusher then
		self._catalogPusher:setUserIsExpired( ... )
	end
	
	return Observable.empty()
	
end

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

function WFCatSyncClientBase:isUploadingMetadata()

	check( 'WFCatSyncClientBase' )
	
	local isPushingMetadata = Observable.just( false )
	
	if self._catalogPusher then
	
		isPushingMetadata = self._catalogPusher:isDoingWork():distinctUntilChanged()
		
	end
	
	if self._spaceFeedsManager then
	
		isPushingMetadata = Observable.combineLatest( 
				function( isPushingCatalogMetadata, isPushingSpaceFeedMetadata )
					return isPushingCatalogMetadata or isPushingSpaceFeedMetadata
				end, 
				self._catalogPusher:isDoingWork():distinctUntilChanged(),
				self._spaceFeedsManager:isUploadingMetadata() )
				
	end
	
	return isPushingMetadata
	
end

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

function WFCatSyncClientBase:isDownloadingMetadata()

	check( 'WFCatSyncClientBase' )
	
	local isPullingMetadata = self._catalogPuller:isDoingWork()
	
	if self._spaceFeedsManager then
	
		isPullingMetadata = Observable.combineLatest( 
				function( isPullingCatalogMetadata, isPullingSpaceMetadata )
					return isPullingCatalogMetadata or isPullingSpaceMetadata
				end, 
				self._catalogPuller:isDoingWork(),
				self._spaceFeedsManager:isDownloadingMetadata() )
	
	end
	
	return isPullingMetadata
	
end

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

function WFCatSyncClientBase:getLatestDownloadedAssetIdsStream()

	check( 'WFCatSyncClientBase' )
	
	return self._catalogPuller:getLatestDownloadedAssetIdsStream()
	
end

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

function WFCatSyncClientBase:isOzCatalogDeleted()

	check( 'WFCatSyncClientBase' )
	
	return self._catalogPuller:isOzCatalogDeleted()
	
end

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

function WFCatSyncClientBase:getRequiredUpdateStream()

	check( 'WFCatSyncClientBase' )
	
	return self._user:getRequiredUpdateStream()
	
end

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

function WFCatSyncClientBase:getSQLiteFatalErrorCodeStream()

	check( 'WFCatSyncClientBase' )

	local docStore = self._wfCatalog:docStore()
	local indexStore = self._wfCatalog:indexStore()

	return Rx.Observable.merge(
			docStore:getSQLiteFatalErrorCodeStream(),
			indexStore:getSQLiteFatalErrorCodeStream() )

end

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

function WFCatSyncClientBase:checkOzForUpdates()

	check( 'WFCatSyncClientBase' )
	
	self._catalogPuller:checkOzForUpdates()
	
	return Observable.just( true )
	
end

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

function WFCatSyncClientBase:setReadNotification( notificationMarkRead, optNotificationMarkUnread )

	check( 'WFCatSyncClientBase', 'table', '?table' )

	return self._notificationSync:setReadNotification( notificationMarkRead, optNotificationMarkUnread )

end

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

function WFCatSyncClientBase:setVisitedNotification( notificationMarkVisited, optNotificationMarkUnvisited )

	check( 'WFCatSyncClientBase', 'table', '?table' )

	return self._notificationSync:setVisitedNotification( notificationMarkVisited, optNotificationMarkUnvisited )

end

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

function WFCatSyncClientBase:notificationUnreadCount()
	
	check( 'WFCatSyncClientBase' )
	
	return self._notificationSync:notificationUnreadCount()
	
end

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

function WFCatSyncClientBase:notificationList( optWaitUntilSyncCycleComplete )
	
	check( 'WFCatSyncClientBase', '?boolean' )
	
	return self._notificationSync:notificationList( optWaitUntilSyncCycleComplete )
	
end

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

function WFCatSyncClientBase:notificationKillSwitch()
	
	check( 'WFCatSyncClientBase' )
	
	return self._notificationSync:notificationKillSwitch()
	
end
--------------------------------------------------------------------------------

function WFCatSyncClientBase:haltAndDeleteNotificationDb()

	check( 'WFCatSyncClientBase' )
	
	return self._notificationSync:haltAndDeleteNotificationDb()

end

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

--[[!
	\brief This API that can be used gather real-time sync diagnostic data.

	\param optCalm (optional number) The maximum frequency at which the stream will
		fire with updates.

	\return Observable< table > An Observable whose value tracks a table of
		sync diagnostic data.
--]]

function WFCatSyncClientBase:syncDiagnostics( optCalm )

	check( 'WFCatSyncClientBase', '?number' )
	
	local spaceFeedsSyncDiagnostics = Observable.just( nil )
	
	if self._spaceFeedsManager then
		spaceFeedsSyncDiagnostics = self._spaceFeedsManager:syncDiagnostics( optCalm )
	end
		
	return Observable.combineLatest(
			self._catalogPuller:syncDiagnostics( optCalm ),
			self._catalogPusher:syncDiagnostics( optCalm ),
			spaceFeedsSyncDiagnostics
		)

		:map( function( catalogPullerSyncDiagnostics, 
						catalogPusherSyncDiagnostics, 
						spaceFeedsManagerSyncDiagnostics )
		
			return {
			
				catalogPullerSyncDiagnostics = catalogPullerSyncDiagnostics,
				catalogPusherSyncDiagnostics = catalogPusherSyncDiagnostics,
				spaceFeedsManagerSyncDiagnostics = spaceFeedsManagerSyncDiagnostics
				
			}
		
		end )

end

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

function WFCatSyncClientBase:_DEBUG_simulateCorruptionNotificationDb()

	check( 'WFCatSyncClientBase' )
	
	return self._notificationSync:_DEBUG_simulateCorruptionNotificationDb()

end

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

function WFCatSyncClientBase:_DEBUG_SyncClientBaseReturnsTrue()

	check( 'WFCatSyncClientBase' )
	
	return Observable.just( true )

end

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

local function wrapInTag( tag, str )
	return string.format( "<%s>%s</%s>", tag, tostring( str ), tag )
end

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

local function makeDetailedSyncStatusHTML( wfCatalog, docInfoForDetailedStatus )
	
	if #docInfoForDetailedStatus == 0 then
		return Observable.just( '' )
	end
	
	local docIds, docInfosByDocId = WFFunctionUtils.multiReturnReduce( function( docInfo, docIds, docInfosByDocId )
			docIds[ #docIds + 1 ] = docInfo.assetId
			docInfosByDocId[ docInfo.assetId ] = docInfo
			
			return docIds, docInfosByDocId
		end,
		docInfoForDetailedStatus, {}, {} )
		
		
	return wfCatalog:docStore():bulkDocValue( docIds )
		:map( function( docValues )

			local detailedDocInfos = WFFunctionUtils.reduce( {}, 
				function( acc, docValue )
					local basicDocInfo = docInfosByDocId[ docValue._id ]
					-- this should work for assets, deleted assets and cover documents
					local maybeFilename = KSTableUtils.derefOpt( docValue, 'importSource', 'fileName' ) or 
								KSTableUtils.derefOpt( docValue, '_localOnly', 'cover', 'importSource', 'fileName' )
					
					local detailedDocInfo = {}
					detailedDocInfo.assetId = docValue._id
					detailedDocInfo.filename = maybeFilename or ''
					detailedDocInfo.status = basicDocInfo.statusString
					detailedDocInfo.subtype = docValue.subtype or docValue.type or ''
					detailedDocInfo.deleted = not not ( docValue._deleted or docValue.empty )
					
					acc[ #acc + 1 ] = detailedDocInfo
					return acc
				end,
				docValues )
				
				return detailedDocInfos

		end )
		:map( function( detailedDocInfos )
		
			local detailedSyncStatusHTMLRows = 
				wrapInTag( 'tr',
					wrapInTag( 'th', 'document id' ) ..
					wrapInTag( 'th', 'sync status' ) ..
					wrapInTag( 'th', 'subtype' ) ..
					wrapInTag( 'th', 'filename') ..
					wrapInTag( 'th', 'deleted' ) )
			
			detailedSyncStatusHTMLRows = WFFunctionUtils.reduce( detailedSyncStatusHTMLRows, 
				function( acc, docInfo )
					return acc .. 
						wrapInTag( 'tr', 
							wrapInTag( 'td', docInfo.assetId ) ..
							wrapInTag( 'td', docInfo.status ) ..
							wrapInTag( 'td', docInfo.subtype ) ..
							wrapInTag( 'td', docInfo.filename ) ..
							wrapInTag( 'td', docInfo.deleted ) )
				end, 
				detailedDocInfos )

			return wrapInTag( 'table', detailedSyncStatusHTMLRows )
	
		end )
end

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

local function makeRegularSyncStatusHTML( docInfoForRegularStatus )
	
	if #docInfoForRegularStatus == 0 then
		return Observable.just( '' )
	end
	
	local regularSyncStatusHTMLRows = 
		wrapInTag( 'tr',
			wrapInTag( 'th', 'document id' ) ..
			wrapInTag( 'th', 'sync status' ) )
	
	regularSyncStatusHTMLRows = WFFunctionUtils.reduce( regularSyncStatusHTMLRows, 
		function( acc, docInfo )
			return acc .. 
				wrapInTag( 'tr', 
					wrapInTag( 'td', docInfo.assetId ) ..
					wrapInTag( 'td', docInfo.statusString ) )
		end, 
		docInfoForRegularStatus )

	return Observable.just( wrapInTag( 'table', regularSyncStatusHTMLRows ) )
	
end

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

function WFCatSyncClientBase:sendDiagnosticLogData()
	
	local docIdsAndStatusStream
	
	if WFPlatformInfo.isClassic() then
		docIdsAndStatusStream = require 'LrClassicSyncStatus'.getDocIdsAndStatusWithSyncPendingOrError( self._catalog )
	elseif WFPlatformInfo.isDesktop then
		docIdsAndStatusStream = require 'WFCatSyncStatus'.getDocIdsAndStatusWithSyncPendingOrError( self._catalog )
	elseif WFPlatformInfo.isMobile() then
		docIdsAndStatusStream = require 'LrmCatSyncStatus'.getDocIdsAndStatusWithSyncPendingOrError( self._catalog )
	else
		docIdsAndStatusStream = Observable.just({})
	end
	
	local maxDocsShown = 500

	docIdsAndStatusStream

		:take( 1 )

		:map( function( results )

			local docInfoForDetailedStatus, docInfoForRegularStatus = {}, {}

			for i, result in ipairs( results ) do

				if i > maxDocsShown then
					break
				elseif i <= 10 then
					docInfoForDetailedStatus[ #docInfoForDetailedStatus + 1 ] = result
				else
					docInfoForRegularStatus[ #docInfoForRegularStatus + 1 ] = result
				end

			end

			return docInfoForDetailedStatus, docInfoForRegularStatus, #results

		end )

		:flatMap( function( docInfoForDetailedStatus, docInfoForRegularStatus, totalDocs )

			return Observable.combineLatest( 
				function( detailedStatusHTML, regularStatusHTML )

					local notDisplayedDocs = totalDocs - maxDocsShown

					local maybeNotDisplayedText = ''

					if notDisplayedDocs > 0 then

						maybeNotDisplayedText =
								string.format( "<br><p><strong>.. an additional %d documents are in a pending/error state but not listed above.</strong></p>", notDisplayedDocs )

					end

					return detailedStatusHTML ..'<br>' .. regularStatusHTML .. maybeNotDisplayedText

				end, 
				makeDetailedSyncStatusHTML( self._catalog, docInfoForDetailedStatus ), 
				makeRegularSyncStatusHTML( docInfoForRegularStatus ) )
		
		end )
		:map( function( syncStatusHtml )
			
			WFDiagnosticLog.addSectionToLog
				{
					name = "Syncing and Error Assets",
					id = 'SyncStatus',
					weight = 499,
					parentSection = WFDiagnosticLog.Sections.SYNC,
					body = syncStatusHtml,
				}
				
		end )
		:call( WFRxRunUntilDone )
	
	return {}
end

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

--[[!
	\brief Prioritize sync for a specific space
	
	This method pauses the catalog puller for 30 seconds and delegates space sync
	prioritization to the WFCatSpaceFeedsManager.
	
	\param spaceId The ID of the space to prioritize for sync
	
	\return Observable(boolean) Observable that emits true if prioritization was set successfully
--]]

function WFCatSyncClientBase:prioritizeSpaceSync( spaceId )

	check( 'WFCatSyncClientBase', '+string' )

	if not self._spaceFeedsManager then
		logger:warn( "No space feeds manager available for space sync prioritization" )
		return Observable.just( false )
	end

	logger:infof( "Prioritizing sync for space %s", spaceId )

	-- If space prioritization is already active, preserve the original catalog running state

	local catalogWasRunning

	if self._spacePrioritizationTimeoutSubscription then

		-- Preserve the original catalog running state from before any prioritization started

		catalogWasRunning = self._originalCatalogWasRunning or false

	else

		-- No prioritization active, capture the current catalog state

		catalogWasRunning = not self._catalogPuller._syncIsStopped

		-- Store the original state for potential future calls

		self._originalCatalogWasRunning = catalogWasRunning

	end

	logger:info( "Pausing catalog puller for space sync prioritization" )

	self._catalogPuller:stopContinuousUpdates()

	-- Start space sync prioritization through space feeds manager

	logger:infof( "Starting space sync prioritization for space %s", spaceId )

	self._spaceFeedsManager:prioritizeSpaceSync( spaceId )

	-- Common cleanup function for all timer events

	local function cleanupSpacePrioritization( reason )

		logger:info( reason )

		-- Stop space prioritization

		maybeCancelSpacePrioritization( self )

		-- Resume catalog puller if it was running

		if catalogWasRunning then
			logger:info( "Resuming catalog puller" )
			self._catalogPuller:getUpdatesContinuously()
		end

		-- Clear the original catalog running state since prioritization is complete

		self._originalCatalogWasRunning = nil

	end

	-- Cancel any existing timeout subscription

	if self._spacePrioritizationTimeoutSubscription then
		self._spacePrioritizationTimeoutSubscription:dispose()
		self._spacePrioritizationTimeoutSubscription = nil
	end

	-- Use shorter timeout for unit tests

	local kSpacePrioritizationTimeout = WFPlatformInfo.isUnitTest() and 2 or 15

	-- Set up timeout subscription and store reference
	self._spacePrioritizationTimeoutSubscription = Observable.timer( kSpacePrioritizationTimeout, WFRxScheduler.sharedScheduler() )
		:subscribe( 
			function()
				logger:info( kSpacePrioritizationTimeout .. " seconds elapsed for space prioritization" )
				cleanupSpacePrioritization( "No sync calls detected during space prioritization - resuming normal sync behavior" )
			end,
			function( err )
				logger:error( "Error during space prioritization timeout: " .. tostring( err ) )
				cleanupSpacePrioritization( "Error during space prioritization timeout - resuming normal sync behavior" )
			end,
			function()
				logger:info( kSpacePrioritizationTimeout .. " seconds elapsed for space prioritization" )
				cleanupSpacePrioritization( "Space prioritization timeout completed - resuming normal sync behavior" )
			end
		)

	-- Return immediately - no need to wait for completion

	return Observable.just( true )

end

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

return Rx.Class( 'WFCatSyncClientBase', WFCatSyncClientBase )
