import Combine
import ComposableArchitecture
import Dependencies
import UIKit
import UserNotifications
import Utilities
import XCTestDynamicOverlay

public typealias PushNotificationAllowStatus = UserNotificationClient.NotificationAllowStatus

@DependencyClient
public struct UserNotificationClient {
    public enum NotificationAllowStatus: Equatable, Hashable {
        case unknown
        case allowed
        case notAllowed
    }

    public var add: @Sendable (UNNotificationRequest) async throws -> Void = { _ in }
    public var delegate: @Sendable () -> AsyncStream<DelegateEvent> = { .init { _ in } }

    public var getNotificationSettings: @Sendable () async -> Notification.Settings = { .init(authorizationStatus: .notDetermined) }
    public var removeDeliveredNotificationsWithIdentifiers: @Sendable ([String]) async -> Void = { _ in }
    public var removePendingNotificationRequestsWithIdentifiers: @Sendable ([String]) async -> Void = { _ in }
    public var requestAuthorization: @Sendable (UNAuthorizationOptions) async throws -> Bool = { _ in false }

    public var refreshPushNotificationAllowStatus: () -> Void = {}
    public var dismissPushNotificationBanner: () -> Void = {}
    public var openNotificationSettings: () -> Void = {}

    public var getNotificationStatus: () async -> NotificationAllowStatus = { .unknown }
    public var notificationStatusChannel: @Sendable () -> AsyncStream<NotificationAllowStatus> = { .init { _ in } }

    public enum DelegateEvent: Equatable {
        case didReceiveResponse(UNNotificationResponse, completionHandler: () -> Void)
        case openSettingsForNotification(Notification?)
        case willPresentNotification(
            UNNotification, completionHandler: @Sendable (UNNotificationPresentationOptions) -> Void
        )

        public static func == (lhs: Self, rhs: Self) -> Bool {
            switch (lhs, rhs) {
            case let (.didReceiveResponse(lhs, _), .didReceiveResponse(rhs, _)):
                return lhs == rhs
            case let (.openSettingsForNotification(lhs), .openSettingsForNotification(rhs)):
                return lhs == rhs
            case let (.willPresentNotification(lhs, _), .willPresentNotification(rhs, _)):
                return lhs == rhs
            default:
                return false
            }
        }
    }

    public struct Notification: Equatable {
        public var date: Date
        public var request: UNNotificationRequest

        public init(
            date: Date,
            request: UNNotificationRequest
        ) {
            self.date = date
            self.request = request
        }

        public struct Response: Equatable {
            public var notification: Notification

            public init(notification: Notification) {
                self.notification = notification
            }
        }

        // TODO: should this be nested in UserNotificationClient instead of Notification?
        public struct Settings: Equatable {
            public var authorizationStatus: UNAuthorizationStatus

            public init(authorizationStatus: UNAuthorizationStatus) {
                self.authorizationStatus = authorizationStatus
            }
        }
    }
}

extension UserNotificationClient: DependencyKey {
    public static let liveValue: Self = {
        @Shared(.appStorage(.hasDismissedPushNotificationBanner)) var hasDismissedPushNotificationBanner = false
        let subject = CurrentValueSubject<NotificationAllowStatus, Never>(.unknown)

        return .init(
            add: { try await UNUserNotificationCenter.current().add($0) },
            delegate: {
                AsyncStream { continuation in
                    let delegate = Delegate(continuation: continuation)
                    UNUserNotificationCenter.current().delegate = delegate
                    continuation.onTermination = { [delegate] _ in
                        _ = delegate
                    }
                }
            },
            getNotificationSettings: {
                await Notification.Settings(
                    rawValue: UNUserNotificationCenter.current().notificationSettings()
                )
            },
            removeDeliveredNotificationsWithIdentifiers: {
                UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: $0)
            },
            removePendingNotificationRequestsWithIdentifiers: {
                UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: $0)
            },
            requestAuthorization: {
                try await UNUserNotificationCenter.current().requestAuthorization(options: $0)
            },
            refreshPushNotificationAllowStatus: {
                Task {
                    let status: NotificationAllowStatus = switch await UNUserNotificationCenter.current().notificationSettings().authorizationStatus {
                    case .notDetermined:
                        .unknown
                    case .denied:
                        .notAllowed
                    case .authorized, .provisional, .ephemeral:
                        .allowed
                    @unknown default:
                        .unknown
                    }

                    subject.send(status)
                }
            },
            dismissPushNotificationBanner: {
                $hasDismissedPushNotificationBanner.withLock { $0 = true }
            },
            openNotificationSettings: {
                guard
                    let appSettings = URL(string: UIApplication.openNotificationSettingsURLString),
                    UIApplication.shared.canOpenURL(appSettings)
                else { return }
                UIApplication.shared.open(appSettings)
            },
            getNotificationStatus: {
                let status: NotificationAllowStatus = switch await UNUserNotificationCenter.current().notificationSettings().authorizationStatus {
                case .notDetermined:
                    .unknown
                case .denied:
                    .notAllowed
                case .authorized, .provisional, .ephemeral:
                    .allowed
                @unknown default:
                    .unknown
                }

                // also update our state stream with the latest
                subject.send(status)

                return status
            },
            notificationStatusChannel: {
                UncheckedSendable(subject.values).eraseToStream()
            }
        )
    }()
}

public extension UserNotificationClient.Notification {
    init(rawValue: UNNotification) {
        self.date = rawValue.date
        self.request = rawValue.request
    }
}

public extension UserNotificationClient.Notification.Response {
    init(rawValue: UNNotificationResponse) {
        self.notification = .init(rawValue: rawValue.notification)
    }
}

public extension UserNotificationClient.Notification.Settings {
    init(rawValue: UNNotificationSettings) {
        self.authorizationStatus = rawValue.authorizationStatus
    }
}

private extension UserNotificationClient {
    class Delegate: NSObject, UNUserNotificationCenterDelegate {
        let continuation: AsyncStream<UserNotificationClient.DelegateEvent>.Continuation

        init(continuation: AsyncStream<UserNotificationClient.DelegateEvent>.Continuation) {
            self.continuation = continuation
        }

        func userNotificationCenter(
            _: UNUserNotificationCenter,
            didReceive response: UNNotificationResponse,
            withCompletionHandler completionHandler: @escaping () -> Void
        ) {
            self.continuation.yield(
                .didReceiveResponse(response) { completionHandler() }
            )
        }

        func userNotificationCenter(
            _: UNUserNotificationCenter,
            openSettingsFor notification: UNNotification?
        ) {
            self.continuation.yield(
                .openSettingsForNotification(notification.map(Notification.init(rawValue:)))
            )
        }

        func userNotificationCenter(
            _: UNUserNotificationCenter,
            willPresent notification: UNNotification,
            withCompletionHandler completionHandler:
            @escaping (UNNotificationPresentationOptions) -> Void
        ) {
            self.continuation.yield(
                .willPresentNotification(notification) { completionHandler($0) }
            )
        }
    }
}

extension UserNotificationClient: TestDependencyKey {
    public static let previewValue = Self.noop

    public static let testValue = Self(
        add: unimplemented("\(Self.self).add"),
        delegate: unimplemented("\(Self.self).delegate", placeholder: .finished),
        getNotificationSettings: unimplemented("\(Self.self).getNotificationSettings", placeholder: Notification.Settings(authorizationStatus: .notDetermined)),
        removeDeliveredNotificationsWithIdentifiers: unimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers"),
        removePendingNotificationRequestsWithIdentifiers: unimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers"),
        requestAuthorization: unimplemented("\(Self.self).requestAuthorization"),
        refreshPushNotificationAllowStatus: unimplemented("\(Self.self).requestAuthorization"),
        dismissPushNotificationBanner: unimplemented("\(Self.self).dismissPushNotificationBanner"),
        openNotificationSettings: unimplemented("\(Self.self).openNotificationSettings"),
        getNotificationStatus: { .unknown },
        notificationStatusChannel: { AsyncStream { _ in } }
    )
}

public extension UserNotificationClient {
    static let noop = Self(
        add: { _ in },
        delegate: { AsyncStream { _ in } },
        getNotificationSettings: { Notification.Settings(authorizationStatus: .notDetermined) },
        removeDeliveredNotificationsWithIdentifiers: { _ in },
        removePendingNotificationRequestsWithIdentifiers: { _ in },
        requestAuthorization: { _ in false },
        refreshPushNotificationAllowStatus: {},
        dismissPushNotificationBanner: {},
        openNotificationSettings: {},
        getNotificationStatus: { .unknown },
        notificationStatusChannel: { AsyncStream { _ in } }
    )
}
