import AVFoundation
import NitroModules

// JS callback registration (onX + the shared addListener helper).
// Split out of HybridPlaybackEngine.swift; shared state and helpers
// remain on the main type.
extension HybridPlaybackEngine {
    // MARK: - Events (callback registration)

    func clearCallbacks() throws {
        // Synchronous under lock — must happen before any subsequent
        // engine command can emit events, otherwise handlers fire on
        // stale entries.
        callbackLock.lock()
        defer { callbackLock.unlock() }
        listenerGeneration += 1 // Invalidates any in-flight wrapped closures
        clearAllCallbacks()
    }

    /// Register a callback into `keyPath`'s entry array under the lock,
    /// returning an unsubscribe closure. `build` receives an `isCurrentGeneration`
    /// predicate it must consult so a callback wrapped before a `clearCallbacks()`
    /// generation bump goes silent. Shared by every `onX` registration and
    /// `subscribeToStateChanges`, which previously each repeated this scaffolding.
    func addListener<C>(
        to keyPath: ReferenceWritableKeyPath<HybridPlaybackEngine, [CallbackEntry<C>]>,
        _ build: (_ isCurrentGeneration: @escaping () -> Bool) -> C
    ) -> () -> Void {
        callbackLock.lock()
        defer { callbackLock.unlock() }
        let gen = listenerGeneration
        let id = nextCallbackId
        nextCallbackId &+= 1
        let isCurrent: () -> Bool = { [weak self] in self?.listenerGeneration == gen }
        self[keyPath: keyPath].append(CallbackEntry(id: id, callback: build(isCurrent)))
        return { [weak self] in
            guard let self else { return }
            callbackLock.lock()
            self[keyPath: keyPath].removeAll { $0.id == id }
            callbackLock.unlock()
        }
    }

    func onStateChange(callback: @escaping (PlaybackState) -> Void) throws {
        _ = addListener(to: \.stateChangeEntries) { isCurrent in
            { state in guard isCurrent() else { return }; callback(state) }
        }
    }

    func onMediaPositionChange(callback: @escaping (MediaPosition) -> Void) throws {
        _ = addListener(to: \.mediaPositionChangeEntries) { isCurrent in
            { pos in guard isCurrent() else { return }; callback(pos) }
        }
    }

    func onCurrentItemChange(callback: @escaping ((any HybridMediaItemSpec)?, ItemSource) -> Void) throws {
        _ = addListener(to: \.currentItemChangeEntries) { isCurrent in
            { item, source in guard isCurrent() else { return }; callback(item, source) }
        }
    }

    func onRemoteCommand(callback: @escaping (RemoteCommandEvent) -> Void) throws {
        _ = addListener(to: \.remoteCommandEntries) { isCurrent in
            { event in guard isCurrent() else { return }; callback(event) }
        }
    }

    func onError(callback: @escaping (String, String) -> Void) throws {
        _ = addListener(to: \.errorEntries) { isCurrent in
            { msg, code in guard isCurrent() else { return }; callback(msg, code) }
        }
    }

    func onPlaybackEnded(callback: @escaping () -> Void) throws {
        _ = addListener(to: \.playbackEndedEntries) { isCurrent in
            { guard isCurrent() else { return }; callback() }
        }
    }

    func onQueueEnd(callback: @escaping () -> Void) throws {
        _ = addListener(to: \.queueEndEntries) { isCurrent in
            { guard isCurrent() else { return }; callback() }
        }
    }

    func onQueueChange(callback: @escaping () -> Void) throws {
        _ = addListener(to: \.queueChangeEntries) { isCurrent in
            { guard isCurrent() else { return }; callback() }
        }
    }

    func onPlaybackMetric(callback: @escaping (PlaybackMetricEvent) -> Void) throws {
        _ = addListener(to: \.playbackMetricEntries) { isCurrent in
            { event in guard isCurrent() else { return }; callback(event) }
        }
    }

    func onDomainEvent(callback: @escaping (DomainEvent) -> Void) throws {
        _ = addListener(to: \.domainEventEntries) { isCurrent in
            { event in guard isCurrent() else { return }; callback(event) }
        }
    }

    func onTracksAvailable(callback: @escaping (AvailableTracks) -> Void) throws {
        _ = addListener(to: \.tracksAvailableEntries) { isCurrent in
            { tracks in guard isCurrent() else { return }; callback(tracks) }
        }
    }
}
