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

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 )

				if self._userSynchronizer then

					self._userSynchronizer:updateUserAccountDocumentOnce()
						:call( runUntilDone )

				end

			end,

			pause = function( self )

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

			end,

			resume = function( self )

				if self._userSynchronizer then
					self._userSynchronizer:startContinuousUpdates()
				end

			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:getAccountDocSettingsFromOz()

	local returnValue = self._currentUser

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

		:flatMapLatest(function( wfCatUser )
			return wfCatUser:getAccountDocSettingsFromOz()
		end )

	return {
		value = returnValue
	}

end

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

function LrmUserSubModel:updateAccountDocSettings( payload )

	local returnValue = self._currentUser

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

		:flatMapLatest(function( wfCatUser )
			return wfCatUser:updateAccountDocSettings( payload )
		end )

	return {
		value = returnValue
	}

end

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

function LrmUserSubModel:setUserAuthenticated( accessToken, optRefreshToken, optDeviceToken )

	WFArguments.check( 'table', 'string', '?string', '?string' )
		-- The deviceToken is not mandatory as for chinese locales, as IMS does not provide
		-- deviceToken.
		--
		-- https://wiki.corp.adobe.com/display/ims/IM-204+Policy+Based+Authentication
		-- With the switch to policy-based-authentication (PBA), the device token offers
		-- more flexibility, which removes the need for the refresh token. This is
		-- why the refreshToken is now optional.

	self._loggedOut:onNext( false )
	self._accessTokenSubject:onNext( accessToken )

	if optRefreshToken and optRefreshToken == '' then
		optRefreshToken = nil
	end

	if optDeviceToken and optDeviceToken == '' then
		optDeviceToken = nil
	end

	local userId = self._userId

	local result = WFOzUserLogin.loginFromTokens( LriCatUserModel.getUserInfoCachePath(),
									OzCatalogUrls.baseServerUrl,
									accessToken,
									optRefreshToken,
									optDeviceToken )

		:flatMapLatest( function( wfCatUser, userInfo, accessToken, optRefreshToken, optDeviceToken )

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

				return wfCatUser:storeTokens( accessToken, optRefreshToken, 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

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

--[[!

	\brief Creates a new Lightroom library model.

	\param catalogId (uuid): catalog ID
	\param syncEnabled (boolean): enable sync or not
	\param binaryPreference (binaryStrategy): binaryStrategy
	\param locale (string): current locale
	\param optArgs (table) Optional arguments:
		isPurgeEnabledForResidualOrOfflineUsers: optional feature flag to enable Purge Agent for offline users
		enableHttpResponseLog: optional feature flag to enable HTTP response log
		enableCrossPlatformPurgeAgent: feature flag to enable the new purge agent.

	\return Library model.

--]]

function LrmUserSubModel:catalogModel( catalogId, syncEnabled, binaryPreference, locale,
		optArgs )

	WFArguments.check( 'table', 'uuid', 'boolean', 'binaryStrategy', 'string', '?table' )

	optArgs = optArgs or {}

	WFCapabilities.setCapability( "WFRxHttpClientImp.enableHttpResponseLog", optArgs.enableHttpResponseLog and "true" or nil )
	WFCapabilities.setCapability( "WFCatVideo.enableVideoStoreLocally", optArgs.enableVideoStoreLocally 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( optArgs )

	if optArgs and optArgs.imsClientId then
		require 'WFImsAppIdentity'.setClientId( optArgs.imsClientId )
	end

	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
