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

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 )
					:map( function()
						return endpointDocValue
					end )

			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
