import APIClient
import ComposableArchitecture
import Foundation
import Localization
import OrderedCollections
import StatsigClient
import Utilities

// swiftlint:disable file_length

public typealias NotificationLoadingState = InAppNotificationClient.LoadingState

@DependencyClient
public struct InAppNotificationClient {
    /* Notification Types not currently in use */
    static let restrictedNotificationTypes: Set<NotificationType> = [
        .clipUnlike,
        .codeRedeem,
        .delete,
        .invite,
        .inviteAccepted,
        .personaFavorite,
        .personaFollow,
        .personaUsed,
        .playlistUnlike,
        .sceneUnlike,
        .unfollow,
        .restrictedByDefault,
    ]

    public enum LoadingState: Hashable, Equatable, Codable {
        case cleared
        case loading
        case loaded
        case failed(_ msg: String)

        public var isLoading: Bool {
            return self == .loading
        }
    }

    public enum EndpointVersion {
        case v1
        case v2
    }

    public enum ResponseVersion {
        case v1(UserNotification)
        case v2(UserNotificationV2)
    }

    // UI Only Tasks
    public var updateFollowOnNotification: (_ handle: String, _ isFollowing: Bool) -> Void
    public var refreshOrderedNotificationMap: () -> Void
    public var clearOrderedNotificationMap: () -> Void
    public var clearNotificationsInPersistentStore: () -> Void

    // Async Request Tasks
    public var enqueueGetNotifications: (_ before: Date?, _ shouldRefreshInMemoryMap: Bool) -> Void
    // TODO: support this later
    public var enqueueSetNotificationsRead: (_ ids: [String]?, _ all: Bool) -> Void
    public var enqueueSetAllNotificationsRead: () -> Void
}

extension InAppNotificationClient: DependencyKey {
    public static let liveValue: Self = {
        /** Only ever write to this from Suno Model Client */
        @Dependency(APIClient.self) var apiClient

        // App Storage
        @Shared(.appStorage(.lastNotificationNotifiedAtV2)) var lastNotificationNotifiedAtV2: Date?

        // In Memory
        @Shared(.inMemory(.hasUnreadNotifications)) var hasUnreadNotifications: Bool = false
        @Shared(.inMemory(.inAppNotificationsState)) var inAppNotificationsState: NotificationLoadingState = .loading
        @Shared(.inMemory(.inAppNotificationMap)) var inAppNotificationMap: OrderedNotificationMap = .defaultValue
        // This is in-memory as we want this to be nil at app launch
        @Shared(.inMemory(.lastNotificationNotifiedAtInMemory)) var lastNotificationNotifiedAtInMemory: Date?

        var stopAllNotifications: Bool {
            FeatureFlag.legacy.stopAllNotifications
        }

        var isNotificationsV2Enabled: Bool {
            FeatureFlag.legacy.notificationsV2
        }

//        var useSimpleNotificationsClient: Bool {
//            @Dependency(StatsigClient.self) var statsigClient
//            return statsigClient.isFeatureEnabled(
//                key: .simpleNotificationsClient,
//                defaultValue: true // Default to true as the sync layer client isn't ready
//            )
//        }

        /*
            Expected use:
            - Hydrate inAppNotificationMap on appear of consumer view
            - Clear inAppNotificationMap on dismiss of consumer view
         */
//        var databaseSync = InAppNotificationDatabaseSync()

        // ios-notifications-v2
        var endpointVersion: EndpointVersion {
            return isNotificationsV2Enabled ? .v2 : .v1
        }

//        func _refreshInMemoryMapFromPersistentStore() {
//            do {
//                let sourceNotifications = try databaseSync
//                    .fetchNotifications()
//                    .compactMap { $0.asInAppNotificationItem }
//
//                let orderedSourceMap = InAppNotificationProcessing
//                    .groupedNotifications(sourceNotifications)
//
//                _updateMapCache(orderedSourceMap)
//            } catch {
//                /* Silent Catch */
//                print(error)
//            }
//        }

        // Using handle because `MiniProfile` don't return user ID
//        func _updateFollowOnNotification(handle: String, isFollowing: Bool) {
//            guard case .loaded = inAppNotificationsState else { return }
//            let newNotificationMap = InAppNotificationProcessing
//                .updateFollowOnNotifications(
//                    inAppNotificationMap,
//                    handle: handle,
//                    isFollowing: isFollowing
//                )
//
//            do {
//                try databaseSync.save()
//            } catch {
//                print(error)
//            }
//            $inAppNotificationMap.withLock { $0 = newNotificationMap }
//            $inAppNotificationsState.withLock { $0 = .loaded }
//        }

        func _hasNotifications(_ notificationItems: [InAppNotificationItem]) -> Bool {
            var result = false
            for item in notificationItems {
                if item.notificationType.canBeRead && item.isUnread {
                    result = true
                }
            }
            return result
        }

//        func _processNotifications(_ response: ResponseVersion) {
//            let processedData = InAppNotificationProcessing.processResponse(response)
//
//            do {
//                try databaseSync.updateNotifications(processedData.wrappedNotifications)
//            } catch {
//                print(error)
//            }
//
//            switch inAppNotificationsState {
//            case .cleared:
//                break /* Keep the in memory cache clear  during this refresh */
//            case .loaded, .failed, .loading:
//                _refreshInMemoryMapFromPersistentStore()
//            }
//
//            Task { @MainActor in
//                $lastNotificationNotifiedAtV2.withLock { $0 = processedData.lastNotifiedAt }
//                $hasUnreadNotifications.withLock { $0 = processedData.hasUnreadNotifications }
//
//                switch inAppNotificationsState {
//                case .cleared:
//                    /*
//                        Flagging means that the memory map is cleared
//                        and has been gated to keep clear until the next allow on refreshing
//                     */
//                    break
//                case .loading, .loaded, .failed:
//                    /*
//                        Any of these states should allow for the memory to be
//                        allocated further if needed as the consumer is active.
//                     */
//                    $inAppNotificationsState.withLock { $0 = .loaded }
//                }
//            }
//        }

        func _setNotificationsAsLoading() {
            Task { @MainActor in
                $inAppNotificationsState.withLock { $0 = .loading }
            }
        }

        func _setNotificationsAsCleared() {
            Task { @MainActor in
                $inAppNotificationsState.withLock { $0 = .cleared }
            }
        }

        func _setNotificationAsFailed(_ msg: String) {
            Task { @MainActor in
                $inAppNotificationsState.withLock {
                    if $0 == .loading {
                        $0 = .failed(msg)
                    }
                }
            }
        }

        func _updateMapCache(_ map: OrderedNotificationMap) {
            Task { @MainActor in
                $inAppNotificationMap.withLock { $0 = map }
                $inAppNotificationsState.withLock { $0 = .loaded }
            }
        }

        func _clearMapCache() {
            Task { @MainActor in
                $inAppNotificationMap.withLock { $0 = .defaultValue }
            }
        }

        /* Empty Client - Break glass in case of emergencies */
        let emptyClient = Self(
            updateFollowOnNotification: { _, _ in
                $hasUnreadNotifications.withLock { $0 = false }
                $inAppNotificationsState.withLock { $0 = .loading }
                $inAppNotificationMap.withLock { $0 = .defaultValue }
            },
            refreshOrderedNotificationMap: {},
            clearOrderedNotificationMap: {},
            clearNotificationsInPersistentStore: {},
            enqueueGetNotifications: { _, _ in
            },
            enqueueSetNotificationsRead: { _, _ in
            },
            enqueueSetAllNotificationsRead: {}
        )

        // This is a simple version of the service that doesn't actually use the underlying database implementation
        let inMemoryClient = Self(
            updateFollowOnNotification: { handle, isFollowing in
                Task { @MainActor in
                    guard case .loaded = inAppNotificationsState else { return }
                    let newNotificationMap = InAppNotificationProcessing
                        .updateFollowOnNotifications(
                            inAppNotificationMap,
                            handle: handle,
                            isFollowing: isFollowing
                        )
                    $inAppNotificationMap.withLock { $0 = newNotificationMap }
                    $inAppNotificationsState.withLock { $0 = .loaded }
                }
            },
            refreshOrderedNotificationMap: {
                // Not needed as we don't pull from database
            },
            clearOrderedNotificationMap: {
                // I don't think we need to handle this case for this version of the client
            },
            clearNotificationsInPersistentStore: {
                // This doesn't clear the database in this client implementation
                // Instead, make sure to reset all inMemory values
                Task { @MainActor in
                    $inAppNotificationMap.withLock { $0 = .defaultValue }
                    $inAppNotificationsState.withLock { $0 = .loading }
                    $hasUnreadNotifications.withLock { $0 = false }
                    $lastNotificationNotifiedAtInMemory.withLock { $0 = nil }
                }
            },
            enqueueGetNotifications: { before, shouldRefreshInMemoryMap in
                // In order to make sure we get future messages we delay one second here
                let after = lastNotificationNotifiedAtInMemory?.addingTimeInterval(-1)
                if shouldRefreshInMemoryMap {
                    _setNotificationsAsLoading()
                }

                // If `after` is nil, we can force a reset of notifications by clearing the existing map
                if after == nil {
                    _clearMapCache()
                }

                Task {
                    do {
                        let processedData: ProcessedInAppNotificationData
                        switch endpointVersion {
                        case .v1:
                            let userNotification = try await apiClient.getNotifications(after)
                            processedData = InAppNotificationProcessing.processResponse(.v1(userNotification))

                        case .v2:
                            let userNotificationV2 = try await apiClient.getNotificationsV2(before, after)
                            processedData = InAppNotificationProcessing.processResponse(.v2(userNotificationV2))
                        }

                        Task { @MainActor in
                            let allNotifications = processedData.wrappedNotifications + inAppNotificationMap.allValues
                            let orderedSourceMap = InAppNotificationProcessing
                                .groupedNotifications(allNotifications)
                            _updateMapCache(orderedSourceMap)
                            $lastNotificationNotifiedAtInMemory.withLock { $0 = processedData.lastNotifiedAt }
                            $hasUnreadNotifications.withLock { $0 = _hasNotifications(allNotifications) }
                        }
                    } catch {
                        _setNotificationAsFailed("Could not load notifications")
                    }
                }
            },
            enqueueSetNotificationsRead: { _, _ in
                // Not supported in the simple client
            },
            enqueueSetAllNotificationsRead: {
                Task {
                    do {
                        switch endpointVersion {
                        case .v1:
                            try await apiClient.setNotificationsRead(nil, true)
                        case .v2:
                            try await apiClient.setNotificationsReadV2(nil, true)
                        }
                    } catch {
                        print(error)
                    }
                }
                // Make sure to set all in-memory notifications as read
                Task { @MainActor in
                    $hasUnreadNotifications.withLock { $0 = false }
                    let allNotificationsSetToRead = inAppNotificationMap.allValues
                        .map { notificationItem in
                            switch notificationItem {
                            case .v1(let notification):
                                var mutable = notification
                                mutable.isRead = true
                                return InAppNotificationItem.v1(mutable)

                            case .v2(let notification):
                                var mutable = notification
                                mutable.isRead = true
                                return InAppNotificationItem.v2(mutable)
                            }
                        }

                    let orderedSourceMap = InAppNotificationProcessing
                        .groupedNotifications(allNotificationsSetToRead)

                    _updateMapCache(orderedSourceMap)
                }
            }
        )

        // TODO: This needs to be revisited and fixed
        // Problems:
        // - The cache / sync layer seems to be broken
        // - There are cases where this doesn't properly reset on logout
        // - The `hasUnseen` logic doesn't seem to be flipping off correctly
//        let syncLayerClient = Self(
//            updateFollowOnNotification: { handle, isFollowing in
//                Task { @MainActor in
//                    _updateFollowOnNotification(handle: handle, isFollowing: isFollowing)
//                }
//            },
//            refreshOrderedNotificationMap: {
//                _refreshInMemoryMapFromPersistentStore()
//            },
//            clearOrderedNotificationMap: {
//                _setNotificationsAsCleared()
//                _clearMapCache()
//            },
//            clearNotificationsInPersistentStore: {
//                Task {
//                    try databaseSync.deleteAllNotifications()
//                }
//            },
//            enqueueGetNotifications: {
//                // In order to make sure we get future messages we delay one second here
//                [after = lastNotificationNotifiedAtV2?.addingTimeInterval(-1)] before, shouldRefreshInMemoryMap in
//                if shouldRefreshInMemoryMap {
//                    _setNotificationsAsLoading()
//                }
//
//                Task {
//                    do {
//                        switch endpointVersion {
//                        case .v1:
//                            let userNotification = try await apiClient.getNotifications(after)
//                            _processNotifications(.v1(userNotification))
//                        case .v2:
//                            let userNotificationV2 = try await apiClient.getNotificationsV2(before, after)
//                            _processNotifications(.v2(userNotificationV2))
//                        }
//                    } catch {
//                        _setNotificationAsFailed("Could not load notifications")
//                    }
//                }
//            },
//            enqueueSetNotificationsRead: { ids, all in
//                Task {
//                    do {
//                        switch endpointVersion {
//                        case .v1:
//                            try await apiClient.setNotificationsRead(ids, all)
//                        case .v2:
//                            try await apiClient.setNotificationsReadV2(ids, all)
//                        }
//                    } catch {
//                        print(error)
//                    }
//                }
//            },
//            enqueueSetAllNotificationsRead: {
//                // Not implemented yet
//                return
//            }
//        )

        if stopAllNotifications {
            return emptyClient
        } else {
//            return useSimpleNotificationsClient ? inMemoryClient : syncLayerClient
            return inMemoryClient
        }
    }()
}

public extension DependencyValues {
    var inAppNotificationClient: InAppNotificationClient {
        get { self[InAppNotificationClient.self] }
        set { self[InAppNotificationClient.self] = newValue }
    }
}
