//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//

import Foundation
import MatrixRustSDK
import UserNotifications

struct NotificationContentBuilder {
    let messageEventStringBuilder: RoomMessageEventStringBuilder
    let settings: CommonSettingsProtocol
    
    /// Process the given notification item proxy
    /// - Parameters:
    ///   - notificationItem: The notification item
    ///   - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations.
    /// - Returns: A notification content object if the notification should be displayed. Otherwise nil.
    func content(for notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        switch notificationItem.event {
        case .none:
            return processEmpty(notificationItem: notificationItem)
        case .invite:
            return try await processInvited(notificationItem: notificationItem, mediaProvider: mediaProvider)
        case .timeline(let event):
            guard let eventType = try? event.eventType() else {
                return processEmpty(notificationItem: notificationItem)
            }

            switch eventType {
            case let .messageLike(content):
                switch content {
                case .roomMessage(let messageType, _):
                    return try await processRoomMessage(notificationItem: notificationItem, messageType: messageType, mediaProvider: mediaProvider)
                case .poll(let question):
                    return try await processPollStartEvent(notificationItem: notificationItem, pollQuestion: question, mediaProvider: mediaProvider)
                case .callInvite:
                    return try await processCallInviteEvent(notificationItem: notificationItem, mediaProvider: mediaProvider)
                case .callNotify:
                    return try await processCallNotifyEvent(notificationItem: notificationItem, mediaProvider: mediaProvider)
                default:
                    return processEmpty(notificationItem: notificationItem)
                }
            case .state:
                return processEmpty(notificationItem: notificationItem)
            }
        }
    }

    // MARK: - Private
    
    func baseMutableContent(for notificationItem: NotificationItemProxyProtocol) -> UNMutableNotificationContent {
        let notification = UNMutableNotificationContent()
        
        notification.receiverID = notificationItem.receiverID
        notification.roomID = notificationItem.roomID
        notification.eventID = switch notificationItem.event {
        case .timeline(let event): event.eventId()
        case .invite, .none: nil
        }
        // So that the UI groups notification that are received for the same room but also for the same user
        // Removing the @ fixes an iOS bug where the notification crashes if the mute button is tapped
        notification.threadIdentifier = "\(notificationItem.receiverID)\(notificationItem.roomID)".replacingOccurrences(of: "@", with: "")
        
        MXLog.info("isNoisy: \(notificationItem.isNoisy)")
        notification.sound = notificationItem.isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
        
        return notification
    }
    
    private func processEmpty(notificationItem: NotificationItemProxyProtocol) -> UNMutableNotificationContent {
        let notification = baseMutableContent(for: notificationItem)
        notification.title = InfoPlistReader(bundle: .app).bundleDisplayName
        notification.body = L10n.notification
        notification.categoryIdentifier = NotificationConstants.Category.message
        return notification
    }

    private func processInvited(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        var notification = baseMutableContent(for: notificationItem)

        notification.categoryIdentifier = NotificationConstants.Category.invite

        let body: String
        if !notificationItem.isDM {
            body = L10n.notificationRoomInviteBody
        } else {
            body = L10n.notificationInviteBody
        }

        notification = try await notification.addSenderIcon(using: mediaProvider,
                                                            senderID: notificationItem.senderID,
                                                            senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName,
                                                            icon: icon(for: notificationItem))
        notification.body = body
        
        return notification
    }

    private func processRoomMessage(notificationItem: NotificationItemProxyProtocol, messageType: MessageType, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
        
        let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
        notification.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName).characters)
        
        guard !settings.hideTimelineMedia else { return notification }
        
        switch messageType {
        case .image(content: let content):
            notification = await notification.addMediaAttachment(using: mediaProvider,
                                                                 mediaSource: .init(source: content.source,
                                                                                    mimeType: content.info?.mimetype))
        case .audio(content: let content):
            notification = await notification.addMediaAttachment(using: mediaProvider,
                                                                 mediaSource: .init(source: content.source,
                                                                                    mimeType: content.info?.mimetype))
        case .video(content: let content):
            notification = await notification.addMediaAttachment(using: mediaProvider,
                                                                 mediaSource: .init(source: content.source,
                                                                                    mimeType: content.info?.mimetype))
        default:
            break
        }
        
        return notification
    }

    private func processPollStartEvent(notificationItem: NotificationItemProxyProtocol, pollQuestion: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
        notification.body = L10n.commonPollSummary(pollQuestion)
        return notification
    }
    
    private func processCallInviteEvent(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
        notification.body = L10n.commonUnsupportedCall
        return notification
    }
    
    private func processCallNotifyEvent(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
        notification.body = L10n.notificationIncomingCall
        return notification
    }

    private func processCommonRoomMessage(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
        var notification = baseMutableContent(for: notificationItem)
        notification.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
        if notification.title != notificationItem.roomDisplayName {
            notification.subtitle = notificationItem.roomDisplayName
        }
        notification.categoryIdentifier = NotificationConstants.Category.message

        let senderName = if let displayName = notificationItem.senderDisplayName {
            notificationItem.hasMention ? L10n.notificationSenderMentionReply(displayName) : displayName
        } else {
            notificationItem.roomDisplayName
        }
        notification = try await notification.addSenderIcon(using: mediaProvider,
                                                            senderID: notificationItem.senderID,
                                                            senderName: senderName,
                                                            icon: icon(for: notificationItem))
        return notification
    }
    
    func icon(for notificationItem: NotificationItemProxyProtocol) -> NotificationIcon {
        if notificationItem.isDM {
            return NotificationIcon(mediaSource: notificationItem.senderAvatarMediaSource, groupInfo: nil)
        } else {
            return NotificationIcon(mediaSource: notificationItem.roomAvatarMediaSource,
                                    groupInfo: .init(name: notificationItem.roomDisplayName, id: notificationItem.roomID))
        }
    }
}
