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

WFCatPurgeOnDelete.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2022 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

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

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

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFFileUtils = require 'WFFileUtils'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxThrottledBatchedBulkApplyOverlay = require 'WFRxThrottledBatchedBulkApplyOverlay'

	-- WF.oz_client.managed_catalog
local WFCatCopies = require 'WFCatCopies'
local WFCatDelete = require 'WFCatDelete'
local WFCatPurgeHelper = require 'WFCatPurgeHelper'

local OzFilePaths, LrmOzPurgeAgentUtils

if WFPlatformInfo.isiOS then
	OzFilePaths = require 'OzFilePaths'
end

if WFPlatformInfo.isMobile() then
	LrmOzPurgeAgentUtils = require 'LrmOzPurgeAgentUtils'
end

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

local logger = WFLogger "WFCatPurgeOnDelete"

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

local check = assert( WFArguments.check )

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )

local purgeBinariesDirectly
		= LrmOzPurgeAgentUtils and assert( LrmOzPurgeAgentUtils.purgeBinariesDirectly )

local absolutePath
		= LrmOzPurgeAgentUtils and assert( LrmOzPurgeAgentUtils.absolutePath )

local renditionPath
		= LrmOzPurgeAgentUtils and assert( LrmOzPurgeAgentUtils.renditionPath )

local removeCacheEntriesAndXmpsForDocument
		= LrmOzPurgeAgentUtils and assert( LrmOzPurgeAgentUtils.removeCacheEntriesAndXmpsForDocument )

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

local WFCatPurgeOnDelete = {}

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

-- @mark Desktop

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

local function setupPurgeOnDelete_Desktop( catalog, deletionBacklog )

	-- When as asset is deleted from the doc store, we immediately purge all
	-- files associated with that asset.

	local docStore = catalog:docStore()

	local purgeAgentChangeCounterLocalDocId = 'WFCatVolumeAgent-changeCounter'
	
	return docStore:deletedDocsSinceNamedChangeCounter(
			purgeAgentChangeCounterLocalDocId,
			{
				_id = true,
				_localOnly = {
					files = true,
					deletedLocally = true
				},
				_localId = true,
				_serverProvidedMetadata = {
					new_id = true,
				},
				type = true
			} )

		:flatMap( function( docValues, continueFrom )

			local docIds = {}
			
			if #docValues > 0 then
				logger:infof( "Now processing %d deleted docs. Continue From: %d", #docValues, continueFrom )
			end
			
			for _, docValue in ipairs( docValues ) do
			
				docIds[ #docIds + 1 ] = docValue._id
			
				-- deleted assets from Oz have no type information by this point, so
				-- pass those through for deletion just in case they were assets, but
				-- we might as well not delete things that we _know_ were not assets
			
				-- xmp_develop are not removed here. The develop settings are cleaned up
				-- automatically by PixelProviderFileCachePurgeWorker.
			
				-- Auxiliaries are not removed here either. They are automatically cleaned up
				-- by the volume agent: See getUntrackedBinariesRemover in WFCatVolumeAgent.
			
				if docValue and (
						docValue.type == "asset" 
							or docValue.type == 'version'
							or docValue.type == nil ) then
				
					local newId = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'new_id' )
					local localOnly = KSTableUtils_derefOpt( docValue, "_localOnly" )
						
					-- local bulk asset deletion takes care of file deletion (because it
					-- is faster to do it in batches), but if we sync asset deletion from Oz,
					-- we need to take care of the items as they arrive.

					-- If the asset has a newId then oz is has moved it to trash or
					-- restored it, don't delete the binaries as the asset will still
					-- refer to it.

					if newId == nil and type( localOnly ) == 'table' and not localOnly.deletedLocally then

						local fileArray = KSTableUtils_derefOpt( localOnly, "files" )
						
						if type( fileArray ) == 'table' then
						
							logger:infof( "Now deleting files for doc %s", docValue._id )

							WFCatDelete.deleteDocStoreFileArray( fileArray, deletionBacklog )
						
						end
					
					end
				
				end
			
			end
			
			-- self._docStore can get closed before self
			if WFCloseSupport.hasBeenOrWillBeClosed( docStore ) then
				return Observable.empty()
			end

			return docStore:updateDeletedDocsSinceNamedChangeCounter(
					purgeAgentChangeCounterLocalDocId, continueFrom )
					
				:map( function()		
					return docIds
				end )
			
		end )
		
		:filter( function( docIds )
			return next( docIds )
		end )

end

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

-- @mark Mobile

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

local function deleteFilesFromDiskForDocument( presetProfileDeletionStream, catalogId, docValue )

	local hasTrashOrRestoreAsset = KSTableUtils_derefOpt( docValue, '_newId' )
			or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'new_id' )

	if hasTrashOrRestoreAsset then
		return docValue
	end

	logger:infof( "Now deleting files for doc %s", docValue._id )

	for binaryName, maybeBinarySizes in pairs( purgeBinariesDirectly ) do

		local function deleteFiles( binarySize )
		
			local path = renditionPath( docValue, binaryName, binarySize )

			if path and path ~= '' then

				local fullPath = absolutePath( catalogId, path )

				local nameAndSize = string.format( "%s::%s", binaryName, binarySize or '' )

				logger:infof( "Now deleting %s for doc %s at %s", nameAndSize, docValue._id, fullPath )

				WFFileUtils.deleteFile( fullPath )

				if binaryName == 'presetProfile' then

					presetProfileDeletionStream:onNext( docValue._id )

				end

			end
		
		end
		
		deleteFiles()
		
		if type( maybeBinarySizes ) == 'table' then

			for _, binarySize in ipairs( maybeBinarySizes ) do

				deleteFiles( binarySize )
			
			end
		
		end

	end

end

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

local function deleteFilesFromDiskAndCacheForDocument(
		wfCatalog, catalogId, dataCache, presetProfileDeletionStream, docValue )

	deleteFilesFromDiskForDocument( presetProfileDeletionStream, catalogId, docValue )

	return removeCacheEntriesAndXmpsForDocument( wfCatalog, dataCache, docValue )
	
		:map( function()
			return { docValue._id, nil }
		end )
		
end

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

local function purgeDeletedBlobFolder( catalogId, docId )

	local relativeBlobPath = OzFilePaths.getRelativeBlobPath( docId )
	
	pcall( WFFileUtils.delete, absolutePath( catalogId, relativeBlobPath ) )

end

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

local function processDeletionFromOzWithNewId( wfCatalog, catalogId, docValue, deletedFromOzWithNewId )

	local localOnly = {}

	if WFPlatformInfo.isiOS then
	
		localOnly.files = {
			master = KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'master' ),
			proxy = KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'proxy' ),
			proxies = KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'proxies' )
		}

	else
		
		localOnly = {
			originalFilePath = KSTableUtils_derefOpt( docValue, '_localOnly', 'originalFilePath' ),
			latestOriginal = KSTableUtils_derefOpt( docValue, '_localOnly', 'latestOriginal' ),
			original = KSTableUtils_derefOpt( docValue, '_localOnly', 'original' ),
			proxy = KSTableUtils_derefOpt( docValue, '_localOnly', 'proxy' ),
			files = KSTableUtils_derefOpt( docValue, '_localOnly', 'files' ),
		}
		
		localOnly.files = {
			proxies = KSTableUtils_derefOpt( docValue, '_localOnly', 'files', 'proxies' )
		}
		
	end
	
	local deletedFromOzDocOverlay = {
		_id = deletedFromOzWithNewId,
		_localOnly = localOnly
	}
	
	-- keep xmp_develop file

	local copyOptions = {
		needToCopyOriginal = false,
		needToCopyProxy = false,
		needToCopyRenditions = false,
		needToCopyExternalDevelopSettings = true,
		needToCopyXmpSidecar = false
	}
	
	local modifiedDeletedFromOzDocOverlay, filesToCopy = WFCatCopies.calculateCopyPathsAndUpdateDocValue( wfCatalog, docValue, deletedFromOzDocOverlay, copyOptions )
				
	local maybeCopyExternalXmpFile = Observable.just( true )
	
	if #filesToCopy > 0 then
	
		-- note, this block is iOS only
		
		local xmpFileInfo = filesToCopy[ 1 ]
		
		local externalXmpPath = xmpFileInfo.srcFilePath
		local externalXmpCopyPath = xmpFileInfo.dstFilePath
		
		if externalXmpPath and externalXmpCopyPath then
			maybeCopyExternalXmpFile = WFCatCopies.copyExternalXmpFile( externalXmpPath, externalXmpCopyPath )
		end
		
	end
	
	return maybeCopyExternalXmpFile
	
		:map( function()
		
			if WFPlatformInfo.isiOS then
				purgeDeletedBlobFolder( catalogId, docValue._id )
			end
		
			return { docValue._id, modifiedDeletedFromOzDocOverlay }
			
		end )

end

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

-- FIXME [jandrew 2022-02-04]: The following implementation was taking from
-- LrmOzPurgeAgent. At some point, we should try to align this logic with
-- the LrD logic and try to leverage WFCatPurgeHelper. I would do this now,
-- but it's complicated by the CT logic that's mobile specific.

local function setupPurgeOnDelete_Mobile( wfCatalog, dataCache, presetProfileDeletionStream )

	-- Auxiliaries are not removed here either. They are automatically cleaned up
	-- by the volume agent: See getUntrackedBinariesRemover in WFCatVolumeAgent.

	local docStore = wfCatalog:docStore()
	
	local catalogId = wfCatalog:ozCatalogId()

	local purgeAgentChangeCounterLocalDocId = 'LrmOzPurgeAgent-changeCounter'
	
	return docStore:deletedDocsSinceNamedChangeCounter( purgeAgentChangeCounterLocalDocId )

		:flatMap( function( docValues, continueFrom )

			if #docValues > 0 then
				logger:infof( "Now processing %d deleted docs. Continue From: %d", #docValues, continueFrom )
			end
			
			return Observable.from( docValues )
			
				:flatMap( function( docValue )

					local result = Observable.just( nil )

					if docValue.type == 'asset' then

						local hasTrashOrRestoreAsset = KSTableUtils_derefOpt( docValue, '_newId' )
						local deletedFromOzWithNewId = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'new_id' )

						if hasTrashOrRestoreAsset then
						
							logger:infof( "Ignoring asset deletion caused by trash operation: %s, newId: %s",
									docValue._id, hasTrashOrRestoreAsset )
						
							result = Observable.just( { docValue._id, nil } )
								-- dont purge as it goes to trash
						
						elseif deletedFromOzWithNewId then
							
							-- The mobile implementation of setupPurgeOnDelete contains logic for
							-- copying local binaries across assets in the event of certain cloud-trash
							-- operations. On Desktop, this lives elsewhere.
							
							result = processDeletionFromOzWithNewId( wfCatalog, catalogId, docValue, deletedFromOzWithNewId )
													
						else
							
							-- if we are here, this is a final delete of the asset
							
							result = deleteFilesFromDiskAndCacheForDocument(
									wfCatalog, catalogId, dataCache,
									presetProfileDeletionStream, docValue )
							
						end
						
					elseif docValue.type == 'version' then
					
						-- If a version is deleted, this is the final state and all
						-- associated binaries should be deleted.
						
						result = deleteFilesFromDiskAndCacheForDocument(
									wfCatalog, catalogId, dataCache,
									presetProfileDeletionStream, docValue )
					
					end
					
					return result
			
				end )
			
				:toArray()

				:flatMap( function( docIdsAndMaybeOverlays )

					-- Note that this processing is only necessary for documents
					-- where deletedFromOzWithNewId was true.

					local docIds =  {}
					local overlays = {}
					
					-- docIdsAndMaybeOverlays is an array of { docId, maybeOverlay }
					
					for i, docIdAndMaybeOverlay in ipairs( docIdsAndMaybeOverlays ) do
						docIds[ i ] = docIdAndMaybeOverlay[ 1 ]
						local overlay = docIdAndMaybeOverlay[ 2 ]
						if overlay then
							overlays[ #overlays + 1 ] = overlay
						end
					end

					if next( overlays ) then
					
						return WFRxThrottledBatchedBulkApplyOverlay( 'LrmOzPurgeAgent_purgeOnDelete', docStore, overlays, 100, 10 )
							:map( function()
								return docIds
							end )
					
					end
					
					return Observable.just( docIds )

				end )			
				
				:flatMap( function( docIds )
			
					if WFCloseSupport.hasBeenOrWillBeClosed( docStore ) then
						return Observable.empty()
					end
			
					return docStore:updateDeletedDocsSinceNamedChangeCounter(
							purgeAgentChangeCounterLocalDocId, continueFrom )
							
							:map( function()
							
								return docIds
							
							end )
				
				end )

		end )
	
end

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

-- @mark APIs

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

--[[!

	\brief Creates an observable that handles the deletion of files for deleted
		documents.
	
	\param catalog (WFCatalog)
	\param optDeletionBacklog (optional NmbDeletionBacklog) LrD-Only
	\param optDataCache (optional KSDataCache ) Android-Only

	\return (Observable) A stream that fires with deleted document Ids.
		(Observable) Mobile-Only: A stream that fires when presetProfile docs are deleted.
	
--]]

function WFCatPurgeOnDelete.setupPurgeOnDelete(
		catalog, optDeletionBacklog, optDataCache )

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

	logger:info( "setupPurgeOnDelete was called" )

	local purgeOnDeleteStream

	local presetProfileDeletionStream = Rx.ReplaySubject( 1 )

	if WFPlatformInfo.isDesktop then
		purgeOnDeleteStream = setupPurgeOnDelete_Desktop( catalog, optDeletionBacklog )
	else
		purgeOnDeleteStream = setupPurgeOnDelete_Mobile( catalog, optDataCache, presetProfileDeletionStream )
	end
	
	return purgeOnDeleteStream, presetProfileDeletionStream

end

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

return WFCatPurgeOnDelete
