import AVFoundation
import NitroModules

final class DomainEventCallbackBox {
    let callback: (Int32, Int32) -> Void

    init(_ callback: @escaping (Int32, Int32) -> Void) {
        self.callback = callback
    }
}

/**
 * HybridPlaybackEngine — Unified playback interface for iOS.
 *
 * Implements the Nitro HybridPlaybackEngineSpec protocol. This is the single
 * HybridObject that JS interacts with for all playback operations.
 *
 * Architecture:
 *   JS -> HybridPlaybackEngine (this class)
 *       -> Pure C domain core (aviation_coordinator_* functions)
 *       -> Command events emitted on C EventBus
 *       -> This bridge subscribes and forwards to AVPlayerDecoderAdapter
 *       -> Decoder reports back via callbacks
 *       -> This bridge translates to aviation_coordinator_on_decoder_event()
 *       -> Domain events emitted on C EventBus
 *       -> This bridge subscribes and fires JS callbacks
 *
 * The C coordinator is the SINGLE SOURCE OF TRUTH for:
 * - Playback state machine (state transitions)
 * - Auto-play logic
 * - Interruption handling
 * - Playback properties (rate, volume, muted, loop)
 * - Queue management (items, shuffle, repeat, navigation, auto-advance)
 *
 * This bridge layer manages:
 * - Promise tracking (Nitro-level concern)
 * - Now playing / remote commands (MPNowPlayingInfoCenter)
 * - Threading (main-thread dispatch for AVPlayer operations)
 * - JS callback registration and dispatch
 * - HybridMediaItem ↔ C AviationMediaItem conversion
 *
 * Threading:
 * All mutable state is read/written on the main thread. Nitro calls
 * methods from the JS thread; methods that mutate state dispatch to main.
 * The C coordinator is thread-safe (internal pthread_mutex).
 */
class HybridPlaybackEngine: HybridPlaybackEngineSpec {
    // MARK: - HybridObject

    let engineId: String = UUID().uuidString

    weak var commandGuardOwner: NSObject?
    var commandGuardBlock: ((UInt32) -> Bool)?

    /// The output that currently owns playback, if any. Owner-keyed on the same
    /// reasoning as the command guard: whichever package installed it is the
    /// only one that may take it away.
    weak var playbackOutputOwner: NSObject?
    var playbackOutput: PlaybackOutputPort?

    var memorySize: Int {
        MemoryLayout<HybridPlaybackEngine>.size + 512
    }

    // MARK: - C Domain Core (pure C, called directly)

    // Internal (not private) so extension files can reach it.
    var coordinator: OpaquePointer? // AviationCoordinator*

    // MARK: - Audio session (interruption sensor)

    /// The session bound via setAudioSession. Retained because the engine is
    /// the sensor's canonical consumer: it forwards interruptions into the
    /// coordinator headlessly, with no JS involvement on the default path.
    var boundAudioSession: HybridAudioSession?

    /// Becoming-noisy policy (default pause) and the last interruption policy
    /// set; the unplug pause honors an IGNORE interruption mode.
    var becomingNoisyPause = true
    var interruptionPolicyIgnoresTransport = false
    // JS default mode is 'resume'; setInterruptionMode() only runs when the
    // option is provided, so the default here must match it.
    var interruptionAutoResumes = true
    // Set when WE paused because headphones were removed; cleared by any
    // newer transport change so replug-resume only follows its own pause.
    var pausedByRouteChange = false
    // Bumped on every user-visible transport command (play/pause/stop/load).
    // Async route handlers capture it at schedule time and re-check before
    // acting, so manual intent landing inside their windows wins.
    var transportCommandGeneration: Int = 0

    // MARK: - Decoder adapter (injected via activateDefaultDecoder or registerDecoder)

    // Internal (not private) so extension files (e.g. +TrackSelection) can reach it.
    var decoder: DecoderPort?

    // MARK: - Cache adapter

    let cacheAdapter = CacheAdapter()

    // MARK: - Pipeline

    let pipeline = AdapterPipeline()

    // MARK: - DRM adapter

    var drmAdapter: DRMAdapter?

    // MARK: - Preload adapter

    var preloadAdapter: PreloadAdapter?

    // MARK: - TTFF tracking

    var ttffTotalStart: CFAbsoluteTime = 0
    var ttffDecoderStart: CFAbsoluteTime = 0
    var ttffSource: String = "cold"
    var ttffUri: String = ""
    var ttffPending: Bool = false

    // MARK: - EventBus subscription IDs

    var commandSubscriptionId: UInt32 = 0
    var domainSubscriptionId: UInt32 = 0

    // MARK: - Domain state (cached from C events for sync reads)

    var _state: PlaybackState = .idle
    var _mediaPosition: MediaPosition = .init(
        currentMs: 0, durationMs: 0, seekableStartMs: 0, seekableEndMs: 0,
        isLive: false, progress: 0, liveEdgeOffsetMs: 0, isAtLiveEdge: false
    )
    var _currentItem: (any HybridMediaItemSpec)?
    var _currentItemSource: ItemSource = .none
    var _isLive: Bool = false
    /// Tracks the last known duration so we can detect when it first resolves
    /// (0 → real value) and push the initial time anchor to Now Playing.
    var _prevDurationMs: Double = 0

    // Player properties (cached for sync reads; source of truth is C coordinator)
    var _volume: Double = 1.0
    var _rate: Double = 1.0
    var _muted: Bool = false
    var _loop: Bool = false

    // Queue: We keep a Swift-side array of HybridMediaItemSpec references
    // for JS access. The C core owns the queue data (items, index, shuffle,
    // repeat). We mirror items here so we can return HybridMediaItemSpec
    // objects to JS (which the C core cannot hold — it only stores
    // AviationMediaItem structs with fixed-size char arrays).
    var _queueItemRefs: [any HybridMediaItemSpec] = []

    // MARK: - Promise tracking

    var loadPromise: Promise<Void>?
    var playPromise: Promise<Void>?
    var seekPromise: Promise<Void>?

    /// Last decoder error details, captured from AVIATION_EVT_ERROR_OCCURRED
    /// so that promise rejections include the actual error message/code.
    var lastErrorMessage: String?
    var lastErrorCode: String?

    // MARK: - Released flag (CRSH-04 — mirrors Android @Volatile released flag)

    var _released: Bool = false
    var isReleased: Bool { _released }

    // MARK: - Safe promise wrappers (CRSH-04)

    // Nitro v0.33.9 calls fatalError() on double-settle. These wrappers prevent crashes.
    // Also guards against "Promise was destroyed" when promises are superseded by new commands.

    func safeResolve(_ promise: Promise<Void>?) {
        guard let promise else { return }
        do { try promise.resolve(withResult: ()) } catch {
            #if DEBUG
                NSLog("[Aviation] Promise already settled, ignoring resolve")
            #endif
        }
    }

    func safeReject(_ promise: Promise<Void>?, error: Error) {
        guard let promise else { return }
        do { try promise.reject(withError: error) } catch {
            #if DEBUG
                NSLog("[Aviation] Promise already settled, ignoring reject: %@", error.localizedDescription)
            #endif
        }
    }

    // MARK: - Listener generation

    var listenerGeneration: Int = 0

    // MARK: - JS callbacks

    /// Subscription-ID tracking — mirrors C event bus pattern (aviation_event_bus.c:63-85)
    struct CallbackEntry<T> {
        let id: UInt32
        let callback: T
    }

    var nextCallbackId: UInt32 = 1

    /// Guards all callback entry arrays. Registration from JS happens
    /// synchronously under this lock; firing callbacks (which originates
    /// from main-queue domain-event dispatch) acquires the lock briefly
    /// to copy the entry array, then iterates the copy outside the lock
    /// to avoid holding it across user JS code.
    ///
    /// Previously registration used `DispatchQueue.main.async` for thread
    /// safety, which created a race: the C coordinator could emit an
    /// event (and that event's main-queue dispatch could land) before
    /// the registration's main-queue dispatch had run, so the entries
    /// array was empty when the event fired and the JS callback never
    /// got the first state transition.
    let callbackLock = NSLock()

    var stateChangeEntries: [CallbackEntry<(PlaybackState) -> Void>] = []
    var mediaPositionChangeEntries: [CallbackEntry<(MediaPosition) -> Void>] = []
    var currentItemChangeEntries: [CallbackEntry<((any HybridMediaItemSpec)?, ItemSource) -> Void>] = []
    var remoteCommandEntries: [CallbackEntry<(RemoteCommandEvent) -> Void>] = []
    var errorEntries: [CallbackEntry<(String, String) -> Void>] = []
    var playbackEndedEntries: [CallbackEntry<() -> Void>] = []
    var queueEndEntries: [CallbackEntry<() -> Void>] = []
    var queueChangeEntries: [CallbackEntry<() -> Void>] = []
    var playbackMetricEntries: [CallbackEntry<(PlaybackMetricEvent) -> Void>] = []
    var domainEventEntries: [CallbackEntry<(DomainEvent) -> Void>] = []
    var tracksAvailableEntries: [CallbackEntry<(AvailableTracks) -> Void>] = []

    // MARK: - Track state (cached for sync property reads)

    /// Cached discovered tracks. Updated when decoder reports track discovery.
    /// Read by availableSubtitleTracks and availableAudioTracks getters.
    var _availableSubtitleTracks: [SubtitleTrack] = []
    var _availableAudioTracks: [AudioTrack] = []

    // MARK: - Now Playing

    var enabledCommands: [RemoteCommand] = []
    var compactCommands: [RemoteCommand] = []
    var skipIntervalMs: Double = 15000
    var overriddenCommands: Set<String> = []
    lazy var nowPlayingAdapter: NowPlayingPort = {
        let adapter = MPNowPlayingAdapter()
        adapter.onRemoteCommand = { [weak self] command, positionMs, intervalMs in
            guard let self else { return }
            // Execute default action if not overridden
            if !adapter.isCommandOverridden(command) {
                executeDefaultRemoteAction(command, positionMs: positionMs, intervalMs: intervalMs)
            }
            // Always notify JS
            let event = RemoteCommandEvent(command: command, positionMs: positionMs, intervalMs: intervalMs)
            for entry in snapshot(\.remoteCommandEntries) {
                entry.callback(event)
            }
        }
        return adapter
    }()

    // MARK: - Initialization

    override init() {
        coordinator = aviation_coordinator_create(
            { ptr in // retain_fn — ARC retain
                guard let ptr else { return }
                _ = Unmanaged<AnyObject>.fromOpaque(ptr).retain()
            },
            { ptr in // release_fn — ARC release
                guard let ptr else { return }
                Unmanaged<AnyObject>.fromOpaque(ptr).release()
            }
        )
        super.init()

        registerEngineAttachment()

        // Create DRM adapter with event bus, coordinator, and queue item provider
        if let coord = coordinator {
            let eventBus = aviation_coordinator_get_event_bus(coord)
            let adapter = DRMAdapter(eventBus: eventBus, coordinator: coord, queueItemProvider: { [weak self] index in
                guard let self else { return nil }
                guard index >= 0, index < _queueItemRefs.count else { return nil }
                return _queueItemRefs[index]
            })
            drmAdapter = adapter
            pipeline.addInterceptor(adapter)

            // Give cache adapter access to event bus for cache event emission
            cacheAdapter.setEventBus(eventBus)
            // Register cache interceptor AFTER DRM (DRM is first, cache is second)
            pipeline.addInterceptor(cacheAdapter)

            // Create preload adapter with event bus, coordinator, cache adapter access, and pipeline
            let preload = PreloadAdapter(
                eventBus: eventBus,
                coordinator: coord,
                cacheAdapter: cacheAdapter,
                pipeline: pipeline,
                queueItemProvider: { [weak self] index in
                    guard let self else { return nil }
                    guard index >= 0, index < _queueItemRefs.count else { return nil }
                    return _queueItemRefs[index]
                }
            )
            preloadAdapter = preload
            pipeline.addInterceptor(preload) // Preload is third (after DRM, Cache)
        }

        subscribeToCoordinatorEvents()
    }

    // MARK: - Decoder Registration

    /// Activate the default platform decoder (AVPlayer).
    /// Called by plugins during setup. Core never creates a decoder itself.
    func activateDefaultDecoder() throws {
        guard decoder == nil else { return } // already activated
        let adapter = AVPlayerDecoderAdapter()
        registerDecoder(adapter)
    }

    /// Register a decoder adapter and wire callbacks.
    func registerDecoder(_ newDecoder: DecoderPort) {
        decoder = newDecoder
        wireDecoderCallbacks()
    }

    /// Wire an output's callbacks into the coordinator. Position and state from
    /// a remote output must reach the state machine exactly as the decoder's do,
    /// or the machine would keep reporting whatever the idle local decoder last
    /// said.
    func wireOutputCallbacks(_ port: PlaybackOutputPort) {
        port.callbacks.onStateChanged = { [weak self] decoderState in
            guard let self, let coord = coordinator else { return }
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_STATE_CHANGED.rawValue)
            event.decoderState = Int32(decoderState.rawValue)
            aviation_coordinator_on_decoder_event(coord, &event)
        }

        port.callbacks.onTimeUpdate = { [weak self] currentMs, durationMs, seekStart, seekEnd, isLive in
            guard let self, let coord = coordinator else { return }
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_TIME_UPDATE.rawValue)
            event.position.currentMs = currentMs.isFinite ? Int64(currentMs) : 0
            event.position.durationMs = isLive ? Int64.max : (durationMs.isFinite ? Int64(durationMs) : 0)
            event.position.seekableStartMs = seekStart.isFinite ? Int64(seekStart) : 0
            event.position.seekableEndMs = seekEnd.isFinite ? Int64(seekEnd) : (isLive ? Int64.max : 0)
            event.position.isLive = isLive
            aviation_coordinator_on_decoder_event(coord, &event)
        }

        port.callbacks.onError = { [weak self] message, code in
            guard let self, let coord = coordinator else { return }
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_ERROR.rawValue)
            withCString(message, maxLen: Int(AVIATION_MAX_ERROR_LEN)) { ptr in
                _ = withUnsafeMutablePointer(to: &event.errorMessage) { dest in
                    memcpy(dest, ptr, Int(AVIATION_MAX_ERROR_LEN))
                }
            }
            withCString(code, maxLen: Int(AVIATION_MAX_ERROR_CODE_LEN)) { ptr in
                _ = withUnsafeMutablePointer(to: &event.errorCode) { dest in
                    memcpy(dest, ptr, Int(AVIATION_MAX_ERROR_CODE_LEN))
                }
            }
            aviation_coordinator_on_decoder_event(coord, &event)
        }
    }

    /// Wire the decoder adapter's callbacks to feed into the C coordinator.
    func wireDecoderCallbacks() {
        guard let decoder else { return }
        decoder.callbacks.onStateChanged = { [weak self] decoderState in
            guard let self, let coord = coordinator else { return }
            #if DEBUG
                NSLog("[Aviation] DecoderCallback.onStateChanged decoderState=%d", decoderState.rawValue)
            #endif
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_STATE_CHANGED.rawValue)
            event.decoderState = Int32(decoderState.rawValue)
            aviation_coordinator_on_decoder_event(coord, &event)
        }

        decoder.callbacks.onTimeUpdate = { [weak self] currentMs, durationMs, seekStart, seekEnd, isLive in
            guard let self, let coord = coordinator else { return }
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_TIME_UPDATE.rawValue)
            event.position.currentMs = currentMs.isFinite ? Int64(currentMs) : 0
            event.position.durationMs = isLive ? Int64.max : (durationMs.isFinite ? Int64(durationMs) : 0)
            event.position.seekableStartMs = seekStart.isFinite ? Int64(seekStart) : 0
            event.position.seekableEndMs = seekEnd.isFinite ? Int64(seekEnd) : (isLive ? Int64.max : 0)
            event.position.isLive = isLive
            aviation_coordinator_on_decoder_event(coord, &event)
        }

        decoder.callbacks.onError = { [weak self] message, code in
            guard let self, let coord = coordinator else { return }
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_ERROR.rawValue)
            withCString(message, maxLen: Int(AVIATION_MAX_ERROR_LEN)) { ptr in
                _ = withUnsafeMutablePointer(to: &event.errorMessage) { dest in
                    memcpy(dest, ptr, Int(AVIATION_MAX_ERROR_LEN))
                }
            }
            withCString(code, maxLen: Int(AVIATION_MAX_ERROR_CODE_LEN)) { ptr in
                _ = withUnsafeMutablePointer(to: &event.errorCode) { dest in
                    memcpy(dest, ptr, Int(AVIATION_MAX_ERROR_CODE_LEN))
                }
            }
            aviation_coordinator_on_decoder_event(coord, &event)
        }

        decoder.callbacks.onBitrateChange = { [weak self] bitrateBps in
            guard let self, let coord = coordinator else { return }
            var event = AviationDecoderEvent()
            event.type = Int32(AVIATION_DECODER_EVT_BITRATE_CHANGE.rawValue)
            event.bitrateBps = bitrateBps
            // iOS reports combined bitrate only — populate both video and combined
            event.videoBitrateBps = bitrateBps
            event.audioBitrateBps = 0 // iOS cannot separate audio bitrate
            aviation_coordinator_on_decoder_event(coord, &event)
        }

        decoder.callbacks.onTracksDiscovered = { [weak self] subtitleTracks, audioTracks in
            guard let self else { return }
            // Cache for sync property reads
            _availableSubtitleTracks = subtitleTracks
            _availableAudioTracks = audioTracks

            // Re-apply stored track selections to the new item
            reapplyTrackSelections()

            // Fire JS callbacks
            let tracks = AvailableTracks(subtitleTracks: subtitleTracks, audioTracks: audioTracks)
            for entry in snapshot(\.tracksAvailableEntries) {
                entry.callback(tracks)
            }
        }
    }

    /// Subscribe to the C coordinator's EventBus for command and domain events.
    func subscribeToCoordinatorEvents() {
        guard let coord = coordinator else { return }
        let eventBus = aviation_coordinator_get_event_bus(coord)

        #if DEBUG
            NSLog("[Aviation] Subscribing to coordinator events")
        #endif

        // Capture `self` weakly via Unmanaged pointer passed as user_data
        let selfPtr = Unmanaged.passUnretained(self).toOpaque()

        // Subscribe to COMMAND events
        commandSubscriptionId = aviation_event_bus_subscribe_commands(
            eventBus,
            { event, userData in
                guard let event, let userData else { return }
                let engine = Unmanaged<HybridPlaybackEngine>.fromOpaque(userData).takeUnretainedValue()
                // Copy event data before dispatching to main thread
                let eventCopy = event.pointee
                DispatchQueue.main.async { [weak engine] in
                    engine?.handleCommandEvent(eventCopy)
                }
            },
            selfPtr
        )

        // Subscribe to DOMAIN events
        domainSubscriptionId = aviation_event_bus_subscribe_domain(
            eventBus,
            { event, userData in
                guard let event, let userData else { return }
                let engine = Unmanaged<HybridPlaybackEngine>.fromOpaque(userData).takeUnretainedValue()
                let eventCopy = event.pointee
                DispatchQueue.main.async { [weak engine] in
                    engine?.handleDomainEvent(eventCopy)
                }
            },
            selfPtr
        )
    }

    /// Execute default native action for a remote command (when not overridden by JS).
    func executeDefaultRemoteAction(_ command: RemoteCommand, positionMs: Double?, intervalMs: Double?) {
        switch command {
        case .play:
            try? _ = play()
        case .pause:
            try? _ = pause()
        case .stop:
            try? _ = stop()
        case .toggleplaypause:
            if isPlaying {
                try? _ = pause()
            } else {
                try? _ = play()
            }
        case .nexttrack:
            try? _ = skipToNext()
        case .previoustrack:
            try? _ = skipToPrevious()
        case .skipforward:
            let intervalSec = (intervalMs ?? skipIntervalMs) / 1000.0
            let newPos = min(mediaPosition.currentMs + intervalSec * 1000.0, mediaPosition.durationMs)
            if newPos.isFinite {
                try? _ = seekTo(positionMs: newPos)
            }
        case .skipbackward:
            let intervalSec = (intervalMs ?? skipIntervalMs) / 1000.0
            let newPos = max(mediaPosition.currentMs - intervalSec * 1000.0, 0)
            try? _ = seekTo(positionMs: newPos)
        case .seekto:
            if let pos = positionMs {
                try? _ = seekTo(positionMs: pos)
            }
        }
    }

    // MARK: - Queue

    // Queue mutations, skip navigation, and queue properties live in
    // HybridPlaybackEngine+Queue.swift (all delegate to the C coordinator).

    // MARK: - Track Selection state

    // Selections are stored here so they persist across item changes. The
    // selection methods live in HybridPlaybackEngine+TrackSelection.swift; the
    // decoder's onTracksDiscovered callback triggers reapplyTrackSelections().
    // These are internal (not private) so that extension file can reach them.
    var _videoTrackSelection = VideoTrackSelection(type: .auto, value: nil)
    var _subtitleTrackSelection = SubtitleTrackSelection(type: .auto, value: nil)
    var _audioTrackSelection = AudioTrackSelection(type: .auto, value: nil)

    // MARK: - Core item conversion

    /// Build a C AviationMediaItem from a HybridMediaItemSpec.
    func buildCoreItem(from item: any HybridMediaItemSpec) -> AviationMediaItem {
        var core = AviationMediaItem()

        // Copy strings into fixed-size char arrays
        copyString(item.uri, to: &core.id, maxLen: Int(AVIATION_MAX_ID_LEN))
        copyString(item.uri, to: &core.uri, maxLen: Int(AVIATION_MAX_URI_LEN))
        copyString(item.title ?? "", to: &core.title, maxLen: Int(AVIATION_MAX_TITLE_LEN))
        copyString(item.artist ?? "", to: &core.artist, maxLen: Int(AVIATION_MAX_ARTIST_LEN))
        copyString(item.album ?? "", to: &core.album, maxLen: Int(AVIATION_MAX_ALBUM_LEN))
        copyString(item.artworkUri ?? "", to: &core.artworkUri, maxLen: Int(AVIATION_MAX_ARTWORK_LEN))
        core.durationHintMs = 0

        // Map media type
        if let hybrid = item as? HybridMediaItem {
            if let mt = hybrid.mediaType {
                switch mt {
                case .audioondemand: core.mediaType = Int32(AVIATION_MEDIA_AUDIO_ON_DEMAND.rawValue)
                case .audiolive: core.mediaType = Int32(AVIATION_MEDIA_AUDIO_LIVE.rawValue)
                case .videoondemand: core.mediaType = Int32(AVIATION_MEDIA_VIDEO_ON_DEMAND.rawValue)
                case .videolive: core.mediaType = Int32(AVIATION_MEDIA_VIDEO_LIVE.rawValue)
                }
            } else {
                core.mediaType = Int32(AVIATION_MEDIA_AUDIO_ON_DEMAND.rawValue)
            }
            // Store native handle — the coordinator passes this through in DECODER_LOAD
            // commands so the adapter can access platform-specific properties (headers, DRM, etc.)
            core.nativeHandle = Unmanaged.passRetained(hybrid).toOpaque()
        } else {
            core.mediaType = Int32(AVIATION_MEDIA_AUDIO_ON_DEMAND.rawValue)
        }

        return core
    }

    // MARK: - PlaybackState conversion

    func cStateToSwift(_ state: Int) -> PlaybackState {
        switch state {
        case Int(AVIATION_STATE_IDLE.rawValue): .idle
        case Int(AVIATION_STATE_LOADING.rawValue): .loading
        case Int(AVIATION_STATE_READY.rawValue): .ready
        case Int(AVIATION_STATE_PLAYING.rawValue): .playing
        case Int(AVIATION_STATE_PAUSED.rawValue): .paused
        case Int(AVIATION_STATE_BUFFERING.rawValue): .buffering
        case Int(AVIATION_STATE_STOPPED.rawValue): .stopped
        case Int(AVIATION_STATE_ERROR.rawValue): .error
        default: .idle
        }
    }

    // MARK: - Helpers

    /// Snapshot a callback-entry array under the lock so iteration can
    /// proceed without holding the lock across user JS callbacks (which
    /// could call back into native code that touches the same arrays).
    func snapshot<C>(
        _ keyPath: KeyPath<HybridPlaybackEngine, [CallbackEntry<C>]>
    ) -> [CallbackEntry<C>] {
        callbackLock.lock()
        defer { callbackLock.unlock() }
        return self[keyPath: keyPath]
    }

    func fireCurrentItemChangeCallbacks() {
        let item = _currentItem
        let source = _currentItemSource
        for entry in snapshot(\.currentItemChangeEntries) {
            entry.callback(item, source)
        }
    }

    func rejectPendingPromises(reason: String) {
        let error = NSError(domain: "aviation", code: -10,
                            userInfo: [NSLocalizedDescriptionKey: reason])
        safeReject(loadPromise, error: error)
        loadPromise = nil
        safeReject(playPromise, error: error)
        playPromise = nil
        safeResolve(seekPromise)
        seekPromise = nil
    }

    /// Settle pending promises when a newer transport command replaces them.
    ///
    /// Supersession is the caller's own newer intent, not a failure — rejecting
    /// here turns every quick next/next into a surfaced error. Mirrors
    /// AVFoundation, where a superseded seek completes with finished=false
    /// instead of erroring.
    func resolvePendingPromises() {
        safeResolve(loadPromise)
        loadPromise = nil
        safeResolve(playPromise)
        playPromise = nil
        safeResolve(seekPromise)
        seekPromise = nil
    }

    func clearAllCallbacks() {
        stateChangeEntries.removeAll()
        mediaPositionChangeEntries.removeAll()
        currentItemChangeEntries.removeAll()
        remoteCommandEntries.removeAll()
        errorEntries.removeAll()
        playbackEndedEntries.removeAll()
        queueEndEntries.removeAll()
        queueChangeEntries.removeAll()
        playbackMetricEntries.removeAll()
        domainEventEntries.removeAll()
        tracksAvailableEntries.removeAll()
        _availableSubtitleTracks = []
        _availableAudioTracks = []
    }

    // MARK: - TTFF event emission

    /// Emit TTFF as adapter telemetry through the C event bus
    /// (AVIATION_EXT_TTFF; slots in aviation_extension_events.h).
    func emitTtffEvent(totalMs: Int64, decoderMs _: Int64) {
        guard let coord = coordinator else { return }
        let bus = aviation_coordinator_get_event_bus(coord)

        let event = AviationExt.event(
            code: AviationExtCode.ttff,
            values: [totalMs],
            text: ttffUri,
            detail: ttffSource
        )
        AviationExt.emit(event, on: bus)
    }

    // MARK: - C string helpers

    /// Copy a Swift String into a fixed-size C char array (tuple).
    /// Uses withUnsafeMutableBytes to write into the tuple's memory.
    func copyString(_ str: String, to dest: inout some Any, maxLen: Int) {
        withUnsafeMutableBytes(of: &dest) { buffer in
            let cStr = str.utf8CString
            let copyLen = min(cStr.count, maxLen)
            for i in 0 ..< copyLen {
                buffer[i] = UInt8(bitPattern: cStr[i])
            }
            // Ensure null-termination
            if copyLen < maxLen {
                buffer[copyLen] = 0
            } else {
                buffer[maxLen - 1] = 0
            }
        }
    }

    /// Execute a closure with a C string pointer, truncated to maxLen.
    func withCString(_ str: String, maxLen _: Int, body: (UnsafePointer<CChar>) -> Void) {
        str.withCString { ptr in
            body(ptr)
        }
    }

    // MARK: - Lifecycle

    deinit {
        unregisterEngineAttachment()

        if let coord = coordinator {
            // CRSH-03: Remove all listeners synchronously before async destroy
            // This eliminates the race window where decoder callbacks could fire
            // referencing a partially-deallocated engine.
            aviation_event_bus_remove_all_listeners(
                aviation_coordinator_get_event_bus(coord)
            )
            let decoderRef = self.decoder
            let nowPlayingRef = self.nowPlayingAdapter
            let drmRef = self.drmAdapter
            DispatchQueue.main.async {
                drmRef?.teardown()
                aviation_coordinator_destroy(coord)
                decoderRef?.handleDestroy()
                nowPlayingRef.teardown()
            }
            coordinator = nil
        }
    }
}

// MARK: - String from C char array tuple

extension String {
    /// Initialize a String from a C fixed-size char array (imported as a tuple).
    /// Reads bytes until null terminator or end of tuple.
    init<T>(cCharArray: T) {
        self = withUnsafePointer(to: cCharArray) { ptr in
            ptr.withMemoryRebound(to: CChar.self, capacity: MemoryLayout<T>.size) { cPtr in
                String(cString: cPtr)
            }
        }
    }
}

// MARK: - RemoteCommand string conversion

extension RemoteCommand {
    /// Get a raw string value for Set storage (Nitro enums aren't natively Hashable in Swift).
    var rawStringValue: String {
        switch self {
        case .play: "play"
        case .pause: "pause"
        case .stop: "stop"
        case .nexttrack: "nextTrack"
        case .previoustrack: "previousTrack"
        case .skipforward: "skipForward"
        case .skipbackward: "skipBackward"
        case .seekto: "seekTo"
        case .toggleplaypause: "togglePlayPause"
        }
    }
}
