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

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

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

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

	-- RxLua
local Rx = require 'Rx'

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

	-- WF.core.os
local KSPrefs = require 'KSPrefs'
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'
local WFDeviceInfo = require "WFDeviceInfo"

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFCapabilities = require 'WFCapabilities'

	-- libs.json
local KSJsonDecoder = require 'KSJsonDecoder'

	-- services.keychain_store
local KSKeychainService = require 'KSKeychainService'

	-- WF.oz_client.catalog
local OzCatalogUrls = require 'OzCatalogUrls'

	-- WF.oz_client.sync_engine.user
local WFOzUserLogin = require 'WFOzUserLogin'
local WFOzSyncConstants = require 'WFOzSyncConstants'
local WFOzUserDeviceManagement = require "WFOzUserDeviceManagement"
local WFCatUserSynchronizer = require 'WFCatUserSynchronizer'

	-- WF.oz_client.managed_catalog
local WFCatUser = require 'WFCatUser'
local WFCatSpec = require 'WFCatSpec'

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

	-- WF.lrmobile
local LrmConfig = require 'LrmConfig'

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

WFArguments.registerChecker( 'binaryStrategy', function( strategy )
	return type( strategy ) == 'string' and
		strategy == LrmConfig.StrategyPreferMaster or
		strategy == LrmConfig.StrategyPreferProxy
end )
--------------------------------------------------------------------------------

local LrmUserSubModel = {}
local LriCatUserModel = {}

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

local logger = WFLogger "LriCatUserModel"
--	:setLevel 'debug'

local Observable = Rx.Observable

local runUntilDone = WFRxRunUntilDone

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

local function registerDeviceToken( currentUser, token )
		
	WFOzUserDeviceManagement.registerDeviceToken( currentUser, token )		
		:tapOnError( function ( message )
			logger:warnf( "Error Registering Token: %s", message )
		end )
		:call( runUntilDone )

end

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

local function maybeRetrieveCatalogId( wfCatUser )

	local userDocStore = wfCatUser:userInfoCache()
	
	if WFOzSyncConstants.isGlobalDocumentId( wfCatUser:userId() ) then
		return Rx.Observable.just( WFOzSyncConstants.kGlobalDocumentId )
	end
	
	return userDocStore:docValue( wfCatUser:userId() )

		:pluck( '_localOnly', 'managedCatalogId' )

		:flatMapLatest( function( catalogId )
			
			if catalogId then
				return Rx.Observable.just( catalogId )
				
			else
				return wfCatUser:findFirstLightroomCatalog()
					:map( function( wfCatSpec )
					
						if wfCatSpec then
							return wfCatSpec:catalogId()
						else
							return nil
						end
						
					end )
			end
			
		end )
	
end

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

function LrmUserSubModel:userInfoModel()
	
	local endpointIndexDocId = 'Oz-EndpointIndexDocument'

	local userInfo = self._currentUser

		:where( function( user ) return user ~= nil end )

		:flatMapLatest( function( wfCatUser )

			local userDocStore = wfCatUser:userInfoCache()

			return Rx.Observable.combineLatest( function( userDoc, endpointDoc, catalogId )

					if not endpointDoc._empty then
						userDoc._endpoint = endpointDoc
					end
					
					if userDoc then
						userDoc._catalogId = catalogId
					end

					return userDoc

				end,
				userDocStore:ongoingDocValue( wfCatUser:userId() ),
				userDocStore:ongoingLocalDocValue( endpointIndexDocId ),
				maybeRetrieveCatalogId( wfCatUser )
					:catch(
						function()
							return Observable.just( nil )
						end ))

		end )
		
	return {
		value = userInfo
	}
end

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

function LrmUserSubModel:userSynchronizer( userId )

	WFArguments.check( 'table', 'uuid' )
	
	local serverBase = OzCatalogUrls.baseServerUrl
	local userInfoCacheFolder = LriCatUserModel.getUserInfoCachePath()
	
	local user = WFCatUser( serverBase, userId, userInfoCacheFolder )
	
	local userSynchronizer = WFCatUserSynchronizer( user )
	userSynchronizer:startContinuousUpdates()
	
	return {
		_userSynchronizer = userSynchronizer,
		
		value = Rx.BehaviorSubject( false ),
		
		events = {
			
			updateUserAccountDocument = function( self )
			
				self._userSynchronizer:updateUserAccountDocumentOnce()
					:call( runUntilDone )
					
			end
		},
		
		close = function( self )
		
			self._userSynchronizer:close()
			self._userSynchronizer = nil
			
		end
	}
		
end

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

function LrmUserSubModel:forceUpgrade()

	error(' ForceUpgrade model no longer supported in UserModel. Moved to LriCatLibraryModel.' )

end

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

function LrmUserSubModel:hasValidConnection()

	local o = self._currentUser
		:where( function( catUser )
		
			return catUser ~= nil
			
		end )
		
		:flatMapLatest( function( catUser )
		
			if catUser:userId() == WFOzSyncConstants.kGlobalDocumentId then
				return Rx.Observable.just( false )
			end
			
			return catUser:hasValidConnectionToOz()
			
		end )

	return {
		value = o
	}
end

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

function LrmUserSubModel:isLoggedOut()

	return {
		value = self._loggedOut
	}
end

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

function LrmUserSubModel:setUserAuthenticated( accessToken, refreshToken, deviceToken )

	WFArguments.check( 'table', 'string', 'string', '?string' )
		
	self._loggedOut:onNext( false )
	self._accessTokenSubject:onNext( accessToken )
	
	if deviceToken and deviceToken == '' then
		deviceToken = nil
	end
	
	local userId = self._userId
	
	local result = WFOzUserLogin.loginFromTokens( LriCatUserModel.getUserInfoCachePath(),
									OzCatalogUrls.baseServerUrl, 
									accessToken, 
									refreshToken, 
									deviceToken )
									
		:flatMapLatest( function( wfCatUser, userInfo, accessToken, refreshToken, optDeviceToken )

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

				return wfCatUser:storeTokens( accessToken, refreshToken, optDeviceToken )
					:concat( Observable.just( wfCatUser ) )
					
		end )
				
		:tapOnError( function( message )
			
				logger:warnf( "Failed to log in to Oz: %s", message )

				userId:onNext( nil )
				self._loggedOut:onNext( true )
				self._accessTokenSubject:onNext( nil )

				-- TODO [pkamin 2016-02-10]: evaluate the error value from Oz
				-- we might have a locked account that we need to pass through to
				-- the client
				
			end	)

	:flatMapLatest( function( wfCatUser )
	
			local _userId = wfCatUser:userId()
			
			KSPrefs.currentUserId = _userId

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

			userId:onNext( _userId )
			
			wfCatUser:close()

			return Observable.just( { userId = _userId } )
		end )
	
	
	return {
		value = result,
	}
end

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

function LriCatUserModel:logout()
	
	if not self._currentUser then return end
	
	self._currentUser
	
		:take( 1 )
		
		:flatMapLatest( function( wfCatUser )
							
			return WFOzUserDeviceManagement.deleteDeviceToken( wfCatUser )
			:catch( Rx.Observable.just( wfCatUser ) )
			
		end )

		:flatMapLatest(function( wfCatUser )

			self._loggedOut:onNext( true )
		
			return wfCatUser:logout()
			:catch( Rx.Observable.just( wfCatUser ) )
				
		end )

		:tap( function ()

			KSPrefs.currentUserId = nil
			self._userId:onNext( WFOzSyncConstants.kGlobalDocumentId )
			self._accessTokenSubject:onNext( nil )

			collectgarbage( "collect" )
			collectgarbage( "collect" )
			collectgarbage( "collect" )

		end )
		
		:call( runUntilDone )

end

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

function LriCatUserModel:simulateForcedLogout()
	
	if not self._currentUser then return end
	
	self._currentUser
	
		:take( 1 )
		
		:tap( function( wfCatUser )
		
			wfCatUser:simulateForcedLogout()
			
		end )
		
		:call( runUntilDone )
		
end

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

local function getWFCatSpec( currentUser )

	return currentUser
	
		:where( function( wfCatUser )
		
			return wfCatUser and not WFOzSyncConstants.isGlobalDocumentId( wfCatUser:userId() )
			
		end )
		
		:flatMapLatest( function( wfCatUser )
		
			logger:infof( "Got new user %s, creating / finding catalog",
										wfCatUser
											and tostring( wfCatUser )
											or ":unknown:" )
											
											
			return wfCatUser:userInfoCache():docValue( wfCatUser:userId() )

				:pluck( '_localOnly', 'managedCatalogId' )
	
				:flatMapLatest( function( catalogId )
				
					if catalogId then
					
						return Rx.Observable.just( WFCatSpec( wfCatUser, catalogId ) )
						
					else
					
						return wfCatUser:findOrCreateCatalog( "Lightroom catalog" )

							:flatMapLatest( function( catalogSpec )
		
								return wfCatUser:userInfoCache():applyOverlay
									{
										_id = wfCatUser:userId(),
										_localOnly = {
											managedCatalogId = catalogSpec:catalogId()
										}
									}
									
									:map( function()
							
										return catalogSpec
										
									end )
		
							end )
					
					end
					
				end )
				
			
			
		end )
		
end

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

function LrmUserSubModel:catalogModel( catalogId, syncEnabled, binaryPreference, locale, 
		enableHttpResponseLog, isPurgeEnabledForResidualOrOfflineUsers, enableDirectUpload, enableVideoProxies )

	WFArguments.check( 'table', 'uuid', 'boolean', 'binaryStrategy', 'string', '?boolean', '?boolean', '?boolean', '?boolean' )
	
	enableDirectUpload = enableDirectUpload == nil and true or enableDirectUpload -- turns nil into true

	WFCapabilities.setCapability( "WFRxHttpClientImp.enableHttpResponseLog", enableHttpResponseLog and "true" or nil )
	WFCapabilities.setCapability( "LrmOzPurgeAgent.isPurgeEnabledForResidualOrOfflineUsers", isPurgeEnabledForResidualOrOfflineUsers and "true" or nil )
	WFCapabilities.setCapability( "WFCatBinaryUploader.enableDirectUpload", enableDirectUpload and "true" or "false" )
	WFCapabilities.setCapability( "WFCatBinaryUploader.enableVideoProxies", enableVideoProxies and "true" or nil )
	
	local catSpec = self._currentUser
		:take( 1 )
		:map( function( wfCatUser )

			return WFCatSpec( wfCatUser, catalogId )
			
		end )
		:shareReplay( 1 )
		
	return require 'LriCatLibraryModel'.makeLibraryModel( catSpec, syncEnabled, binaryPreference, locale )
	
end

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

local function queryAccessToken( currentUser )
		
	return currentUser
	
		:where( function( wfCatUser ) return wfCatUser ~= nil end )
		
		:flatMapLatest( function( wfCatUser )
		
			return KSKeychainService.read( wfCatUser:userId() )
			
		end )
		
		:map( function( userJson )
		
			local decodedJson = KSJsonDecoder.decodeWebSafeJson_stripNull( userJson )
				
			if decodedJson and decodedJson.access_token then
				return decodedJson.access_token
			end
			
			return nil
			
		end )
		
		:catch( Rx.Observable.just( nil ) )
				
end

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

function LrmUserSubModel:defaultCatalog()

	local catalogId = getWFCatSpec( self._currentUser )
		:map( function( catSpec )
		
			local catalogId = catSpec:catalogId()
			
			return {
				catalogId = catalogId,
				ozUserId = catSpec:ozUserId(),
				deviceId = WFDeviceInfo.uniqueDeviceId()
			}
			
		end )
		
	return {
		value = catalogId
	}
	
end
--------------------------------------------------------------------------------

function LriCatUserModel.getUserInfoCachePath_deprecated()

	local userInfoCachePath = KSPathUtils.child( KSPathUtils.getStandardFilePath 'caches',
											 'Adobe' )
											 
	userInfoCachePath = KSPathUtils.child( userInfoCachePath,
						   'user' )
	
	return userInfoCachePath
	
end

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

function LriCatUserModel.getUserInfoCachePath_new()

	local userInfoCachePath = KSPathUtils.getStandardFilePath 'adobeAppData'
											 
	WFFileUtils.mkdir( userInfoCachePath )
	
	userInfoCachePath = KSPathUtils.child( userInfoCachePath,
						   'user' )
	
	WFFileUtils.mkdir( userInfoCachePath )
	
	return userInfoCachePath
	
end

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

function LriCatUserModel.getUserInfoCachePath()

	return LriCatUserModel.getUserInfoCachePath_new()
	
end


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

function LriCatUserModel.migrateUserDocStoreToNewLocation()

	local userInfoCachePathDeprecated = LriCatUserModel.getUserInfoCachePath_deprecated()
	
	local userInfoCachePathNew = KSPathUtils.child( KSPathUtils.getStandardFilePath 'adobeAppData',
						   'user' )
	
	if WFFileUtils.exists( userInfoCachePathDeprecated ) and 
		not WFFileUtils.exists( userInfoCachePathNew ) then
			WFFileUtils.move( userInfoCachePathDeprecated, userInfoCachePathNew )
	end
											 
end

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

local function closeUser( closingUser )

	closingUser:close()

end

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

function LriCatUserModel.makeModel()

	local accessTokenSubject = Rx.ReplaySubject( 1 )
	accessTokenSubject:onNext( nil )
	
	local serverBase = OzCatalogUrls.baseServerUrl
	local serverBaseUrl = Rx.ReplaySubject( 1 )
	serverBaseUrl:onNext( serverBase )
	
	local currentUserId = KSPrefs.currentUserId
	local userId = Rx.ReplaySubject( 1 )
	if currentUserId then
		userId:onNext( currentUserId )
	else
		userId:onNext( WFOzSyncConstants.kGlobalDocumentId )
	end
	
	local loggedOut = Rx.ReplaySubject( 1 )
	loggedOut:onNext( false )
	
	local previousUser
	
	local currentUser = 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

				local isLoggedOut = WFOzSyncConstants.isGlobalDocumentId( userId )
				
				local userInfoCachePath = ':memory:'
				
				if not isLoggedOut then
				
					userInfoCachePath = LriCatUserModel.getUserInfoCachePath()
				
				end

				newUser = WFCatUser( serverBaseUrl, userId, userInfoCachePath )
				
			end

			if previousUser and (previousUser ~= newUser) then

				closeUser( previousUser )

			end
			
			if previousUser and ( newUser == nil ) then
				closeUser( previousUser )
			end

			previousUser = newUser

			return newUser

		end, serverBaseUrl, userId )
			
		:shareReplay( 1 )
	
	local currentUserSubscription = currentUser
		:subscribe( function( wfCatUser )
		
			-- IMPORTANT ! READ BELOW
			
			-- INFO [pkamin 2015-04-22]: if we move the logic to update the communication preference
			-- to some other app state, please notify the Oz team as they built logic around this 
			-- to send email communication out to the user
			-- See https://www.pivotaltracker.com/n/projects/933778/stories/92597188 for further details
			if wfCatUser and not WFOzSyncConstants.isGlobalDocumentId( wfCatUser:userId() ) then
				
				wfCatUser:updateCommunicationPreference()
					:call( runUntilDone )
			
			end

		end )
		
	
	local accessToken = Rx.Observable.combineLatest( function( tokenFromSubject, tokenFromKeychain )
	
		return tokenFromSubject or tokenFromKeychain
		
	end, accessTokenSubject, queryAccessToken( currentUser ) )
	
	return {
		_userId = userId,
		_currentUser = currentUser,
		_accessTokenSubject = accessTokenSubject,
		_loggedOut = loggedOut,
		
		_currentUserSubscription = currentUserSubscription,
				
		value = {
			user = userId,
			accessToken = accessToken,
		},
		
		models = LrmUserSubModel,
		
		events = {
			
			logout = function( self )
				LriCatUserModel.logout( self )
			end,
			
			simulateForcedLogout = function( self )
				LriCatUserModel.simulateForcedLogout( self )
			end,
			
			registerDeviceToken = function( self, deviceToken )
			
				registerDeviceToken( currentUser, deviceToken )
				
			end,
			
			resetOzCatalog = function( self )
			
				currentUser
					:flatMap( function( user )
					
						return user:userInfoCache():applyOverlay {
							_id = user:userId(),
							_localOnly = {
								managedCatalogId = { _delete = true },
							}
						}
						
					end )
					
					:call( runUntilDone )
						
			end
		}
	}
end

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

return LriCatUserModel
