import AppKit
import ApplicationServices
import Foundation
import Logger
import Preferences
import Status

public final class AXNotificationStream: AsyncSequence {
    public typealias Stream = AsyncStream<Element>
    public typealias Continuation = Stream.Continuation
    public typealias AsyncIterator = Stream.AsyncIterator
    public typealias Element = (name: String, element: AXUIElement, info: CFDictionary)

    private var continuation: Continuation
    private let stream: Stream

    private let file: StaticString
    private let line: UInt
    private let function: StaticString

    public func makeAsyncIterator() -> Stream.AsyncIterator {
        stream.makeAsyncIterator()
    }

    deinit {
        continuation.finish()
    }

    public convenience init(
        app: NSRunningApplication,
        element: AXUIElement? = nil,
        notificationNames: String...,
        file: StaticString = #file,
        line: UInt = #line,
        function: StaticString = #function
    ) {
        self.init(
            app: app,
            element: element,
            notificationNames: notificationNames,
            file: file,
            line: line,
            function: function
        )
    }

    public init(
        app: NSRunningApplication,
        element: AXUIElement? = nil,
        notificationNames: [String],
        file: StaticString = #file,
        line: UInt = #line,
        function: StaticString = #function
    ) {
        self.file = file
        self.line = line
        self.function = function

        let mode: CFRunLoopMode = UserDefaults.shared
            .value(for: \.observeToAXNotificationWithDefaultMode) ? .defaultMode : .commonModes

        let runLoop: CFRunLoop = CFRunLoopGetMain()

        var cont: Continuation!
        stream = Stream { continuation in
            cont = continuation
        }
        continuation = cont
        var observer: AXObserver?

        func callback(
            observer: AXObserver,
            element: AXUIElement,
            notificationName: CFString,
            userInfo: CFDictionary,
            pointer: UnsafeMutableRawPointer?
        ) {
            guard let pointer = pointer?.assumingMemoryBound(to: Continuation.self)
            else { return }
            pointer.pointee.yield((notificationName as String, element, userInfo))
        }

        _ = AXObserverCreateWithInfoCallback(
            app.processIdentifier,
            callback,
            &observer
        )
        guard let observer else {
            continuation.finish()
            return
        }

        let observingElement = element ?? AXUIElementCreateApplication(app.processIdentifier)
        continuation.onTermination = { @Sendable _ in
            for name in notificationNames {
                AXObserverRemoveNotification(observer, observingElement, name as CFString)
            }
            CFRunLoopRemoveSource(
                runLoop,
                AXObserverGetRunLoopSource(observer),
                mode
            )
        }

        Task { @MainActor [weak self] in
            CFRunLoopAddSource(
                runLoop,
                AXObserverGetRunLoopSource(observer),
                mode
            )
            var pendingRegistrationNames = Set(notificationNames)
            var retry = 0
            var shouldLogAXDisabledEvent: Bool = true
            while !pendingRegistrationNames.isEmpty, retry < 100 {
                guard let self else { return }
                retry += 1
                for name in notificationNames {
                    await Task.yield()
                    let e = withUnsafeMutablePointer(to: &self.continuation) { pointer in
                        AXObserverAddNotification(
                            observer,
                            observingElement,
                            name as CFString,
                            pointer
                        )
                    }
                    switch e {
                    case .success:
                        shouldLogAXDisabledEvent = true
                        pendingRegistrationNames.remove(name)
                        await Status.shared.updateAXStatus(.granted)
                    case .actionUnsupported:
                        Logger.service.info("AXObserver: Action unsupported: \(name)")
                        pendingRegistrationNames.remove(name)
                    case .apiDisabled:
                        if shouldLogAXDisabledEvent { // Avoid keeping log AX disabled too many times
                            shouldLogAXDisabledEvent = false
                            Logger.service
                                .error("AXObserver: Accessibility API disabled, will try again later")
                        }
                        retry -= 1
                        await Status.shared.updateAXStatus(.notGranted)
                    case .invalidUIElement:
                        Logger.service
                            .info("AXObserver: Invalid UI element, notification name \(name)")
                        pendingRegistrationNames.remove(name)
                    case .invalidUIElementObserver:
                        Logger.service.info("AXObserver: Invalid UI element observer")
                        pendingRegistrationNames.remove(name)
                    case .cannotComplete:
                        Logger.service
                            .info("AXObserver: Failed to observe \(name), will try again later")
                    case .notificationUnsupported:
                        Logger.service.info("AXObserver: Notification unsupported: \(name)")
                        pendingRegistrationNames.remove(name)
                    case .notificationAlreadyRegistered:
                        Logger.service.info("AXObserver: Notification already registered: \(name)")
                        pendingRegistrationNames.remove(name)
                    default:
                        Logger.service
                            .info(
                                "AXObserver: Unrecognized error \(e) when registering \(name), will try again later"
                            )
                    }
                }
                try await Task.sleep(nanoseconds: 1_500_000_000)
            }
        }
    }
}

