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

WFCatSimpleDocStoreUpdater.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2024 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'

	-- WF.core.os
local KSStringUtils = require 'KSStringUtils'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFDiagnosticLog = require 'WFDiagnosticLog'
local WFFileUtils = require 'WFFileUtils'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.core.utils
local OzFilePaths
local WFAnalytics = require 'WFAnalytics'
if WFPlatformInfo.isMobile() then
	OzFilePaths = require 'OzFilePaths'
end
local WFRxThrottledBatchedBulkApplyOverlay = require 'WFRxThrottledBatchedBulkApplyOverlay'

	-- WF.managed_catalog
local WFCatAssetUtils = require 'WFCatAssetUtils'

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

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

local kDelimiter = ","
local kSubDelimiter = "::"

local kSimpleUpdateFunctions = {}

local kMissingLocalOnlyXmpDevelop = "missingLocalOnlyXmpDevelop"
local kNumOverlaysApplied = 'numOverlaysApplied'

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

local function generateIndexSchema( _, indexStoreSchema )

	check( '_', 'WFRxIndexStoreSchema' )

	indexStoreSchema:addTable {

		sql = [[CREATE TABLE docsToUpdate( updates )]],

		relevantTypeSubtypeTuples = {
			{ 'asset', 'image' },
			{ 'asset', 'deleted_image' },
			{ 'version', '*' },
		},

		map = function( docValue, emit )

			local updates = {}

			local isValidAssetDocument = WFCatIndexUtils.isValidAssetDocument( docValue )
			local isImageDocument = WFCatIndexUtils.isImageDocument( docValue )
			local isValidVersionDocument = WFCatIndexUtils.isValidVersionDocument( docValue )

			-- WF-2507 For xmp_develop file exists in file system, but missing
			-- xmp_develop localOnly annotation

			if ( isValidAssetDocument and isImageDocument ) or isValidVersionDocument then

				local localOnlyXmpDevelopSha256 = WFCatAssetUtils.getLocalExternalDevelopSettingsContentId( docValue )
				local spmXmpDevelop = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop' )
				local ozHasValidXmpDevelop = spmXmpDevelop and not spmXmpDevelop.invalid and ( spmXmpDevelop.href or spmXmpDevelop.exists )
				local ozXmpDevelopLinkSha256 = spmXmpDevelop and spmXmpDevelop.sha256

				local xmpDevelopSha256 = derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )

				local missingXmpDevelopFileSha256 = derefOpt( docValue, '_localOnly', 'universal', 'simpleDocStoreUpdater', 'missingXmpDevelopFileSha256' )
					-- this annotation is added if this fix is attempted and fails to ensure we do not try to fix the same asset or version
					-- over and over again every time it updates when we know that the file is missing. Include the sha256 in the annotation
					-- so that if the xmp develop changes and has a new sha256 a similar fix attempt can be attempted on the same document again.

				local ozErrorSubtype = derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
				local spmTempXmpDevelop = xmpDevelopSha256 and derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/temp/xmp/develop/' .. xmpDevelopSha256 )

				-- if xmpDevelopSha256 is set and it matches the value in _localOnly.files.xmp_develop, then local
				-- file is available no change needed.

				-- if xmpDevelopSha256 matches missingXmpDevelopFileSha256 then we have already looked for the
				-- local file and not found it, do not look again.

				-- if Oz has an invalid xmp develop then some client needs to upload xmp develop, maybe this one.
				-- For ADS workflow we do not get invalid flag so need to make additional checks: if ozError is
				-- BinaryMissingError (or it can also be DirectUploadComplete this is something that WFCatBinaryUploader
				-- writes when aux is successfully uploaded) AND the sha256 in the link from oz does not match the
				-- version we have locally AND there is not a temp xmp develop link uploaded with the sha256,
				-- THEN we should look for a local file and fix up the _localOnly.files.

				if xmpDevelopSha256 and xmpDevelopSha256 ~= localOnlyXmpDevelopSha256 and
					xmpDevelopSha256 ~= missingXmpDevelopFileSha256 and ( not ozHasValidXmpDevelop or
					( ( ozErrorSubtype == 'BinaryMissingError' or ozErrorSubtype == 'DirectUploadComplete' )
						and ozXmpDevelopLinkSha256 ~= xmpDevelopSha256 and not spmTempXmpDevelop ) ) then

					local update = string.format("%s%s%s", kMissingLocalOnlyXmpDevelop, kSubDelimiter, xmpDevelopSha256)
					table.insert( updates, update )

				end

			end

			if next( updates ) then
				local updatesAsString = table.concat( updates, kDelimiter )
				emit( updatesAsString )
			end

		end,

		hasOneRowPerDocument = true,
	}

end

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

local function maybeOverlayToUpdateMissingLocalOnlyXmpDevelop( self, docId, xmpDevelopSha256 )

	-- check if file exists in file system

	local xmpDevelopFilePath, absolutePath

	if WFPlatformInfo.isMobile() then
		xmpDevelopFilePath, absolutePath = OzFilePaths.getXmpDevelopPath( self._catalogId, { _id = docId }, xmpDevelopSha256 )
	else
		xmpDevelopFilePath = self._fileSpec:pathForSettingsXmp( xmpDevelopSha256 )
		absolutePath = xmpDevelopFilePath
	end

	if WFFileUtils.exists( absolutePath ) then

		local fileSize = 0
		local ok, fileAttributes = pcall(WFFileUtils.fileAttributes, absolutePath)
		if ok and fileAttributes then
			fileSize = fileAttributes.fileSize
		end

		local sha256Key = 'sha256'
		if WFPlatformInfo.isiOS then
			sha256Key = 'contentId'
		end

		WFAnalytics.trackEvent {
			['event.type'] = 'info',
			['event.subtype'] = 'local-only-files-repaired',
			['event.appWorkflow'] = 'SYNC',
			['event.subcategory'] = 'Xmp_develop',
			['content.id'] = docId,
			['sha256'] = xmpDevelopSha256,
		}

		return {
			_id = docId,
			_localOnly = {
				files = {
					xmp_develop = {
						path = xmpDevelopFilePath,
						fileSize = fileSize,
						[ sha256Key ] = xmpDevelopSha256,
					}
				}
			}
		}

	else

		-- mark the file as missing so we do not keep looking for it when this document updates

		return {
			_id = docId,
			_localOnly = {
				universal = {
					simpleDocStoreUpdater = {
						missingXmpDevelopFileSha256 = xmpDevelopSha256,
					}
				}
			}
		}

	end

end

kSimpleUpdateFunctions[ kMissingLocalOnlyXmpDevelop ] = maybeOverlayToUpdateMissingLocalOnlyXmpDevelop

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

local function filterUpdates( self, rows )

	local overlays = {}

	for _, row in ipairs( rows ) do

		local docId = row[ 1 ]
		local updatesAsString = row[ 2 ] or ""
		local updates = KSStringUtils.split( updatesAsString, kDelimiter )

		local overlay = {}
		for _, update in ipairs( updates ) do

			local updateKey, updateData = unpack( KSStringUtils.split( update, kSubDelimiter ) )

			local updateFn = kSimpleUpdateFunctions[ updateKey ]
			local maybeUpdateOverlay

			if updateFn then
				maybeUpdateOverlay = updateFn( self, docId, updateData )

				if maybeUpdateOverlay then

					local updateCount = self._updatesAppliedForDiagnostics[ updateKey ] or 0
					self._updatesAppliedForDiagnostics[ updateKey ] = updateCount + 1

					updateCount = self._updatesAppliedForDiagnostics[ kNumOverlaysApplied ] or 0
					self._updatesAppliedForDiagnostics[ kNumOverlaysApplied ] = updateCount + 1

					overlay = KSTableUtils.merge( {}, overlay, maybeUpdateOverlay )

				end
			end

		end

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

	end

	return overlays

end

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

local function performUpdates( self )

	return self._indexStore:queryOnce {
			sql = [[SELECT docId, updates FROM docsToUpdate]],
			quiesceUntilUpToDate = true,
		}

		:flatMap( function( rows )

			local overlays = filterUpdates( self, rows )

			return WFRxThrottledBatchedBulkApplyOverlay( "updatingDocs", self._docStore, overlays, 100, 10 )

		end )

		:takeLast( 0 )

end

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

local WFCatSimpleDocStoreUpdaterPrototype = {

	_docStore = false,
		-- (WFRxDocStore)

	_indexStore = false,
		-- (WFRxIndexStore)

	_fileSpec = false,
		-- (WFCatFileSpec)

	_catalogId = false,
		-- (string) catalogId

	_updatesAppliedForDiagnostics = false,
		-- (array) counts of updates applied indexed by update function key (used by diagnostic log and unit tests)

	_performUpdatesSubscription = false,
		-- A subscription on the observer returned by calling performUpdates()

	class = {
		generateIndexSchema = generateIndexSchema,
	},

	WFCloseSupport.addCloseSupportMethods,

}

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

--[[!
	\brief Creates a WFCatSimpleDocStoreUpdater which can be used to make simple updates to
		documents in the doc store. The simple updater can be used to make minor tweaks to
		formats of documents in the doc store that can be performed while the app is otherwise
		running.

		Additionally, if the document changes are likely to cause significant work to be done
		by the app, it may not be the most suitable approach.

		When adding update functions care should be taken that the updater will not cause an
		undue impact to the performance of the client during the update. This may require that
		future updaters modify the logic for performing updates to be less aggressive than the
		initial implementation for keywords.

		Also note that unlike NmbSimpleDocStoreUpdater, this class does not have the concept
		hasBlockingUpdate. Would need to check with mobile clients and confirmt that they would
		be able to support the UI needed for that before adding it.

	\param docStore (WFCatalog) catalog that owns the doc store to be updated

]]--

function WFCatSimpleDocStoreUpdaterPrototype:__init( docStore, indexStore, wfCatalog )

	check( 'WFCatSimpleDocStoreUpdater', 'WFRxDocStore', 'WFRxIndexStore', 'table' )

	self._docStore = docStore

	self._indexStore = indexStore

	self._fileSpec = wfCatalog:fileSpec()

	self._catalogId = wfCatalog:ozCatalogId()

	self._updatesAppliedForDiagnostics = {}

	WFDiagnosticLog.addLogProvider( self )

end

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

--[[!
	\brief Tears down the WFCatSimpleDocStoreUpdater instance

	\return (void)
--]]

function WFCatSimpleDocStoreUpdaterPrototype:close()

	check( 'WFCatSimpleDocStoreUpdater' )

	local performUpdateSubscription = self._performUpdatesSubscription
	self._performUpdatesSubscription = nil

	if performUpdateSubscription then
		performUpdateSubscription:dispose()
	end

end

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

--[[!
	\brief Sets the synchronizer instance.

	\return (void)
--]]

function WFCatSimpleDocStoreUpdaterPrototype:start()

	check( 'WFCatSimpleDocStoreUpdater' )

	self._performUpdatesSubscription = self._performUpdatesSubscription or performUpdates( self ):subscribe()

end

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

--[[!
	\brief send diagnostic data for the diagnostic log.
--]]

function WFCatSimpleDocStoreUpdaterPrototype:sendDiagnosticLogData()

	WFDiagnosticLog.addSectionToLog
		{

			id = string.format( 'WFCatSimpleDocStoreUpdater' ),

			name = string.format( "WFCatSimpleDocStoreUpdater" ),
			weight = 1620,
			parentSection = WFDiagnosticLog.Sections.DOC_STORE,
			body = self._updatesAppliedForDiagnostics

		}

end

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

--[[!
	\brief Debug function used by unit test to validate proper execution.

	\return (array) counts of updates applied indexed by update function key.
--]]

function WFCatSimpleDocStoreUpdaterPrototype:DEBUG_getUpdatesApplied()

	return self._updatesAppliedForDiagnostics

end

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

return  Rx.Class 'WFCatSimpleDocStoreUpdater'( WFCatSimpleDocStoreUpdaterPrototype )
