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

WFCatNotification.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2020 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains 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 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 Systems Incorporated.

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

local Rx = require "Rx"

	-- WF.core.utils
local KSDate = require 'KSDate'
local KSTableUtils = require "KSTableUtils"
local KSJsonDecoder = require 'KSJsonDecoder'
local WFArguments = require 'WFArguments'
local WFConfiguration = require 'WFConfiguration'

local WFLogger = require 'WFLogger'

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

local logger = WFLogger "WFCatNotification"
--	:setLevel( 'debug' )


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

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

local WFCatNotification = {}

WFCatNotification.ANS_NOTIFICATION_EXPIRY_IN_DAYS = 60 or WFConfiguration.Notifications.expiryInDays


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

local function endsWith( str, endStr )

   return string.sub( str, -#endStr ) == endStr
   
end

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

-- reverses in place
local function reverse( t )

	local n = #t
	local i = 1

	while i < n do
		t[i],t[n] = t[n],t[i]
		i = i + 1
		n = n - 1
	end

end

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

local function synthesizeFieldsAndReverse( notification )

	local function extractAggregated( fieldName, ... )

		local aggregatedNotifications = notification.aggregated_notifications

		if not aggregatedNotifications then
			return nil
		end

		local result = {}

		--  [immisch 2020-12-02]: ANS delivers the notifications with the most recent at the bottom,
		-- but we pass them the other way round, so reverse here for consistency
		for i = #aggregatedNotifications, 1, -1 do

			local payload = aggregatedNotifications[ i ]

			local values = {}

			for _, identifier in ipairs{ ... } do
				values[ identifier ] = derefOpt( payload, fieldName, identifier )
			end

			if next( values ) then
				result[#result+1] = values
			end

		end

		return result

	end

	for _, targetName in ipairs{ 'primaryImageUI', 'secondaryImageUI' } do

		-- for example, target can be asset-stack, person-stack or album-stack
		local target = notification[ targetName ]

		if target and endsWith( target, '-stack' ) then

			local fieldName = string.sub( target, 1, -1 * (#'-stack' + 1) )

			notification[ target ] = extractAggregated( fieldName, 'thumbnail_url', 'id' )

		end

	end

	for _, targetName in ipairs{ 'primaryImage', 'secondaryImage' } do
	
		local targetNameUI = targetName .. 'UI'
	
		if notification[ targetName ] and not notification[ targetNameUI ] then
			
			notification[ targetNameUI ] = notification[ targetName ]
			
		end
	
	end

	if notification.aggregated_notifications then
		-- see [immisch 2020-12-02] above
		reverse( notification.aggregated_notifications )
	end

	return notification

end

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

function WFCatNotification.defaultSubtypeAllowList()

	return {
		comment = true,
		favorite = true,
		["alert-v1"] = true,
		["asset-added"] = true
	}

end
--------------------------------------------------------------------------------

function WFCatNotification.DEBUG_synthesizeFieldsAndReverse( notification )
	return synthesizeFieldsAndReverse( notification )
end

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

--[[!
	\brief Reports as an Observable of notification unread count
	
	\param notificationDb (WFRxDatabase) database containing the notifications
	
	\return Observable< boolean > An Observable whose value indicates if there's new notifications
--]]
function WFCatNotification.notificationUnreadCount( notificationDb, optAllowList )

	check( 'WFRxDatabase', '?table' )

	local allowed = KSTableUtils.extractKeys( optAllowList or WFCatNotification.defaultSubtypeAllowList() )

	return notificationDb:query({
			sql = "SELECT COUNT(*) FROM notifications WHERE NOT read AND subtype IN CARRAY( ?, ?, 'char*' )",
			allowed,
			#allowed
		})
		:pluck( 1, 1 )
	
end

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

--[[!
	\brief Reports as an Observable of notification list
	
	\param notificationDb (WFRxDatabase) database containing the notifications
	
	\return Observable< table > An Observable which is a list of notifications, unread and most recent first
		- \c notificationId: (string) notificationId
		- \c subtype: (string) subtype of the notification. \c comment | \c favorite 
				| \c asset-added | \c asset-edited | \c alert-v1
		- \c time: (string) updatedTime of notification
		- \c read: (boolean) if the notification is read or not
		
		- \c catalog_id: (string) the catalog id of the target of the notification
		- \c space_id: (string) the space id of the target of the notification (if it is a contributed asset)
		
		- \c targetDestination: (string) Either \c asset | \c album
		- \c primaryImage: (string) \c asset | \c album | \c person | \c asset-stack | \c album-stack | \c person-stack
		- \c secondaryImage: (string) \c asset | \c album | \c person | \c asset-stack | \c album-stack | \c person-stack
		
		- \c asset-stack: (list of tables) list of asset Ids and thumbnail URLs. Each item in the list contains the keys \c id and \c thumbnail_url (also `localId` on Desktop). Only present if primaryImageUI or secondaryImageUI is \c asset-stack
		- \c album-stack: (list of tables) list of album Ids and thumbnail URLs. Each item in the list contains the keys \c id and \c thumbnail_url (also `localId` on Desktop). Only present if primaryImage or secondaryImageUI is \c album-stack
		- \c person-stack: (list of tables) list of tables containing thumbnail URLs. Each item in the list contains the keys \c id and \c thumbnail_url. Only present if primaryImage or secondaryImageUI is \c person-stack
		
		- \c text: (string) notification localized string
		- \c subtext: (string) localized string
		
		- \c asset: (table) 
			- \c id: (string) asset docId
			- \c thumbnail_url: (string) url of thumbnail2x 
		- \c album: (table) 
			- \c id: (string) album docId
			- \c name: (string) album name
			- \c coverAssetId: (string) album cover asset docId
			- \c thumbnail_url: (string) url of album cover asset thumbnail2x 
		- \c person: (table) 
			- \c id: (string) author docId
			- \c full_name: (string) author full name
			- \c thumbnail_url: (string) url of person avatar thumbnail2x 
		
		- \c comment: (table, required if subtype is comment) 
			- \c id: (string) comment docId
			- \c body: (string) comment body
		- \c favorite: (table, required if subtype is favorite) 
			- \c id: (string) favorite docId
			
		- \c unique_count_field: (string, only for aggregated notifications) \c asset | \c album | \c person 

		- \c unique_count: (number, only for aggregated notifications)
			Counts \c unique_count_field in aggregated_notifications
		
		- \c aggregated_notifications: (array of tables, only for aggregated notifications)
			Each item in the table contains a single notification with a structure like above. In particular 
			with the \c subtype, \c asset, \c album, \c person, \c comment and \c favorite fields like above.
			The most recent notification is on top.
				
--]]
function WFCatNotification.notificationList( notificationDb, optAllowList )

	check( 'WFRxDatabase', '?table' )
	
	local allowed = KSTableUtils.extractKeys( optAllowList or WFCatNotification.defaultSubtypeAllowList() )
	
	return notificationDb:query{
			sql = [[SELECT notificationId, subtype, time, read, visited, payloadJsonString
				FROM notifications WHERE subtype IN CARRAY( ?, ?, 'char*' )
				ORDER BY read ASC, time DESC]],
				allowed,
				#allowed
		}
		:map( function( rows )
		
			local notifications = {}
			
			for _, row in ipairs( rows ) do
				
				local notificationId = row[ 1 ]
				local subtype = row[ 2 ]
				local time = row[ 3 ]
				local read = row[ 4 ]
				local visited = row[ 5 ]
				local payloadJsonString = row[ 6 ]

				local success, data = pcall(KSJsonDecoder.decodeWebSafeJson_stripNull, payloadJsonString )

				if not success then
					logger:errorf( 'could not decode json: "%s"',  payloadJsonString )
				else

					local now = KSDate.timeInIso8601Format()

					local ageInDays = KSDate.differenceInDays( time, now )

					if ageInDays <= WFCatNotification.ANS_NOTIFICATION_EXPIRY_IN_DAYS then

						data.notificationId = notificationId
						data.subtype = subtype
						data.time = time
						data.read = (read == 1)
						data.visited = (visited == 1)

						notifications[ #notifications + 1 ] = synthesizeFieldsAndReverse( data )
						
					else

						logger:warnf( 'notification %s is locally expired',  notificationId )

					end
				end
			end

			return notifications
		
		end )
	
end

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

--[[!
	\brief Mark notification as read
	
	\param notificationDb (WFRxDatabase) database containing the notifications
	\param notificationsMarkRead (table) An array with the \c notificationIds to mark as read 
	\param notificationsMarkUnread (optional table) An array with the \c notificationIds to mark as unread
	
	\return Observable(number) returns the number of notifications that were updated
--]]
function WFCatNotification.setReadNotification( notificationDb, notificationsMarkRead, optNotificationsMarkUnread )
	
	check( 'WFRxDatabase', 'table', '?table')

	notificationsMarkRead = notificationsMarkRead or {}

	-- bail out early if there is nothing to do
	if not next( notificationsMarkRead ) and 
			( not optNotificationsMarkUnread or not next( optNotificationsMarkUnread ) ) then
		
		return Rx.Observable.just( 0 )
	end
	
	
	local updateRead
	
	if next( notificationsMarkRead ) then

		local setRead = {
			values = { read = true, needsUpdateOnANS = true },
			condition = "WHERE NOT read AND notificationId IN CARRAY( ?, ?, 'char*' )",
			notificationsMarkRead,
			#notificationsMarkRead
		}
		
		updateRead = notificationDb:update( 'notifications', setRead )

	else
		
		updateRead = Rx.Observable.just( 0 )
		
	end
	
	if not optNotificationsMarkUnread or not next( optNotificationsMarkUnread ) then
		return updateRead
	end
		
	return updateRead
		:flatMap( function( nMarkedAsRead )
			local setUnread = {
				values = { read = false, needsUpdateOnANS = true },
				condition = "WHERE read AND notificationId IN CARRAY( ?, ?, 'char*' )",
				optNotificationsMarkUnread,
				#optNotificationsMarkUnread
			}
			
			return notificationDb:update( 'notifications', setUnread )
				:map( function( nMarkedAsUnread )
					return nMarkedAsRead + nMarkedAsUnread
				end )
		end )

end


--[[!
	\brief Mark notification as visited
	
	\param notificationDb (WFRxDatabase) database containing the notifications
	\param notificationsMarkVisited (table) An array with the \c notificationIds to mark as visited 
	\param notificationsMarkUnvisited (optional table) An array with the \c notificationIds to mark as unvisited
	
	\return Observable(number) returns the number of notifications that were updated
--]]
function WFCatNotification.setVisitedNotification( notificationDb, notificationsMarkVisited, optNotificationsMarkUnvisited )
	
	check( 'WFRxDatabase', 'table', '?table')

	notificationsMarkVisited = notificationsMarkVisited or {}

	-- bail out early if there is nothing to do
	if not next( notificationsMarkVisited ) and 
			( not optNotificationsMarkUnvisited or not next( optNotificationsMarkUnvisited ) ) then
		
		return Rx.Observable.just( 0 )
	end
	
	
	local updateVisited
	
	if next( notificationsMarkVisited ) then

		local setVisited = {
			values = { visited = true, needsUpdateOnANS = true },
			condition = "WHERE NOT visited AND notificationId IN CARRAY( ?, ?, 'char*' )",
			notificationsMarkVisited,
			#notificationsMarkVisited
		}
		
		updateVisited = notificationDb:update( 'notifications', setVisited )

	else
		
		updateVisited = Rx.Observable.just( 0 )
		
	end
	
	if not optNotificationsMarkUnvisited or not next( optNotificationsMarkUnvisited ) then
		return updateVisited
	end
		
	return updateVisited
		:flatMap( function( nMarkedAsVisited)
			local setUnvisited = {
				values = { visited = false, needsUpdateOnANS = true },
				condition = "WHERE visited AND notificationId IN CARRAY( ?, ?, 'char*' )",
				optNotificationsMarkUnvisited,
				#optNotificationsMarkUnvisited
			}
			
			return notificationDb:update( 'notifications', setUnvisited )
				:map( function( nMarkedAsUnvisited )
					return nMarkedAsVisited + nMarkedAsUnvisited
				end )
		end )

end

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

--[[!
	\brief Remove notifications older than a specified number of days
	
	\param notificationDb (WFRxDatabase) database containing the notifications
	\param days (number) integer number of days
	
	\return Observable that will delete any older notifications
--]]
function WFCatNotification.deleteNotificationsOlderThan( notificationDb, days )
	
	check( 'WFRxDatabase', 'number' )
	
	local daysInSeconds = days * 24 * 60 * 60
	
	local expiryDate = KSDate.shiftIso8601DateTime( KSDate.timeInIso8601Format(), - daysInSeconds )
	
	return notificationDb:deleteFromWhere( 'notifications', 'WHERE time < ?', { expiryDate } )
	
end

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

return WFCatNotification
