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

WFCatErrorHandler.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.

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

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

local KSDate = require 'KSDate'
local KSJsonDecoder = require 'KSJsonDecoder'
local KSIdGenerator = require 'KSIdGenerator'
local KSStringUtils = require 'KSStringUtils'
local KSTableUtils = require 'KSTableUtils'
local WFAnalytics = require 'WFAnalytics'
local WFArguments = require 'WFArguments'
local WFCatAssetAux = require 'WFCatAssetAux'
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatOzErrors = require 'WFCatOzErrors'
local WFCatTrash = require 'WFCatTrash'
local WFCatTrashUtils = require "WFCatTrashUtils"
local WFCloseSupport = require 'WFCloseSupport'
local WFCrypto = require 'WFCrypto'
local WFDiagnosticLog = require 'WFDiagnosticLog'
local WFFileUtils = require 'WFFileUtils'
local WFLogger = require 'WFLogger'
local WFOzDocUtils = require 'WFOzDocUtils'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxDocValueWatcher = require 'WFRxDocValueWatcher'
local WFRxRetryObservable = require 'WFRxRetryObservable'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxThrottledBatchedBulkApplyOverlay = require 'WFRxThrottledBatchedBulkApplyOverlay'
local WFRxThrottledBatchedBulkDocValue = require "WFRxThrottledBatchedBulkDocValue"
local WFRxScheduler = require 'WFRxScheduler'
local WFTableArchiving = require 'WFTableArchiving'
local WFURI = require 'WFURI'

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

local logger = WFLogger 'WFCatErrorHandler'

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

local derefOpt = KSTableUtils.derefOpt
local check = assert( WFArguments.check )
local setNested = KSTableUtils.setNested

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

local function errors_table( )

	return {

		sql = [[CREATE TABLE errors( type )]],

		relevantTypeSubtypeTuples = {
			{ 'error', '*' }
		},

		map = function( docValue, emit )

			if docValue.type == 'error' and docValue._deleted ~= true then

				local isUnknownError = derefOpt( docValue, '_localOnly', 'unknownError' )

				if not isUnknownError then
					emit( docValue.subtype )
				else

					if docValue.subtype == WFCatOzErrors.Errors.kStorageExceededError then
						emit( docValue.subtype )
					end

					-- Here is where we try to look at previously unknown errors and see if it's a type
					-- that we now know about.  If so, we should be able to emit to the errors
					-- table and handle it.
					if docValue.subtype == WFCatOzErrors.Errors.kInputValidationError then

						local previouslyFailedHandling = derefOpt( docValue, '_localOnly', 'localError', WFCatOzErrors.Errors.kInputValidationError, 'handlingFailed' )

						-- missing 'download' field? there was a short time where Lightroom CC wasn't sending
						-- this correctly in some cases.
						local wasMissingDownloadValue = derefOpt( docValue, '_serverProvidedMetadata', 'errors', 'download', 1 )

						if wasMissingDownloadValue and not previouslyFailedHandling then
							emit( docValue.subtype )
							return
						end

						-- missing 'if_match'? there appear to be some cases where we are missing the server provided metadata
						-- and the revision_ids and so while we send an update, we don't send an if_match revision.  Not yet
						-- sure why this occurs, but it appears to happen mainly to deleted assets. See NMB-6190
						local wasIfMatchMissing = derefOpt( docValue, '_serverProvidedMetadata', 'errors', 'if_match', 1 )
						if wasIfMatchMissing == 'is required' and not previouslyFailedHandling then
							emit( docValue.subtype )
							return
						end

						-- Oz returns InputValidationError, not a ResourceNotFoundError. This might've caused this
						-- error to not be handled in the past. If this error was not handled before we handle it
						-- in the same way we would if we saw the deletion via the sync feed.

						local assetDoesNotExist = derefOpt( docValue, '_serverProvidedMetadata', 'errors', 'asset', 1 ) == 'does not exist'
						if not previouslyFailedHandling and assetDoesNotExist then
							emit( docValue.subtype )
							return
						end

					end

					-- NMB-6352: Any errors previously marked unknown that have a status code of 5XX should be automatically
					-- retried by the default handler which now should handle these appropriately
					local maybeStatusCode = tonumber( derefOpt( docValue, '_serverProvidedMetadata', 'status' ) )
					if maybeStatusCode and ( 500 <= maybeStatusCode and maybeStatusCode < 600 ) then
						emit( docValue.subtype )
						return
					end

				end
			end
		end,

		hasOneRowPerDocument = true,
	}

end

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

local blockedUploadSubtypes = {
	[ WFCatOzErrors.Errors.kTrialOrSubscriptionExpiredError ] = true, [ WFCatOzErrors.Errors.kEntitlementExpiredError ] = true,
	[ WFCatOzErrors.Errors.kResourceNotFoundError ] = true,
	[ WFCatOzErrors.Errors.kInsufficientPermissionError ] = true,
}

local function blocked_uploads_table( )

	return {

		sql = [[CREATE TABLE blocked_uploads( spaceId, reason, type )]],

		useLocalId = WFPlatformInfo.isDesktop and true or false,

		hasOneRowPerDocument = true,

		relevantTypeSubtypeTuples = {
			{ '*', '*' }
		},

		map = function( docValue, emit )

			local errorSubtype = derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			local docType = derefOpt( docValue, 'type' )
			local spaceId = docValue._spaceId

			if errorSubtype and blockedUploadSubtypes[ errorSubtype ] and not docValue._deleted then
				emit( spaceId, errorSubtype, docType )
			end

		end,
	}

end

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

local function generateIndexSchema( indexStoreSchema )

	check( 'WFRxIndexStoreSchema' )

	indexStoreSchema:addTable( errors_table( ) )

	indexStoreSchema:addIndex [[CREATE INDEX error_type ON errors( type )]]

	indexStoreSchema:addTable( blocked_uploads_table( ) )

	indexStoreSchema:addIndex [[CREATE INDEX blocked_uploads_reason ON blocked_uploads( reason )]]

end

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

local function getErrorHandler( self, errSubtype )

	return self._errorDocumentHandlerFns[ errSubtype ]

end

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

local function addErrorDocToDiagnosticLog( self, errorDoc )

	-- for each error subtype record
	-- subtype
	--    * subtype count
	--    * status
	--    * source type (e.g. asset)
	--    * source action (e.g. create, update)
	--    * errors section (parse differently for most errors and PVE)
	--    * source ids (for easier troubleshooting)???

	local spm = derefOpt( errorDoc, '_serverProvidedMetadata' )
	local sourceDocType = derefOpt( spm, 'source', 'type' )
	local action = derefOpt( spm, 'source', 'action' )
	local status = derefOpt( spm, 'status' )
	local sourceDocId = derefOpt( spm, 'source', 'id' )

	local subtype = errorDoc.subtype
	local summaryForSubtype = self._errorDocSummary[ subtype ] or {}

	-- increment the count for all documents which hit this error subtype
	summaryForSubtype.count = ( summaryForSubtype.count or 0 ) + 1

	-- get the errors that this error doc represents (may be multiple)
	local errorDetails = {}
	local errors = derefOpt( spm, 'errors' )
	if type( errors ) == 'table' then
		for err, detailTable in pairs( errors ) do
			errorDetails[ #errorDetails + 1 ] = string.format( '%s:%s', err, detailTable[ 1 ] )
		end
	elseif type( errors ) == 'string' then
		errorDetails[ #errorDetails + 1 ] = errors
	end

	if not next( errorDetails ) then
		-- if, for some reason, there's no errors table then provide a default message so that
		-- it's still logged
		errorDetails[ #errorDetails + 1 ] = 'missing error info'
	end

	for _, errorDetail in ipairs( errorDetails ) do

		local errorKey = string.format( '%s:%s:%s:%s', sourceDocType, action, status, errorDetail )

		local errorSummary = summaryForSubtype[ errorKey ] or {}

		errorSummary.count = ( errorSummary.count or 0 ) + 1

		errorSummary.sourceDocIds = errorSummary.sourceDocIds or {}

		errorSummary.sourceDocIds[ #errorSummary.sourceDocIds + 1 ] = sourceDocId

		summaryForSubtype[ errorKey ] = errorSummary

	end

	self._errorDocSummary[ subtype ] = summaryForSubtype

end

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

local function _addErrorResolutionToDiagnosticLog( self, errorDoc, resolutionMethod )

	-- for each error subtype record
	-- subtype
	--    * count of number of resolutions recorded for this subtype
	--    * table indexed by resolution
	--         * count of number of errors docs resolved by this resolution method
	--         * source ids resolved by this resolution method

	local spm = derefOpt( errorDoc, '_serverProvidedMetadata' )
	local sourceDocId = derefOpt( spm, 'source', 'id' )

	local subtype = errorDoc.subtype

	local resolutionSummaryForSubtype = self._errorDocResolution[ subtype ] or {}
	resolutionSummaryForSubtype.count = ( resolutionSummaryForSubtype.count or 0 ) + 1

	resolutionSummaryForSubtype.resolution = resolutionSummaryForSubtype.resolution or {}
	local resolutionInfo = resolutionSummaryForSubtype.resolution[ resolutionMethod ] or {}
	resolutionInfo.count = ( resolutionInfo.count or 0 ) + 1

	local sourceDocIds = resolutionInfo.sourceDocIds or {}
	sourceDocIds[ #sourceDocIds + 1 ] = sourceDocId
	resolutionInfo.sourceDocIds = sourceDocIds

	resolutionSummaryForSubtype.resolution[ resolutionMethod ] = resolutionInfo
	self._errorDocResolution[ subtype ] = resolutionSummaryForSubtype

end

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

local function addErrorResolutionToDiagnosticLog( self, errorDoc, resolutionMethod )
	pcall( _addErrorResolutionToDiagnosticLog, self, errorDoc, resolutionMethod )
end

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

local function deleteErrorDoc( docStore, errorDocId )
	return docStore:deleteDoc( errorDocId )
end

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

local function markErrorAsUnknown( docStore, errorDocId )
	return docStore:applyOverlay {
			_id = errorDocId,
			_localOnly = {
				unknownError = true,
			}
		}
end

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

local function resolveErrorWithResult( self, errorDoc, resolutionCaller, observableReturningResolution )

	return observableReturningResolution
		:tap( function( resolution )
			resolutionCaller = resolutionCaller and ( "_" .. resolutionCaller ) or ""
			addErrorResolutionToDiagnosticLog( self, errorDoc, resolution .. resolutionCaller )
		end )
		:catch( Observable.just() )
		:flatMap( function()
			-- delete the error doc once we've handled the error document
			return deleteErrorDoc( self._catalog:docStore(), errorDoc._localId )
		end )

end

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

local function resolveError( self, errorDoc, resolution, resolvingObservable )

	return resolvingObservable
		:tap( function()
			addErrorResolutionToDiagnosticLog( self, errorDoc, resolution )
		end )
		:catch( Observable.just() )
		:flatMap( function()
			-- delete the error doc once we've handled the error document
			return deleteErrorDoc( self._catalog:docStore(), errorDoc._localId )
		end )
		:map( function()
			return resolution
		end )

end

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

local function resolveErrorByIgnoring( self, errorDoc, resolution )

	resolution = resolution or ""

	return resolveError( self, errorDoc, "Ignored" .. resolution, Observable.just( true ) )

end

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

local function resolveErrorByApplyingOverlay( self, errorDoc, resolution, overlay )

	resolution = resolution or ""

	return resolveError( self, errorDoc, resolution, self._catalog:docStore():applyOverlay( overlay ) )

end

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

local function resolveErrorByMarkingWithOzError( self, errorDoc, resourceId )

	return resolveError( self, errorDoc, "MarkedWithOzError", self._catalog:docStore():applyOverlay(
		WFCatOzErrors.createOverlayForOzErrorSubtype( resourceId, errorDoc.subtype )
	) )

end

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

local function resolveByRestoringAssetFromOz( self, errorDoc, resourceId )

	-- The following code discards the current, unsynced revision of a document
	-- and instead makes sure the local revision matches what's on Oz. It does
	-- this by fetching the latest revision from Oz, figuring out the delta
	-- against our local revision, then applying that as an overlay while
	-- forcing the new revision Id to be that of the Oz revision.

	local refreshAssetFromOz = self._catalog:docStore():docValue( resourceId )

		:flatMap( function( localDocValue )

			local catalogSpec = self._catalog:ozCatalogSpec()

			local documentUrl = KSTableUtils.derefOpt( localDocValue, '_serverProvidedMetadata', 'links', "self", 'href' )

			return WFRxRetryObservable {

				maxAttempts = 3,
				retryDelay = 10,

				factory = function()

					local options = {
						bypassRateLimiter = true,
					}

					return catalogSpec:get( documentUrl, options )

						:flatMap( function( result, headers )

							if headers.status_code == '200' then

								local decodedDoc = KSJsonDecoder.decodeWebSafeJson_stripNull( result )

								local ozDocValue = WFOzDocUtils.remapBasicOzDocument( decodedDoc, decodedDoc.base )

								-- Calculate the overlay that we need to apply to the existing revision so that
								-- after applying the overlay, the document will exactly match the revision
								-- on Oz.

								local overlay = require 'WFTableMerge'.createOverlay( localDocValue, ozDocValue )

								-- Remove all immutable or local fields from the overlay. Mostly this removes things
								-- like the _changeCounter, _localOnly, etc.

								for k in pairs( overlay ) do
									if k:match( '^_' ) then
										overlay[ k ] = nil
									end
								end

								-- Add back in the doc Id, as well as a the 'oz' label which indicates we got this
								-- data from Oz and will avoid an unnecessary sync.

								overlay._id = ozDocValue._id
								overlay._newLabel = 'oz'

								-- Make sure that the new revision that we are creating has the same revision Id as
								-- the latest revision on Oz. Forcing a specific revision Id can cause there to
								-- be multiple revisions with the same rev id, but this is fine. More than one revision
								-- with the same rev id happens all the time for _localOnly only changes made by
								-- client local processes

								local ozRevisions = derefOpt( ozDocValue, '_serverProvidedMetadata', 'revision_ids' )
								overlay._newRevId = ozRevisions and ozRevisions[ #ozRevisions ]

								return self._catalog:docStore():applyOverlay( overlay )

							end

							return Observable.throw( "Unable to refresh asset from Oz" )

						end )

				end
			}

		end )

	return resolveError( self, errorDoc, "RefreshAssetFromOz", refreshAssetFromOz )

		:catch( function()

			-- If we are not able to refresh the document from Oz. Just mark it with an Oz error.

			return resolveError( self, errorDoc, "MarkedWithOzError", self._catalog:docStore():applyOverlay(
					WFCatOzErrors.createOverlayForOzErrorSubtype( resourceId, errorDoc.subtype )
				) )

		end )

end

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

local function resolveByDeletingOrRestoringFromOz( self, errorDoc, docValueThatIncurredError )
	
	local catalog = self._catalog
	local docStore = catalog:docStore()
	
	local spm =  derefOpt( errorDoc, '_serverProvidedMetadata' )
	local resourceId = derefOpt( spm, 'source', 'id' )
	local resourceRevId = derefOpt( spm, 'source', 'revision_id' )
	
	local actionOnDocThatIncurredError = derefOpt( spm, 'source', 'action' )
	local idOfDocThatIncurredError = derefOpt( spm, 'source', 'id' )
	
	local errors = derefOpt( spm, 'errors' )
	
	if errors and type( errors ) == 'table' then
		
		for key, errorCode in pairs( errors ) do
			
			-- errorCode comes from Oz as an array of a single item - the actual errorCode
			errorCode = type( errorCode ) == 'table' and derefOpt( errorCode, 1 ) or errorCode
			logger:warnf( "payload validation error on '%s' for %s revId: %s, action: %s, error code: %s", 
					key, resourceId, resourceRevId, actionOnDocThatIncurredError, errorCode )
			
		end
	end
	
	if actionOnDocThatIncurredError == 'create' then
		
		return resolveError( self, errorDoc, "RemovedLocalDocument_NotMatchingMissingResourceId",
			docStore:deleteDoc( idOfDocThatIncurredError ) )
			
	else
		
		return resolveByRestoringAssetFromOz( self, errorDoc, resourceId )
		
	end
	
end

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

local function resolveErrorAsUnknown( self, errorDoc, resolution )

	return markErrorAsUnknown( self._catalog:docStore(), errorDoc._id )
		:tap( function()
			addErrorResolutionToDiagnosticLog( self, errorDoc, resolution )
		end )

end

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

local dependentDocTypes = {
	[ 'album_asset' ] = true,
	[ 'stack_asset' ] = true,
	[ 'comment' ] = true,
	[ 'favorite' ] = true,
	[ 'version' ] = true
}

local function isDependentDocumentType( docType )

	return docType and dependentDocTypes[ docType ]

end

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

local function mimicOzDeletionEvent( docStore, docId, type, subtype )

	-- Rather than just 'deleteDoc' we create a deletion document that looks like the
	-- deletion we would get from Oz in the sync feed.  This ensures that we clean up the
	-- document in the same way we would if we saw the deletion via the sync feed.
	local deletionDoc = {
			_id = docId,
			_deleted = true,
			_revIds = {
				KSIdGenerator.uuidInLowercaseHex(), -- Oz does not send revision ids on deletions
				force = true,
			},
			_serverProvidedMetadata = {
				deleted_by = 'WFCatErrorHandler'
			},
			type = type,
			subtype = subtype
		}

	return docStore:bulkSetDocValue( { label = 'oz', deletionDoc } )

end

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

local function getOrCreateSubtablesForPath( overlay, path )

	local sub = overlay

	for i, v in ipairs( path ) do

		local d = sub[ v ]
		if not d then
			d = {}
			sub[ v ] = d
		end

		sub = d

	end

	return overlay, sub

end

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

local function createValueAtPath( overlay, path, value )

	local key = path[ #path ]

	table.remove( path, #path )

	local sub
	overlay, sub = getOrCreateSubtablesForPath( overlay, path )

	sub[ key ] = value


	return overlay

end

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

local function removeOffendingKey( overlay, docPath )


	-- INFO [pkamin 2016-07-25]: validate errorCode for correct handling
	-- ( disallowed, invalid, required )
	-- Right now we delete the value to unblock sync which is very aggressive.
	-- We might be able to act smarter if the logic can be driven by the Data Model
	-- Schema JSON - escpecially for the `required` error code

	createValueAtPath( overlay, docPath, { _delete = true } )

end

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

local function fixTimestamp( overlay, docPath, value, errorCode )

	if errorCode == 'disallowed' then
		-- Some video 1.0 assets have userUpdated in the develop section
		-- which is no longer allowed for video 2.0 assets.
		removeOffendingKey( overlay, docPath )
		return
	end

	local date = KSDate.recoverDateForOz( value )
	if not date then
		date = KSDate.timeInIso8601Format( nil, false )
	end

	createValueAtPath( overlay, docPath, date )

end

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

local function fixCaptureDate( overlay, docPath, value, errorCode )

	local date = KSDate.recoverDateForOz( value )
	if not date then
		date = "0000-00-00T00:00:00"
	end

	createValueAtPath( overlay, docPath, date )

end

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

local function fixTruncate( overlay, docPath, value, errorCode )

	-- if it's a number, truncate it, otherwise, remove the field
	if type( value ) == 'number' then
		createValueAtPath( overlay, docPath, math.floor( value ) )
	else
		createValueAtPath( overlay, docPath, { _delete = true } )
	end

end

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

local function fixFaceDimension( overlay, docPath, value, errorCode )

	if not value or value < 0 then
		createValueAtPath( overlay, docPath, 0 )
	elseif value > 1 then
		createValueAtPath( overlay, docPath, 1 )
	end

end

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

local function fixProcessingModel( overlay, docPath, value, errorCode )

	if not value and errorCode == 'required' then
		createValueAtPath( overlay, docPath, 'lightroom' )
	end

end

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

local function isUUID( str )

	return ( type( str ) == 'string' and
		#str == 32 and
		string.match( str, '^[0-9a-f]+$' ) )

end

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

local function fixRating( overlay, docPath, value, errorCode )

	if not value and errorCode == 'required' then
		-- with no rating it's invalid so we should just delete the
		-- full ratings section. So if the last section of the docPath is
		-- 'rating', then remove it. This check is mostly redundant because
		-- we shouldn't otherwise get here, but I don't want to blindly remove
		-- a path and cause way too much to be deleted.
		if #docPath >= 2 and
			docPath[ #docPath ] == 'rating' and
			isUUID( docPath[ #docPath - 1 ] ) then

			-- strip off 'rating'
			table.remove( docPath, #docPath )

			-- remove ratings.uuid section
			createValueAtPath( overlay, docPath, { _delete = true } )

		end
	end

end

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

local function fixSpaceDocument( catalog, docValue, errorKey, errorValue )

	local overlay = {}

	if errorKey == 'download' and docValue.download == nil then
		overlay.download = false
	end

	if next( overlay ) then
		overlay._id = docValue._id
		return catalog:docStore():applyOverlay( overlay )
	else
		return Observable.just( false )
	end

end

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

local function fixPayloadPrimary( catalog, docValue, errorKey, errorValue )

	if errorKey == 'payload.primary'
		and docValue.type == 'asset'
		and docValue.subtype == 'person'
		and docValue.primary then

		--NMB-10093 -> delete primary due to cyclic relationships.
		local overlay = {
			primary = { _delete = true },
			_id = docValue._id
		}
		return catalog:docStore():applyOverlay( overlay )
	end

	return Observable.just( false )
end

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

local function fixAssetDoesNotExist( catalog, docValue, errorKey, errorValue )

	if errorKey == 'asset' and errorValue == 'does not exist' then

		-- consider this a deletion from Oz
		return mimicOzDeletionEvent( catalog:docStore(), docValue._id, docValue.type, docValue.subtype )

	end

	return Observable.just( false )

end

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

local pathComponentBySubtype = {
	image = 'assets',
	video = 'assets',
}

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

local function fixIfMatchIssue( catalog, docValue )

	-- happy path for fixing this error assumes internet access
	local docId = docValue._id

	-- get link from docValue's self href if it's there
	-- otherwise construct the url manually
	local documentUrl = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', 'self', 'href' )
	if not documentUrl then
		local href = catalog:ozCatalogSpec():catalogHref()

		local pathComponent = pathComponentBySubtype[ docValue.subtype ]
		if pathComponent then
			href = WFURI.stringByAppendingPathComponents( href, pathComponent )
			href = WFURI.stringByAppendingPathComponents( href, docId )
			documentUrl = href
		end

	end

	if not documentUrl then
		logger:warnf( "No URL, unable to handle InputValidationError for %s", docId )
		return Observable.just()
	end

	return catalog:ozCatalogSpec():get( documentUrl, { returnHttpErrorsAsData = true } )
		:flatMap( function( result, headers )
			if headers.status_code == '200' then
				-- convert JSON document and insert from Oz
				local decodedDoc = KSJsonDecoder.decodeWebSafeJson_stripNull( result )

				local docValue = WFOzDocUtils.remapBasicOzDocument( decodedDoc, decodedDoc.base )

				if docValue and not docValue._deleted then
					return catalog:docStore():bulkSetDocValue( { label = 'oz',
																docValue } )
				end

			elseif headers.status_code == '404' then

				-- consider this a deletion from Oz
				return mimicOzDeletionEvent( catalog:docStore(), docId, docValue.type, docValue.subtype )

			end

			return Observable.just( )

		end )
		:catch( Observable.just( ) )

end

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

local function fixInvalidStackCover( catalog, docValue, errorKey, errorValue )

	-- send a dumanis event for tracking

	local event = {}

	event['event.type'] = 'error'
	event['event.subcategory'] = 'Error_handler'
	event['event.error_type'] = errorKey
	event['event.error_message'] = errorValue

	event['content.docId'] = derefOpt( docValue, '_id' )
	event['content.subtype'] = derefOpt( docValue, 'subtype' )
	event['content.action'] = derefOpt( docValue, 'action' )

	WFAnalytics.trackEvent( event )

	if WFPlatformInfo.isDesktop and
		errorKey == 'cover' and
		( errorValue == 'must be a valid GUID' or
		  errorValue == 'must have an asset ID' ) then

		local idOfDocThatIncurredError = docValue._id
		local WFCatStacks = require 'WFCatStacks'

		return WFCatStacks.getOverlaysForNewCovers( catalog, { idOfDocThatIncurredError }, {} )
		:flatMapLatest( function( overlays )
			local newStackCoverOverlay = overlays and overlays[ 1 ]
			if newStackCoverOverlay then
				return catalog:docStore():applyOverlay( newStackCoverOverlay )
			else
				return WFCatStacks.deleteStack( catalog, idOfDocThatIncurredError )
					:concat( Rx.Observable.just( true ) )
			end
		end )
		
	end
	
	return Observable.just( true )

end

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

-- these functions just add to an overlay
local payloadValidationFixups = {
	-- fixups for asset documents
	[ 'payload.importSource.importTimestamp' ] = fixTimestamp,
	[ 'payload.captureDate' ] = fixCaptureDate,
	[ 'payload.develop.userUpdated' ] = fixTimestamp,
	[ 'payload.develop.adjustments.userUpdated' ] = fixTimestamp,
	[ 'payload.develop.temporal.userUpdated' ] = fixTimestamp,
	[ 'payload.develop.processingModel' ] = fixProcessingModel,
	[ 'payload.importSource.originalWidth' ] = fixTruncate,
	[ 'payload.importSource.originalHeight' ] = fixTruncate,
	[ 'payload.xmp.mwg-rs.Regions.RegionList.uuid.Area.x' ] = fixFaceDimension,
	[ 'payload.xmp.mwg-rs.Regions.RegionList.uuid.Area.y' ] = fixFaceDimension,
	[ 'payload.ratings.uuid.rating' ] = fixRating,
}

local exifPayloadValidationFields = {
	[ 'FNumber' ] = true,
	[ 'ApertureValue' ] = true,
	[ 'ExposureTime' ] = true,
	[ 'FocalLength' ] = true,
}

-- these functions must return an Observable that fixes the document
local inputValidationFixups = {
	-- fixups for space documents
	[ 'download' ] = fixSpaceDocument,
	[ 'if_match' ] = fixIfMatchIssue,
	[ "payload.primary" ] = fixPayloadPrimary,
	[ 'asset' ] = fixAssetDoesNotExist,
	[ 'cover' ] = fixInvalidStackCover,
}

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

local function maybeGetFixFunctionForKey( key )

	-- some keys are at variable places in the data model
	-- e.g. payload.xmp.mwg-rs.Regions.RegionList.0185a47f32354e3d9cf56634a8b20109.Area.x
	-- so we need to possible replace it before looking up the fixFn
	local canonicalKey = string.gsub( key, 'RegionList%..*%.Area', 'RegionList.uuid.Area' )
	canonicalKey = string.gsub( key, 'ratings%..*%.rating', 'ratings.uuid.rating' )

	canonicalKey = canonicalKey or key

	return payloadValidationFixups[ canonicalKey ]

end

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

local function parseDocPathFromKey( key, docValueThatIncurredError )

	local docPath = KSStringUtils.split( key, '%.' )

	if #docPath < 1 then
		return nil
	end

	if #docPath == 1 then
		-- Something below payload level...this generally makes no sense and is not fixable, but it may make sense
		-- for some doc types (spaces) so if we *know* it's fixable, use it, otherwise don't consider the docPath
		-- fixable.  Add future cases to this if
		if docValueThatIncurredError.type == 'space' then
			return docPath
		else
			return nil
		end
	end

	-- remove 'payload' level
	table.remove( docPath, 1 )

	local actualKey = docPath[ #docPath ]

	-- remove final key from path (we'll add it back)
	table.remove( docPath, #docPath )

	-- If key is a number, then we should probably do the following:
	-- Check to make sure it being considered a string isn't correct by seeing
	-- whether the docValue has something at that key.
	-- If so, assume it should be treated as a string for the delete
	-- If not, treat it as a number index into an array but we need to add 1
	-- since Oz indexes are 0 based and lua is 1 based
	local maybeNumber = tonumber( actualKey )
	if maybeNumber then

		local valueAtStringPath
		if #docPath > 0 then
			valueAtStringPath = derefOpt( docValueThatIncurredError, unpack( docPath ), actualKey )
		else
			valueAtStringPath = derefOpt( docValueThatIncurredError, actualKey )
		end

		if valueAtStringPath then
			-- it looks like a number, but it quacks like a string so treat it as such
			table.insert( docPath, actualKey )

		else
			-- it looks like a number and it doesn't quack like a string so treat it as a number
			-- (but add 1 because our arrays are 1 based)
			local parentOfActualKey = docPath[ #docPath ] -- parent key is same as #docPath because last value was removed previously.

			if not exifPayloadValidationFields [ parentOfActualKey ] then
				table.insert( docPath, maybeNumber + 1 )
			end

		end

	else
		-- not even close to a number
		table.insert( docPath, actualKey )

	end

	return docPath

end

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

local function lastRetryIsOlderThanXHours( maybeDate, hours )

	if not maybeDate then
		return true
	end

	local olderThanInSeconds = hours * 60 * 60
	local now = KSDate.timeInIso8601Format()

	local differenceInSeconds = KSDate.differenceInSeconds( maybeDate, now )

	return differenceInSeconds > olderThanInSeconds
end

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

local function maybeTrashOperationNewId( errorDoc )

	return derefOpt( errorDoc, '_serverProvidedMetadata', 'new_id' )

end

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

local function isErrorFromALocalDelete( errorDoc, missingResourceDocValue, docThatIncurredError )

	local missingResourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'missing_resource_id' )

	return missingResourceId
				and missingResourceId == derefOpt( docThatIncurredError, '_id' )
				and derefOpt( docThatIncurredError, '_deleted' )
				and missingResourceDocValue._newId

end

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

local function isErrorFromALocalRestore( errorDoc, missingResourceDocValue, docThatIncurredError )

	local missingResourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'missing_resource_id' )

	return missingResourceId
				and missingResourceId ~= derefOpt( docThatIncurredError, '_id' )
				and WFCatTrashUtils.getCreateFromId( docThatIncurredError ) == missingResourceId
				and not derefOpt( docThatIncurredError, '_deleted' )

end

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

local function isDeletedByAnotherUserWithLocalNewId( self, errorDoc, missingResourceDocValue, docThatIncurredError )

	local maybeDeletedBy = derefOpt( errorDoc, '_serverProvidedMetadata', 'deleted_by' )
	local maybeNewId = derefOpt( docThatIncurredError, '_newId' )

	return not not ( maybeDeletedBy and maybeDeletedBy ~= self._catalog:ozUserId() and
		maybeNewId )

end

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

local function isSmartAlbumPayloadError( errorDoc, docThatIncurredError )
	
	return errorDoc.subtype == WFCatOzErrors.Errors.kPayloadValidationError
		and docThatIncurredError.type == "album"
		and docThatIncurredError.subtype == "smart"
		
end

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

local function maybeMarkDocumentForRetry( docStore, docValue, subtype, statusCode, maybeErrorDoc )

	local prevSubtype = derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
	local prevStatus = derefOpt( docValue, '_localOnly', 'ozError', 'status' )

	local existingRetries = tonumber( derefOpt( docValue, '_localOnly', 'ozError', 'retries' ) ) or 0
	local maybeLastRetry = derefOpt( docValue, '_localOnly', 'ozError', 'lastRetry' )

	local maybeOverlayObservable

	-- if we've already retried 3 times in the last hour, don't do it again as it seems likely
	-- to not really be a retryable error and instead something more persistent. a manual change
	-- may be required. If it is a different error or it has been an hour since last retry,
	-- reset the retries.

	if lastRetryIsOlderThanXHours( maybeLastRetry, 1 )
			or subtype ~= prevSubtype or statusCode ~= prevStatus then
		existingRetries = 0
	end

	if existingRetries < 3 then

		-- applyOverlay uses runUntilDone, and we want to delay execution of it
		-- until maybeOverlayObservable is subscribed to. Hence the Observable.just( true ).
		-- https://lightroom.slack.com/archives/C04NX2H98/p1731009433281959

		maybeOverlayObservable = Observable.just( true )

			:flatMap( function()

				return docStore:applyOverlay {
					_id = docValue._id,
					_localOnly = {
						ozError = {
							subtype = subtype,
							status = statusCode,
							retries = existingRetries + 1,
							lastRetry = KSDate.timeInIso8601Format()
						}
					}
				}

			end )

			:map( function()
				return "MarkedForRetry"
			end )

	else

		if maybeErrorDoc then
			logger:warnf( "Exceeded retries for error document %s", WFTableArchiving.tableToString( maybeErrorDoc ) )
		else
			logger:warnf( "Exceeded retries for error subtype %s and status code %d", subtype, statusCode )
		end

		maybeOverlayObservable = Observable.just( "Ignored_ExceededRetries" )

	end

	return maybeOverlayObservable

end

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

local function markDocumentForRetry( docStore, docValue, errorDoc )

	local spm =  derefOpt( errorDoc, '_serverProvidedMetadata' )
	local subtype = derefOpt( errorDoc, 'subtype' )
	local statusCode = tonumber( derefOpt( spm, 'status' ) )

	return maybeMarkDocumentForRetry( docStore, docValue, subtype, statusCode, errorDoc )

end

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

local function markVersionForRetryOnceAssetAuxSync( self, docStore, versionDoc, errorDoc, missingAuxIds )

	local assetId = derefOpt( versionDoc, 'asset', 'id' )

	if not assetId then
		return resolveErrorByIgnoring( self, errorDoc, "VersionHasNoParentAsset" ) -- This should never happen.
	end

	-- Watch the Version's Asset for document updates, and mark the Version
	-- for re-upload once all the corresponding auxiliaries have been uploaded.
	--
	-- We are using WFRxDocValueWatcher here, which relies on WFRxDocStoreChangesFeed
	-- under the hood. This should be cheaper than using something like ongoingDocValue,
	-- especially in the case where there are many Version documents being monitored
	-- by this code. One potential optimization would be to aggregate all Versions
	-- being monitored and issue a single call to WFRxDocValueWatcher with all the parent
	-- Asset IDs. However, I'm not sure this would really buy us much, considering
	-- all updates are coming from a single feed anyway (WFRxDocStoreChangesFeed).

	local watcherDesc = string.format( "WFCatErrorHandler:markVersionForRetryOnceAssetAuxSync:%s", assetId )

	return Observable.just( { assetId } )

		:call( WFRxDocValueWatcher.make, docStore, watcherDesc )

		:flatMapLatest( function( assetDoc )

			local versionReadyForUpload = true

			local anAuxHasLocalError = false

			for _, auxId in ipairs( missingAuxIds ) do

				local existsOnOz, auxLinkInvalid = WFCatAssetAux.isAuxObjectOnOz( assetDoc, auxId )

				if not existsOnOz or auxLinkInvalid then

					if WFCatAssetAux.hasLocalAuxError( assetDoc, auxId ) then
						anAuxHasLocalError = true
					else
						versionReadyForUpload = false
					end

					break

				end

			end

			if anAuxHasLocalError then

				-- The aux will not be uploaded, so we'll just resolve the error by ignoring.

				return resolveErrorByIgnoring( self, errorDoc, "AssetAuxHasLocalError" )
					:map( function() return true end )

			elseif versionReadyForUpload then

				-- Need to wait for the aux to upload before we can resolve the error and retry.

				local actionObservable = markDocumentForRetry( docStore, versionDoc, errorDoc )
				local resolveObservable = resolveErrorWithResult( self, errorDoc, 'MarkedForRetryForBinaryMissingError', actionObservable )

				return resolveObservable
					:map( function() return true end )

			end

			return Observable.just( false ) -- Still waiting

		end )

		:filter( function( hasResolved )
			return hasResolved
		end )

		:take( 1 )

end

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

-- @mark Error Handler Functions

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

-- STEP ONE: Create the subtype-specific error handlers

local function defaultHandler( self, errorDoc, docValue )

	local docStore = self._catalog:docStore()

	local stringOfErrorDoc = WFTableArchiving.tableToString( errorDoc )

 	local spm =  derefOpt( errorDoc, '_serverProvidedMetadata' )
	local statusCode = tonumber( derefOpt( spm, 'status' ) )
 	local idOfDocThatIncurredError = derefOpt( spm, 'source', 'id' )
 	local actionOnDocThatIncurredError = derefOpt( spm, 'source', 'action' )

 	local errorEvent = {}
 	errorEvent['event.type'] = 'error'
 	errorEvent['event.appWorkflow'] = 'SYNC'
 	errorEvent['event.subcategory'] = 'Error_handler'
 	errorEvent['event.error_type'] = tostring( errorDoc.subtype )
 	errorEvent['event.error_desc'] = stringOfErrorDoc
	errorEvent['event.error_code'] = statusCode
 	errorEvent['content.id'] = idOfDocThatIncurredError
	errorEvent['content.subtype'] = derefOpt( docValue, 'subtype' )
	errorEvent['content.action'] = actionOnDocThatIncurredError

	-- on 5XX errors - regardless of the error type, we should be able to retry the document
	-- update on Oz. So annotate the document with some error info which will also update the
	-- sequence and cause it to try to be resent
	if statusCode and ( 500 <= statusCode and statusCode < 600 ) then

		WFAnalytics.trackEvent( errorEvent )

		return resolveErrorWithResult( self, errorDoc, "MarkedForRetryByDefaultHandler",
			markDocumentForRetry( docStore, docValue, errorDoc ) )

	else

		-- Note to future maintainers: We have no special handling for 'TooManyRevisionsError',
		-- or for 'TooManyAuxError'. These are considered unknown errors by our error handler,
		-- and up in being handled by the following code.

		-- When an unknown error occurs, we do not automatically retry uploading the revision
		-- when this error is received, but we do not explicitly prevent the uploading of future
		-- revisions either.

		-- A document that encounters an unknown error will sit in a pending sync state where it
		-- has an unsynced local revision. The document will remain in a pending sync state until
		-- either:
		--
		-- a.) A new revision is created locally, which will trigger sync of the new revision.
		--
		-- b.) Oz sends us a new revision that resolves the issue.

		-- Log a warning that we received an error document from Oz that we are not sure how to handle.

		local subtypeStr = tostring( errorDoc.subtype )

		if subtypeStr == 'RevisionExistsError' then

			-- SPAM [jandrew 2022-09-28]: RevisionExistsError can occur in normal workflows, and we
			-- always ignore it. This error occurs when WF sends a revision to Oz that Oz already
			-- knows about. This is actually pretty common, and can be reproduced very easily if
			-- you know the right tricks.
			--
			-- After we send a revision to Oz, there exists a window in which we've sent the revision
			-- to Oz, but have yet to receive that same revision back down on the sync feed.
			-- If a change to the localOnly section of the document occurs in this window, then WF
			-- will attempt to resend the revision to Oz. This is known behavior, but it's harmless.

			logger:infof( "Ignoring error document type %s: %s", subtypeStr, WFTableArchiving.tableToString( errorDoc ) )

			errorEvent[ 'event.type' ] = 'warn' -- Reduce level from error to warn

		else

			logger:warnf( "Unhandled error document type %s: %s", subtypeStr, WFTableArchiving.tableToString( errorDoc ) )

		end

		WFAnalytics.trackEvent( errorEvent )

		-- mark it so we're not stuck on it.
		return resolveErrorAsUnknown( self, errorDoc, "MarkedAsUnknownByDefaultHandler" )

	end
end

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

local function payloadValidationHandler( self, errorDoc, docValueThatIncurredError )
	
	local spm =  derefOpt( errorDoc, '_serverProvidedMetadata' )
	local resourceId = derefOpt( spm, 'source', 'id' )
	local resourceRevId = derefOpt( spm, 'source', 'revision_id' )
	local typeOfDocThatIncurredError = derefOpt( spm, 'source', 'type' )
	local actionOnDocThatIncurredError = derefOpt( spm, 'source', 'action' )
	
	local errors = derefOpt( spm, 'errors' )
	
	logger:warnf( "payload validation error for %s revId: %s", resourceId, resourceRevId )
	
	if isSmartAlbumPayloadError( errorDoc, docValueThatIncurredError ) then
		
		return resolveByDeletingOrRestoringFromOz( self, errorDoc, docValueThatIncurredError )
		
	end
	
	local overlay = {}
	local interestingSourceDocSections = {}
	local hasUnknownError = false

	if errors and type( errors ) == 'table' then

		for key, errorCode in pairs( errors ) do

			-- errorCode comes from Oz as an array of a single item - the actual errorCode
			errorCode = type( errorCode ) == 'table' and derefOpt( errorCode, 1 ) or errorCode

			logger:warnf( "payload validation error on '%s' for %s revId: %s, error code: %s", key, resourceId, resourceRevId, errorCode )

			local docPath = parseDocPathFromKey( key, docValueThatIncurredError )

			if docPath then

				local existingValue = derefOpt( docValueThatIncurredError, unpack( docPath ) )

				setNested( interestingSourceDocSections, existingValue, unpack( docPath ) )

				local fixFn = maybeGetFixFunctionForKey( key )
				if fixFn then

					fixFn( overlay, docPath, existingValue, errorCode )

				else

					-- http://apidocs.adbecrsl.com/v2/data-model/error/index.html
					--
					-- required: The key indicated is missing from the payload and it is required.
					--
					-- dependency_required: The key indicated is missing from the payload, and is
					-- required because another key is present that has a dependency on this key.

					if errorCode ~= 'required'
							and errorCode ~= 'dependency_required'
							and errorCode ~= 'cannot be deleted'
							and errorCode ~= 'cannot be updated'
							and existingValue then
						-- If it's required and we remove it again, we've created a looper, so don't
						-- Also, if we don't have it...don't remove it, that may add cruft back in (NMB-11045) and so
						-- we shouldn't just trust Oz saying we sent something at this path
						removeOffendingKey( overlay, docPath )
					end

				end

			else

				hasUnknownError = true

			end

		end

	end

	local errorEvent = {}

	errorEvent['event.type'] = 'error'
	errorEvent['event.appWorkflow'] = 'SYNC'
	errorEvent['event.subcategory'] = 'Error_handler'
	errorEvent['event.error_type'] = 'PayloadValidationError'
	errorEvent['event.error_desc'] = WFTableArchiving.tableToString( errorDoc )
	errorEvent['event.error_code' ] = hasUnknownError and 'UnknownError'
	errorEvent['event.value'] = WFTableArchiving.tableToString( overlay )

	errorEvent['content.id'] = resourceId
	errorEvent['content.revision_id'] = resourceRevId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.subtype'] = derefOpt( docValueThatIncurredError, 'subtype' )
	errorEvent['content.action'] = actionOnDocThatIncurredError
	errorEvent['content.value'] = WFTableArchiving.tableToString( interestingSourceDocSections )

	WFAnalytics.trackEvent( errorEvent )

	if hasUnknownError and not next( overlay ) then
		-- there's nothing we fixed and at least one error seen was unknown
		return resolveErrorAsUnknown( self, errorDoc, "MarkedAsUnknown" )
	end

	if next( overlay ) then
		overlay._id = resourceId
		return resolveErrorByApplyingOverlay( self, errorDoc, "AppliedOverlay", overlay )
	end

	return resolveErrorByIgnoring( self, errorDoc, "NoOverlayToApply", overlay )

end

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

local function inputValidationHandler( self, errorDoc, docValue )

	local catalog = self._catalog

	local spm =  derefOpt( errorDoc, '_serverProvidedMetadata' )
	local resourceId = derefOpt( spm, 'source', 'id' )
	local resourceRevId = derefOpt( spm, 'source', 'revision_id' )
	local typeOfDocThatIncurredError = derefOpt( spm, 'source', 'type' )
	local actionOnDocThatIncurredError = derefOpt( spm, 'source', 'action' )

	local errors = derefOpt( spm, 'errors' )
	local errorKey, errorValue
	
	if errors and type( errors ) == 'table' then

		-- the input validation table is in an odd form like
		-- errors = {
		--     download = {
		--         "must be true or false"
		--     }
		-- }
		-- so parse the last level like an array
		
		errorKey = next( errors )
		
		if errorKey then
			errorValue = derefOpt( errors, errorKey, 1 )
		end

	end

	logger:warnf( "InputValidationError for %s revId: %s", resourceId, resourceRevId )

	return Observable.just( docValue )

		:flatMap( function( docValueThatIncurredError )

			if errorKey and inputValidationFixups[ errorKey ] then

				local fixFn = inputValidationFixups[ errorKey ]

				return fixFn( catalog, docValueThatIncurredError, errorKey, errorValue )
						:map( function( wasHandled )
							return not not wasHandled
						end )
			end

			return Observable.just( false )
		end )
		:flatMap( function ( wasHandled )

			 if wasHandled then
				return Observable.just( true )
			else
				-- this error is for us, but we don't know what to do with it
				-- mark the error document as unable to be handled right now so
				-- we don't try to process it anymore
				return self._catalog:docStore():applyOverlay( {
							_id = errorDoc._id,
							_localOnly = {
								localError = {
									[ errorDoc.subtype ] = {
										handlingFailed = true
									}
								},
							}
						} )
						:map( function()
							-- don't delete it, we may be able to process in the future
							return false
						end )
			end
		end )
		:flatMap( function( wasHandled )
			if wasHandled then

				-- fire an analytics event that we should be able to fix the error in the document
				local errorEvent = {}
				errorEvent['event.type'] = 'error'
				errorEvent['event.appWorkflow'] = 'SYNC'
				errorEvent['event.subcategory'] = 'Error_handler'
				errorEvent['event.error_type'] = WFCatOzErrors.Errors.kInputValidationError
				errorEvent['event.error_desc'] = errorValue
				errorEvent['event.error_code'] = 'fixed'

				errorEvent['content.id'] = resourceId
				errorEvent['content.revision_id'] = resourceRevId
				errorEvent['content.type'] = typeOfDocThatIncurredError
				errorEvent['content.subtype'] = derefOpt( docValue, 'subtype' )
				errorEvent['content.action'] = actionOnDocThatIncurredError
				errorEvent['content.url'] = errorKey

				-- track the error event
				WFAnalytics.trackEvent( errorEvent )

				-- delete error doc once we've marked the resource as storage exceeded
				return resolveError( self, errorDoc, "Fixed", Observable.just( true ) )
			else
				-- if we don't know how to handle a specific InputValidationError, use the default handler to mark it as
				-- unhandled rather than deleting it as a future update might be able to help here.
				return defaultHandler( self, errorDoc, docValue )
			end
		end )

end

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

local function createBaseErrorEvent()

	local errorEvent = {}
	errorEvent['event.type'] = 'error'
	errorEvent['event.appWorkflow'] = 'SYNC'
	errorEvent['event.subcategory'] = 'Error_handler'

	return errorEvent

end

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

local function resourceExistsHandler( self, errorDoc, docThatIncurredError )

	local catalog = self._catalog
	local docStore = catalog:docStore()

	local spmSource =  derefOpt( errorDoc, '_serverProvidedMetadata', 'source' )

	local idOfDocThatIncurredError = derefOpt( spmSource, 'id' )
	local typeOfDocThatIncurredError = derefOpt( spmSource, 'type' )
	local actionOnDocThatIncurredError = derefOpt( spmSource, 'action' )

	local documentUrl = derefOpt( spmSource, 'links', 'self', 'href' )

	local eventErrorCode

	local handleErrorDoc
	if not documentUrl then

		logger:warnf( "No document url for ResourceExistsError for resource id of %s, ignoring error doc", idOfDocThatIncurredError )
		handleErrorDoc = Observable.just()

	else

		-- let's handle the happy path...we're online...so check whether Oz has the asset by
		-- directly asking for it. If it does, we can ignore the ResourceExistsError - sync
		-- should resolve this eventually. If it does not, then it's a deleted asset and so
		-- we should delete it locally
		handleErrorDoc = catalog:ozCatalogSpec():get( documentUrl )
			:flatMap( function( result, headers )

				if headers.status_code == '200' then

					local ok, resultJson = pcall( KSJsonDecoder.decodeWebSafeJson, result )
					resultJson = ok and resultJson or {}

					local isDeleted = derefOpt( resultJson, 'deleted' ) == true

					if docThatIncurredError.type == 'asset' and isDeleted then

						if not self._deleteAssetsFn then
							logger:errorf( "Setup error: no deleteAssets implementation" )

							return Observable.just()
						end

						-- if it's an asset document, we need to clean up any associated local files
						-- NOTE: We use concat here because deleteAssets impls are provided by the client and we
						-- can't assume that it will give us only a single event. So take only the completion with
						-- takeLast( 0 ) and then concat with Observable.just so we do get a single value for our
						-- purposes
						eventErrorCode = 'deleted'
						return Observable.concat(
								self._deleteAssetsFn( catalog, { idOfDocThatIncurredError } ):takeLast( 0 ),
								Observable.just() )

					elseif docThatIncurredError.type == 'album' and isDeleted then

						if not self._deleteIndividualAlbumFn then
							logger:errorf( "Setup error: no deleteIndividualAlbum implementation" )

							return Observable.just()
						end

						-- if it's an album we need to clean up and associated album_assets)
						-- NOTE: We use concat here because deleteIndividualAlbum impls are provided by the
						-- client and we can't assume that it will give us only a single event. So take only
						-- the completion with takeLast( 0 ) and then concat with Observable.just so we do
						-- get a single value for our purposes
						eventErrorCode = 'deleted'
						return Observable.concat(
								self._deleteIndividualAlbumFn(  catalog, idOfDocThatIncurredError ):takeLast( 0 ),
								Observable.just() )

					elseif docThatIncurredError.type == 'album_asset' and ( isDeleted or resultJson.id ~= idOfDocThatIncurredError ) then

						-- album_assets may get a ResourceExistsError if the same album_asset association is
						-- made with a different album_asset_id than is already being used. so also delete the
						-- album_asset doc in that case
						if resultJson.id ~= idOfDocThatIncurredError then
							eventErrorCode = 'duplicate'
						else
							eventErrorCode = 'deleted'
						end

						return docStore:deleteDoc( idOfDocThatIncurredError )

					elseif docThatIncurredError.type == 'favorite' and ( isDeleted or resultJson.id ~= idOfDocThatIncurredError ) then


						-- 'favorite' may get a ResourceExistsError if a user marked the same shared asset document as
						-- favorite from two different clients and let them sync to Oz. In this case, we should delete
						-- the favorite doc that triggered the error
						if resultJson.id ~= idOfDocThatIncurredError then
							eventErrorCode = 'duplicate'
						else
							eventErrorCode = 'deleted'
						end

						return docStore:deleteDoc( idOfDocThatIncurredError )

					elseif isDeleted then

						-- other doc types just delete the doc itself
						eventErrorCode = 'deleted'
						return docStore:deleteDoc( idOfDocThatIncurredError )

					else

						eventErrorCode = 'missing confirmation'

						-- don't alter the existing document based on the error doc received, trust the sync process
						-- to resolve the issue that caused the ResourceExistsError
						return Observable.just( KSJsonDecoder.decodeWebSafeJson_stripNull( result ) )
							:map( function( decodedDocumentJson )

								local docValue = WFOzDocUtils.remapBasicOzDocument( decodedDocumentJson, decodedDocumentJson.base )

								return docValue
							end )
							:flatMap( function( docValue )

								if not docValue or docValue._deleted then
									return Observable.just()
								end

								return self._catalog:docStore():bulkSetDocValue( { label = 'oz',
                                                                                     docValue } )
							end )

					end
				end

				return Observable.just( false )

			end )
			:catch( function( responseData, headers )
				logger:warnf( "Error retrieving doc in ResourceExistsError for resource id of %s status_code %s, ignoring error doc", idOfDocThatIncurredError, derefOpt( headers, 'status_code' ) )
				return resolveError( self, errorDoc, "ignoredNotFound", Observable.just( false ) )
			end )
	end

	return handleErrorDoc:flatMap( function( result )

		local errorEvent = createBaseErrorEvent()
		errorEvent['event.error_type'] = WFCatOzErrors.Errors.kResourceExistsError
		errorEvent['event.error_code'] = eventErrorCode
		errorEvent['content.id'] = idOfDocThatIncurredError
		errorEvent['content.type'] = typeOfDocThatIncurredError
		errorEvent['content.subtype'] = derefOpt( docThatIncurredError, 'subtype' )
		errorEvent['content.action'] = actionOnDocThatIncurredError

		-- track the error event
		WFAnalytics.trackEvent( errorEvent )

		return resolveError( self, errorDoc, eventErrorCode or "ignored", Observable.just( result ) )

	end )

end

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

local function inlineSettingsHandler( self, errorDoc, docValue )

	local catalog = self._catalog

	local maybeLimit = derefOpt( errorDoc, '_serverProvidedMetadata', 'limit' )

	local spmSource =  derefOpt( errorDoc, '_serverProvidedMetadata', 'source' )
	local idOfDocThatIncurredError = derefOpt( spmSource, 'id' )
	local typeOfDocThatIncurredError = derefOpt( spmSource, 'type' )
	local actionOnDocThatIncurredError = derefOpt( spmSource, 'action' )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kInlineXmpDevelopSettingTooBigError

	errorEvent['content.id'] = idOfDocThatIncurredError
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.subtype'] = derefOpt( docValue, 'subtype' )
	errorEvent['content.action'] = actionOnDocThatIncurredError

	WFAnalytics.trackEvent( errorEvent )

	return resolveError( self, errorDoc, "ExternalizedXMP",
		self._externalizeSettingsFn( catalog, idOfDocThatIncurredError, tonumber( maybeLimit ) ) )

end

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

-- Generate an overlay for the trash asset from Oz from the local trash asset
-- before deleting the local trash asset. (Scenario: An asset was modified
-- and deleted from a paused sync client, while it was also deleted from a
-- different online/syncing client. The other client's trash asset gets kept,
-- after grabbing applicable data from the local trash asset and merging it in.)

local function mergeDeleteOrUpdateCreateFromOfLocalDocWithOzDoc( catalog, newAssetIdFromOz, localAssetId )

	local docStore = catalog:docStore()

	return WFCatTrashUtils.getTrashOrRestoredDocFromNewId( docStore, localAssetId )
	:combineLatest( docStore:docValue( newAssetIdFromOz ) )
	:flatMap( function( localTrashDoc, ozTrashDoc )

		if derefOpt( localTrashDoc, '_deleted' ) then

			-- Asset was permanently deleted locally, delete the oz doc.
			return docStore:applyOverlay ( {
				_id = newAssetIdFromOz,
				_deleted = true,
			} )
			:map( function()
				return "DeletedOzAssetLocalWasPermanentlyDeleted"
			end )

		elseif localTrashDoc._id ~= localAssetId or ozTrashDoc._deleted or localTrashDoc.subtype ~= ozTrashDoc.subtype then

			-- Asset was deleted and restored locally, update the createFrom to
			-- create it off of the other client's doc instead. Or, if the newId
			-- provided by oz was also deleted just update the createFrom.

			return WFCatTrash.updateLocalCreateFromTargetingOzDoc( docStore, localTrashDoc, ozTrashDoc )
			:map( function()
				return "UpdatedLocalCreateFromTargetingOzDoc"
			end )

		elseif derefOpt( localTrashDoc, '_revId' ) ~= WFCatTrashUtils.getCreateWithRevisionId( localTrashDoc ) then

			return WFCatTrash.createOverlaysToMergeRestoredAssets( catalog, localTrashDoc, ozTrashDoc )
			:flatMap( function( overlays )
				return docStore:bulkApplyOverlay( overlays )
			end )
			:map( function()
				return "CreatedOverlaysToMergeTheRestoredAssetsDeletingLocal"
			end )

		else

			-- just delete the local trash asset
 			return docStore:applyOverlay ( {
				_id = localAssetId,
				_deleted = true,
			} )
			:map( function()
				return "DeletedLocalAsset"
			end )

		end

	end )

end

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

local function mergeOrUpdateCreateFromOfRestoredLocalDocWithOzDoc( catalog, ozRestoredAssetId, localRestoredDoc )

	local docStore = catalog:docStore()

	return docStore:docValue( ozRestoredAssetId )
		:flatMap( function( ozRestoredDoc )

			if ozRestoredDoc._deleted then

				return WFCatTrash.updateLocalCreateFromTargetingOzDoc( docStore, localRestoredDoc, ozRestoredDoc )
					:map( function()
						return "UpdatedLocalCreateFromTargetingOzDoc"
					end )

			end

			return WFCatTrash.createOverlaysToMergeRestoredAssets( catalog, localRestoredDoc, ozRestoredDoc )
				:flatMap( function( overlays )

					if #overlays >= 2 and overlays[1]._id == overlays[2]._id then
						logger:errorf( "mergeOrUpdateCreateFromOfRestoredLocalDocWithOzDoc updating and deleting the same asset id %s", overlays[1]._id )
					end

					return docStore:bulkApplyOverlay( overlays )
						:map( function()
							return "CreatedOverlaysToMergeTheRestoredAssetsDeletingLocal"
						end )

				end )

		end )
end

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

local function createAndApplyOverlaysToCreateAssetDirectly( catalog, docThatIncurredError )

	local overlay = WFCatTrash.overlayForRemoveTrashInfo( docThatIncurredError )

	-- NMB-9395/NMB-9398: The document that incurred the error was a cloud trash document (e.g. restore from trash)
	-- where the source document was no longer on Oz. In this case, just turn this into a regular doc
	-- create by removing the createFrom so that we push a regular create.

	-- When we turn this into a proper create, we need to add the userCreated field
	local createFromInfo = WFCatTrashUtils.createFromInfo( docThatIncurredError )
	overlay.userCreated = createFromInfo.lastTrashUserCreated

	-- also, check the local binaries - if we don't have a master, but *do* have a proxy, we need to turn this into a
	-- Lightroom style asset (willNotUploadMaster = true)
	local hasMasterLocally = WFCatAssetUtils.hasLocalOriginal( docThatIncurredError )
	local hasProxyLocally = WFCatAssetUtils.hasLocalProxy( docThatIncurredError )

	if hasProxyLocally and not hasMasterLocally then
		overlay.importSource = overlay.importSource or {}
		overlay.importSource.willNotUploadMaster = true
	end

	-- clean uploaded info for a clean asset create
	if hasMasterLocally then
		local deletionModifier = { _delete = true }

		if WFPlatformInfo.isAndroid then
			setNested( overlay, deletionModifier, '_localOnly', 'original', 'ozUrl' )
			setNested( overlay, deletionModifier, '_localOnly', 'original', 'uploaded' )
		elseif WFPlatformInfo.isiOS then
			setNested( overlay, deletionModifier, '_localOnly', 'files', 'master', 'ozUrl' )
			setNested( overlay, deletionModifier, '_localOnly', 'files', 'master', 'uploaded' )
		end
	end

	return WFCatTrash.moveAssociatedDocsToNewDocs( catalog, overlay._id )
	:flatMap( function( overlays )

		-- Add the overlay for the asset
		overlays[ #overlays + 1 ] = overlay

		return catalog:docStore():bulkApplyOverlay( overlays )

	end )

end

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

local function isImageOrVideo( doc )

	return derefOpt( doc, 'type' ) == 'asset'
		and (
			derefOpt( doc, 'subtype' ) == 'image'
			or derefOpt( doc, 'subtype' ) == 'video'
		)

end

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

local function resourceNotFoundHandler( self, errorDoc, docThatIncurredError )
	local catalog = self._catalog
	local docStore = catalog:docStore()

	local spm =  derefOpt( errorDoc, '_serverProvidedMetadata' )
	local idOfDocThatIncurredError = derefOpt( spm, 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( spm, 'source', 'type' )
	local subtypeOfDocThatIncurredError = derefOpt( spm, 'source', 'subtype' )
	local actionOnDocThatIncurredError = derefOpt( spm, 'source', 'action' )

	local missingResourceId = derefOpt( spm, 'missing_resource_id' ) or idOfDocThatIncurredError
	logger:infof( "received resource not found error for %s", missingResourceId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kResourceNotFoundError

	errorEvent['content.id'] = idOfDocThatIncurredError
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.subtype'] = derefOpt( docThatIncurredError, 'subtype' )
	errorEvent['content.action'] = actionOnDocThatIncurredError
	errorEvent['content.missing_resource_id'] = missingResourceId

	local maybeNewAssetIdFromOz = maybeTrashOperationNewId( errorDoc )

	return docStore:docValue( missingResourceId )
		:flatMap( function( missingResourceDocValue )

			local errors = derefOpt( spm, 'errors' )
			local deleteWasSentToOzBeforeAssetExisted = missingResourceDocValue._deleted and missingResourceDocValue._newId and not maybeTrashOperationNewId( errorDoc )
			local docThatIncurredErrorWasTrashAsset = missingResourceDocValue._empty and errors[ 'deleted_asset' ] and ( WFCatTrashUtils.getCreateFromId( docThatIncurredError ) == missingResourceId )

			-- If multiple trash operations take place on a different client for the same asset
			-- that also undergoes multiple trash operations on this client, we resort to the
			-- following to identify the scenario, rather than trying to follow the new_id trail
			-- to the end.

			local localRestoreOfAssetMoreThanOneTrashOperationBehindOz = isImageOrVideo( docThatIncurredError )
					and derefOpt( missingResourceDocValue, '_deleted' )
					and derefOpt( missingResourceDocValue, '_labels', '_dontsendtooz' )
					and actionOnDocThatIncurredError == 'create'

			if docThatIncurredError.type == 'asset' and docThatIncurredError.subtype == 'stack'
				and derefOpt( errorDoc, '_serverProvidedMetadata', 'errors', 'cover', 1 ) == 'does not exist' then

				if not WFPlatformInfo.isDesktop then
					return resolveErrorByIgnoring( self, errorDoc, "StackCoverErrorUnsupported" )
				end

				-- If the stack document's cover is missing replace it with another one in the stack.
				if missingResourceDocValue._empty then

					local WFCatStacks = require 'WFCatStacks'
					return WFCatStacks.getOverlaysForNewCovers( catalog, { idOfDocThatIncurredError }, {} )
					:flatMapLatest( function( overlays )
						local newStackCoverOverlay = overlays and overlays[ 1 ]
						if newStackCoverOverlay then
							return resolveErrorByApplyingOverlay( self, errorDoc, "ChangeMissingStackCover", newStackCoverOverlay )
						else
							return resolveError( self, errorDoc, "DeleteStackWithWithMissingCover",
								WFCatStacks.deleteStack( catalog, idOfDocThatIncurredError )
									:concat( Rx.Observable.just( true ) ) )
						end
					end )

				end

			elseif maybeNewAssetIdFromOz then

				if ( actionOnDocThatIncurredError == 'create' and missingResourceId == WFCatTrashUtils.getCreateFromId( docThatIncurredError ) and maybeNewAssetIdFromOz == idOfDocThatIncurredError )
						-- restore where the missing resource is the deleted trash asset and the doc that incurred error is the new restored asset doc
					or
					( actionOnDocThatIncurredError == 'delete' and missingResourceId == docThatIncurredError._id and maybeNewAssetIdFromOz == docThatIncurredError._newId )
						-- delete to trash where the missing resource is the asset being deleted and the doc that caused the error and the new asset id is the same as the deleted assets new id
					then
					-- NMB-12753/NMB-12848: If we get a ResourceNotFoundError for a doc that we're trying to delete and the
					-- new_id in the ResourceNotFoundError matches the _newId that we were trying to create from that
					-- then we want to ignore this ResourceNotFoundError because it's basically saying this operation
					-- has already happened

					return resolveErrorByIgnoring( self, errorDoc, "OperationAlreadyHappened" )

				elseif isErrorFromALocalDelete( errorDoc, missingResourceDocValue, docThatIncurredError ) then

					return resolveErrorWithResult( self, errorDoc, "LocalDelete",
						mergeDeleteOrUpdateCreateFromOfLocalDocWithOzDoc( catalog, maybeNewAssetIdFromOz, missingResourceDocValue._newId ) )

				elseif isErrorFromALocalRestore( errorDoc, missingResourceDocValue, docThatIncurredError ) then

					return resolveErrorWithResult( self, errorDoc, "LocalRestore",
						mergeOrUpdateCreateFromOfRestoredLocalDocWithOzDoc( catalog, maybeNewAssetIdFromOz, docThatIncurredError ) )

				elseif docThatIncurredError._deleted and docThatIncurredError._newId and actionOnDocThatIncurredError == 'create' then

					-- Handle cases where both a different client and this client restored the photo,
					-- and this client also deleted it again and maybe did other trash actions as well.
					return resolveErrorWithResult( self, errorDoc, "LocalRestoreDeleted",
						mergeDeleteOrUpdateCreateFromOfLocalDocWithOzDoc( catalog, maybeNewAssetIdFromOz, docThatIncurredError._id ) )

				else
					logger:warnf( "ignoring %s error from Oz for %s with new_id %s", errorDoc.subtype, idOfDocThatIncurredError, maybeNewAssetIdFromOz )
				end

			elseif isDeletedByAnotherUserWithLocalNewId( self, errorDoc, missingResourceDocValue,  docThatIncurredError ) then

				return WFCatTrashUtils.getTrashOrRestoredDocFromNewId( docStore, docThatIncurredError._newId )
						:flatMap( function( localDocValue )
							return resolveError( self, errorDoc, "RemovedLocalDocument_DeletedByContributor",
					 			mimicOzDeletionEvent( docStore, localDocValue._id, typeOfDocThatIncurredError, subtypeOfDocThatIncurredError ) )
						end )

			elseif localRestoreOfAssetMoreThanOneTrashOperationBehindOz then

				-- Handled via the Delete RNFE which updates the createFromId.
				return resolveErrorByIgnoring( self, errorDoc, "MoreThanOneOperationBehind" )

			elseif docThatIncurredErrorWasTrashAsset then

				return resolveError( self, errorDoc, "RemovedTrashInfoToCreateAssetDirectly_docThatIncurredErrorWasTrashAsset",
					createAndApplyOverlaysToCreateAssetDirectly( catalog, docThatIncurredError ) )

			elseif deleteWasSentToOzBeforeAssetExisted then

				if docThatIncurredError._deleted then

					-- Get the local asset that exists corresponding to docThatIncurredError
					-- by following the _newId change, and create an overlay to remove the
					-- trash info and update importSource.willNotUploadMaster if need be.

					return WFCatTrashUtils.getTrashOrRestoredDocFromNewId( docStore, docThatIncurredError._newId )
						:flatMap( function( localDocValue )

							if localDocValue._deleted then

								-- User permanently deleted the photo, ignore the error.
								return resolveErrorByIgnoring( self, errorDoc, "AssetWasPermanentlyDeleted_deleteWasSentToOzBeforeAssetExisted" )

							elseif WFCatTrashUtils.getCreateFromId( localDocValue ) == missingResourceId then

								-- Since thing createFromId matches the missingResourceId and there isn't
								-- a new_id provided by oz (as maybeNewAssetIdFromOz is nil) we can't do
								-- anything but remove the createFrom info and create the photo from scratch.

								return resolveError( self, errorDoc, "RemovedTrashInfoToCreateAssetDirectly_deleteWasSentToOzBeforeAssetExisted",
									createAndApplyOverlaysToCreateAssetDirectly( catalog, localDocValue ) )

							else

								-- The error was reported for the second part of a two
								-- part delete/restore or restore/delete which should just be
								-- ignored as the first part will update or remove the createFrom info.
								-- to retry the operation.
								return resolveErrorByIgnoring( self, errorDoc, "SecondPartOfTrashPair_deleteWasSentToOzBeforeAssetExisted" )

							end

						end )

				else

					-- This is hit by test_CT_MockRestore_OtherRestoreOtherDelete
					return resolveErrorByIgnoring( self, errorDoc, "_deleteWasSentToOzBeforeAssetExisted" )

				end

			elseif idOfDocThatIncurredError == missingResourceId then

				-- Is the missing resource id the same as the id of the document we were trying to send?
				-- if so, then we either have a create/update to a doc that doesn't exist on Oz yet
				-- (ignore the error, it should clear up eventually) or an update to a doc that doesn't
				-- exist on Oz any more (remove the document locally)

				local didPreviouslyExistOnOz = not not derefOpt( missingResourceDocValue, '_serverProvidedMetadata', 'created' )

				if missingResourceDocValue._empty or didPreviouslyExistOnOz then

					return resolveError( self, errorDoc, "RemovedLocalDocument_MatchingMissingResourceId",
						 mimicOzDeletionEvent( docStore, missingResourceId, typeOfDocThatIncurredError, subtypeOfDocThatIncurredError ) )

				else
					logger:warnf( "ignoring %s error from Oz for idOfDocThatIncurredError == missingResourceId == %s", errorDoc.subtype, idOfDocThatIncurredError )
				end

			else

				-- If the missing resource id is not the same as the id of the document we were trying to send,
				-- then we are looking at doc that incurred the error as a dependent document of the missing
				-- resource. In that case we'll need to determine why the missing resource is missing and
				-- mark the dependent document with the same error message. This will allow us to clear the
				-- dependent documents error at the same time as it's 'parent' resource

				if missingResourceDocValue._empty and isDependentDocumentType( typeOfDocThatIncurredError ) then

					-- the missing resource is missing becaue it's deleted. so we should delete the dependent
					-- document too

					return resolveError( self, errorDoc, "RemovedLocalDocument_NotMatchingMissingResourceId",
						docStore:deleteDoc( idOfDocThatIncurredError ) )

				else

					local ozErrorForMissingResource = WFCatOzErrors.maybeGetOzErrorHash( missingResourceDocValue )

					local errorOverlay
					if ozErrorForMissingResource then

						errorOverlay = {
								_id = idOfDocThatIncurredError,
								_localOnly = {
									ozError = ozErrorForMissingResource
								}
							}

						errorEvent[ 'content.missing_resource_error_subtype' ] = ozErrorForMissingResource.subtype

					else

						errorOverlay = WFCatOzErrors.createOverlayForOzErrorSubtype( idOfDocThatIncurredError, errorDoc.subtype )

					end

					return resolveErrorByApplyingOverlay( self, errorDoc, "MarkedWithOzError_NotMatchingMissingResourceId", errorOverlay )

				end

			end

			-- If we receive a resource not found error for other reasons than those handled explicitly we are
			-- hopefully in a situation that should clear itself up. Log that we received this error, but there
			-- is not much more we should do to handle the ResourceNotFound error locally.
			logger:warnf( "ignoring %s error from Oz for %s", errorDoc.subtype, idOfDocThatIncurredError )

			return resolveErrorByIgnoring( self, errorDoc, "UnknownReason" )

		end )

		:map( function( resolution )

			if not resolution or not resolution:match( '^Ignored' ) then

				-- Don't bother logging analytics events for cases of ResourceNotFound that we're ignoring.

				errorEvent['event.resolution'] = resolution
				WFAnalytics.trackEvent( errorEvent )

			end

		end )

end

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

local function assetGoneHandler( self, errorDoc, assetGoneDoc )

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local actionOnDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'action' )
	local newAssetId = derefOpt( errorDoc, '_serverProvidedMetadata', 'new_id' )

	logger:infof( "received assetGoneError for %s id %s updating to asset.id = %", typeOfDocThatIncurredError, resourceId, newAssetId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kAssetGoneError
	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.action'] = actionOnDocThatIncurredError
	errorEvent['content.new_id'] = newAssetId
	WFAnalytics.trackEvent( errorEvent )

	local isErrorForAssociatedDoc =
		typeOfDocThatIncurredError == 'album_asset' or
		typeOfDocThatIncurredError == 'comment' or
		typeOfDocThatIncurredError == 'favorite' or
		typeOfDocThatIncurredError == 'version'

	if isErrorForAssociatedDoc then

		-- Update the asset.id in the document that failed with error 410
		return resolveErrorByApplyingOverlay( self, errorDoc, "UpdatedAssetIdOfAssocDoc", {
			_id = resourceId,
			asset = {
				id = newAssetId
			}
		} )

	else

		-- Don't know what to do with this type of asset, just add an OzError overlay
		return resolveErrorByMarkingWithOzError( self, errorDoc, resourceId )

	end

end

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

local function storageExceededHandler( self, errorDoc, storageExceededDoc )

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local actionOnDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'action' )
	local spaceId = errorDoc._spaceId

	logger:infof( "received storage exceeded error for %s", resourceId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kStorageExceededError

	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.action'] = actionOnDocThatIncurredError
	errorEvent['lr.wf.spaceId'] = spaceId

	WFAnalytics.trackEvent( errorEvent )

	-- Note: if storageExceededDoc._deleted and storageExceededDoc._newId are set
	-- the applyOverlay call will forward the change to the destination.
	return resolveErrorByMarkingWithOzError( self, errorDoc, resourceId )

end

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

local function multipartCompleteHandler( self, errorDoc, multipartCompleteErrorDoc )

	local resourceId = KSTableUtils.derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'asset_id' )

	if not resourceId then
		resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	end

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kMultipartCompleteError

	errorEvent['content.id'] = resourceId

	WFAnalytics.trackEvent( errorEvent )

	if not multipartCompleteErrorDoc._empty then

		logger:infof( "received multipart complete error for %s", resourceId )
		return resolveErrorByMarkingWithOzError( self, errorDoc, resourceId )

	end

	return Observable.just()

end

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

local function trialOrSubscriptionEntitlementExpiredHandler( self, errorDoc, subscriptionExpiredDoc )

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local actionOnDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'action' )
	local spaceId = errorDoc._spaceId

	logger:infof( "received trial/subscription expired error for %s", resourceId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kTrialOrSubscriptionExpiredError

	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.action'] = actionOnDocThatIncurredError
	errorEvent['lr.wf.spaceId'] = spaceId

	WFAnalytics.trackEvent( errorEvent )

	return resolveErrorByMarkingWithOzError( self, errorDoc, resourceId )

end

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

local function invalidAccessRequestHandler( self, errorDoc, docValue )

	local source = derefOpt( errorDoc, '_serverProvidedMetadata', 'source' )
	local spaceInviteId = derefOpt( source, 'id' )
	local typeOfDocThatIncurredError = derefOpt( source, 'type' )
	local actionOnDocThatIncurredError = derefOpt( source, 'action' )

	logger:infof( "invalid access-request error for %s", spaceInviteId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kInvalidAccessRequestError

	errorEvent['content.id'] = spaceInviteId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.action'] = actionOnDocThatIncurredError

	WFAnalytics.trackEvent( errorEvent )

	-- This error is sent if the client tries to approve a request - but that request has already
	-- been approved on Oz. Since the update that the client tried sending is the same as the
	-- update that already happened on Oz, we don't need to do anything, other than delete the
	-- error document.
	return resolveErrorByIgnoring( self, errorDoc, "AlreadyApproved" )

end

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

local function spaceUserExistsHandler( self, errorDoc, docValue )

	local source = derefOpt( errorDoc, '_serverProvidedMetadata', 'source' )
	local spaceInviteId = derefOpt( source, 'id' )
	local typeOfDocThatIncurredError = derefOpt( source, 'type' )
	local actionOnDocThatIncurredError = derefOpt( source, 'action' )

	logger:infof( "space user exists error for %s", spaceInviteId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kSpaceUserExistsError

	errorEvent['content.id'] = spaceInviteId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.action'] = actionOnDocThatIncurredError

	WFAnalytics.trackEvent( errorEvent )

	return resolveErrorByApplyingOverlay( self, errorDoc, "SpaceInviteDeleted", {
		_id = spaceInviteId,
		_deleted = true,
	} )

end

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

local function isRestoreAssetError( errorDoc )

	local spm = derefOpt( errorDoc, '_serverProvidedMetadata' )
	local action = derefOpt( spm, 'source', 'action' )

	return errorDoc.subtype == WFCatOzErrors.Errors.kRestoreAssetError and
		action == 'create'

end

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

local function isRestoreAssetErrorForThisClient( docStore, errorDoc, docThatIncurredError )
	-- NMB-13281/LRMI-4193007/LRMA-4917671/NMB-14814
	-- In certain restore cases Oz may partially create the restored asset and
	-- send it in the sync feed but fail before it's finally created and so it
	-- will send a RestoreAssetError. This means that all clients will get the
	-- partial asset (that doesn't really exist on Oz) as well as the error doc.

	-- This function returns true if the error is a 502/RestoreAssetError and
	-- originated from this client (docThatIncurredError should have local cloud
	-- trash data related to how to create the restored asset.
	local spm = derefOpt( errorDoc, '_serverProvidedMetadata' )
	local sourceDocId = derefOpt( spm, 'source', 'id' )
	local sourceDocRevId = derefOpt( spm, 'source', 'revision_id' )

	local maybeCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( docThatIncurredError )

	if not isRestoreAssetError( errorDoc ) then

		-- I don't think you can get here, preserving previous implementation's check.
		return Observable.just( false )

	elseif sourceDocId == docThatIncurredError._id and sourceDocRevId == maybeCreateWithRevId then

		-- The doc still exists and it's the correct revision, return true as
		-- we had to have issued the restore request.
		return Observable.just( true )

	else

		local restoreFromDocId = derefOpt( spm, 'source', 'restore_from', 'id' )

		if restoreFromDocId == nil then
			-- Probably can't get here either.
			return Observable.just( false )
		end

		-- NMB-14814 Check to see if the errorDoc's restore_from doc has a _newId that
		-- points to the docThatIncurredError._id (also known as sourceDocId). If it
		-- does then this client issued the restore and either the user modified or
		-- deleted the asset.

		return docStore:docValue( restoreFromDocId )
		:map( function( restoreFromDocValue )

			-- Note that docThatIncurredError may be deleted if so it
			-- will have a _newId and the overlay will be applied to
			-- what that _newId points at by the docStore.

			return restoreFromDocValue._newId == docThatIncurredError._id

		end )

	end

end

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

local function isDeletedByAnotherUser( errorDoc, currentUserId )

	local subtype = errorDoc.subtype

	local maybeDeletedBy = derefOpt( errorDoc, '_serverProvidedMetadata', 'deleted_by' )

	return not not ( subtype == WFCatOzErrors.Errors.kResourceNotFoundError
		and maybeDeletedBy and maybeDeletedBy ~= currentUserId )
end

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

local function restoreAssetErrorHandler( self, errorDoc, docValue )
	-- NMB-13281/LRMI-4193007/LRMA-4917671
	-- When we receive a RestoreAssetError/502 error from Oz we may have received a partial
	-- asset confirmation from Oz but Oz didn't end up creating the restored asset.
	local catalog = self._catalog
	local docStore = catalog:docStore()

	return isRestoreAssetErrorForThisClient( docStore, errorDoc, docValue )
	:flatMap( function( wasRestoredFromThisClient )

		if wasRestoredFromThisClient then

			-- If we are the client that sent the restore, then we need to resend the attempt to
			-- restore the asset which should either result in the successful restoration of the asset
			-- or a non-502 error (e.g. 404 if the asset was restored simultaneously in another client).

			-- In order to ensure this happens we need to
			-- 1. Remove *any* 'oz' label from revisions of this document.
			-- 2. Bump the sequence so the pusher will look at it again and attempt the restore again

			local source = derefOpt( errorDoc, '_serverProvidedMetadata', 'source' )
			local resourceId = derefOpt( source, 'id' )
			local typeOfDocThatIncurredError = derefOpt( source, 'type' )
			local actionOnDocThatIncurredError = derefOpt( source, 'action' )

			local errorEvent = {}
			errorEvent['event.type'] = 'error'
			errorEvent['event.appWorkflow'] = 'SYNC'
			errorEvent['event.subcategory'] = 'Error_handler'
			errorEvent['event.error_type'] = WFCatOzErrors.Errors.kRestoreAssetError

			errorEvent['content.id'] = resourceId
			errorEvent['content.type'] = typeOfDocThatIncurredError
			errorEvent['content.action'] = actionOnDocThatIncurredError

			WFAnalytics.trackEvent( errorEvent )

			return docStore:getRevisionForLabel( docValue._id, 'oz' )
				:flatMap( function( revisionId )
					return docStore:removeLabel( docValue._id, 'oz', revisionId )
						:takeLast(0)
						:concat( Observable.just( nil ) )
				end )
				:catch( function()
					-- no revision with 'oz' label so return nil and we'll
					return Observable.just( nil )
				end )
				:flatMap( function()
					return resolveErrorWithResult( self, errorDoc, "MarkedForRetryByThisClient",
						markDocumentForRetry( docStore, docValue, errorDoc ) )
				end )

		else

			-- If we are not the client that sent the restore that generated the RestoreAssetError then
			-- we need to make sure the partial doc doesn't look full enough to appear on the client so
			-- we will make sure that the _serverProvidedMetadata is removed
			if isRestoreAssetError( errorDoc ) and not docValue._empty then

				return resolveErrorByApplyingOverlay( self, errorDoc, "RemovedServerProvidedMetadata", {
					_id = docValue._id,
					_serverProvidedMetadata = { _delete = true },
				} )

			end

			return resolveErrorByIgnoring( self, errorDoc, "ErrorFromOtherClient" )

		end

	end )

end

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

local function insufficientPermissionErrorHandler( self, errorDoc, docValue )

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local actionOnDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'action' )
	local spaceId = errorDoc._spaceId

	logger:infof( "received insufficient permission error for %s", resourceId )

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kInsufficientPermissionError
	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.action'] = actionOnDocThatIncurredError
	errorEvent['lr.wf.spaceId'] = spaceId

	WFAnalytics.trackEvent( errorEvent )

	if typeOfDocThatIncurredError == 'asset' and
			actionOnDocThatIncurredError == 'update' and
			errorDoc._spaceId then

		-- This handling addresses the following scenario:

		-- 1. Alice grants Bob editor role in a shared album containing SomeImage.
		-- 2. Bob syncs down SomeImage and goes offline where he makes edits.
		-- 3. In the meantime, Alice downgrades Bob's privileges SomeImage album to contributor while Bob is offline.
		-- 4. Bob comes back online

		-- After Step 4, Bobs edits are invalidated as he can no longer sync them. To resolve
		-- this, we completely throw away bobs local revision and resync the latest revision
		-- from Oz.

		return resolveByRestoringAssetFromOz( self, errorDoc, resourceId )

	else

		return resolveErrorByMarkingWithOzError( self, errorDoc, resourceId )

	end

end

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

local function isAuxSha256MismatchError( errorDoc )

	-- Oz will update the doc with invalid rendition links before sending this
	-- error so don't ignore these errors for the NMB-9050 check below.
	return errorDoc.subtype == WFCatOzErrors.Errors.kAuxSha256MismatchError

end

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

local function isBinaryMissingErrorForVersion( errorDoc, docThatIncurredError )

	return errorDoc.subtype == WFCatOzErrors.Errors.kBinaryMissingError and
		docThatIncurredError.type == 'version'
			-- versions will NOT have synced to Oz when BinaryMissingError is received
			-- since as part of Atomic Develop Settings the binary must sync first,
			-- so if the version is present locally then the error should be covered
			-- without any revision checks.

end

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

local function isXmpDevelopSha256MismatchError( errorDoc )

	-- this error is triggered by a direct upload completion api call and
	-- NOT by sending Oz a revision, so is not appropriate to ignore it if
	-- we are not up to date with Oz, sending new revision will not cause
	-- this error again.
	return errorDoc.subtype == WFCatOzErrors.Errors.kXmpDevelopSha256MismatchError

end

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

local function doesLocalSha256MatchExpectedSha256( path, expectedSha256 )

	return WFCrypto.asyncDigestFile( path, 'SHA256' )

		:map( function( actualSha256 )

			return actualSha256 == expectedSha256

		end )

		:catch( function()

			return Observable.just( false )

		end )

end

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

local function auxSha256MismatchErrorHandler( self, errorDoc, docValue )

	-- AuxSha256MismatchError: 403. Uploaded aux file's sha256 doesn't match
	-- with the aux sha256 in the payload. (VERY similar handling
	-- to XmpDevelopSha256MismatchError below)

	-- Resolutions:

	-- Case 0. This client doesn't have the the local aux file referenced by the error document,
	-- therefore the error document can be ignored.

	-- Case 1. The Aux file present locally has the correct sha256; ignore the error as the
	-- aux should be automatically reuploaded.

	-- Case 2. The Aux file present locally has the wrong sha256; The error is unrecoverable
	-- and so the local aux is marked with an error. This error annotation is used to prevent the
	-- re-upload of the corrupt local aux file.

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local expectedSha256 = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'expected_sha256' )
	local actualSha256 = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'actual_sha256' )

	local maybeAuxInfo = WFCatAssetAux.getAuxInfoBySha256( docValue, expectedSha256 )

	logger:infof( "received AuxShaMismatchError error for %s", resourceId )

	-- Analytics

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kAuxSha256MismatchError
	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.expectedSha256'] = expectedSha256
	errorEvent['content.actualSha256'] = actualSha256
	errorEvent['content.sizeOfAuxiliary'] = maybeAuxInfo and maybeAuxInfo.fileSize

	WFAnalytics.trackEvent( errorEvent )

	-- Resolution

	if maybeAuxInfo then

		local relativePath = WFCatAssetUtils.getLocalAuxObjectPath( docValue, maybeAuxInfo.digest )

		if relativePath then

			local absolutePath = WFCatAssetAux.getAbsolutePath( self._catalog, relativePath )

			return doesLocalSha256MatchExpectedSha256( absolutePath, expectedSha256 )

				:flatMap( function( sha256Matches )

					if sha256Matches then

						-- Case 1

						return resolveErrorByIgnoring( self, errorDoc, "Reupload" )

					end

					-- Case 2

					local overlay = {
						_id = resourceId
					}

					KSTableUtils.setNested( overlay,
							WFCatOzErrors.Errors.kAuxSha256MismatchError, '_localOnly', 'files', 'aux', maybeAuxInfo.digest, 'error' )

					return resolveError( self, errorDoc, "MarkedWithLocalAuxError",
							self._catalog:docStore():applyOverlay( overlay ) )

						:map( function( ... )

							if WFFileUtils.exists( absolutePath ) then

								local archivedCorruptAuxPath = string.format( "%s_invalidAux", absolutePath )

								pcall( WFFileUtils.move, absolutePath, archivedCorruptAuxPath )

							end

							return ...

						end )

				end )

		end

	end

	-- Case 0:

	-- This client has no local aux with the expected Sha256, so the error
	-- message is not intended for this particular client. Ignore it.

	return resolveErrorByIgnoring( self, errorDoc, "OtherClientWillResolve" )

end

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

local function auxMissingErrorHandler( self, errorDoc, docValue )

	-- AuxMissingError: 403. Some of the aux files are not uploaded which is
	-- required for rendition generation. Missing aux_ids are returned in the
	-- error body.

	-- This should be impossible. The upload code guards against uploading develop settings
	-- if there are unsynced auxiliaries (whether or not they exist locally). Because of
	-- this, the handling here is really more about logging rather than resolution.

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local missingAuxIds = derefOpt( errorDoc, '_serverProvidedMetadata', 'errors', 'missing_aux_ids' ) or {}

	logger:infof( "received AuxMissingError error for %s", resourceId )

	local auxEntries = derefOpt( docValue, 'aux' ) or {}

	local totalNumberOfAuxiliaries = 0
	local totalSizeOfAuxiliaries = 0

	for _, auxInfo in pairs( auxEntries ) do

		totalNumberOfAuxiliaries = totalNumberOfAuxiliaries + 1
		totalSizeOfAuxiliaries = totalSizeOfAuxiliaries + auxInfo.fileSize

	end

	-- Analytics

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.type'] = 'warning'
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kAuxMissingError
	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.numberOfMissingAuxiliaries'] = #missingAuxIds
	errorEvent['content.totalNumberOfAuxiliaries'] = totalNumberOfAuxiliaries
	errorEvent['content.totalSizeOfAuxiliaries'] = totalSizeOfAuxiliaries

	WFAnalytics.trackEvent( errorEvent )

	return resolveErrorByIgnoring( self, errorDoc, "Ignored" )

end

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

local function referencedAuxMissingErrorHandler( self, errorDoc, docValue )

	-- ReferencedAuxMissingError: This error occurs when a referenced Asset
	-- does not contain Aux entries that a dependent doc value requires. For
	-- example, a Version document may require that the parent Asset document
	-- contains auxiliaries A, B, and C. If the parent Asset document does not
	-- include references to any these auxiliaries, and we attempt to create the
	-- Version on Oz, Oz will return a ReferencedAuxMissingError document.

	-- Usually, when this occurs, we can ignore the error; we are not guaranteed
	-- to upload the Asset revision that contains the auxiliary references before
	-- the corresponding Version.

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local missingAuxIds = derefOpt( errorDoc, '_serverProvidedMetadata', 'errors', 'aux_ids' )

	logger:infof( "received ReferencedAuxMissingError error for %s", resourceId )

	-- Analytics

	local errorEvent = createBaseErrorEvent()
	errorEvent[ 'event.error_type' ] = WFCatOzErrors.Errors.kReferencedAuxMissingError
	errorEvent[ 'content.id' ] = resourceId
	errorEvent[ 'content.type' ] = typeOfDocThatIncurredError
	errorEvent[ 'content.numberOfReportedMissingAuxiliaries' ] = missingAuxIds and #missingAuxIds

	-- Resolution

	local docStore = self._catalog:docStore()

	if WFCatIndexUtils.isVersionDocument( docValue ) and missingAuxIds then

		local assetId = derefOpt( docValue, 'asset', 'id' )

		return docStore:docValue( assetId )

			:flatMap( function( assetDocValue )

				local auxInfoList = WFCatAssetAux.getAuxInfoList( assetDocValue )

				local confirmedMissingAuxIds = {}

				for _, auxId in ipairs( missingAuxIds ) do

					local auxInfo = auxInfoList[ auxId ]

					if auxInfo then

						logger:infof( "Parent asset %s is not missing auxiliary: %s", assetId, auxId )

					else

						logger:warnf( "Parent asset %s is missing auxiliary: %s", assetId, auxId )

						confirmedMissingAuxIds[ #confirmedMissingAuxIds + 1 ] = auxId

					end

				end

				if #confirmedMissingAuxIds > 0 then

					-- Mark the Version with a permanent, unrecoverable error. This prevents it
					-- from being reuploaded, and from showing up in the list versions APIs.

					-- Technically speaking, we may be able to recover here by annotating the Asset
					-- with the auxiliary information. However, clients should never put us in this
					-- situation so it's not worth that level of handling.

					errorEvent[ 'content.numberOfConfirmedMissingAuxiliaries' ] = #confirmedMissingAuxIds
					WFAnalytics.trackEvent( errorEvent )

					return resolveErrorByMarkingWithOzError( self, errorDoc, docValue._id )

				else

					-- Mark the Version with a ResourceNotFound Error. This indicates that the
					-- latest asset revision that includes the auxiliary just hasn't made it to
					-- Oz. The Version should automatically recover once the Asset revision is
					-- uploaded.

					return resolveError( self, errorDoc, "MarkedWithOzError", self._catalog:docStore():applyOverlay(
						WFCatOzErrors.createOverlayForOzErrorSubtype( resourceId, WFCatOzErrors.Errors.kResourceNotFoundError )
					) )

				end

			end )

	else

		WFAnalytics.trackEvent( errorEvent )

		return resolveErrorByMarkingWithOzError( self, errorDoc, docValue._id )

	end

end

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

local function xmpDevelopSha256MismatchErrorHandler( self, errorDoc, docValue )

	-- XmpDevelopSha256MismatchError: 403. Uploaded xmpDevelop file's sha256 does not
	-- match with the sha256 given when initiating direct upload. (VERY similar handling
	-- to AuxSha256MismatchError above)

	-- Resolutions:

	-- Case 0. This client doesn't have the the local file referenced by the error document,
	-- therefore the error document can be ignored.

	-- Case 1. The xmpDevelop file present locally has the correct sha256; ignore the error as the
	-- xmpDevelop should be automatically reuploaded.

	-- Case 2. The xmpDevelop file present locally has the wrong sha256; The error is unrecoverable
	-- and so the local file is marked with an error. This error annotation is used to prevent the
	-- re-upload of the corrupt local file.

	local resourceId = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'id' )
	local typeOfDocThatIncurredError = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'type' )
	local expectedSha256 = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'expected_sha256' )
	local actualSha256 = derefOpt( errorDoc, '_serverProvidedMetadata', 'source', 'actual_sha256' )

	local maybeLocalXmpDevelopSha256 = WFCatAssetUtils.getLocalExternalDevelopSettingsContentId( docValue )
	local maybeLocalXmpDevelopPath
	if maybeLocalXmpDevelopSha256 == expectedSha256 then
		maybeLocalXmpDevelopPath = WFCatAssetUtils.getLocalExternalDevelopSettingsPath( docValue )
	end

	logger:infof( "received XmpSha256MismatchError error for %s", resourceId )

	-- Analytics

	local errorEvent = createBaseErrorEvent()
	errorEvent['event.error_type'] = WFCatOzErrors.Errors.kXmpDevelopSha256MismatchError
	errorEvent['content.id'] = resourceId
	errorEvent['content.type'] = typeOfDocThatIncurredError
	errorEvent['content.expectedSha256'] = expectedSha256
	errorEvent['content.actualSha256'] = actualSha256
	errorEvent['content.sizeOfXmpDevelop'] = WFCatAssetUtils.getLocalExternalDevelopSettingsSize( docValue )

	WFAnalytics.trackEvent( errorEvent )

	-- Resolution

	if maybeLocalXmpDevelopPath then

		local fullXmpDevelopPath = maybeLocalXmpDevelopPath

		if WFPlatformInfo.isMobile() and fullXmpDevelopPath:sub(1,1) ~= '/' then

			if WFPlatformInfo.isAndroid then

				local KSPathUtils = require 'KSPathUtils'
				fullXmpDevelopPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), fullXmpDevelopPath )

			else

				local OzFilePaths = require 'OzFilePaths'
				local LrmAssetUtils = require 'LrmAssetUtils'

				local catalogBasePath = OzFilePaths.getCatalogBasePath( self._catalog:ozCatalogId() )
				local fileFullPath = LrmAssetUtils.createFullPath( catalogBasePath, fullXmpDevelopPath, 'xmp_develop' )
				if fileFullPath then
					fullXmpDevelopPath = fileFullPath
				end

			end

		end

		return doesLocalSha256MatchExpectedSha256( fullXmpDevelopPath, expectedSha256 )
			:flatMap( function( sha256Matches )

				if sha256Matches then

					-- Case 1
					return resolveErrorByIgnoring( self, errorDoc, "Reupload" )

				end

				-- Case 2

				local overlay = { _id = resourceId }
				KSTableUtils.setNested( overlay, WFCatOzErrors.Errors.kXmpDevelopSha256MismatchError, '_localOnly', 'files', 'xmp_develop', 'error' )

				return resolveError( self, errorDoc, "MarkedWithLocalXmpDevelopError", self._catalog:docStore():applyOverlay( overlay ) )
					:map( function( ... )

						if WFFileUtils.exists( fullXmpDevelopPath ) then
							local archivedCorruptXmpDevelopPath = string.format( "%s_invalidXmpDevelop", fullXmpDevelopPath )
							pcall( WFFileUtils.move, fullXmpDevelopPath, archivedCorruptXmpDevelopPath )
						end

						return ...

					end )

			end )

	end

	-- Case 0:

	-- This client has no local aux with the expected sha256, so the error
	-- message is not intended for this particular client. Ignore it.
	return resolveErrorByIgnoring( self, errorDoc, "OtherClientWillResolve" )

end

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

local function maybeCopyAuxInfoToAssetFromVersion( catalog, versionDoc, versionDevelopAuxes, errorDoc )

	local docStore = catalog:docStore()

	return docStore:docValue( versionDoc.asset.id )

		:flatMap( function( assetDoc )

			if assetDoc._empty then
				return Observable.just( )
			end

			local auxesToCopy = {}

			for _, auxInfo in ipairs( versionDevelopAuxes ) do
				if not derefOpt( assetDoc, 'aux', auxInfo.auxId ) then
					auxesToCopy[ #auxesToCopy + 1 ] = auxInfo
				end
			end

			if #auxesToCopy == 0 then
				return Observable.just( )
					-- if there is not at least one aux to copy to the asset not more work to do
			end

			return Observable.from( auxesToCopy )

				:flatMap( function ( auxInfo )

					return WFCrypto.asyncDigestFile( auxInfo.absolutePath, 'SHA256' )

					:map( function( sha256 )
						return auxInfo, sha256
					end )

				end )

				:map( function( auxInfo, sha256 )

					local absolutePath = auxInfo.absolutePath
					auxInfo.absolutePath = nil
						-- was just passed in for convenience, do not want to keep it in overlay

					local fileSize =
							derefOpt( WFFileUtils.fileAttributes( absolutePath ), 'fileSize' )

					if not fileSize or fileSize == 0 then
						-- this should not happen as we already checked that file exists
						-- but if for some reason it does, good time to bail!
						return false
					end

					-- auxInfo.type is set by caller

					auxInfo.fileSize = fileSize
					auxInfo.sha256 = sha256

					return auxInfo

				end )

				:bufferWithCount( #auxesToCopy )

				:flatMap( function ( finalAuxList )

					local overlay = {
						_id = assetDoc._id,
						aux = {},
						_localOnly = { files = { aux = { } } }
					}

					for _, auxInfo in ipairs( finalAuxList ) do

						if not auxInfo then
							return Observable.just()
								-- means there was a failure in the previous flatMap function
						end

						overlay.aux[ auxInfo.auxId ] = auxInfo
						auxInfo.auxId = nil
							-- we do not want to leave the auxId in the overlay

						local auxLocalOnly = derefOpt( versionDoc, '_localOnly', 'files', 'aux', auxInfo.digest )
						setNested( overlay, auxLocalOnly, '_localOnly', 'files', 'aux', auxInfo.digest )

					end

					local errorEvent = {

						[ 'event.type' ] = 'warn',
						[ 'event.appWorkflow' ] = 'SYNC',
						[ 'event.subcategory' ] = 'Error_handler',
						[ 'event.error_type' ] = 'BinaryMissingError_CopyAuxToAssetFromVersion',
						[ 'event.error_desc' ] = WFTableArchiving.tableToString( errorDoc ),

						[ 'content.id' ] = versionDoc._id,
						[ 'content.type' ] = 'version',
						[ 'content.subtype' ] = versionDoc.subtype,
						[ 'content.parent_id' ] = assetDoc._id,

					}

					local version_aux = WFCatAssetAux.getAuxInfoList( versionDoc )
					if version_aux then
						errorEvent[ 'content.aux' ] = WFTableArchiving.tableToString( version_aux )
					end

					if assetDoc.aux then
						errorEvent[ 'content.parent_aux' ] = WFTableArchiving.tableToString( assetDoc.aux )
					end

					WFAnalytics.trackEvent( errorEvent )

					return docStore:applyOverlay( overlay )

				end )

				:catch( function( msg )

					logger:warnf( "Error copying aux from version %s to asset %s: %s", versionDoc._id, assetDoc._id, msg )

					return Observable.just( )

				end )

		end )

end

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

--[[
	\brief Recovers missing auxiliary files by checking if they exist on disk and adding local-only annotations.

	When a BinaryMissingError occurs, this function verifies if the auxiliary files are actually
	present on the device. For each missing auxiliary file:
	1. Checks if it has a local-only annotation
	2. If no annotation exists, verifies if the file exists at its expected location
	3. If the file exists, adds the local-only annotation

	\param catalog (WFCatalog) The catalog instance containing the document
	\param docValue (table) The document value containing auxiliary information
	\param optMissingAuxIds (table, optional) List of auxiliary IDs to check. If not provided,
		all auxiliary IDs from the document will be checked.

	\return (table) An overlay containing local-only annotations for any found auxiliary files.
]]--

local function recoverMissingAuxOverlay( catalog, docValue, optMissingAuxIds )

	local docId = docValue._id

	local auxIds = optMissingAuxIds

	local normalizedAuxInfoList = WFCatAssetAux.getAuxInfoList( docValue )

	if not auxIds then

		auxIds = KSTableUtils.extractKeys( normalizedAuxInfoList )

	end

	local overlay = {}

	for _, auxId in ipairs( auxIds ) do

		local digest = derefOpt( normalizedAuxInfoList, auxId, 'digest' )
		local relativePath = WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, digest )

		if digest and not relativePath then

			-- There is no local-only annotation, so we need to check if the file exists on disk
			-- and if it does, add the local-only annotation.

			local basePath = WFCatAssetAux.getBasePath( catalog )
			local absoluteFilePath, relativeFilePath = WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

			if absoluteFilePath and WFFileUtils.exists( absoluteFilePath ) then

				overlay._id = docId
				WFCatAssetAux.createAuxLocalOnlyOverlay( digest, relativeFilePath, overlay )

			end

		end

	end

	return overlay

end

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

local function refineOverlayForRemovedAux(
		docValue, normalizedAuxInfoList, removedAuxIds, overlay, masksOverlay,
		filtersOverlay, optPayloadAuxOverlay )

	-- This function is used to update the overlay, to remove the parent aux, masks, and filters sections
	-- if the removal of a the auxiliaries results in empty sections.

	if next( removedAuxIds ) then

		local shouldRemoveEntireMasksSection = true
		local shouldRemoveEntireFiltersSection = true
		local shouldRemoveEntireAuxSection = true

		for auxId, auxInfo in pairs( normalizedAuxInfoList ) do

			if not removedAuxIds[ auxId ] then

				shouldRemoveEntireAuxSection = false

				if derefOpt( docValue, 'develop', 'filters', auxInfo.groupDigest, auxId ) then
					shouldRemoveEntireFiltersSection = false
				elseif derefOpt( docValue, 'develop', 'masks', auxId ) then
					shouldRemoveEntireMasksSection = false
				end

			end

			if not shouldRemoveEntireMasksSection
					and not shouldRemoveEntireFiltersSection
					and not shouldRemoveEntireAuxSection then

				break

			end

		end

		if shouldRemoveEntireMasksSection then
			KSTableUtils.setNested( overlay, { _delete = true }, 'develop', 'masks' )
		else
			overlay = KSTableUtils.merge( {}, overlay, masksOverlay )
		end

		if shouldRemoveEntireFiltersSection then
			KSTableUtils.setNested( overlay, { _delete = true }, 'develop', 'filters' )
		else
			overlay = KSTableUtils.merge( {}, overlay, filtersOverlay )
		end

		if optPayloadAuxOverlay and shouldRemoveEntireAuxSection then
			KSTableUtils.setNested( overlay, { _delete = true }, 'aux' )
		else
			overlay = KSTableUtils.merge( {}, overlay, optPayloadAuxOverlay )
		end

	end

	return overlay

end

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

-- Get the list of version documents that reference the provided aux, and build a list
-- of overlays to remove the aux from the develop section of each version.

local function getVersionOverlaysToRemoveMissingAux( catalog, assetDocValue, auxIds )

	if not auxIds or not next( auxIds ) then
		return Observable.just( {} )
	end

	-- Query the index store to find all version documents that reference this asset

	return catalog:indexStore():queryOnce(
		{
			sql = [[ SELECT versions.docId
						FROM versions
						WHERE assetId = ? ]],
			singleColumnQuery = true,
			quiesceUntilUpToDate = true,
			assetDocValue._id
		} )

		:flatMap( function( versionIds )

			if #versionIds == 0 then
				return Observable.just( {} )
			end

			return catalog:docStore():bulkDocValue( versionIds )

		end )

		:map( function( versionDocs )

			local overlays = {}

			for _, versionDoc in ipairs( versionDocs ) do

				local versionOverlay = {
					_id = versionDoc._id
				}

				local removedAuxIds = {}
				local masksOverlay = {}
				local filtersOverlay = {}

				-- Check each aux ID to see if this version references it

				local normalizedAuxInfoList = WFCatAssetAux.getAuxInfoList( versionDoc )

				for auxId in pairs( auxIds ) do

					local hasAuxId = not not derefOpt( normalizedAuxInfoList, auxId )
					local groupDigest = derefOpt( normalizedAuxInfoList, auxId, 'groupDigest' )

					if hasAuxId then

						if groupDigest then
							KSTableUtils.setNested( filtersOverlay, { _delete = true }, 'develop', 'filters', groupDigest, auxId )
						else
							KSTableUtils.setNested( masksOverlay, { _delete = true }, 'develop', 'masks', auxId )
						end

						removedAuxIds[ auxId ] = true

					end

				end

				-- Check if we need to remove the entire masks or filters sections

				versionOverlay = refineOverlayForRemovedAux(
						versionDoc,normalizedAuxInfoList, removedAuxIds,
						versionOverlay, masksOverlay, filtersOverlay )

				if next( versionOverlay ) then
					overlays[ #overlays + 1 ] = versionOverlay
				end

			end

			return overlays

		end )

		:catch( function( err )

			logger:warnf( "Error getting version overlays to remove aux: %s", err )
			return Observable.just( {} )

		end )

end

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

local function recoverOrRemoveMissingAux( catalog, docValue, missingAuxIds )

	-- When a BinaryMissingError occurs, we must verify if the binaries are actually
	-- present on the device. The error may resolve itself if we wait, but if the binaries
	-- are genuinely missing, we must take the following steps:
	--
	-- 1. For each missing auxiliary file:
	--    a. Check if it has a local-only annotation
	--    b. If no annotation exists, verify if the file exists at its expected location
	--    c. If the file exists, add the local-only annotation
	--
	-- 2. For any remaining missing auxiliary files (those not found on disk AND not
	--    referenced in Oz):
	--    a. Remove their references from the auxiliary and develop sections
	--    b. Remove their auxiliary develop references from any associated Version documents

	local docStore = catalog:docStore()

	local docId = docValue._id

	missingAuxIds = missingAuxIds or {}

	local auxesToMaybeRemove = {}

	local normalizedAuxInfoList = WFCatAssetAux.getAuxInfoList( docValue )

	for _, auxId in ipairs( missingAuxIds ) do
	
		local digest = derefOpt( normalizedAuxInfoList, auxId, 'digest' )
		local groupDigest = derefOpt( normalizedAuxInfoList, auxId, 'groupDigest' )
		local relativePath = WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, digest )

		local absolutePath

		if digest then

			if relativePath then

				absolutePath = WFCatAssetAux.getAbsolutePath( catalog, relativePath )

			else

				local basePath = WFCatAssetAux.getBasePath( catalog )
				absolutePath = WFCatAssetAux.calculatePathUsingDigest( basePath, digest )

			end

		end

		-- If the file does not exist on disk, then we need to remove the auxiliary
		-- reference from the asset.

		if not absolutePath or not WFFileUtils.exists( absolutePath ) then

			auxesToMaybeRemove[ #auxesToMaybeRemove + 1 ] = {
				auxId = auxId,
				groupDigest = groupDigest
			}

		end

	end

	local auxToRemoveOverlayObservable = Observable.just( {} )

	if #auxesToMaybeRemove > 0 then

		-- We can only remove an auxiliary reference from the asset if the revision
		-- of the asset on Oz does not reference the auxiliary.

		auxToRemoveOverlayObservable = docStore:docValue( docId, 'oz' )

			:map( function( ozDocValue )

				if ozDocValue._empty then
					return {} -- Don't have an Oz revision to check
				end

				-- Check to see if the auxiliary references are present in the oz revision.

				local removedAuxIds = {}
				local overlay = {}
				local masksOverlay = {}
				local filtersOverlay = {}
				local payloadAuxOverlay = {}

				local ozAuxInfoList = WFCatAssetAux.getAuxInfoList( ozDocValue )

				for _, auxInfo in ipairs( auxesToMaybeRemove ) do

					local auxId = auxInfo.auxId
					local groupDigest = KSTableUtils.derefOpt( normalizedAuxInfoList, auxId, 'groupDigest' )

					if not derefOpt( ozAuxInfoList, auxId ) then

						-- auxId is not present in the oz revision, so we can remove it.

						overlay._id = docId

						KSTableUtils.setNested( payloadAuxOverlay, { _delete = true }, 'aux', auxId )

						if groupDigest then
							KSTableUtils.setNested( filtersOverlay, { _delete = true }, 'develop', 'filters', groupDigest, auxId )
						else
							KSTableUtils.setNested( masksOverlay, { _delete = true }, 'develop', 'masks', auxId )
						end

						removedAuxIds[ auxId ] = true

					end

				end

				overlay = refineOverlayForRemovedAux(
						docValue, normalizedAuxInfoList, removedAuxIds,
						overlay, masksOverlay, filtersOverlay, payloadAuxOverlay )

				return overlay, removedAuxIds

			end )

		:catch( function()

			return Observable.just( {} ) -- No Oz labeled revision found for asset.

		end )

	end

	return auxToRemoveOverlayObservable

		:flatMap( function( removeAuxOverlay, removedAuxIds )

			local recoverAuxOverlay = recoverMissingAuxOverlay( catalog, docValue, missingAuxIds )

			local assetOverlay = KSTableUtils.merge( {}, removeAuxOverlay, recoverAuxOverlay )

			local result = Observable.just( false )

			if next( assetOverlay ) then

				-- Get the list of version documents that reference the aux, and build a list of
				-- overlays to remove the aux from the develop section of each version.

				result = getVersionOverlaysToRemoveMissingAux( catalog, docValue, removedAuxIds )

					:flatMap( function( versionOverlays )

						local overlays = { assetOverlay }

						if next( versionOverlays ) then

							KSTableUtils.appendArray( overlays, versionOverlays )

						end

						return docStore:bulkApplyOverlay( overlays )

					end )

			end

			return result

		end )

end

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

local function binaryMissingErrorHandler( self, errorDoc, docValue )

	local catalog = self._catalog
	local docStore = catalog:docStore()

	-- binary upload must still be in progress, should retry pushing changes

	local result = "MarkedForRetryForBinaryMissingError"
	local retryObservable = markDocumentForRetry( docStore, docValue, errorDoc )
	local resolveObservable = resolveErrorWithResult( self, errorDoc, result, retryObservable )

	if WFCatIndexUtils.isFileBasedAssetDocument( docValue ) then

		local missingAuxIds = derefOpt( errorDoc, '_serverProvidedMetadata', 'errors', 'missing_aux_ids' )

		if missingAuxIds then

			resolveObservable = recoverOrRemoveMissingAux( catalog, docValue, missingAuxIds )
				
				:flatMap( function()
				
					return resolveErrorWithResult( self, errorDoc, result, retryObservable )
				
				end )

		end

	elseif docValue.type == 'version' then

		-- for versions with missing aux, check if aux info is available on version
		-- but missing on asset (edge case that should not happen but it was observed
		-- at least once). If version has all the info needed for the aux, can copy
		-- it over from the version to the asset.

		local missingAuxIds = derefOpt( errorDoc, '_serverProvidedMetadata',
				'errors', 'missing_aux_ids' )

		if missingAuxIds then

			-- if all of these are present in version and local only file
			-- are available and files exists, then check if asset doc needs repairing

			local versionDevelopAuxes = {}

			local versionNormalizedAuxInfoList = WFCatAssetAux.getAuxInfoList( docValue )

			for _, auxId in ipairs( missingAuxIds ) do

				local digest = derefOpt( versionNormalizedAuxInfoList, auxId, 'digest' )
				local groupDigest = derefOpt( versionNormalizedAuxInfoList, auxId, 'groupDigest' )

				local relativePath = WFCatAssetAux.getAuxLocalOnlyFilePath( docValue, digest )

				if digest and relativePath then

					-- For Versions, don't resolve the error and mark the document for retry until
					-- all of the referenced auxiliaries are confirmed as being synced within it's
					-- associated Asset document.

					resolveObservable = markVersionForRetryOnceAssetAuxSync( self, docStore, docValue, errorDoc, missingAuxIds )

					local absolutePath = WFCatAssetAux.getAbsolutePath( catalog, relativePath )
					
					versionDevelopAuxes[ #versionDevelopAuxes + 1 ] = { 
						auxId = auxId, 
						type = groupDigest and 'filter' or 'mask',
						digest = digest,
						absolutePath = absolutePath 
					}

				else

					versionDevelopAuxes = nil
					break
						-- if any missing auxId is not available on version then do not attempt repair

				end

			end

			if versionDevelopAuxes and next( versionDevelopAuxes ) then

				result = "MaybeCopyAuxAnd" .. result

				resolveObservable = maybeCopyAuxInfoToAssetFromVersion( catalog, docValue, versionDevelopAuxes, errorDoc )

					:flatMap( function( copiedAux )

						if copiedAux then

							-- If we've successfully copied across aux info, then we should wait for it
							-- to sync before resolving the error.

							return markVersionForRetryOnceAssetAuxSync( self, docStore, docValue, errorDoc, missingAuxIds )

						else

							-- Something went wrong. Just resolve the error right away.

							return resolveErrorWithResult( self, errorDoc, result, retryObservable )

						end

					end )

			end

		end

	end

	return resolveObservable

end

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

local function notHeadRevisionPreconditionErrorHandler( self, errorDoc, docValue )

	return resolveError( self, errorDoc, "FixByGettingOzDoc", fixIfMatchIssue( self._catalog, docValue ) )

end

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

-- @mark Error document processing

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

local function isRestoreRelatedError( action, revIdThatIncurredError, docThatIncurredError )

	-- Restored assets may have a modification applied to them so check if the
	-- createWithRevId matches and allow the create to be processed as an error
	-- as well. This resolves issues with Stuck syncs when restoring modified
	-- assets for kStorageExceededError. NMB-10393

	-- Also we may get a storage exceeded error when trying to delete an asset
	-- that was restored from the trash while out of storage and then put back
	-- into the trash as we are required to send a restore/delete combo. NMB-11108

	-- note that this method does not check the error subtype, so it will impact
	-- many trash/restore related errors. Also update check to confirm that
	-- revIdThatIncurredError is not nil (can be for some version related errors)

	return action == 'create' and revIdThatIncurredError and
		( WFCatTrashUtils.getCreateWithRevisionId( docThatIncurredError ) == revIdThatIncurredError -- restore modify
			or docThatIncurredError._deleted and docThatIncurredError._newId ) -- restore delete

end

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

local function revisionIdMatchesAndNotEmptyOrAlreadyDeleted( subtype, action, revIdThatIncurredError, docThatIncurredError )

	return docThatIncurredError._revId == revIdThatIncurredError and

		( not docThatIncurredError._empty or

			( subtype == WFCatOzErrors.Errors.kResourceNotFoundError and action == 'delete' )
				-- This is an edge case in NMB-9050, but if we sent a delete to Oz and Oz responds with a
				-- ResourceNotFoundError on the delete we'll never get a tombstone, but we should consider
				-- it deleted on Oz and so we need to let the ResourceNotFoundError handler handle it.

		)

end

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

local function processOneErrorDocument( self, errorDocId )

	if self._docsBeingProcessed[ errorDocId ] then

		-- Query can return a new result while error doc is being processed
		-- avoid processing the same error docs multiple times.

		return -- Already being processed ignore this doc.

	end

	self._docsBeingProcessed[ errorDocId ] = true

	local docStore = self._catalog:docStore()

	return self._catalog:docStore():docValue( errorDocId )
		:flatMap( function( errorDoc )

			if errorDoc._empty then
				return Observable.just()
			end

			-- all error documents should have the id of the document that caused the error
			-- make sure we have that document before sending it to a handler since clients
			-- can receive error documents generated by other ecosystem clients
			local spm_source =  derefOpt( errorDoc, '_serverProvidedMetadata', 'source' )
			local idOfDocThatIncurredError = derefOpt( spm_source, 'asset_id' ) or derefOpt( spm_source, 'id' )
			local revIdThatIncurredError = derefOpt( spm_source, 'revision_id' )
			local action = derefOpt( spm_source, 'action' )

			if not idOfDocThatIncurredError then
				-- this error doesn't match the form we expect, mark it unknown for now
				logger:errorf( "Unexpected format in error document, missing source document id : %s", WFTableArchiving.tableToString( errorDoc ) )
				return markErrorAsUnknown( docStore, errorDocId )
			end

			return docStore:docValue( idOfDocThatIncurredError )
				:flatMap( function( docThatIncurredError )

					-- don't let any unexpected error formats that fail to get to the diagnostic loc cause any issues
					-- with processing more error documents
					pcall( addErrorDocToDiagnosticLog, self, errorDoc )

					local subtype = errorDoc.subtype

					-- Check that the document is not empty/deleted and that the revision matches the latest revision.
					--
					-- This is intended to filter out the error documents that may have been generated by changes from
					-- other clients. However, it also means that it filters out any error documents generated from this
					-- client, but at an earlier revision than what is current.
					--
					-- In these cases we expect that the later revision will be sent to Oz and, if that again results
					-- in an error, the error will be handled with an error document pertaining to that revision.
					if not isAuxSha256MismatchError( errorDoc ) and
						not isXmpDevelopSha256MismatchError( errorDoc ) and
						not maybeTrashOperationNewId( errorDoc ) and
						not isRestoreAssetError( errorDoc ) and -- NMB-13281/LRMI-4193007/LRMA-4917671
						not isRestoreRelatedError( action, revIdThatIncurredError, docThatIncurredError ) and
						not isDeletedByAnotherUser( errorDoc, self._catalog:ozUserId() ) and
						not isBinaryMissingErrorForVersion( errorDoc, docThatIncurredError ) and
						not revisionIdMatchesAndNotEmptyOrAlreadyDeleted( subtype, action, revIdThatIncurredError,
							docThatIncurredError ) then
						return resolveErrorByIgnoring( self, errorDoc, "RevisionDoesNotMatch" )
					end

					local handler = getErrorHandler( self, subtype ) or defaultHandler

					return handler( self, errorDoc, docThatIncurredError )

			end )

		end )
		:finally( function()
			self._docsBeingProcessed[ errorDocId ] = nil
		end )
		:call( WFRxRunUntilDone )

end

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

local function clearTrialOrSubscriptionExpiredErrors( self )

	self._clearTrialOrSubscriptionExpiredErrorsSubscription = Observable.combineLatest(
			self._blockedDocsStream, self._catalog:ozUser():isSubscriptionExpired() )

		:filter( function( blockedDocs, isExpired )
			return not isExpired
		end )

		:map( function( blockedDocs )

			local bulkOverlays = {}

			-- we should only be here if we have ids blocked because of subscription expiry and
			-- we are not in an expired state so we can go ahead and clear all of these errors

			for _, blockedDoc in ipairs( blockedDocs ) do

				if blockedDoc.errorSubtype == WFCatOzErrors.Errors.kTrialOrSubscriptionExpiredError then
					bulkOverlays[ #bulkOverlays + 1 ] = WFCatOzErrors.createOverlayToRemoveOzError( blockedDoc.docId )
				end

			end

			return bulkOverlays

		end )

		:flatMap( function( bulkOverlays )

			logger:info( [[Cleared %d errors on documents previously blocked from upload due to
							an expired trial or subscription.]], #bulkOverlays )

			if #bulkOverlays > 0 then

				return self._catalog:docStore():bulkApplyOverlay( bulkOverlays )

			else

				return Observable.just( true )

			end

		end )

		:subscribe()

end

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

local function checkAlbumAssets( self, blockedAlbumAssetIds )

	if #blockedAlbumAssetIds == 0 then
		return Observable.just( {} )
	end

	-- Check Oz status of associated assets and albums.

	local blockedDocValueStream =
			WFRxThrottledBatchedBulkDocValue( "WFCatErrorHandler_checkAlbumAssets", self._catalog:docStore(), blockedAlbumAssetIds, 100, 10 )

				:scan( function( docValues, batchOfDocValues )

					docValues = KSTableUtils.appendArray( docValues, batchOfDocValues )
					return docValues

				end, {} )

				:takeLast( 1 )

	return Observable.combineLatest(
			blockedDocValueStream,
			self._getSyncStatusOfAlbumsOfAlbumAssetsFn( self._catalog, blockedAlbumAssetIds ),
			self._getSyncStatusOfAssetsOfAlbumAssetsFn( self._catalog, blockedAlbumAssetIds ) )

		:map( function( blockedAlbumAssetDocValues, syncedAlbumsById, syncedAssetsById )

			local albumAssetIdsToClear = {}

			for _, docValue in ipairs( blockedAlbumAssetDocValues ) do

				local albumId = derefOpt( docValue, 'album', 'id' )
				local assetId = derefOpt( docValue, 'asset', 'id' )

				if albumId and syncedAlbumsById[ albumId ] and
					assetId and syncedAssetsById[ assetId ] then

					logger:infof( "The associated album %s and asset %s are now synced for album_asset %s.",
							albumId, assetId, docValue._id )

					albumAssetIdsToClear[ #albumAssetIdsToClear + 1 ] = docValue._id

				else

					logger:infof( "The album or asset for album_asset %s is not yet synced.", docValue._id )

				end

			end

			return albumAssetIdsToClear

		end )

end

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

local function checkAlbums( self, blockedAlbumIds )

	if #blockedAlbumIds == 0 then
		return Observable.just( {} )
	end

	-- Check Oz status of associated collection_set parent

	local blockedDocValueStream =
			WFRxThrottledBatchedBulkDocValue( "WFCatErrorHandler_checkAlbums", self._catalog:docStore(), blockedAlbumIds, 100, 10 )

				:scan( function( docValues, batchOfDocValues )

					docValues = KSTableUtils.appendArray( docValues, batchOfDocValues )
					return docValues

				end, {} )

				:takeLast( 1 )

	return Observable.combineLatest(
			blockedDocValueStream,
			self._getSyncStatusOfParentCollectionOfAlbumsFn( self._catalog, blockedAlbumIds ) )

		:map( function( blockedAlbumDocValues, syncedAlbumsById )

			local albumIdsToClear = {}

			for _, docValue in ipairs( blockedAlbumDocValues ) do

				local parentId = derefOpt( docValue, 'parent', 'id' )

				if parentId and parentId ~= 'root' then

					if syncedAlbumsById[ parentId ] then

						logger:infof( "The parent collection_set for album %s is now synced.", docValue._id )

						albumIdsToClear[ #albumIdsToClear + 1 ] = docValue._id

					else

						logger:infof( "The parent collection_set for album %s is not yet synced.", docValue._id )

					end

				else

					logger:warnf( "This album is blocked from upload, and we don't know why." )

				end

			end

			return albumIdsToClear

		end )

end

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

local function checkAssetDependentDocs( self, blockedDocIds, forceUseFullBlockedDocIds, getSyncStatusOfAssetsOfDependentDocFn )

	if #blockedDocIds == 0 then
		return Observable.just( {} )
	end

	-- Check the Oz status of associated assets.

	local blockedDocValueStream =
			WFRxThrottledBatchedBulkDocValue( "WFCatErrorHandler_checkAssetDependentDocs", self._catalog:docStore(), blockedDocIds, 100, 10 )

				:scan( function( docValues, blockedFullDocIds, batchOfDocValues )

					for _, docValue in ipairs( batchOfDocValues ) do

						docValues[ #docValues + 1 ] = docValue
						blockedFullDocIds[ #blockedFullDocIds + 1 ] = docValue._id

					end

					return docValues, blockedFullDocIds

				end, {}, {} )

				:takeLast( 1 )

	return blockedDocValueStream

		:flatMap( function( blockedDependentDocValues, blockedFullDocIds )

			local blockedIds = blockedDocIds

			if forceUseFullBlockedDocIds then
				blockedIds = blockedFullDocIds
			end

			return getSyncStatusOfAssetsOfDependentDocFn( self._catalog, blockedIds )

				:map( function( ... )
					return blockedDependentDocValues, ...
				end )

		end )

		:map( function( blockedDependentDocValues, syncedAssetsById )

			local dependentDocIdsToClear = {}

			for _, docValue in ipairs( blockedDependentDocValues ) do

				local assetId = derefOpt( docValue, 'asset', 'id' )

				if assetId and syncedAssetsById[ assetId ] then

					logger:infof( "The associated asset %s is now synced for dependent doc %s.",
							assetId, docValue._id )

					dependentDocIdsToClear[ #dependentDocIdsToClear + 1 ] = docValue._id

				else

					logger:infof( "The asset for dependent doc %s is not yet synced.", docValue._id )

				end

			end

			return dependentDocIdsToClear

		end )

end

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

local function checkVersions( self, blockedVersionIds )

	return checkAssetDependentDocs( self, blockedVersionIds, false, self._getSyncStatusOfAssetsOfVersionsFn )

end

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

local function checkFavorites( self, blockedFavoriteIds )

	return checkAssetDependentDocs( self, blockedFavoriteIds, true, self._getSyncStatusOfAssetsOfFavoritesFn )

end

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

local function checkComments( self, blockedCommentIds )

	return checkAssetDependentDocs( self, blockedCommentIds, true, self._getSyncStatusOfAssetsOfCommentsFn )

end

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

local function clearResourceNotFoundErrors( self )

	self._clearResourceNotFoundErrorsSubscription = self._blockedDocsStream

		:flatMapLatest( function( blockedDocs )

			local blockedAlbumAssets = {}
			local blockedAlbums = {}
			local blockedVersions = {}
			local blockedFavorites = {}
			local blockedComments = {}

			for _, blockedDoc in ipairs( blockedDocs ) do

				if blockedDoc.errorSubtype == WFCatOzErrors.Errors.kResourceNotFoundError then

					if blockedDoc.type == 'album_asset' then
						blockedAlbumAssets[ #blockedAlbumAssets + 1 ] = blockedDoc.docId
					elseif blockedDoc.type == 'album' then
						blockedAlbums[ #blockedAlbums + 1 ] = blockedDoc.docId
					elseif blockedDoc.type == 'version' then
						blockedVersions[ #blockedVersions + 1 ] = blockedDoc.docId
					elseif blockedDoc.type == 'favorite' then
						blockedFavorites[ #blockedFavorites + 1 ] = blockedDoc.docId
					elseif blockedDoc.type == 'comment' then
						blockedComments[ #blockedComments + 1 ] = blockedDoc.docId
					end

				end

			end

			return Observable.combineLatest(
					checkAlbumAssets( self, blockedAlbumAssets ),
					checkAlbums( self, blockedAlbums ),
					checkVersions( self, blockedVersions ),
					checkFavorites( self, blockedFavorites ),
					checkComments( self, blockedComments ) )

		end )

		:map( function( albumAssetIdsToClear, albumIdsToClear, versionIdsToClear, favoriteIdsToClear, commentIdsToClear )

			local bulkOverlays = {}

			for _, docId in ipairs( albumAssetIdsToClear ) do

				bulkOverlays[ #bulkOverlays + 1 ] = WFCatOzErrors.createOverlayToRemoveOzError( docId )

			end

			for _, docId in ipairs( albumIdsToClear ) do

				bulkOverlays[ #bulkOverlays + 1 ] = WFCatOzErrors.createOverlayToRemoveOzError( docId )

			end

			for _, docId in ipairs( versionIdsToClear ) do

				bulkOverlays[ #bulkOverlays + 1 ] = WFCatOzErrors.createOverlayToRemoveOzError( docId )

			end

			for _, docId in ipairs( favoriteIdsToClear ) do

				bulkOverlays[ #bulkOverlays + 1 ] = WFCatOzErrors.createOverlayToRemoveOzError( docId )

			end

			for _, docId in ipairs( commentIdsToClear ) do

				bulkOverlays[ #bulkOverlays + 1 ] = WFCatOzErrors.createOverlayToRemoveOzError( docId )

			end

			return bulkOverlays

		end )

		:flatMapLatest( function( bulkOverlays )

			logger:info( [[Cleared %d errors on documents previously blocked from upload due to
							resource not found errors.]], #bulkOverlays )

			if #bulkOverlays > 0 then

				return WFRxThrottledBatchedBulkApplyOverlay( "WFCatErrorHandler_clearResourceNotFoundErrors", self._catalog:docStore(), bulkOverlays, 100, 10 )

			else

				return Observable.just( true )

			end

		end )

		:subscribe()

end

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

local function distinctTableCompare( _, tbl1, _, tbl2 )

	return KSTableUtils.deepEquals( tbl1, tbl2 )

end

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

local function monitorBlockedUploads( self )

	-- This function only monitors blocked uploads that belong to the catalog.
	-- Space-based blocked uploads are handled WFCatSpaceFeedsManager

	local docIdRowsQuery = self._indexStore:query {
			sql = [[ SELECT docId, reason, type
						FROM blocked_uploads
						WHERE type IN ( 'asset', 'album_asset', 'album', 'collection',
										'version', 'comment', 'favorite' )
							AND spaceId IS NULL ]],
			calm = self._debugMode and nil or 5,
			enqueue = self._debugMode and 'fast' or 'slow',
		}

	local startupDelay = self._debugMode and 0.01 or 10

	return Observable.combineLatest( docIdRowsQuery, Observable.timer( startupDelay, WFRxScheduler.sharedScheduler() ) )

		:filter( function( docIdRows )
			return #docIdRows > 0
		end )

		:distinctUntilChanged( nil, distinctTableCompare )

		:map( function( docIdRows )

			local blockedDocs = {}

			for _, docIdRow in ipairs( docIdRows ) do

				local blockedDoc = {}

				blockedDoc.docId = docIdRow[ 1 ]
				blockedDoc.errorSubtype = docIdRow[ 2 ]
				blockedDoc.type = docIdRow[ 3 ]

				blockedDocs[ #blockedDocs + 1 ] = blockedDoc

			end

			return blockedDocs

		end )

		:subscribe( function( blockedDocs )

			self._blockedDocsStream:onNext( blockedDocs )

		end )

end

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

local function wrapFnForClass( fn )
	return function( _, ... )
		return fn( ... )
	end
end

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

-- @mark WFCatErrorHandler namespace

local WFCatErrorHandler = {

	_catalog = false,
		-- (WFCatalog)

	_indexStore = false,
		-- (WFRxIndexStore)

	_errorHandlerSubscription = false,
		-- (Subscription) Subscription to the Observable that will watch for process error documents

	_errorDocumentHandlerFns = false,
		-- (table) table mapping error document subtypes to error document handler functions

	_externalizeSettingsFn = false,
		-- (function) function that takes a catalog, document id, and optional enforced limit and returns
		-- an Observable that will externalize the develop settings and return true when complete

	_deleteAssetsFn = false,
		-- (function) function that takes a catalog and list of document ids, and returns
		-- an Observable that will delete the assets when complete

	_deleteIndividualAlbumFn = false,
		-- (function) function that takes a catalog and document id, and returns
		-- an Observable that will delete the individual album when complete

	_getSyncStatusOfAssetsOfAlbumAssetsFn = false,
		-- (function) function that takes a catalog and a list of album asset document ids, and returns
		-- an Observable which yields an array of isComplete status of said assets

	_getSyncStatusOfAlbumsOfAlbumAssetsFn = false,
		-- (function) function that takes a catalog and a list of album asset document ids, and returns
		-- an Observable which yields an array of isComplete status of the associated albums

	_getSyncStatusOfParentCollectionOfAlbumsFn = false,
		-- (function) function that takes a catalog and a list of albums document ids, and returns
		-- an Observable which yields an array of isComplete status of the albums

	_getSyncStatusOfAssetsOfVersionsFn = false,
		-- (function) function that takes a catalog and a list of version document ids, and returns
		-- an Observable which yields an array of isComplete status of the assets

	_getSyncStatusOfAssetsOfCommentsFn = false,
		-- (function) function that takes a catalog and a list of comment document ids, and returns
		-- an Observable which yields an array of isComplete status of the assets

	_getSyncStatusOfAssetsOfFavoritesFn = false,
		-- (function) function that takes a catalog and a list of favorite document ids, and returns
		-- an Observable which yields an array of isComplete status of the assets

	_errorDocSummary = false,
		-- (table) summary of the error documents received by the client

	_errorDocResolution = false,
		-- (table) table describing resolutions used to handle the encountered errors

	_blockedUploadSubscription = false,
		-- (Subscription) watches uploads blocked by Oz

	_blockedDocsStream = false,
		-- (Rx.ReplaySubject) Fires with documents blocked from upload.

	_clearResourceNotFoundErrorsSubscription = false,
		-- (Subscription) Clears uploads blocked because of resource not found errors.

	_clearTrialOrSubscriptionExpiredErrorsSubscription = false,
		-- (Subscription) Clears uploads blocked because of a expired trial/subscription.

	_docsBeingProcessed = false,
		-- table indexed by errorDocId to indicate the document is currently being processed.

	_debugMode = false,
		-- (boolean) If set, reduces many of the built in delays/calm values. Useful for
		-- speeding up unit tests.

	class = {
		generateIndexSchema = wrapFnForClass( generateIndexSchema ),
		mimicOzDeletionEvent = wrapFnForClass( mimicOzDeletionEvent ),
		maybeMarkDocumentForRetry = wrapFnForClass( maybeMarkDocumentForRetry ),
		recoverMissingAuxOverlay = wrapFnForClass( recoverMissingAuxOverlay ),
		_DEBUG_recoverOrRemoveMissingAux = wrapFnForClass( recoverOrRemoveMissingAux ),
		_DEBUG_refineOverlayForRemovedAux = wrapFnForClass( refineOverlayForRemovedAux )
	},

	WFCloseSupport.addCloseSupportMethods,

}

local function maybeLogMissingHandlerError( debugMode, maybeHandlerFn, handlerName )
	if not debugMode and not maybeHandlerFn then
		logger:errorf( "WFCatErrorHandler missing handler function for %s", handlerName )
	end
end

--------------------------------------------------------------------------------
--[[
	\class WFCatErrorHandler

	\brief Indexes and process documents of type \c error sent back from Oz in the metadata sync feed.

	Oz may send documents of type \c error and various subtypes in the updates feed that indicate errors in
	either metadata sync or binary sync from the client. These documents will be indexed to the \c errors table
	and then processed by this class which will clear the documents from the \c errors table.

	Error documents generally need to explicitly register a new handler to properly resolve the error, however
	there are some error documents (e.g. NotHeadRevisionPreconditionError) that do not need to be handled as they
	will be resolved naturally as part of sync.

	Any error documents received without an explicit handler will be marked with an 'unknownError' flag which will
	remove it from the \c errors table. If a handler is added later, it can easily be reindexed back in to the \c
	errors table to be processed with the new handler.

	IMPORTANT: Each client receives all error documents generated by *ANY* ecosystem client so error documents are
	immediately filtered out by revision id which is generally specific to a single client (this is true for metadata
	sync errors but not for error docs related to binary sync). If the revision id does not exist in this client's
	doc store then the document is immediately deleted.


	To add a handler for a new error document type:

	1. Add the error document subtype to the \c Errors array in \c WFCatOzErrors
	2. Create a handler function in \c WFCatErrorHandler to address the error and returns an Observable
		a. The handler function will be passed \c self (WFCatErrorHandler), the error document, and the document that
			caused the error to be sent.
		b. The handler function should delete the error doc if it handles the error successfully or mark it as an unknown
			error if the type/subtype is correct but it does not know how to handle that specific error. Marking it unknown
			allows a future client the ability to retry the processing of the error after handling for the specific case has
			been handled.
	3. Register the error handler by calling \c self:registerErrorHandler in the \c WFCatErrorHandler:__init function
	4. Update the implementation of the handler function to properly resolve the error. Handler functions will commonly
		a. Request Oz's version of the document
		b. Apply an overlay to alter the document that caused the error (e.g. truncating an album name)
		c. Look up associated documents (e.g. assets for an album_asset error) either locally or on Oz to determine how
			to handle the error
	5. Some error handlers may need client specific handling. For these cases we usually require clients to pass in client-
		specific handling functions to the \c WFCatErrorHandler constructor that encapsulate the client-specific code. See the
		\c optClientHandlerFnTbl usage in \c __init
	6. Add tests in WFCatErrorHandlerTest to validate that the error handler works correctly

]]--
function WFCatErrorHandler:__init( catalog, clientHandlerFnTbl, optArgs )

	check( '_', 'WFCatalog', 'table', '?table' )

	self._errorDocumentHandlerFns = {}
	self._errorDocSummary = {}
	self._errorDocResolution = {}
	self._docsBeingProcessed = {}

	self:setOwned {
		_catalog = catalog:relayObject()
	}

	self._indexStore = catalog:indexStore()

	optArgs = optArgs or {}

	self._debugMode = optArgs.debugMode

	-- STEP TWO: Register subtype-specific error document handlers

	self:registerErrorHandler( WFCatOzErrors.Errors.kResourceNotFoundError, resourceNotFoundHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kStorageExceededError, storageExceededHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kTrialOrSubscriptionExpiredError, trialOrSubscriptionEntitlementExpiredHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kEntitlementExpiredError, trialOrSubscriptionEntitlementExpiredHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kPayloadValidationError, payloadValidationHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kInputValidationError, inputValidationHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kResourceExistsError, resourceExistsHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kMultipartCompleteError, multipartCompleteHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kAssetGoneError, assetGoneHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kInvalidAccessRequestError, invalidAccessRequestHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kSpaceUserExistsError, spaceUserExistsHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kRestoreAssetError, restoreAssetErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kInsufficientPermissionError, insufficientPermissionErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kAuxSha256MismatchError, auxSha256MismatchErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kAuxMissingError, auxMissingErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kReferencedAuxMissingError, referencedAuxMissingErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kNotHeadRevisionPreconditionError, notHeadRevisionPreconditionErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kXmpDevelopSha256MismatchError, xmpDevelopSha256MismatchErrorHandler )
	self:registerErrorHandler( WFCatOzErrors.Errors.kBinaryMissingError, binaryMissingErrorHandler )

	-- Some handlers need client specific implementations which can be passed in the fn
	-- table. If the table exists and the fns are provided, add them to the error handler

	self._externalizeSettingsFn = clientHandlerFnTbl.externalizeSettings
	maybeLogMissingHandlerError( self._debugMode, self._externalizeSettingsFn, 'externalizeSettings' )

	self._deleteAssetsFn = clientHandlerFnTbl.deleteAssets
	maybeLogMissingHandlerError( self._debugMode, self._deleteAssetsFn, 'deleteAssets' )

	self._deleteIndividualAlbumFn = clientHandlerFnTbl.deleteIndividualAlbum
	maybeLogMissingHandlerError( self._debugMode, self._deleteIndividualAlbumFn, 'deleteIndividualAlbum' )

	self._getSyncStatusOfAssetsOfAlbumAssetsFn = clientHandlerFnTbl.getSyncStatusOfAssetsOfAlbumAssets
	maybeLogMissingHandlerError( self._debugMode, self._getSyncStatusOfAssetsOfAlbumAssetsFn, 'getSyncStatusOfAssetsOfAlbumAssets' )

	self._getSyncStatusOfAlbumsOfAlbumAssetsFn = clientHandlerFnTbl.getSyncStatusOfAlbumsOfAlbumAssets
	maybeLogMissingHandlerError( self._debugMode, self._getSyncStatusOfAlbumsOfAlbumAssetsFn, 'getSyncStatusOfAlbumsOfAlbumAssets' )

	self._getSyncStatusOfParentCollectionOfAlbumsFn = clientHandlerFnTbl.getSyncStatusOfParentCollectionOfAlbums
	maybeLogMissingHandlerError( self._debugMode, self._getSyncStatusOfParentCollectionOfAlbumsFn, 'getSyncStatusOfParentCollectionOfAlbums' )

	self._getSyncStatusOfAssetsOfVersionsFn = clientHandlerFnTbl.getSyncStatusOfAssetsOfVersions
	maybeLogMissingHandlerError( self._debugMode, self._getSyncStatusOfAssetsOfVersionsFn, 'getSyncStatusOfAssetsOfVersions' )

	self._getSyncStatusOfAssetsOfCommentsFn = clientHandlerFnTbl.getSyncStatusOfAssetsOfComments
	maybeLogMissingHandlerError( self._debugMode, self._getSyncStatusOfAssetsOfCommentsFn, 'getSyncStatusOfAssetsOfComments' )

	self._getSyncStatusOfAssetsOfFavoritesFn = clientHandlerFnTbl.getSyncStatusOfAssetsOfFavorites
	maybeLogMissingHandlerError( self._debugMode, self._getSyncStatusOfAssetsOfFavoritesFn, 'getSyncStatusOfAssetsOfFavorites' )

	-- IMPORTANT: If you're adding additional, client-specific handlers, make sure those handlers
	-- are passed into the WFCatErrorHandler constructors within NmbErrorHandler & LrmSyncClientImp.

	if self._externalizeSettingsFn then
		self:registerErrorHandler( WFCatOzErrors.Errors.kInlineXmpDevelopSettingTooBigError, inlineSettingsHandler )
	end

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

	local errorQuery = self._indexStore:query {
		sql = [[SELECT docId FROM errors ORDER BY docId LIMIT 10]],
		calm = self._debugMode and 0.1 or 1,
		enqueue = 'slow',
		singleColumnQuery = true
	}

	self._errorHandlerSubscription = errorQuery:subscribe(
			function( docIds )
				if #docIds > 0 then
					logger:infof( "Error document handler identified %d error documents", #docIds )
				end
				for _, docId in ipairs( docIds ) do
					processOneErrorDocument( self, docId )
				end
			end,
			function( message )
				logger:errorf( "Error document handler query failed with message: %s", message )
			end,
			function()
				logger:debugf( "Error document handler query notified as done" )
			end )

	self._blockedUploadSubscription = monitorBlockedUploads( self )

	self._blockedDocsStream = Rx.ReplaySubject( 1 )

	clearTrialOrSubscriptionExpiredErrors( self )

	clearResourceNotFoundErrors( self )

	WFDiagnosticLog.addLogProvider( self )

end

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

function WFCatErrorHandler:registerErrorHandler( errSubtype, handlerFn )

	self._errorDocumentHandlerFns[ errSubtype ] = handlerFn

end

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

function WFCatErrorHandler:getDefaultErrorHandlerFn()
	return defaultHandler
end

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

function WFCatErrorHandler:sendDiagnosticLogData()

	WFDiagnosticLog.addSectionToLog
		{

			id = string.format( 'WFCatErrorHandler' ),

			name = string.format( "Sync Error Handler" ),
			weight = 510,
			parentSection = WFDiagnosticLog.Sections.SYNC,

			body = {
				errors = self._errorDocSummary,
				resolutions = self._errorDocResolution,
			}

		}

end

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

function WFCatErrorHandler:DEBUG_errorsProcessed()

	-- Returns error summary table, and boolean that is true if currently processing an error

	return self._errorDocSummary, next( self._docsBeingProcessed ) ~= nil

end

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

function WFCatErrorHandler:DEBUG_errorsResolved()

	-- Returns error summary table, and boolean that is true if currently processing an error

	return self._errorDocResolution

end

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

function WFCatErrorHandler:close()

	self._errorHandlerSubscription:dispose()
	self._blockedUploadSubscription:dispose()

	local clearResourceNotFoundErrorsSubscription = self._clearResourceNotFoundErrorsSubscription
	self._clearResourceNotFoundErrorsSubscription = nil

	if clearResourceNotFoundErrorsSubscription then
		clearResourceNotFoundErrorsSubscription:dispose()
	end

	local clearTrialOrSubscriptionExpiredErrorsSubscription = self._clearTrialOrSubscriptionExpiredErrorsSubscription
	self._clearTrialOrSubscriptionExpiredErrorsSubscription = nil

	if clearTrialOrSubscriptionExpiredErrorsSubscription then
		clearTrialOrSubscriptionExpiredErrorsSubscription:dispose()
	end

	local blockedDocsStream = self._blockedDocsStream
	self._blockedDocsStream = nil

	if blockedDocsStream then
		blockedDocsStream:dispose()
	end

end

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

return Rx.Class 'WFCatErrorHandler'( WFCatErrorHandler )
