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

 NmbErrorHandler.lua

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

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

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

local Rx = require 'Rx'
local Observable = Rx.Observable

local KSJsonDecoder = require 'KSJsonDecoder'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFAsyncFileUtils = require 'WFAsyncFileUtils'
local WFFileUtils = require 'WFFileUtils'
local WFCatAlbums = require 'WFCatAlbums'
local WFCatDevelopSettings = require 'WFCatDevelopSettings'
local WFCatErrorHandler = require 'WFCatErrorHandler'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatOzErrors = require 'WFCatOzErrors'
local WFCatSyncStatus = require 'WFCatSyncStatus'
local WFLogger = require 'WFLogger'
local WFOzDocUtils = require 'WFOzDocUtils'
local WFRxThrottledBatchedApplyOverlayToIds = require 'WFRxThrottledBatchedApplyOverlayToIds'

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

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

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

local logger = WFLogger( 'NmbErrorHandler' )

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

local nmbErrorHandlerPrototype = {

	WFCatErrorHandler,
		--superclass
		
}

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

local function docAlreadyHasMatchingLocalMasterFor( doc, sha256 )
	
	local maybeAssetPath = WFCatFileSpec.buildOriginalPathFromDocValue( doc )
	
	local sha256Matches = WFCatFileSpec.getLocalOriginalSha256WithFallback( doc ) == sha256
	
	local localFileExists = false
	if sha256Matches and maybeAssetPath then
		localFileExists = not not WFFileUtils.exists( maybeAssetPath )
	end
	
	return Observable.just( localFileExists )
end

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

local function pathForOriginalForDoc( catalog, docValue )

	local subtype = docValue.subtype
	
	if subtype == 'preset' or subtype == 'profile' then
		return catalog:fileSpec():pathForStyle( docValue._id )
	elseif subtype == 'camera_profile' then
		local filename = docValue.fileName or derefOpt( docValue, 'importSource', 'fileName' )
		if filename then 
			return catalog:fileSpec():pathForDCP( filename )
		end
	end
	
	logger:warnf( "Unexpected subtype (%s) or bad doc value found in pathForOriginalForDoc %s", subtype, docValue._id )
	return nil

end

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

local function transferMaster( catalog, fromDoc, toDoc )
	
	-- get from Master file path where existing file should be
	local fromPath = WFCatFileSpec.buildOriginalPathFromDocValue( fromDoc )
	
	-- get master filename that we should be moving *to*
	local toPath = pathForOriginalForDoc( catalog, toDoc )
	
	local fromDocMasterHash = derefOpt( fromDoc, '_localOnly', 'files', 'original', 'master' )
	
	-- we either don't have a path to copy from or to..so skip this
	if not fromPath or not toPath or not fromDocMasterHash then
		return Observable.just()
	end
	
	-- attempt to move the file (do not overwrite)
	return WFAsyncFileUtils.moveFile( fromPath, toPath )
		:flatMap( function( destPath )	
			-- if move was successful, create overlays to remove original hash from 'from' docValue
			-- and add to 'to' docValue with the destination filename
					
				local fromDocOverlay = {
					_id = fromDoc._id,
					_localOnly = {
						files = {
							original = { _delete = true }
						}
					}
				}
				
				-- sense to transfer
				fromDocMasterHash.volumeGuid = WFFileUtils.getVolumeGuidForPath( destPath )
				fromDocMasterHash.relativePath = WFFileUtils.removeVolumeFromPath( destPath )

				local toDocOverlay = {
					_id = toDoc._id,
					_localOnly = {
						files = {
							original = {
								master = fromDocMasterHash,
							}
						}
					} 
				}
		
				return catalog:docStore():bulkApplyOverlay( { fromDocOverlay, toDocOverlay } )
	
		end )
		:catch( Observable.just() ) -- if the move fails, just move on
end

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

local function maybeRemoveMasterAnnotationFromSourceDoc( catalog, sourceDoc, toDoc )

	-- In the case of subtype camera_profile, the ingest mechanism
	-- will re-use an existing file in the cr_cameraprofiles location,
	-- if it exists during the local ingest, which it might as
	-- a result of having been synced down from Oz, after having
	-- been uploaded to Oz via a different Lightroom CC instance.
	-- So we have to account for that here.
				
	local subtype = KSTableUtils.derefOpt( toDoc, 'subtype' )
	
	if subtype == 'camera_profile' then
		
		local sourceLocalMasterPath = WFCatFileSpec.buildOriginalPathFromDocValue( sourceDoc )
		local destLocalMasterPath = WFCatFileSpec.buildOriginalPathFromDocValue( toDoc )
		
		if sourceLocalMasterPath == destLocalMasterPath then
		
			logger:infof( "REMOVING master path annotation for %s",
														sourceLocalMasterPath )

			return catalog:docStore():applyOverlay {
				_id = sourceDoc._id,
				_localOnly = {
					files = {
						original = {
							master = {
								_delete = true
							},
						},
					},
				},
			}
			
			:map( function()
				return nil
			end )
		
		end
		
	end
	
	logger:infof( "NOT removing master path annotation for %s",
					WFCatFileSpec.buildOriginalPathFromDocValue( sourceDoc ) )
	
	return Observable.just()

end

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

local function maybeTransferMasterFromDocToDoc( catalog, sourceDoc, toDoc )
	
	local sourceDocSha256 = derefOpt( sourceDoc, 'importSource', 'sha256' )
	
	return docAlreadyHasMatchingLocalMasterFor( toDoc, sourceDocSha256 )
		:flatMap( function( alreadyHasLocal )
			if alreadyHasLocal then
				-- the matching file already has a local master so we don't
				-- need to do anything in terms of transferring the master doc
				
				return maybeRemoveMasterAnnotationFromSourceDoc( catalog, sourceDoc, toDoc )
				
			else

				-- move master from failedDoc to matchingDoc
				return transferMaster( catalog, sourceDoc, toDoc )
			end
		end )

end

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

local function preconditionFailedHandler( self, errorDoc, failedDuplicateDoc )
	
	local docStore = self._catalog:docStore()
	
	local matchingAssetUrl = derefOpt( errorDoc, '_serverProvidedMetadata', 'matching_asset', 'href' )
	local isCreate = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'action' ) == 'create'
	
	local handleErrorDoc
	
	if matchingAssetUrl and isCreate then
		-- we're an if_none_match on a preset/profile/camera_profile document and Oz
		-- says a duplicate already exists for this
		
		-- use the url for the asset that already exists
		handleErrorDoc = self._catalog:ozCatalogSpec():get( matchingAssetUrl )
			:map( KSJsonDecoder.decodeWebSafeJson_stripNull )
			:map( function( matchingAssetDecodedJson )
				return WFOzDocUtils.remapBasicOzDocument( matchingAssetDecodedJson, matchingAssetDecodedJson.base )
			end )
			:flatMap( function( matchingOzDoc )
				-- apply the document update as if it came from Oz in the sync feed
				matchingOzDoc._labels = { oz = true }
				return docStore:setDocValue( matchingOzDoc )
					:flatMap( function()
						-- get the document for the matching asset once any local/oz conflicts 
						-- have been resolved and continue processing the error document with the 
						-- resolved matching document value
						return docStore:ongoingDocValue( matchingOzDoc._id )
							:filter( function( docValue )
								return docValue._conflict ~= true
							end )
							:take( 1 )
							:map( function( resolvedMatchingOzDoc )
								return failedDuplicateDoc, resolvedMatchingOzDoc
							end )
					end )
			end )
			:catch( function( maybeData, maybeHeaders, ... )
				-- this catch is most likely catching any HTTP errors above
				if type( maybeHeaders ) == 'table' and maybeHeaders.status_code == '404' then
					-- a 404 should usually indicate that the asset isn't on Oz so respond with an
					-- _empty matchingDoc to let it try resending to Oz
					return Observable.just( failedDuplicateDoc, { _empty = true } )
				else
					return Observable.throw( maybeData, maybeHeaders, ... ) 
				end
				
			end )
			:flatMap( function( failedDuplicateDoc, matchingDoc )
			
				local matchingDocIsDeleted = matchingDoc._deleted
				local failedDuplicateDocSha256 = derefOpt( failedDuplicateDoc, 'importSource', 'sha256' )
				local matchingDocSha256 = derefOpt( matchingDoc, 'importSource', 'sha256' )
					
				if failedDuplicateDoc._id == matchingDoc._id then
				
					return Observable.just() 
						-- WF-2059: Noop, the client must have issued two create calls for the same asset.
				
				elseif matchingDocIsDeleted or ( failedDuplicateDocSha256 ~= matchingDocSha256 ) then
					-- matchingDoc either no longer exists or the sha256s no longer match
					-- so we should bump the doc sequence so that we try to resend to Oz
					-- and see if Oz allows it through. 
					-- Since we'll also want to limit the number of times we retry, we'll 
					-- annotate the document with an error a certain number of times (5 seems 
					-- very unlikely to be hit in any normal scenario). We'll stop annotating 
					-- if we're over.
					local previousErrorCount = derefOpt( failedDuplicateDoc, '_localOnly', 'ozError', 'retryCount' ) or 0					
					
					if previousErrorCount > 5 then
						logger:errorf( "if_none_match handling failed too many times for %s, giving up", failedDuplicateDoc._id )
						return Observable.just()
					else
						return docStore:applyOverlay {
							_id = failedDuplicateDoc._id,
							_localOnly = {
								ozError = {
									subtype = WFCatOzErrors.Errors.kPreconditionFailedError,
									reason = 'if_none_match',
									retryCount = previousErrorCount + 1
								}
							}
						}
					end
				else
					-- local doc exists and sha256 values match so see validate that we have
					-- the master locally for the one on Oz and then deleted the failed doc
					return maybeTransferMasterFromDocToDoc( self._catalog, failedDuplicateDoc, matchingDoc )
						:flatMap( function()
							-- SPAM [matthewj 2018-04-20]: camera_profiles do not have an asset unique name
							-- for the original file and so the master info may need to be removed from the
							-- doc before deleting it or else we can delete the master out from under the 
							-- matchingDoc
							
							-- Remove the document that is a duplicate now that we've fixed up the
							-- local match with all the same data
							return docStore:deleteDoc( failedDuplicateDoc._localId )
						end )

				end
				
				return Observable.just()
				
			end )
			:finally( function()
				-- delete error document once we've dealt with it
				return docStore:deleteDoc( errorDoc._localId )
			end ) 
				
		
	else
		-- some other PreconditionFailed error that we aren't expecting, send to defaultHandler
		-- as we don't know what to do with this
		local defaultHandler = self:getDefaultErrorHandlerFn()
		handleErrorDoc =  defaultHandler( self, errorDoc, failedDuplicateDoc )
		
	end
		
	return handleErrorDoc
end

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

local function deleteAssets( catalog, assetIds )

	local overlay = {
		type = 'asset',
		_deleted = true
	}

	return WFRxThrottledBatchedApplyOverlayToIds( "deleteAssets", catalog:docStore(), overlay, assetIds, 100, 10 )
		:call( runUntilDone )

end

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

--[[
	\brief Creates an NmbErrorHandler which is a subclass of WFCatErrorHandler providing
	Nimbus only customizations to the WFCatErrorHandler functionality.
	
	\param wfCatalog 
]]--

function nmbErrorHandlerPrototype:__init( wfCatalog )
	
	check( 'NmbErrorHandler', 'WFCatalog' )
	
	local clientHandlerFns = {
		externalizeSettings = WFCatDevelopSettings.externalizeDevelopSettings,
		deleteAssets = deleteAssets,
		deleteIndividualAlbum = WFCatAlbums.deleteIndividualAlbum,
		getSyncStatusOfAssetsOfAlbumAssets = WFCatSyncStatus.getSyncStatusOfAssetsOfAlbumAssets,
		getSyncStatusOfAlbumsOfAlbumAssets = WFCatSyncStatus.getSyncStatusOfAlbumsOfAlbumAssets,
		getSyncStatusOfParentCollectionOfAlbums = WFCatSyncStatus.getSyncStatusOfParentCollectionOfAlbums,
		getSyncStatusOfAssetsOfVersions = WFCatSyncStatus.getSyncStatusOfAssetsOfVersions,
		getSyncStatusOfAssetsOfComments = WFCatSyncStatus.getSyncStatusOfAssetsOfComments,
		getSyncStatusOfAssetsOfFavorites = WFCatSyncStatus.getSyncStatusOfAssetsOfFavorites,
	} 
	
	return wfCatalog, clientHandlerFns

end

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

--[[
	\brief Completes initialization of the NmbErrorHandler after the superclass has been initialized
]]--

function nmbErrorHandlerPrototype:__inited( )
	
	check( 'NmbErrorHandler' )
	
	self:registerErrorHandler( WFCatOzErrors.Errors.kPreconditionFailedError, preconditionFailedHandler )
	
end

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

return Rx.Class( 'NmbErrorHandler', nmbErrorHandlerPrototype )
