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

WFCatUserSynchronizer.lua

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

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

NOTICE: All information contained herein is, and remains 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 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 Systems Incorporated.

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

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

	-- WF.core.rxlua
local WFRxRunUntilDone = require 'WFRxRunUntilDone'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFObjectProxy = require 'WFObjectProxy'
local WFRxContinuousTask = require 'WFRxContinuousTask'
local WFTableMerge = require 'WFTableMerge'
local KSTableUtils = require 'KSTableUtils'

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

	-- WF.core.os
local KSDate = require 'KSDate'

	-- WF.web.json
local KSJsonDecoder = require 'KSJsonDecoder'
local KSJsonEncoder = require 'KSJsonEncoder'

	-- WF.web.utils
local WFURI = require 'WFURI'

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

local check = assert( WFArguments.check )
local derefOpt = assert( KSTableUtils.derefOpt )

local WFCloseSupport_fetchCached = assert( WFCloseSupport.fetchCached )

local runUntilDone = WFRxRunUntilDone

local logger = WFLogger 'WFCatUserSynchronizer'

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

local WFCatUserSynchronizerMethods = {
		
	_wfCatUser = false,
		-- (WFCatUser)
	
	_accountUpdateObservable = false,
		-- ( Rx.Observable ) 
		
	_continuousSyncTask = false,
		-- (WFRxContinuousTask) A continuous task to handle continuous user account updates

	_updateForceLightroomUpgradeAttempts = false,


	WFCloseSupport.addCloseSupportMethods,
}

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

local function tryToUpdateSettings( wfCatUser )

	local payloadTable = {
		lightroom = {
			force_lightroom_upgrade = true
		}
	}
	
	local payloadJson = KSJsonEncoder.encodeOzUploadJson( payloadTable )

	return wfCatUser:put( string.format( "%s/settings", wfCatUser:getAccountUrl() ),
				payloadJson,
				{
					requestMethod = 'POST',
					returnHttpErrorsAsData = true,
					requestHeaders = {
						{
							header = "Content-Type",
							value = "application/json",
						},
					}
				} )

end

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

local function maybeEnsureForceLightroomUpgradeFlagIsSet( self, userAccountDoc,
														doSetForceUpgradeFlag,
														wfCatUser )
	
	if not doSetForceUpgradeFlag or not userAccountDoc then
		return Observable.just()
	end

	local isUpdateNeeded = not ( KSTableUtils.derefOpt( userAccountDoc,
										"payload", "lightroom",
										"force_lightroom_upgrade" ) == true )
	
	if isUpdateNeeded and self._updateForceLightroomUpgradeAttempts < 3 then
		self._updateForceLightroomUpgradeAttempts =
								self._updateForceLightroomUpgradeAttempts + 1
		return tryToUpdateSettings( wfCatUser )
	else
	
		if isUpdateNeeded then
			logger:warnf( "gave up on updating lightroom account flag" )
		end
		
		return Observable.just()
	end

end

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

local function updateUserAccountDocument( self )
	
	check( 'WFCatUserSynchronizerImp' )
	
	local wfCatUser = self._wfCatUser
	
	return wfCatUser:getAccountDocFromOz( )
		:flatMap( function( userAccountDoc )
			local userDocStore = wfCatUser:userInfoCache()
			
			if userDocStore then
				return userDocStore:localDocValue( "userHasPerformedMigration" )
				:map( function( docValue )
					local doSetForceUpgradeFlag
					if docValue and docValue.lastUpdated then
						doSetForceUpgradeFlag = true
					end
					
					return userAccountDoc, doSetForceUpgradeFlag
				end )
			end
			
			return Observable.just( userAccountDoc )
		end )
		:flatMap( function( userAccountDoc, doSetForceUpgradeFlag )
			return maybeEnsureForceLightroomUpgradeFlagIsSet( self,
											userAccountDoc, doSetForceUpgradeFlag,
											wfCatUser )
		end )
		:catch( Rx.Observable.just( nil ) )		
	
end

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

-- @mark Update Oz endpoint index document

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

local kRefreshLocalEndpointIndex = true
local kDontRefreshLocalEndpointIndex = false

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

local function isEndpointIndexOutOfDate( lastUpdated )

	if not lastUpdated then
		return true
	end
			
	local nowDate = KSDate.timeInIso8601Format( nil, false )
	
	local differenceInDays = KSDate.differenceInDays( lastUpdated, nowDate )
			
	return differenceInDays > 0
	
end

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

local function compareLocalAndOzIndexDocument( ozIndexDocValue, localIndexDocValue )
	
	local localLinks = derefOpt( localIndexDocValue, 'links' )
	local ozLinks = derefOpt( ozIndexDocValue, 'links' )
	
	local lastUpdated = derefOpt( localIndexDocValue, 'lastUpdated' )
	
	-- if there are no changes on Oz, keep the local version
	
	if KSTableUtils.deepEquals( localLinks, ozLinks ) then
	
		local doUpdateLocally = isEndpointIndexOutOfDate( lastUpdated )
		
		return localIndexDocValue, doUpdateLocally and kRefreshLocalEndpointIndex or kDontRefreshLocalEndpointIndex
		
	end
	
	-- if we don't have the endpoint document locally
	
	if not localLinks then
		return ozIndexDocValue, kRefreshLocalEndpointIndex
	end
	
	if not ozLinks then
		return localIndexDocValue, kDontRefreshLocalEndpointIndex
	end
	
	local overlay = WFTableMerge.threeWayMerge( localLinks, localLinks, ozLinks )
	
	localIndexDocValue.links = overlay
	
	return localIndexDocValue, kRefreshLocalEndpointIndex
	
end

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

local function updateEndpointIndexDocument( self )

	check( 'WFCatUserSynchronizerImp' )
	
	local endpointIndexDocId = 'Oz-EndpointIndexDocument'
	
	local wfCatUser = self._wfCatUser
	local userDocStore = wfCatUser:userInfoCache()
	
	-- fetch endpoint document from User DocStore
	
	local localIndexDocValue = userDocStore
		
		:localDocValue( endpointIndexDocId )
		
		:map( function( docValue )
		
			local docCopy = KSTableUtils.deepCopy( docValue )
			
			docCopy._empty = nil
			
			return docCopy
			
		end )
		
		:shareReplay( 1 )
	
	-- maybe fetch index document from Oz
	
	local ozIndexDocValueObservable = localIndexDocValue
	
		:pluck( 'lastUpdated' )
		
		:map( isEndpointIndexOutOfDate )
	
		:flatMapLatest( function( fetchFromOz )
		
			if fetchFromOz then
			
				local indexUrl = WFURI.resolve( wfCatUser:serverBaseUrl(), "/v2/" )
				
				return wfCatUser:get( indexUrl )
				
					:map( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty )
					
					:map( function( indexDocValue )
						
						indexDocValue._id = endpointIndexDocId
						
						return indexDocValue
						
					end )
					
					:catch( Rx.Observable.just( {} ) )
					
			else
				return localIndexDocValue
			end
			
		end )

	-- 

	return Rx.Observable.combineLatest( compareLocalAndOzIndexDocument, ozIndexDocValueObservable, localIndexDocValue )
	
		:flatMapLatest( function( endpointDocValue, refreshLocalEndpointIndex )
		
			if refreshLocalEndpointIndex then
			
				endpointDocValue.lastUpdated = KSDate.timeInIso8601Format( nil, false )
				
				return userDocStore:setLocalDocValue( endpointDocValue )
					:concat( Rx.Observable.just( endpointDocValue ) )
			
			else
			
				return Rx.Observable.just( endpointDocValue )
				
			end
			
		end )
	
end

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

-- @mark WFCatUserSynchronizer

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

local function updateDocuments( self )

	return updateUserAccountDocument( self )
		
		:flatMapLatest( function()
			
			return updateEndpointIndexDocument( self )
			
		end )
end

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

function WFCatUserSynchronizerMethods:__init( wfCatUser )
	
	check( 'WFCatUserSynchronizerImp', 'WFCatUser' )
	
	self._updateForceLightroomUpgradeAttempts = 0
	
	self._wfCatUser = wfCatUser
	
	local continuousSyncTask = WFRxContinuousTask( "WFCatUserSynchronizer", 
		function() 
			return updateDocuments( self ) 
		end, 
		{
			progressiveBackoff = false,
			noWorkDelay = 60,
			maxNoWorkDelay = 120,
			errorDelay = 240,
			maxDelay = 240,
		} )
	
	self:setOwned {
		_continuousSyncTask = continuousSyncTask,
	}

end

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

function WFCatUserSynchronizerMethods:userId()
	
	check( 'WFCatUserSynchronizerImp' )
	
	if self._wfCatUser then
		return self._wfCatUser:userId()
	end

end

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

function WFCatUserSynchronizerMethods:close()
	
	self._continuousSyncTask:stopContinuousTask()
	
	if self._wfCatUser then
		self._wfCatUser:close()
		self._wfCatUser = nil
	end
	
end

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

function WFCatUserSynchronizerMethods:stopContinuousUpdates()

	self._continuousSyncTask:stopContinuousTask()

end

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

function WFCatUserSynchronizerMethods:startContinuousUpdates()

	self._continuousSyncTask:startContinuousTask()

end

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

function WFCatUserSynchronizerMethods:updateEndpointIndexDocumentOnce()

	return updateEndpointIndexDocument( self )
	
end

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


function WFCatUserSynchronizerMethods:updateUserAccountDocumentOnce()

	return updateUserAccountDocument( self )
	
end

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

function WFCatUserSynchronizerMethods:triggerForceLightroomClassicUpgradeFlag()

	local wfCatUser = self._wfCatUser
	
	if wfCatUser then
		local userDocStore = wfCatUser:userInfoCache()
	
		if userDocStore then
    		userDocStore:setLocalDocValue {
        		_id = "userHasPerformedMigration",
        		lastUpdated = KSDate.timeInIso8601Format( nil, false )
    		}
    		:tapOnCompleted( function()
        		updateDocuments( self )
    		end )
    		:call( runUntilDone )
	
		else
			updateDocuments( self )
		end
	end

end

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

local WFCatUserSynchronizerImp = Rx.Class( 'WFCatUserSynchronizerImp', WFCatUserSynchronizerMethods )

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

local function validateCached( cache, catUser, userId, serverBase )

	local cachedSynchronizer = WFCloseSupport_fetchCached( cache, catUser:userId() )
	
	local oldServerBase = cachedSynchronizer and cachedSynchronizer._serverBase
	
	if cachedSynchronizer
			and ( oldServerBase ~= serverBase ) then
			
			-- Don't reuse a user that doesn't match all arguments
		cache[ userId ] = nil
		cachedSynchronizer = nil
	end
	
	return userId, cachedSynchronizer
	
end

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

local WFCatUserSynchronizer = WFObjectProxy( 'WFCatUserSynchronizer', WFCatUserSynchronizerImp, validateCached, true )

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

return WFCatUserSynchronizer
