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

NmbFreemiumUtils.lua

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

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

	-- WF.core.os
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'
local WFAsyncFileUtils = require 'WFAsyncFileUtils'

local WFSQLiteDatabaseImp = require "WFSQLiteDatabaseImp"

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

local function pack( ... )

	return { ..., n = select( "#", ... ) }

end

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

local function packPcallImp( ok, ... )

	if not ok then

		local errMsg = ...

		return false, errMsg

	end

	return true, pack( ... )

end

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

local function packPcall( f, ... )

	return packPcallImp( pcall( f, ... ) )

end

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

local function maybeGetFileAttributesImp( path )

	local fileAttr = WFFileUtils.fileAttributes( path )

	if type( fileAttr ) ~= "table" or
			type( fileAttr.fileSize ) ~= "number" or
			type( fileAttr.fileCreationDate ) ~= "number" or
			type( fileAttr.fileModificationDate ) ~= "number" then

		return

	end

	return fileAttr.fileSize, fileAttr.fileCreationDate,
			fileAttr.fileModificationDate 

end

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

local function getFreemiumBackupPaths( freemiumDocStorePath )

	local freemiumDocStoreBackupFolder =
			KSPathUtils.child(
				KSPathUtils.parent( freemiumDocStorePath ),
				"ManagedCatalogBackup" )

	local freemiumDocStoreBackupPath =
			KSPathUtils.child(
				freemiumDocStoreBackupFolder,
				KSPathUtils.leafName( freemiumDocStorePath ) )

	return freemiumDocStoreBackupFolder, freemiumDocStoreBackupPath

end

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

local k1Mb = 1024 * 1024

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

local k5Mb = 5 * k1Mb

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

local function dataSizeDeltaShouldTriggerBackup( sizeDeltaInBytes )

	return math.abs( sizeDeltaInBytes ) >= k5Mb

end

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

local k1WeekInSeconds = 60 * 60 * 24 * 7

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

local function timeDeltaShouldTriggerBackup( timeDeltaInSeconds )

	return timeDeltaInSeconds >= k1WeekInSeconds

end

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

local function runDbQuickCheck( db )

	local stmt = db:prepare( "PRAGMA quick_check" )

	local rslt = stmt:gatherArray()

	stmt:finalize()

	return rslt

end

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

local function runWithDbAtPath( f, path, ... )

	local db = WFSQLiteDatabaseImp.makeDatabase( path )

	local ok, errOrPackedRslt = packPcall( f, db, ... )

	db:close()

	if not ok then

		error( errOrPackedRslt )

	end

	return unpack( errOrPackedRslt, 1, errOrPackedRslt.n )

end

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

local function isDocstoreValidImp( freemiumDocStorePath )

	return runWithDbAtPath( runDbQuickCheck, freemiumDocStorePath )
					
end

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

local function safeCheckDocStoreValidity( docstoreCheckerFunc, 
											freemiumDocStorePath )

	if type( docstoreCheckerFunc ) ~= "function" then

		return false, "invalid doc store checker func"

	end

	local ok, rslt =
		pcall( docstoreCheckerFunc, freemiumDocStorePath )

	if not ok then

		return false, string.format( "docstore check threw error: %s", rslt )

	end

	if type( rslt ) ~= "table" then

		return false, "invalid doc store check result"

	end
	
	if rslt[ 1 ] ~= "ok" then

		return false, string.format( "corrupt error seen: %s", rslt[ 1 ] )

	end

	return true

end

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

local function loadHelpers( overrideFunctionsForTest )

	return overrideFunctionsForTest.maybeGetFileAttributesImp or 
				maybeGetFileAttributesImp,
			overrideFunctionsForTest.isDocstoreValidImp or 
				isDocstoreValidImp,
			overrideFunctionsForTest[ "WFFileUtils.mkdir" ] or WFFileUtils.mkdir,
			overrideFunctionsForTest[ "WFFileUtils.delete" ] or WFFileUtils.delete,
			overrideFunctionsForTest[ "WFAsyncFileUtils.copyToUniquelyNamedFile" ] or 
				WFAsyncFileUtils.copyToUniquelyNamedFile

end

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

local function maybeBackupDocstoreResultStreamImp2( 
				freemiumDocStorePath, overrideFunctionsForTest )

	local maybeGetFileAttributes, 
			isDocstoreValid, 
			WFFileUtils_mkdir,
			WFFileUtils_delete, 
			WFAsyncFileUtils_copyToUniquelyNamedFile = 
				loadHelpers( overrideFunctionsForTest )

	-- 1) Does orig exist?
	if not WFFileUtils.exists( freemiumDocStorePath ) then

		-- not an error: it's probably the first time they've launched in
		-- freemium mode.
		return Rx.Observable.just( false, "new docstore" )

	end

	-- 2) Can we get attributes of orig?
	--
	local freemiumDocStoreSize, freemiumDocStoreCreationDate, 
			freemiumDocStoreModificationDate  =
				maybeGetFileAttributes( freemiumDocStorePath )

	if not freemiumDocStoreSize then

		-- if we can't get the file attributes of the current docstore, we
		-- won't be able to tell to run the "should backup?" heuristics later
		return Rx.Observable.throw( "Couldn't get orig file attributes" )

	end

	-- 3) Is there an existing backup?
	local freemiumDocStoreBackupFolder, freemiumDocStoreBackupPath =
			getFreemiumBackupPaths( freemiumDocStorePath )

	local haveBackup = WFFileUtils.exists( freemiumDocStoreBackupPath )

	-- 4) Can we get properties of the backup?
	local backupDocStoreSize, backupDocStoreCreationDate

	if haveBackup then

		backupDocStoreSize, backupDocStoreCreationDate =
			maybeGetFileAttributes( freemiumDocStoreBackupPath )

	end
	
	-- 5) Is it time to backup? 
	local shouldBackup =

			-- no backup: use the size and mod dates to determine initial
			-- backup
			( not haveBackup and
				( dataSizeDeltaShouldTriggerBackup( 
						freemiumDocStoreSize ) or
					timeDeltaShouldTriggerBackup(
						freemiumDocStoreModificationDate -
							freemiumDocStoreCreationDate ) ) ) or

			-- we have a backup but can't get file stats: this is very
			-- unexpected and we should go ahead with backup in that case.
			( haveBackup and not backupDocStoreSize ) or

			-- otherwise compare backup size and creation date to decide
			-- if we should make a new backup
			( haveBackup and
				( dataSizeDeltaShouldTriggerBackup( 
						freemiumDocStoreSize - backupDocStoreSize ) or
					timeDeltaShouldTriggerBackup(
						freemiumDocStoreModificationDate -
							backupDocStoreCreationDate ) ) )

	if not shouldBackup then

		return Rx.Observable.just( false, "backup not needed" )

	end
	
	-- 6) Is the orig corrupt? If not corrupt, this has side effect of removing the the WAL
	local isValid, invalidReason = 
			safeCheckDocStoreValidity( isDocstoreValid, freemiumDocStorePath ) 

	if not isValid then

		return Rx.Observable.throw(
				string.format( "Corruption seen in %s during backup: %s",
								freemiumDocStorePath, invalidReason ) )

	end
	
	-- 7) Can we create the backup folder?
	
	if not haveBackup then

		local ok, failureReason = 
			pcall( WFFileUtils_mkdir, freemiumDocStoreBackupFolder )

		if not ok then

			return Rx.Observable.throw(
					string.format( "Can't create backup folder %s: %s",
									freemiumDocStoreBackupFolder, 
									failureReason ) )

		end

	end

	-- 8) Can we delete the backup?
	
	if haveBackup then

		local ok, deleteFailedReason = 
			pcall( WFFileUtils_delete, freemiumDocStoreBackupPath )

		if not ok then

			return Rx.Observable.throw( 
					string.format( "Failed to delete existing backup at path %s: %s",
									freemiumDocStoreBackupPath,
									deleteFailedReason ) )

		end

	end
	
	-- 9) Did we copy the backup?

	return WFAsyncFileUtils_copyToUniquelyNamedFile( 
				freemiumDocStorePath, freemiumDocStoreBackupPath )
			:take( 1 )
			:map(
				function( actualBackupPath )

					return true, actualBackupPath

				end )

end


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

local function maybeBackupDocstoreResultStreamImp( freemiumDocStorePath, 
													overrideFunctionsForTest )

	assert( type( overrideFunctionsForTest ) == "table", 
			"must supply overrideFunctionsForTest (use empty table for no overrides" )

	-- wrap this in an anonymous observable. This guarantees that if the 
	-- maybeBackupDocstoreResultStreamImp( ... ) call throws, it is relayed
	-- downstream as an exception instead of failing to return a stream.
	return Rx.Observable.create(
			function( observer )

				return maybeBackupDocstoreResultStreamImp2( 
							freemiumDocStorePath, overrideFunctionsForTest )
						:subscribe( observer )

			end )

end

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

local function maybeBackupDocstoreResultStream( freemiumDocStorePath )

	return maybeBackupDocstoreResultStreamImp( freemiumDocStorePath, {} )

end

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

local function maybeBackupDocstoreResultStreamUsingTestOverrideFunctions( 
				freemiumDocStorePath, overrideFunctionsForTest )

	return maybeBackupDocstoreResultStreamImp( 
			freemiumDocStorePath, overrideFunctionsForTest )

end

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

return {

	maybeBackupDocstoreResultStream = maybeBackupDocstoreResultStream,

	privateUnitTestAPI = {

		maybeBackupDocstoreResultStreamUsingTestOverrideFunctions =
			maybeBackupDocstoreResultStreamUsingTestOverrideFunctions ,

		getFreemiumBackupPaths = getFreemiumBackupPaths,

	}

}
