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

LrClassicUserModel.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2023 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.

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

	-- Rx
local Rx = require 'Rx'
local BehaviorSubject = Rx.BehaviorSubject
local ReplaySubject = Rx.ReplaySubject
local Observable = Rx.Observable

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

	-- WF.core.diagnostic_log
local WFDiagnosticLog = require 'WFDiagnosticLog'

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

	-- WF.core.os
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'
local WFInsomnia = require 'WFInsomnia'

	-- WF.core.utils

local KSTableUtils = require 'KSTableUtils'
local WFConfiguration = require 'WFConfiguration'
local KSIdGenerator = require "KSIdGenerator"
local KSPrefs = require 'KSPrefs'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'

	-- WF.oz_client.managed_catalog
local LrClassicCatalog = require 'LrClassicCatalog'
local WFCatSpec = require 'WFCatSpec'
local WFCatStorage = require 'WFCatStorage'
local WFCatSynchronizer = require 'WFCatSynchronizer'
local WFCatSyncStatus = require 'WFCatSyncStatus'
local WFCatUser = require 'WFCatUser'
local WFCatUserSynchronizer = require 'WFCatUserSynchronizer'
local WFCatAvatar = require 'WFCatAvatar'
local WFCatAlbums = require 'WFCatAlbums'
local NmbTrackedDataBroadcaster = require 'NmbTrackedDataBroadcaster'
local NmbDbCorruptionUtils = require "NmbDbCorruptionUtils"

	-- WF.lrclassic
local LrClassicBinaryDownloader = require "LrClassicBinaryDownloader"
local LrClassicVolumeAgent = require 'LrClassicVolumeAgent'
local LrClassicDevelopSettings = require 'LrClassicDevelopSettings'
	-- WF.oz_client.ims
local WFImsAppIdentity = require 'WFImsAppIdentity'

	-- WF.oz_client.sync_engine
local WFOzUserLogin = require 'WFOzUserLogin'

	-- WF.web.http_client
local WFReachability = require 'WFReachability'

	-- WF.web
local KSJsonDecoder = require "KSJsonDecoder"

	-- WF trash
local WFCatTrash = require 'WFCatTrash'

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

local kGlobalDocumentId = '00000000000000000000000000000000'

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

local logger = WFLogger 'LrClassicUserModel'

local DEBUG_OZ_IS_DOWN = WFConfiguration.Debug.debugOzIsDown

local USE_NON_CC_CREDENTIALS = WFConfiguration.LrMobile.useNonCCCredentials

local kClientUpgradeRequired = "upgrade_required"

local kReadOnlyThreadCount = WFConfiguration.WFRxIndexStore.readOnlyThreadCount or 2

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

local function addModelValueToDiagnosticLog( modelValue )

	WFDiagnosticLog.addLogSectionFunc( function()

		local modelValue_copy = {}

		for k, v in pairs( modelValue ) do
			if type( v ) == 'function' then
				modelValue_copy[ k ] = v()
			else
				modelValue_copy[ k ] = v
			end
		end

		modelValue_copy.email = nil
		modelValue_copy.fullName = nil
		modelValue_copy.firstName = nil
		modelValue_copy.lastName = nil

		WFDiagnosticLog.addSectionToLog
		{
			name = "Oz logged in user",
			id = 'LrClassicUserModel',
			weight = -100,

			body = modelValue_copy,
		}

	end )

end

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

local function takeOneValueAndRunFunc( observable, functionToRun )

	observable
		:take( 1 )
		:tap( functionToRun )
		:call( runUntilDone )

end

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

local function logoutUser( closingUser )

	closingUser:logout()

		:map( function()
			return closingUser:close()
		end )

		:call( runUntilDone )

end

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

local wfCatUser_now

local function createCurrentUserStream( serverBaseUrlStream, userIdStream, userInfoCachePath )

	local previousUser

	return Observable.combineLatest( function( serverBaseUrl, userId )

		logger:debugf( "currentUser from serverBaseUrl %s, userId %s via %s",
					   serverBaseUrl, userId, ':traceback:' )

		local newUser

		if serverBaseUrl and userId then
			newUser = WFCatUser( serverBaseUrl, userId, userInfoCachePath )
		end

		if previousUser and previousUser ~= newUser then
			logoutUser( previousUser )
		end

		previousUser = newUser

		wfCatUser_now = newUser

		return newUser

	end,
	serverBaseUrlStream:distinctUntilChanged(), userIdStream:distinctUntilChanged() )
	:replay( nil, 1 )
	:refCount()

end

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

local function createHasValidConnectionToOz( currentUserStream )

	return currentUserStream

		:flatMap( function( currentUser )

			if currentUser then
				return currentUser:hasValidConnectionToOz()
			else
				return Observable.just( false )
			end

		end )

end

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

local function getUserIdStream( imsOverrides )

	if imsOverrides and imsOverrides.imsClientId then
		WFImsAppIdentity.setClientId( imsOverrides.imsClientId )
	end

	local userIdStream = ReplaySubject( 1 )

	userIdStream
		:call( runUntilDone )

	return userIdStream

end

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

local wfCatSynchronizer

local function startSyncingUserAndCatalog( modelData )

	modelData._shouldSyncUserInfoStream:onNext( true )

	if wfCatSynchronizer then
		wfCatSynchronizer:startSyncing()
	end

end

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

local function stopSyncingUserAndCatalog( modelData )

	modelData._shouldSyncUserInfoStream:onNext( false )

	if wfCatSynchronizer then
		wfCatSynchronizer:stopSyncing()
	end

end

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

local function activateOnlineMode( modelData )

	logger:info( "Online mode activated" )

	modelData._isOnlineStream:onNext( true )

	if not KSPrefs.syncPaused then
		startSyncingUserAndCatalog( modelData )
	end

end

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

local function activateOfflineMode_imp( modelData, userId )

	modelData._isOnlineStream:onNext( false )

	stopSyncingUserAndCatalog( modelData )

	if not userId then
		logger:infof( "Offline mode requested, but no stored user ID. Ignoring." )
		return
	end

	modelData._userIdStream:onNext( userId )

end

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

local function activateOzLoginFailureMode( modelData, userId )

	logger:warnf( "offline mode activated with user id %s due to Oz login failure", userId )
	activateOfflineMode_imp( modelData, userId )

end

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

local function activateOfflineMode( modelData, userId )

	logger:warnf( "offline mode activated with user id %s due to network network connectivity problem", userId )
	activateOfflineMode_imp( modelData, userId )

end

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

local function monitorLoginTask( modelData, loginTaskObservable )

	modelData._loginStatusStream:onNext( { status = 'pending' } )

	local observable = loginTaskObservable

	if DEBUG_OZ_IS_DOWN then
		observable = Observable.throw( 'Oz is down!', 'server_error' )
	end

	observable

		:flatMapLatest( function( wfCatUser, userInfo, accessToken, refreshToken, optDeviceToken, optPerfData )

			if optPerfData then

				NmbTrackedDataBroadcaster.broadcastMessage( 'OzAuthenticationPerfData', optPerfData )

			end

			logger:debugf( "Storing tokens for new user %s", userInfo )

			return wfCatUser:storeTokens( accessToken, refreshToken, optDeviceToken )
				:concat( Observable.just( wfCatUser ) )

		end )

		:tap(

			function( wfCatUser )

				local _userId = wfCatUser:userId()

				logger:infof( "Successfully logged in to Oz with user ID %s",
							  _userId  )

				modelData._userIdStream:onNext( _userId )

				-- clear out any prior error if we are successful
				modelData._ozLoginServerErrorStream:onNext( nil )

				modelData._loginStatusStream:onNext( { status = "success" } )

			end,

			function( message, errCode, isRetryable, retryDelay, errorData )

				logger:errorf( "Failed to log in to Oz: %s, ( code: %s)", message, errCode )

				if isRetryable == false then

					local errorMessage = message

					local ok, errorInfo = pcall( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty, message, true )

					if ok then
						errorMessage = KSTableUtils.derefOpt( errorInfo, "code" ) or message
						local description = KSTableUtils.derefOpt( errorInfo, "description" )

						if errorMessage == 4999 and description == "Upgrade required" then
							modelData._isUpdateRequiredStream:onNext( errorInfo )
						end
					end

					modelData._loginStatusStream:onNext( { status = "error", errorMessage = errorMessage } )

				elseif errCode == 'server_error' and errorData then

					local ok, errorInfo = pcall( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty, errorData )

					if ok then
						local code = KSTableUtils.derefOpt( errorInfo, "code" )
						if code == 'MaintenanceError' then

							local outageInfo = {
								id = 'catalog',
								isUnderMaintenance = true,
								accountAndErrorInfo = errorInfo,
							}

							modelData._isOutageOccurringStream:onNext( outageInfo )

						end
					end

				end

				--[[
					SPAM [jandrew 2018-07-24]: When Oz or IMS is down, the call to store tokens
					will throw an error. The error codes can be found in WFOzUserLogin. The possible
					values are:

					"client_error" -- Occurs when the login credentials are wrong ( HTTP 400 -> 500 )
					"server_error" -- Occurs when there is an unexpected Oz error.
					"connection_error" -- General network failures.
					"failure" -- Occurs when the response from IMS/Oz is invalid.

					In all the cases but the "client_error", we want to put the user in offline mode.
				]]--

				if errCode ~= 'client_error' then
					modelData._ozLoginServerErrorStream:onNext( string.format( "login server error '%s': %s", errCode, message ) )
				elseif isRetryable == false then
					modelData._userIdStream:onNext( nil )
				end

			end )

			:retryWhen( function( failure )
				return failure:flatMap( function( errorMessage, code, isRetryable, retryDelay )

					if isRetryable then

						-- Default to retry every minute if not present
						retryDelay = retryDelay or 60

						return Observable.just( true )
							:delay( retryDelay, WFRxScheduler.sharedScheduler() )

					end

					return Observable.throw( errorMessage, code, isRetryable, retryDelay )

				end )

			end )

		:call( runUntilDone )

	-- TODO [scouten 2015-07-22]: This subscription should probably
	-- have a lifetime so we can cancel it.

end

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

local function loginFromTokens( modelData, _,
								baseServerUrl,
								accessToken,
								refreshToken,
								optDeviceToken )

	logger:infof( "loginFromTokens url = %s", baseServerUrl )

	if optDeviceToken and #optDeviceToken == 0 then
		optDeviceToken = nil
	end

	local loginResult = WFOzUserLogin.loginFromTokens( modelData._userInfoCachePath,
														baseServerUrl,
														accessToken,
														refreshToken,
														optDeviceToken )

	monitorLoginTask( modelData, loginResult )

end

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

local function currentUserFieldValueForPath( modelData, memberFieldPath )

	return modelData._currentUserStream and modelData._currentUserStream

		:map( function( wfCatUser )

			if wfCatUser then
				return wfCatUser:userInfoCache():ongoingFieldValue( wfCatUser:userId(), memberFieldPath )
			else
				return Observable.just( nil )
			end

		end )

		:switch()

end

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

local function distinctUserCompare( _, user1, _, user2 )

	return KSTableUtils.deepEquals( user1, user2 )

end

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

local function watchForRequiredUpdate( modelData, myWFCatSynchronizer )

	local requiredUpdateStreamSub = myWFCatSynchronizer:getRequiredUpdateStream()
		:subscribe( function( updateRequired )

			if updateRequired then

				modelData._isUpdateRequiredStream:onNext( true )

				if myWFCatSynchronizer then
					KSPrefs.syncPaused = true
					stopSyncingUserAndCatalog( modelData )
				end

			end

		end )

	myWFCatSynchronizer:addCloseAction( function()
		requiredUpdateStreamSub:dispose()
		requiredUpdateStreamSub = nil
	end )

end

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

local function restrictedCountryMode( modelData )

	KSPrefs.restrictedCountryMode = true

	modelData._restrictedCountryModeStream:onNext( true )

	wfCatSynchronizer:enableRestrictedCountryMode()

	wfCatSynchronizer:stopSyncing()

	if KSPrefs.syncPaused then
		KSPrefs.syncPaused = false
	end

end

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

local function checkForRestrictedCountry( wfCatUser, modelData )

	local chineseAccountCheckSub = wfCatUser:getCountryCodeForUser()
			:subscribe( function( countryCode )

				local chinaCountryCode = 'CN'

				if countryCode == chinaCountryCode then
					restrictedCountryMode( modelData )
				else
					KSPrefs.restrictedCountryMode = false
				end

			end )

	wfCatUser:addCloseAction( function()
		chineseAccountCheckSub:dispose()
		chineseAccountCheckSub = nil
	end )

end

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

local function getFreemiumCatalogBaseDir( rootCatalogDataDir )

	return KSPathUtils.child( rootCatalogDataDir, kGlobalDocumentId )

end

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

local function maybeMigrateFreemiumCatalog ( wfCatalogSpec, rootCatalogDataDir, simulateMoveError)

	local catalogID = wfCatalogSpec:catalogId()

	if catalogID ~= kGlobalDocumentId then

		local migratedCatalogBaseDir = KSPathUtils.child( rootCatalogDataDir, catalogID)
		local freemiumCatalogBaseDir = getFreemiumCatalogBaseDir( rootCatalogDataDir )

		if not WFFileUtils.exists( migratedCatalogBaseDir ) and WFFileUtils.exists( freemiumCatalogBaseDir ) then

			-- we are guaranteed that we are on the same Volume for large file moves
			local mkdirSuccess, mkdirMaybeError = pcall(WFFileUtils.mkdir, migratedCatalogBaseDir)

			if mkdirSuccess and WFFileUtils.exists( migratedCatalogBaseDir ) then

				-- MOVE FILES

				local filesAndFoldersToMove = {
					-- Files --
					"Managed Catalog.mcat",
					"Managed Catalog.mcat-wal",
					"Managed Catalog.wfindex",
					"Managed Catalog.wfindex-wal",
					"Managed Catalog.mru",
					-- Folders --
					"settings",
					"cr_settings",
					"cr_defaults",
					"cr_cameraprofiles",
					"people",
					"previews",
					"xmp"
				}

				-- check if files/folder exist and move
				for _, item in ipairs ( filesAndFoldersToMove ) do
					local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
					-- check if files exist and copy
					if WFFileUtils.exists( itemPath ) then
						local fileMoveSuccess
						local fileMoveMaybeError
						if not simulateMoveError then
							fileMoveSuccess, fileMoveMaybeError = pcall(WFFileUtils.move, itemPath, KSPathUtils.child( migratedCatalogBaseDir, item))
						else
							fileMoveSuccess = false
							fileMoveMaybeError = "SimulatedMoveError"
						end
						if not fileMoveSuccess then
							logger:errorf("Error during catalog transfer: %s, %s", item, fileMoveMaybeError)
						end

					else
						logger:warnf("Catalog migration could not find and move file: %s.", item)
					end
				end

				-- DELETE FILES

				-- Some files cannot move because they depend on the absolute paths
				-- We need to clear these files from the local catalog to ensure the
				-- next local catalog user does not use old database.
				local filesAndFolderToDelete = {
					"previews.db"
				}

				-- Delete files and folders that cannot be transfered
				for _, item in ipairs ( filesAndFolderToDelete ) do
					local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
					-- check if files exist and move
					if WFFileUtils.exists( itemPath ) then

						if WFFileUtils.exists( itemPath ) == 'directory' and not ( WFFileUtils.empty( itemPath ) ) then

							logger:warnf("Error during catalog transfer: %s %s", item, "directory is not empty. Will not delete.")

						elseif WFFileUtils.exists( itemPath ) == 'directory' and ( WFFileUtils.empty( itemPath ) ) then
							-- deletable directory.
							local moveToTrashSuccess, moveToTrashMaybeError = pcall(WFFileUtils.delete, itemPath, true)

							if not moveToTrashSuccess then
								logger:errorf("Error during catalog transfer: %s, %s", item, moveToTrashMaybeError)
							end

						else
							-- is not a directory
							local moveToTrashSuccess, moveToTrashMaybeError = pcall(WFFileUtils.moveToTrash, itemPath)

							if not moveToTrashSuccess then
								logger:errorf("Error during catalog transfer: %s, %s", item, moveToTrashMaybeError)
							end
						end

					else
						logger:warnf("Catalog migration could not find and remove file: %s.", item)
					end
				end

				-- BACKUP RESIDUAL FILES

				-- Check and clear the local catalog in order to have a clean directory for the next freemium user
				local backupFolderBaseDir = KSPathUtils.child( freemiumCatalogBaseDir, "backup" )
				local backupMkdirSuccess, backupMkdirMaybeError = pcall(WFFileUtils.mkdir, backupFolderBaseDir )

				-- either succesfully created or already exists
				if backupMkdirSuccess or WFFileUtils.exists( backupFolderBaseDir ) then
					-- Check in the move list
					for _, item in ipairs ( filesAndFoldersToMove ) do
						local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
						-- check if files exist and move to backup
						if WFFileUtils.exists( itemPath ) then
							local maybeUniqueFileName = WFFileUtils.ensureFileNameIsUnique(item)
							local fileBackupSuccess, fileBackupMaybeError = pcall(WFFileUtils.move, itemPath, KSPathUtils.child( backupFolderBaseDir, maybeUniqueFileName))

							if not fileBackupSuccess then
								logger:errorf("Error during catalog transfer backup: %s, %s", item, fileBackupMaybeError)
							end

						else
							logger:infof("Catalog migration could not find and backup file: %s.", item)
						end
					end

					-- Check in the delete list
					for _, item in ipairs ( filesAndFolderToDelete ) do
						local itemPath = KSPathUtils.child( freemiumCatalogBaseDir, item )
						if WFFileUtils.exists( itemPath ) then

							local maybeUniqueFileName = WFFileUtils.ensureFileNameIsUnique(item)
							local fileBackupSuccess, fileBackupMaybeError = pcall(WFFileUtils.move, itemPath, KSPathUtils.child( backupFolderBaseDir, maybeUniqueFileName))

							if not fileBackupSuccess then
								logger:errorf("Error during catalog transfer backup: %s, %s", item, fileBackupMaybeError)
							end

						else
							logger:warnf("Catalog migration could not find and backup file: %s.", item)
						end
					end

				else

					logger:errorf("Error during catalog transfer: %s", backupMkdirMaybeError)

				end

			else
				-- Something Went Wrong creating the new catalog base directory
				logger:errorf("Error during catalog transfer: %s", mkdirMaybeError)
			end

			return true

		end
	end

	return false

end

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

local function createOzCatalog( wfCatUser, wfCatalogSpec, maybeProfileCatalogSpec,
		rootCatalogDataDir, pixelProviderSettings, modelData, syncState )

	local catalogOptions = {
		indexSpecScript = 'LrClassicIndexDefinitions.lua',
		indexStoreReadThreads = kReadOnlyThreadCount,
		docStoreInitProgressRequested = true,
	}

	-- Set Up Freemium

	local isFreemium = wfCatalogSpec:catalogId() == kGlobalDocumentId

	local wfCatalog = LrClassicCatalog( wfCatalogSpec, rootCatalogDataDir, catalogOptions )

	-- If a catalog is freemium. We do not get a local document back from Oz.
	-- Instead, create a dummy document so that docValue._empty == false.
	if isFreemium then
		local catalogDoc = {
			_id = kGlobalDocumentId,
			type = 'catalog',
			subtype = 'lightroom',
			}
		wfCatalog:docStore():setDocValue( catalogDoc ):call( runUntilDone )
	end

	local pixelProviderProcessor = {}

	local maybeProfileCatalogOptions
	local maybeProfileCatalogIdOrDoc
	local maybeProfileCatalog
	if maybeProfileCatalogSpec then

		local profilesDir = KSPathUtils.child( rootCatalogDataDir, 'profiles' )
		local profileCatalogPath = KSPathUtils.child( profilesDir, maybeProfileCatalogSpec:catalogId() .. '.mcat' )
		maybeProfileCatalogOptions = {
			docStorePath = profileCatalogPath,
			indexSpecScript = 'NmbProfilesIndexDefinitions.lua',
			noIdleDownloader = true,
			noCatalogSearch = true,
		}

		maybeProfileCatalog = require 'NmbProfileCatalog'( maybeProfileCatalogSpec, rootCatalogDataDir,
							maybeProfileCatalogOptions )
		if isFreemium then
			maybeProfileCatalogIdOrDoc = maybeProfileCatalogSpec:catalogDoc() or maybeProfileCatalogSpec:catalogId()
		else
			maybeProfileCatalogIdOrDoc = maybeProfileCatalogSpec:catalogDoc() or maybeProfileCatalogSpec:ozCatalogId()
		end
	end


	local workerThreadArgs = { wfCatUser:serverBaseUrl(),
								wfCatUser:userId(),
								wfCatUser:userInfoCacheFolder(),
								wfCatalogSpec:catalogDoc() or wfCatalogSpec:catalogId(),
								rootCatalogDataDir,
								catalogOptions,
								maybeProfileCatalogIdOrDoc,
								maybeProfileCatalogOptions,
							}

	wfCatSynchronizer = WFCatSynchronizer( wfCatalog, maybeProfileCatalog, nil, workerThreadArgs,
		syncState, KSPrefs.residualMode, KSPrefs.freemiumMode, modelData._locale )

	wfCatalog:addCloseAction( function()

		wfCatSynchronizer:close()
		wfCatSynchronizer = nil

	end )

	local lrClassicBinaryDownloader = LrClassicBinaryDownloader( wfCatalog )

	wfCatalog:addCloseAction( function()

		lrClassicBinaryDownloader:close()
		lrClassicBinaryDownloader = nil

	end )

	local docStoreUpdater = wfCatalog:getDocStoreUpdater()
	docStoreUpdater:setSynchronizer( wfCatSynchronizer )
	docStoreUpdater:start()

	local catalogValidationSub = wfCatSynchronizer:catalogValidationStream()
		:subscribe( function( isValid )
			modelData._catalogValidationStatusStream:onNext( isValid )
		end )

	local isUploadingMetadataSub = wfCatSynchronizer:isUploadingMetadata()
		:subscribe( function( isUploading )
			modelData._isSyncUploadingMetadataStream:onNext( isUploading )
		end )

	local isDownloadingMetadataSub = wfCatSynchronizer:isDownloadingMetadata()
		:subscribe( function( isDownloading )
			modelData._isSyncDownloadingMetadataStream:onNext( isDownloading )
		end )

	local isUploadingBinariesSub = wfCatSynchronizer:isUploadingBinaries()
		:subscribe( function( isUploading )
			modelData._isSyncUploadingBinariesStream:onNext( isUploading )
		end )

	local isSyncPendingSub = WFCatSyncStatus.syncPendingForAnyAsset( wfCatalog )
		:subscribe( function( isPending )
			modelData._isSyncPendingStream:onNext( isPending )
		end )

	local isPendingRestoreSub = WFCatSyncStatus.getNumAssetsWithRestorationPending( wfCatalog )
		:subscribe( function( isPendingRestore )
			modelData._isPendingRestoreStream:onNext( isPendingRestore )
		end )

	local getOzMaintenanceNotificationStreamSub = wfCatSynchronizer:getOzMaintenanceNotificationStream()
		:subscribe( function( ozMaintenanceInfo )

			local catalogId = ozMaintenanceInfo and ozMaintenanceInfo.id
			local maintenanceActive = ozMaintenanceInfo and ozMaintenanceInfo.isUnderMaintenance
			local errorAndAccountInfo = ozMaintenanceInfo and ozMaintenanceInfo.accountAndErrorInfo

			if catalogId == 'catalog' then

				local outageInfo -- table of errorAndAccountInfo or false

				if maintenanceActive then
					outageInfo = errorAndAccountInfo or {}
				else
					outageInfo = false
				end

				modelData._isOutageOccurringStream:onNext( outageInfo )

			else

				if maintenanceActive then
					modelData._spaceOutages[ catalogId ] = true
				else
					modelData._spaceOutages[ catalogId ] = nil
				end

				modelData._spaceOutageOccurringStream:onNext( modelData._spaceOutages )

			end

		end )

	local havePersistentSyncErrorSub = WFCatSyncStatus.persistentSyncErrorForAnyAsset( wfCatalog )
		:subscribe( function( haveError )
			modelData._havePersistentSyncErrorStream:onNext( haveError )
		end )

	local isInitialSyncSub = WFCatSyncStatus.isDoingInitialSync( wfCatalog )
		:subscribe( function( isInitialSyncActive )
			modelData._isInitialSyncStream:onNext( isInitialSyncActive )
		end )

	local isOutOfStorageSub = WFCatStorage.overStorageQuota( wfCatalog )
		:subscribe( function( isOverQuota )
			modelData._isOutOfOzStorageStream:onNext( isOverQuota )
		end )

	local isLowOnStorageSub = WFCatStorage.lowOnStorage( wfCatalog )
		:subscribe( function( isLow )
			modelData._isLowOnOzStorageStream:onNext( isLow )
		end )

	local deletedCatalogSub = wfCatSynchronizer:isOzCatalogDeleted()
		:filter( function( isDeleted )
			return isDeleted
		end )
		:subscribe( function( isDeleted )
			modelData._deletedCatalogStream:onNext( isDeleted )
		end )

	local sleepPreventionSubscription = Observable.combineLatest(
			modelData._isActivelySyncingStream, modelData._shouldPreventSleepWhileSyncingStream )
		:filter( function( isSyncing, shouldPreventSleepWhileSyncing )
			return isSyncing and shouldPreventSleepWhileSyncing ~= false
		end )
		:subscribe( function()
			WFInsomnia.preventSystemSleep( "Syncing" )
				-- Prevent the system from sleeping when syncing while
				-- on AC Power. This will not prevent sleep if the user
				-- is on Battery Power.
				-- preventSystemSleep prevents the system from
				-- sleeping for ~30 seconds.
		end )

	-- Set up Trash Auto Purge

	local autoPurgeSubscription = wfCatUser:isSubscriptionExpired()
		:map(function( isSubscriptionExpired )
			-- Allow trash purging for freemium and residual (trial_exired and subscription_expired) accounts
			if isSubscriptionExpired or isFreemium then
				return true
			end
			return false
		end	)
		:distinctUntilChanged()
		:flatMap( function( autoPurgeAll )
			local autoPurgeOnlyIfNotOnOz = not autoPurgeAll
			return WFCatTrash.autoPurge( wfCatalog, autoPurgeOnlyIfNotOnOz )
		end )
		:subscribe()

	wfCatSynchronizer:addCloseAction( function()

		catalogValidationSub:dispose()
		catalogValidationSub = nil

		isUploadingMetadataSub:dispose()
		isUploadingMetadataSub = nil

		isDownloadingMetadataSub:dispose()
		isDownloadingMetadataSub = nil

		isUploadingBinariesSub:dispose()
		isUploadingBinariesSub = nil

		isSyncPendingSub:dispose()
		isSyncPendingSub = nil

		isPendingRestoreSub:dispose()
		isPendingRestoreSub = nil

		getOzMaintenanceNotificationStreamSub:dispose()
		getOzMaintenanceNotificationStreamSub = nil

		havePersistentSyncErrorSub:dispose()
		havePersistentSyncErrorSub = nil

		isInitialSyncSub:dispose()
		isInitialSyncSub = nil

		isOutOfStorageSub:dispose()
		isOutOfStorageSub = nil

		isLowOnStorageSub:dispose()
		isLowOnStorageSub = nil

		deletedCatalogSub:dispose()
		deletedCatalogSub = nil

		sleepPreventionSubscription:dispose()
		sleepPreventionSubscription = nil

		autoPurgeSubscription:dispose()
		autoPurgeSubscription = nil

		modelData:onSynchronizerClose()

	end )

	local fnExternalAux = LrClassicDevelopSettings.getExternalClientAux()

	if fnExternalAux then
		modelData._volumeAgent:setupUploadedXmpDevelopAndAuxPurgeTask( wfCatalog )
	end

	watchForRequiredUpdate( modelData, wfCatSynchronizer )

	checkForRestrictedCountry( wfCatUser, modelData )

	WFCatAlbums.addDiagnosticLogSectionFunc( wfCatalog )
		-- Track album cycles in the diagnostic log.

	return wfCatalog, pixelProviderProcessor, maybeProfileCatalog, wfCatSynchronizer, lrClassicBinaryDownloader

end

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

local _wfOzCatalogStream, _wfOzCatalog_now

local _lastWFCatUserId, _lastIsDeletedCatalogVal

local function getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData, syncState )

	if _wfOzCatalogStream then

		return _wfOzCatalogStream

	end

	_wfOzCatalogStream = Observable.combineLatest( modelData._currentUserStream, modelData._userInfoStream, modelData._deletedCatalogStream, modelData._freemiumEnabledStream )

		:filter( function( wfCatUser )
			return wfCatUser
		end )

		:distinctUntilChanged(nil, distinctUserCompare)
		:flatMapLatest( function( wfCatUser, userInfo, isDeletedCatalog, isFreemiumEnabled)

			logger:infof( "Got new user %s, creating / finding catalog",
								wfCatUser
									and tostring( wfCatUser )
									or ":unknown:" )

			local newUserId = type( wfCatUser ) == "table" and
								type( wfCatUser.userId ) == "function" and
								wfCatUser:userId()

			if _wfOzCatalog_now and _lastWFCatUserId and _lastWFCatUserId ~= newUserId then

				logger:errorf( "WF is unlikely to continue working properly because the active LrClassicCatalog instance was closed and reopened due to userId change from %s -> %s and/or deleted Oz catalog state changed from %s -> %s",
								newUserId, _lastWFCatUserId,
								isDeletedCatalog, _lastIsDeletedCatalogVal )

				_wfOzCatalog_now:close()
				_wfOzCatalog_now = nil
			end

			_lastWFCatUserId = newUserId

			_lastIsDeletedCatalogVal = isDeletedCatalog

			-- TODO [scouten 2015-08-25]: If we have the cached catalog ID
			-- available locally, we'll launch faster. Consider using that before
			-- going out to the network.

			return Observable.combineLatest(
					wfCatUser:findOrCreateCatalog( "lightroom" ),
					wfCatUser:findFirstCatalogWithSubtype( 'profile', 'any' ) )
				:take( 1 )

				:flatMapLatest( function( catalogSpec )

					local managedCatalogDoc = catalogSpec:catalogDoc()
					if managedCatalogDoc then
						managedCatalogDoc._replace = true
					end

					return wfCatUser._userInfoCache:applyOverlay
						{
							_id = wfCatUser:userId(),
							_localOnly = {
								managedCatalog = managedCatalogDoc,
								managedCatalogId = catalogSpec:catalogId(),
							}
						}

						:map( function( ... )

							return createOzCatalog( wfCatUser,
													catalogSpec,
													nil,
													rootCatalogDir,
													pixelProviderSettings,
													modelData, syncState )

						end )

				end )

				:catch( function( whatWentWrong )

					logger:warnf( "Failed to get catalog ID from Oz( %s ), trying cached reference",
								  whatWentWrong )

					return Observable.combineLatest( wfCatUser:hasValidConnectionToOz(), modelData._isOnlineStream )

                        :take( 1 )

                        :flatMapLatest( function( hasValidConnection, isOnlineStream )

                            -- if there is valid connection or the current model is offline mode
                            -- we will enable the cached catalog.
                            if hasValidConnection or not isOnlineStream then

								return wfCatUser._userInfoCache:docValue( wfCatUser:userId() )
									:map( function( userDoc )

										local managedCatalog = KSTableUtils.derefOpt( userDoc, '_localOnly', 'managedCatalog' )
										local managedCatalogId = KSTableUtils.derefOpt( userDoc, '_localOnly', 'managedCatalogId' )
										local profileCatalog = KSTableUtils.derefOpt( userDoc, '_localOnly', 'profileCatalog' )
										local profileCatalogId = KSTableUtils.derefOpt( userDoc, '_localOnly', 'profileCatalogId' )

										logger:infof( "Found cached catalog ID %s", managedCatalogId )

										if managedCatalog or managedCatalogId then

											local catalogSpec = WFCatSpec( wfCatUser, managedCatalog or managedCatalogId )

											local profileSpec
											if profileCatalog or profileCatalogId then
												profileSpec = WFCatSpec( wfCatUser, profileCatalog or profileCatalogId )
											end

											return createOzCatalog( wfCatUser, catalogSpec,
																	profileSpec, rootCatalogDir,
																	pixelProviderSettings,
																	modelData )

										else

											modelData._userIdStream:onNext( nil ) -- force logout
											error( whatWentWrong )

										end

									end )

							else

								modelData._userIdStream:onNext( nil ) -- force logout
								error( whatWentWrong )

							end

					end )

				end )

		end )
		:map( function( wfOzCatalog, pixelProviderProcessor, profileCatalog, myWFCatSynchronizer, lrClassicBinaryDownloader )

			_wfOzCatalog_now = wfOzCatalog

			modelData._catalogIdStream:onNext( wfOzCatalog:ozCatalogId() )

			return wfOzCatalog, pixelProviderProcessor, profileCatalog, myWFCatSynchronizer, lrClassicBinaryDownloader

		end )
		:replay( nil, 1 )
		:refCount()

	return _wfOzCatalogStream

end

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

local function getAppModel( rootCatalogDir, pixelProviderSettings, modelData, syncState, _, namespace, func, isTiedToCatalog, ... )

	local packedN = select( '#', ... )
	local packed = { ... }

	local wfCatalogObservable = getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData, syncState )

	if isTiedToCatalog == nil then
		isTiedToCatalog = true
	end

	if isTiedToCatalog then
		wfCatalogObservable = wfCatalogObservable:take( 1 )
	end

	local value = wfCatalogObservable
		:flatMapLatest( function( wfOzCatalog )
			if wfOzCatalog then
				return require( namespace )[ func ]( wfOzCatalog,
													 unpack( packed, 1, packedN ) )
			else
				return Observable.just( {} )
			end
		end )

	return {

		value = value,

		close = function()
			logger:debugf( "Model %s.%s CLOSED", namespace, func )
		end,

	}

end


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

-- @mark LrClassicModelData class

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

local LrClassicModelDataImp = {

	-- User Info

	_userInfoCachePath = false,
	_userIdStream = false,
	_catalogIdStream = false,
	_deletedCatalogStream = false,
	_currentUserStream = false,
	_loginStatusStream = false,
	_isLoggedInStream = false,
	_userInfoStream = false,
	_shouldSyncUserInfoStream = false,
	_isSyncingUserInfoStream = false,

	-- purge

	_volumeAgent = false,

	-- Sync

	_serverBaseUrlStream = false,
	_hasValidConnectionToOzStream = false,
	_isOnlineStream = false,
	_isSyncPendingStream = false,
	_isPendingRestoreStream = false,
	_havePersistentSyncErrorStream = false,
	_isInitialSyncStream = false,
	_isSyncUploadingMetadataStream = false,
	_isSyncDownloadingMetadataStream = false,
	_isSyncUploadingBinariesStream = false,
	_isSyncDownloadingOriginalsOrProxiesStream = false,
	_isSyncDownloadingStylesStream = false,
	_shouldPreventSleepWhileSyncingStream = false,
	_isSyncPausedStream = false,
	_isActivelySyncingStream = false,
	_currentSyncStatusStream = false,
	_hostIsReachableStream = false,
	_ozLoginServerErrorStream = false,
	_catalogValidationStatusStream = false,
	_isOutageOccurringStream = false,
	_spaceOutageOccurringStream = false,
	_spaceOutages = false,

	-- Cloud Storage

	_isOutOfOzStorageStream = false,
	_isLowOnOzStorageStream = false,

	-- App Updates / Residual / Trial Mode

	_isUpdateAvailableStream = false,
	_isUpdateRequiredStream = false,
	_residualModeStream = false,
	_freemiumModeStream = false,
	_freemiumEnabledStream = false,
	_restrictedCountryModeStream = false,
	_trialEndDateStream = false,
	_deletionDateStream = false,
	_updateCommunicationPrefSubscription = false,

	_purgeAssetsAfterStream = false,

	_userModelIdStream = false,

	_dbCorruptionEventStream = false,
	_dbCorruptionEventStreamSubscription = false,

	_locale = false,

	WFCloseSupport.addCloseSupportMethods,

}

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

local function createUserInfoStream( currentUserStream )

	return currentUserStream
		:flatMap( function( wfCatUser )

			if wfCatUser then
				return wfCatUser:userInfoCache():docValue( wfCatUser:userId() )
			else
				return Observable.just( nil )
			end

		end )
		:replay( nil, 1 )
		:refCount()

end

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

local userInfoSynchronizer_now

local function createIsSyncingUserInfoStream( shouldSyncUserInfoStream, currentUserStream )

	return Observable.combineLatest( function( shouldSync, wfCatUser )

		if not wfCatUser then
			if userInfoSynchronizer_now then
				userInfoSynchronizer_now:close()
				userInfoSynchronizer_now = nil
			end
			return false
		end

		if not userInfoSynchronizer_now or
			( userInfoSynchronizer_now and
			( userInfoSynchronizer_now:userId() ~= wfCatUser:userId() ) ) then

			if userInfoSynchronizer_now then
				userInfoSynchronizer_now:close()
			end

			userInfoSynchronizer_now = WFCatUserSynchronizer( wfCatUser )

		end

		if shouldSync then
			userInfoSynchronizer_now:startContinuousUpdates()
		else
			userInfoSynchronizer_now:stopContinuousUpdates()
		end

		return shouldSync

	end,
	shouldSyncUserInfoStream, currentUserStream )

end

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

local function getTrialEndDateStream( userInfoStream )

	return userInfoStream
		:map( function( userInfo )

			local status = KSTableUtils.derefOpt( userInfo,
												  'entitlement',
												  'status' )

			local trialEnd = KSTableUtils.derefOpt( userInfo,
													'entitlement',
													'trial',
													'end' )

			if status == 'trial' then
				return trialEnd
			else
				return nil
			end

		end )

end

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

local function getDeletionDateStream( userInfoStream )

	return userInfoStream
		:map( function( userInfo )

			local deletionDate = KSTableUtils.derefOpt( userInfo,
													'entitlement',
													'deletion_date' )

			return deletionDate

		end )

end

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

local function getPurgeAssetsAfterStream( userInfoStream )

	return userInfoStream:map( function( userInfo )
		return KSTableUtils.derefOpt( userInfo,  'config', 'purge_assets_after' )
	end )

end

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

local function getIsSyncPausedStream( shouldSyncUserInfoStream )

	return shouldSyncUserInfoStream
		:map( function()

			-- Could use shouldSyncUserInfo, but re-inforce that the pref
			-- controls the paused state.
			-- Using shouldSyncUserInfo also updates observable.
			return KSPrefs.syncPaused

		end )

end

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

local function getIsActivelySyncingStream( modelData )

	local self = modelData

	-- This observable emits a boolean at least once every 15 seconds that
	-- indicates whether or not the system is actively syncing.

	return Observable.combineLatest(

		function( syncPaused, online, loggedIn,
			syncMetaUp, syncMetaDown,
			syncOrigUp, syncOrigOrProxyDown,
			syncStylesDown )

			if not syncPaused and online and loggedIn then

				if syncMetaUp or
					syncMetaDown or
					syncOrigUp or
					syncOrigOrProxyDown or
					syncStylesDown then

					return true

				end

			end

			return false

		end,
		self._isSyncPausedStream, self._isOnlineStream, self._isLoggedInStream,
		self._isSyncUploadingMetadataStream, self._isSyncDownloadingMetadataStream,
		self._isSyncUploadingBinariesStream, self._isSyncDownloadingOriginalsOrProxiesStream,
		self._isSyncDownloadingStylesStream,
		Observable.timer( 0, 15, WFRxScheduler.sharedScheduler() )

	)

end

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

local function getCurrentSyncStatusStream( modelData )

	local self = modelData

	return Observable.combineLatest(

		function( syncPaused, online, loggedIn, syncPending,
			initialSync, syncMetaUp, syncMetaDown,
			syncOrigUp, syncOrigOrProxyDown, syncStylesDown,
			outOfStorage, lowOnStorage,
			volumesOutOfSpace, offlineVolumes, volumesRequiringSyncing,
			updateRequired, persistentSyncError,
			catalogValidationStatus, outageOccurring )

			local status

			if not loggedIn then
				status = nil -- no status available when not signed in
			elseif updateRequired then
				status = 'SyncPausedUpdateRequired'
			elseif not catalogValidationStatus then
				status = 'SyncDisconnectedInvalidCatalog'
			elseif outageOccurring then
				status = 'SyncPausedForOutage'
			elseif syncPaused then
				status = 'SyncPaused'
			elseif not online then
				status = 'SyncDisconnectedNoNetwork'
			elseif outOfStorage then
				status = 'SyncErrorOutOfStorage'
			elseif initialSync and ( syncMetaUp or syncOrigUp ) then
				status = 'SyncInitialAndUp'
			elseif initialSync then
				status = 'SyncingInitial'
			elseif ( syncMetaDown or syncOrigOrProxyDown ) and ( syncMetaUp or syncOrigUp ) then
				status = 'SyncingUpAndDown'
			elseif syncMetaUp or syncOrigUp then
				status = 'SyncingUp'
			elseif syncMetaDown or syncOrigOrProxyDown or syncStylesDown then
				status = 'SyncingDown'
			elseif syncPending then
				status = 'SyncPending'
			elseif lowOnStorage then
				status = 'SyncLowOnStorage'
			elseif persistentSyncError then
				status = 'SyncCompletedWithError'
			else
				status = 'SyncNull'
			end

			return status

		end,
		self._isSyncPausedStream, self._isOnlineStream, self._isLoggedInStream, self._isSyncPendingStream,
		self._isInitialSyncStream, self._isSyncUploadingMetadataStream, self._isSyncDownloadingMetadataStream,
		self._isSyncUploadingBinariesStream, self._isSyncDownloadingOriginalsOrProxiesStream,
		self._isSyncDownloadingStylesStream,
		self._isOutOfOzStorageStream, self._isLowOnOzStorageStream,
		self._isUpdateRequiredStream, self._havePersistentSyncErrorStream,
		self._catalogValidationStatusStream, self._isOutageOccurringStream )

	:distinctUntilChanged()

	:tap( function( status )
		logger:infof( "Sync Status: %s", status )
	end )

end

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

function LrClassicModelDataImp:__init( serverBase, rootCatalogDir, imsOverrides, locale, userModelId )

	check( 'LrClassicModelData', 'string', '?string', '?table', '?string' )

	-- User Info

	local userIdStream = getUserIdStream( imsOverrides )

	self._userIdStream = userIdStream

	self._userModelIdStream = ReplaySubject( 1 )
	self._userModelIdStream:onNext( userModelId )

	self._catalogIdStream = ReplaySubject( 1 )

	self._deletedCatalogStream = BehaviorSubject( false )

	self._userInfoCachePath = rootCatalogDir == ':memory:' and rootCatalogDir or
										KSPathUtils.child( rootCatalogDir, 'user' )

	self._serverBaseUrlStream = BehaviorSubject( serverBase )

	self._currentUserStream = createCurrentUserStream( self._serverBaseUrlStream,
			self._userIdStream, self._userInfoCachePath )

	self._locale = locale
	self._updateCommunicationPrefSubscription = self._currentUserStream

		:subscribe( function( wfCatUser )

				local globalDocumentId = '00000000000000000000000000000000'

				if wfCatUser and wfCatUser:userId() ~= globalDocumentId then

					wfCatUser:updateCommunicationPreference( locale )
						:call( runUntilDone )

				end

			end )

	self._userInfoStream = createUserInfoStream( self._currentUserStream )

	self._shouldSyncUserInfoStream = BehaviorSubject( false )

	self._isSyncingUserInfoStream = createIsSyncingUserInfoStream(
			self._shouldSyncUserInfoStream, self._currentUserStream )

	self._loginStatusStream = BehaviorSubject( { status = "init" } )

	self._isLoggedInStream = self._userIdStream
		:map( function( userId )
			return not not userId
		end )

	-- Purge

	self._volumeAgent = LrClassicVolumeAgent()

	-- Sync

	self._hasValidConnectionToOzStream = createHasValidConnectionToOz( self._currentUserStream )
	self._catalogValidationStatusStream = BehaviorSubject( true )

	if USE_NON_CC_CREDENTIALS then

		-- For automation, start wf as if it's online.
		self._isOnlineStream = BehaviorSubject( true )

	else

		-- For standard thor login, start wf as if it's offline.
		self._isOnlineStream = BehaviorSubject( false )

	end

	self._isSyncPendingStream = BehaviorSubject( false )
	self._isPendingRestoreStream = BehaviorSubject( false )
	self._havePersistentSyncErrorStream = BehaviorSubject( false )
	self._isInitialSyncStream = BehaviorSubject( true )
	self._isSyncUploadingMetadataStream = BehaviorSubject( false )
	self._isSyncDownloadingMetadataStream = BehaviorSubject( false )
	self._isSyncUploadingBinariesStream = BehaviorSubject( false )
	self._isSyncDownloadingOriginalsOrProxiesStream = BehaviorSubject( false )
	self._isSyncDownloadingStylesStream = BehaviorSubject( false )
	self._shouldPreventSleepWhileSyncingStream = BehaviorSubject( KSPrefs.shouldPreventSleepWhileSyncing )
	self._isSyncPausedStream = getIsSyncPausedStream( self._shouldSyncUserInfoStream )
	self._isActivelySyncingStream = getIsActivelySyncingStream( self )
	self._isOutageOccurringStream = BehaviorSubject( false )

	self._spaceOutages = {}
	self._spaceOutageOccurringStream = BehaviorSubject( self._spaceOutages )


	self._hostIsReachableStream = self._currentUserStream
		:flatMap(function( wfCatUser )

			if wfCatUser == nil then
				return Observable.just( nil )
			end

			return WFReachability.hostIsReachable( wfCatUser:serverVersionUrl() or wfCatUser:serverBaseUrl() )

		end)

	-- Cloud Storage

	self._isOutOfOzStorageStream = BehaviorSubject( false )
	self._isLowOnOzStorageStream = BehaviorSubject( false )

	-- App Updates / Residual / Trial Mode

	self._isUpdateAvailableStream = BehaviorSubject( false )
	self._isUpdateRequiredStream = BehaviorSubject( false )
	self._residualModeStream = BehaviorSubject( not not KSPrefs.residualMode )
	self._freemiumModeStream = BehaviorSubject( not not KSPrefs.freemiumMode )
	self._freemiumEnabledStream = BehaviorSubject( false )
	self._restrictedCountryModeStream = BehaviorSubject( false )
	self._trialEndDateStream = getTrialEndDateStream( self._userInfoStream )
	self._deletionDateStream = getDeletionDateStream( self._userInfoStream )

	self._purgeAssetsAfterStream = getPurgeAssetsAfterStream( self._userInfoStream )

	-- The following streams rely on data from both file management and sync streams.

	self._currentSyncStatusStream = getCurrentSyncStatusStream( self )

	self._ozLoginServerErrorStream = ReplaySubject( 1 )

	-- db corruption handling setup.
	--
	-- There are multiple sqlite dbs associated with a user.
	--
	-- our policy is to shutdown the app when a corruption event is seen is any
	-- of them. Therefore we need only capture one corruption event
	self._dbCorruptionEventStream = ReplaySubject( 1 )

	-- We subscribe to the db corruption stream before we have even launched
	-- any of the SQLite db threads associated with the user. This ensures that
	-- corruption events don't occur before we have a chance to observe them.
	self._dbCorruptionEventStreamSubscription =
		NmbDbCorruptionUtils.corruptionEventOnce()
			:take( 1 )
			:subscribe( self._dbCorruptionEventStream )


end

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

function LrClassicModelDataImp:onSynchronizerClose()

	self._isSyncUploadingMetadataStream:onNext( false )

	self._isSyncDownloadingMetadataStream:onNext( false )

	self._isSyncUploadingBinariesStream:onNext( false )

	self._isSyncDownloadingOriginalsOrProxiesStream:onNext( false )

	self._isSyncDownloadingStylesStream:onNext( false )

	self._isSyncPendingStream:onNext( false )

	self._isPendingRestoreStream:onNext( false )

	self._havePersistentSyncErrorStream:onNext( false )

	self._isInitialSyncStream:onNext( false )

	self._isOutOfOzStorageStream:onNext( false )

	self._isLowOnOzStorageStream:onNext( false )

end

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

function LrClassicModelDataImp:close()

	self._userIdStream:dispose()

	self._catalogIdStream:dispose()

	if self._updateCommunicationPrefSubscription then

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

	end

	self._dbCorruptionEventStreamSubscription:dispose()

	if self._volumeAgent then
		self._volumeAgent:close()
		self._volumeAgent = nil
	end

end

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

local LrClassicModelData = Rx.Class 'LrClassicModelData'( LrClassicModelDataImp )


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

-- @mark LrClassicUserModel

local LrClassicUserModel = {}

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

LrClassicUserModel.maybeMigrateFreemiumCatalog = maybeMigrateFreemiumCatalog

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

function LrClassicUserModel.makeUserModel( serverBase, rootCatalogDir, processId,
		imsOverrides, pixelProviderSettings, locale , cameraRawVersion, syncState)

	check( 'string', '?string', '?string', '?table', '?table', '?string' )

	pixelProviderSettings = pixelProviderSettings or {}

	imsOverrides = imsOverrides or {}

	logger:infof( "LrClassicUserModel.makeUserModel( %s, %s, %s, %s, %s )",
				  serverBase, rootCatalogDir, processId,
				  pixelProviderSettings.maxSaves, pixelProviderSettings.maxRenders )

	local userModelId = KSIdGenerator.uuidInLowercaseHex()

	local modelData = LrClassicModelData( serverBase, rootCatalogDir, imsOverrides, locale, userModelId )

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

	local modelValue = {

		-- model info

		userModelId = Observable.just( userModelId ),

		-- basic user info

		ozUserId = modelData._userIdStream,
		catalogId = modelData._catalogIdStream:startWith( nil ),
		isLoggedIn = modelData._isLoggedInStream,
		loginStatus = modelData._loginStatusStream,
		email = currentUserFieldValueForPath( modelData, 'email' ),
		fullName = currentUserFieldValueForPath( modelData, 'full_name' ),
		firstName = currentUserFieldValueForPath( modelData, 'first_name' ),
		lastName = currentUserFieldValueForPath( modelData, 'last_name' ),

		-- sync

		isSyncingUserInfo = modelData._isSyncingUserInfoStream,
		syncPaused = modelData._isSyncPausedStream,
		isOnline = modelData._isOnlineStream,
		hasValidConnectionToOz = modelData._hasValidConnectionToOzStream,
		syncStatus = modelData._currentSyncStatusStream,
		syncUploadingMetadata = modelData._isSyncUploadingMetadataStream,
		syncDownloadingMetadata = modelData._isSyncDownloadingMetadataStream,
		syncUploadingBinaries = modelData._isSyncUploadingBinariesStream,
		syncDownloadingOriginalsOrProxies = modelData._isSyncDownloadingOriginalsOrProxiesStream,
		syncDownloadingStyles = modelData._isSyncDownloadingStylesStream,
		syncDownloadingMetadataInitial = modelData._isInitialSyncStream,
		syncPending = modelData._isSyncPendingStream,
		pendingRestore = modelData._isPendingRestoreStream,
		outageOccurring = modelData._isOutageOccurringStream,
		spaceOutageOccurring = modelData._spaceOutageOccurringStream,

		-- oz storage

		isOutOfOzStorage = modelData._isOutOfOzStorageStream,
		isLowOnOzStorage = modelData._isLowOnOzStorageStream,
		storageInfo = currentUserFieldValueForPath( modelData, 'entitlement/storage' ),

		-- account status

		isInResidualMode = modelData._residualModeStream,
		isInFreemiumMode = modelData._freemiumModeStream,
		isFreemiumEnabled = modelData._freemiumEnabledStream,
		isInRestrictedCountryMode = modelData._restrictedCountryModeStream,
		accountStatus = currentUserFieldValueForPath( modelData, 'entitlement/status' ),
		trialEndDate = modelData._trialEndDateStream,
		deletionDate = modelData._deletionDateStream,

		autoPurgeWindow = modelData._purgeAssetsAfterStream,

		-- upgrades

		isLightroomCCUpdateAvailable = modelData._isUpdateAvailableStream,
		isLightroomCCUpdateRequired = modelData._isUpdateRequiredStream,

	}

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

	addModelValueToDiagnosticLog( modelValue )

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

	return {

		_imsOverrides = imsOverrides,

		value = modelValue,

		models = {

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

			appModel = function( ... )
				return getAppModel( rootCatalogDir, pixelProviderSettings, modelData, syncState, ... )
			end,

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

			catalog = function( self, catalogDir )

				local wfCatalogStream =
						getWFCatalog( catalogDir or rootCatalogDir, pixelProviderSettings, modelData, syncState )

				self._catalogModel =
					require 'LrClassicRootModel'.makeModel(
								wfCatalogStream,
								modelData._dbCorruptionEventStream )

				return self._catalogModel

			end,

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

			hostIsReachable = function()
				return {
					value = modelData._hostIsReachableStream
				}
			end,

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

			ozLoginServerError = function()
				return {
					value = modelData._ozLoginServerErrorStream
				}
			end,

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

			appNeedsRestart = function()
				return {
					value = modelData._deletedCatalogStream
				}
			end,

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

			userModelId = function()
				return {
					value = modelData._userModelIdStream
				}
			end,

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

			diagnosticLog = function( _, logName, logFileName )
				return require 'WFDiagnosticLog'.makeSimpleModel( logName, logFileName )
			end,

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

			innerHTMLDiagnosticLog = function( _, logName, logFileName )

				return {

					value = require 'WFDiagnosticLog'.makeInnerHTMLReportAsFile(
														logName, logFileName )
								:map(
									function( ... )

										return { ... }

									end )

				}

			end,

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

			getImsAuthUrl = function( _, optOverrides )

				logger:infof( "getImsAuthUrl original serverBase = %s, optOverrides = %s",
							  serverBase, optOverrides )

				optOverrides = optOverrides or {}

				if not optOverrides.baseServerUrl then
					optOverrides.baseServerUrl = serverBase
				end

				optOverrides.apiVersion = 'v2'

				if optOverrides.imsClientId then
					require 'WFImsAppIdentity'.setClientId( optOverrides.imsClientId )
				end

				return {
					value = require 'WFOzClient'.getImsAuthUrl( optOverrides )
				}

			end,

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

			getJumpUrlForUrl = function( _, url, targetClientId, optOverrides )

				logger:infof( "getJumpUrlForUrl url = %s, webCid = %s",
							  url, targetClientId )

				optOverrides = optOverrides or {}

				if not optOverrides.baseServerUrl then
					optOverrides.baseServerUrl = serverBase
				end

				return {

					value = getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData )

						:flatMap( function( catalog )
							return catalog:ozUser():fetchTokens()
						end )

						:flatMap( function( accessToken )
							return require 'WFOzClient'.getJumpUrlForUrl( url, targetClientId, accessToken, optOverrides )
						end )

				}

			end,

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

			loginFromTokens = function( ... )
				loginFromTokens( modelData, ... )
				return {
					value = true,
				}
			end,

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

			avatar = function( _, avatarDir, adobeId )

				return {
					value = modelData._currentUserStream
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return WFCatAvatar.retrieveAvatar( wfCatUser, adobeId, avatarDir )
									else
										return Observable.empty()
									end
								end )
				}

			end,

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

			startProxyAuthErrorMonitoring = function()

				return {
					value = require "NmbProxyAuthErrorMonitor".startProxyAuthErrorMonitor()
				}

			end,

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

			getAccountDocSettingsFromOz = function()

				return {
					value = modelData._currentUserStream
								:take( 1 )
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return wfCatUser:getAccountDocSettingsFromOz()
									else
										return Observable.empty()
									end
								end )
				}
			end,

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

			updateAccountDocSettings = function( payload )

				return {
					value = modelData._currentUserStream
								:take( 1 )
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return wfCatUser:updateAccountDocSettings( payload )
									else
										return Observable.empty()
									end
								end )
				}
			end,

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

			isFaceDetectionAllowed = function()

				return {
					value = modelData._currentUserStream
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return wfCatUser:isFaceDetectionAllowed()
									else
										return Observable.empty()
									end
								end )
				}
			end,

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

			sharingPolicies = function()

				return {
					value = modelData._currentUserStream
								:flatMapLatest( function( wfCatUser )
									if wfCatUser then
										return wfCatUser:sharingPolicies()
									else
										return Observable.empty()
									end
								end )
				}
			end,

			--------------------------------------------------
			-- restartRequiredForDbIssue
			--
			-- refer to NmbDbCorruptionUtils.makeCleanupCorruptDbResultStream
			-- for details, but it will emit at most one result
			-- and the client app should quit and possibly restart
			-- if an event is emitted.
			--------------------------------------------------
			restartRequiredForDbIssue = function()

				-- if the oz-user-UUID.db is extremely corrupt, so that if
				-- fires when calling PRAGMA statements on open, we will never
				-- send data on modelData._currentUserStream and the cleanup
				-- code won't fire.
				--
				-- So we start modelData._currentUserStream with a wfCatUser
				-- stub object. Stub object implements :userInfoCache() which
				-- returns another stub object which in turn will throw when
				-- downstream clients subscribe to
				-- userInfoCacheStub:haltAndDelete()
				--
				-- Most of the time oz-user-UUID.db corruption will happen
				-- after establish wfCatUser and the "regular" corruption
				-- cleanup code will then run.

				local userInfoCacheStub = {

						haltAndDelete =
							function()

								-- We just want to return an observable that
								-- throws on :subscribe(). That will ensure the
								-- failsafe code will ultimately delete the
								-- extremely corrupt db.
								return Rx.Observable.throw( "no wfCatUser created, so can't call wfCatUser:haltAndDelete()!" )

							end,

						_DEBUG_simulateCorruption =
							function()

								error( "not implemented!" )

							end,

					}

				local wfCatUserStub = {

					userInfoCache =
						function()

							return userInfoCacheStub

						end

				}

				local wfCatUserOrStubStream =
						modelData._currentUserStream
							:startWith( wfCatUserStub )

				return {

					value = Rx.Observable.combineLatest(
									Rx.Observable.just( modelData._dbCorruptionEventStream ),
									wfCatUserOrStubStream )
								:take( 1 )
								:flatMapLatest(
									NmbDbCorruptionUtils.handleCorruptUserInfoCacheDbResultStream )
								:map(
									function( errorType, canBeSolvedByRestart, dbName, errStr )

										return { errorType, canBeSolvedByRestart, dbName, errStr }

									end )

				}

			end,


			--------------------------------------------------
			-- _DEBUG_simulateUserInfoCacheDbCorruption
			--------------------------------------------------
			_DEBUG_simulateUserInfoCacheDbCorruption = function()

				return {

					value = modelData._currentUserStream
								:flatMapLatest(
									NmbDbCorruptionUtils.makeSimulateCorruptUserInfoCacheStream )

				}

			end,

		},

		events = {

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

			activateOfflineMode = function( _, userId, reason )
				if reason == "ozLoginFailure" then
					activateOzLoginFailureMode( modelData, userId )
				else
					activateOfflineMode( modelData, userId )
				end
			end,

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

			activateOnlineMode = function( ... )
				activateOnlineMode( modelData, ... )
			end,

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

			useUser = function( _, userId )
				modelData._userIdStream:onNext( userId )
			end,

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

			loginFromTokens = function( ...)
				loginFromTokens( modelData, ... )
			end,

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

			-- FIXME [jandrew 2022-09-27]:
			-- The following event was added to address a token-refresh issue. WF
			-- does not have the capability of refreshing the access token using
			-- the device token. LrD, on the other hand, does this periodically
			-- through NGL; this event was added as a way for LrD to pass the
			-- updated token(s) into WF.
			--
			-- This approach should be considered temporary. The proper fix is for
			-- LrD to provide WF with a native implementation that does the actual
			-- refreshing of the access token, that can be called from WF code. WF
			-- has a mechanism for this already leveraged by the mobile clients.
			--
			-- Basic steps for LrD:
			--
			-- 1. Provide a Lua module to WF that implements the token refresh logic via
			-- a requestNewTokens method. For reference, to see how iOS does it,
			-- LrmOAuth2NativeTokenRefreshProvider in
			-- https://git.corp.adobe.com/Thio/Thio/blob/main/dev-eng/Thio/support/THFoundation/source/THWFSetup.cpp
			--
			-- 2. Provide the implementation to WF using WFCapabilities:
			-- WFCapabilities_setCapability( "WFOAuth2NativeTokenRefreshProvider", "<lua namespace>" )

			updateTokens = function( _, accessToken, optRefreshToken, optDeviceToken )

				getWFCatalog( rootCatalogDir, pixelProviderSettings, modelData )

						:take( 1 )

						:flatMap( function( catalog )

							if not optRefreshToken and not optDeviceToken then

								-- As long as accessToken is valid, then not having optRefreshToken
								-- or optDeviceToken is technically okay. However, if accessToken expires
								-- in WF before it's updated by LrD, then WF will end up dead in the water
								-- until the tokens are refreshed and this event is called.

								logger:warn( "Neither a refresh token or device token were passed into updateTokens. This is suspicious." )

							end

							return catalog:ozUser():storeTokens( accessToken, optRefreshToken, optDeviceToken )

						end )

						:call( runUntilDone )

			end,

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

			logout = function()
				modelData._userIdStream:onNext( nil )
				modelData._loginStatusStream:onNext( { status = "logout" } )
			end,

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

			startSync = function()
				takeOneValueAndRunFunc( modelData._isUpdateRequiredStream, function( updateRequired )
					if updateRequired then
						logger:info( "Not starting sync, because a client upgrade is required." )
						return
					elseif not KSPrefs.syncPaused or KSPrefs.syncPaused == kClientUpgradeRequired then

						-- When KSPrefs.syncPaused is "upgrade_required", we attempt to resume sync,
						-- because the user didn't choose to pause it. If an upgrade is still required,
						-- that will become clear soon enough, and sync will be paused with the
						-- the corresponding notification presented to the user.

						logger:infof( "Resuming sync with (previous) syncPaused preference of %s", KSPrefs.syncPaused )
						KSPrefs.syncPaused = false
						startSyncingUserAndCatalog( modelData )
					end
				end )
			end,

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

			stopSync = function()
				stopSyncingUserAndCatalog( modelData )
			end,

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

			pauseSync = function()
				KSPrefs.syncPaused = true
				stopSyncingUserAndCatalog( modelData )
			end,

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

			preventSleep = function( _, reason, optKeepDisplayAwake )
				WFInsomnia.preventSystemSleep( reason, optKeepDisplayAwake )
			end,

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

			residualMode = function()

				if not KSPrefs.residualMode and not KSPrefs.restrictedCountryMode then

					KSPrefs.freemiumMode = false
					modelData._freemiumModeStream:onNext( KSPrefs.freemiumMode )

					KSPrefs.residualMode = true
					modelData._residualModeStream:onNext( KSPrefs.residualMode )

					-- If wfCatSynchronizer doesn't yet exist,
					-- it will be created using the current value
					-- in KSPrefs.residualMode

					if wfCatSynchronizer then

						wfCatSynchronizer:stopSyncing()
							-- Stop the normal mode sync.

						wfCatSynchronizer:enableResidualMode()

						startSyncingUserAndCatalog( modelData )

					end

					if KSPrefs.syncPaused then
						KSPrefs.syncPaused = false
					end

				end

			end,

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

			freemiumMode = function()

				if not KSPrefs.freemiumMode and not KSPrefs.restrictedCountryMode then

					KSPrefs.residualMode = false
					modelData._residualModeStream:onNext( KSPrefs.residualMode )

					KSPrefs.freemiumMode = true
					modelData._freemiumModeStream:onNext( KSPrefs.freemiumMode )

					-- If wfCatSynchronizer doesn't yet exist,
					-- it will be created using the current value
					-- in KSPrefs.freemiumMode

					if wfCatSynchronizer then

						wfCatSynchronizer:stopSyncing()
							-- Stop the normal mode sync.

						wfCatSynchronizer:enableFreemiumMode()

						startSyncingUserAndCatalog( modelData )

					end

					if KSPrefs.syncPaused then
						KSPrefs.syncPaused = false
					end

				end

			end,

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

			normalMode = function()

				if ( KSPrefs.residualMode or KSPrefs.freemiumMode )
						and not KSPrefs.restrictedCountryMode then

					KSPrefs.residualMode = false
					modelData._residualModeStream:onNext( KSPrefs.residualMode )

					KSPrefs.freemiumMode = false
					modelData._freemiumModeStream:onNext( KSPrefs.freemiumMode )

					-- If wfCatSynchronizer doesn't yet exist,
					-- it will be created using the current value
					-- in KSPrefs.residualMode

					if wfCatSynchronizer then

						wfCatSynchronizer:stopSyncing()

						wfCatSynchronizer:disableResidualMode()
						wfCatSynchronizer:disableFreemiumMode()

						startSyncingUserAndCatalog( modelData )

					end

					if KSPrefs.syncPaused then
						KSPrefs.syncPaused = false
					end

				end

			end,

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

			freemiumEnabled = function()
				modelData._freemiumEnabledStream:onNext( true )
			end,

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

			resumeSync = function()
				takeOneValueAndRunFunc( modelData._isUpdateRequiredStream, function( updateRequired )
					if updateRequired then
						logger:info( "Not resuming sync, because a client upgrade is required." )
						return
					end
					KSPrefs.syncPaused = false
					startSyncingUserAndCatalog( modelData )
				end )
			end,

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

			togglePreventSleepWhileSyncing = function( _, shouldPreventSleepWhileSyncing )

				modelData._shouldPreventSleepWhileSyncingStream:onNext( shouldPreventSleepWhileSyncing )
				KSPrefs.shouldPreventSleepWhileSyncing = shouldPreventSleepWhileSyncing

			end,

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

			updateAvatar = function( _, newAvatarFilePath, ext )

				return modelData._currentUserStream
					:take( 1 )
					:flatMap( function( wfCatUser )
						if wfCatUser then
							return WFCatAvatar.updateAvatar( wfCatUser, newAvatarFilePath, ext )
						else
							return Observable.empty()
						end
					end )
					:call( WFRxRunUntilDone )

			end,

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

			triggerForceLightroomClassicUpgradeFlag = function()
				if userInfoSynchronizer_now then
					userInfoSynchronizer_now:triggerForceLightroomClassicUpgradeFlag()
				end
			end,

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

			setProxyCredentials = function( _, username, password )
				require "WFRxHttpClient".setProxyCredentials( username, password )
				modelData._serverBaseUrlStream
					:tap( function( baseUrl )
						require "WFReachability".refreshReachabilityStatus( baseUrl )
					end )
					:call( WFRxRunUntilDone )

			end,

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

			allowFaceDetection = function()
				modelData._currentUserStream
					:take( 1 )
					:flatMap( function( wfCatUser )
						if wfCatUser then
							return wfCatUser:allowFaceDetection()
						else
							return Observable.empty()
						end
					end )
					:call( WFRxRunUntilDone )
			end,

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

			disallowFaceDetection = function()
				modelData._currentUserStream
					:take( 1 )
					:flatMap( function( wfCatUser )
						if wfCatUser then
							return wfCatUser:disallowFaceDetection()
						else
							return Observable.empty()
						end
					end )
					:call( WFRxRunUntilDone )
			end,

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

			DEBUG_dumpCoverage = function( _, path )
				require 'SetupCoverage'.dumpCoverage( path )
			end,

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

			DEBUG_incStorageUsed = function( _, amountUsed )
				WFCatStorage.debugStorageUsed( amountUsed )
			end,

		},

		close = function()

			logger:info( "Closing" )

			if wfCatUser_now then
				wfCatUser_now:close()
				wfCatUser_now = nil
			end

			if modelData then
				modelData:close()
				modelData = nil
			end

			if userInfoSynchronizer_now then
				userInfoSynchronizer_now:close()
				userInfoSynchronizer_now = nil
			end

			if _wfOzCatalog_now then
				_wfOzCatalog_now:close()
				_wfOzCatalog_now = nil
			end

		end,

	}

end

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

return LrClassicUserModel
