import AVFoundation
import NitroModules

// Public player/coordinator access for VideoView and ads adapters.
// Split out of HybridPlaybackEngine.swift; shared state and helpers
// remain on the main type.
extension HybridPlaybackEngine {
    // MARK: - Public: Player and coordinator access (for VideoView and ads adapters)

    func installCommandGuard(
        owner: NSObject,
        block: @escaping (UInt32) -> Bool
    ) {
        commandGuardOwner = owner
        commandGuardBlock = block
    }

    func clearCommandGuard(owner: NSObject) {
        guard commandGuardOwner === owner else { return }
        commandGuardOwner = nil
        commandGuardBlock = nil
    }

    func shouldSuppressCommand(_ commandType: UInt32) -> Bool {
        commandGuardBlock?(commandType) ?? false
    }

    // MARK: - Playback output

    func installPlaybackOutput(owner: NSObject, port: PlaybackOutputPort) {
        playbackOutputOwner = owner
        playbackOutput = port
        wireOutputCallbacks(port)
    }

    /// Installs an output that crossed the ObjC boundary, adapting it to the
    /// Swift port so the rest of the engine only ever sees `PlaybackOutputPort`.
    func installPlaybackOutput(owner: NSObject, objcOutput: AviationPlaybackOutput) {
        installPlaybackOutput(owner: owner, port: ObjCPlaybackOutputAdapter(objcOutput))
    }

    /// No-op unless `owner` still holds the output.
    func clearPlaybackOutput(owner: NSObject) {
        guard playbackOutputOwner === owner else { return }
        playbackOutput?.callbacks = DecoderEventCallbacks()
        playbackOutputOwner = nil
        playbackOutput = nil
    }

    func subscribeToDomainEvents(
        callback: @escaping (Int32, Int32) -> Void
    ) -> UInt32 {
        guard let bus = eventBus else { return 0 }

        let box = DomainEventCallbackBox(callback)
        let boxPtr = Unmanaged.passRetained(box).toOpaque()

        let subscriptionId = aviation_event_bus_subscribe_domain(bus, { event, userData in
            guard let event, let userData else { return }
            let type = event.pointee.type
            let queueIndex = event.pointee.queueIndex
            let box = Unmanaged<DomainEventCallbackBox>
                .fromOpaque(userData)
                .takeUnretainedValue()
            DispatchQueue.main.async {
                box.callback(type, queueIndex)
            }
        }, boxPtr)

        if subscriptionId == 0 {
            Unmanaged<DomainEventCallbackBox>.fromOpaque(boxPtr).release()
            NSLog("[Aviation] subscribeToDomainEvents failed: event bus listener table is full")
        }
        return subscriptionId
    }

    func unsubscribeFromDomainEvents(_ subscriptionId: UInt32) {
        guard let bus = eventBus else { return }
        let userData = aviation_event_bus_unsubscribe_domain(
            bus,
            subscriptionId
        )
        if let userData {
            Unmanaged<DomainEventCallbackBox>.fromOpaque(userData).release()
        }
    }

    func subscribeToStateChanges(
        callback: @escaping (Int32) -> Void
    ) -> (() -> Void)? {
        addListener(to: \.stateChangeEntries) { isCurrent in
            { state in guard isCurrent() else { return }; callback(state.rawValue) }
        }
    }

    // MARK: - Engine attachment (cross-package hooks)

    /// Wires the engine-scoped hooks every attachment flavour shares, so a
    /// VideoView sidecar and a headless subsystem behave identically.
    /// Blocks hold the engine weakly — the registry's strong reference to an
    /// attachment must not keep the engine alive.
    func configureEngineHooks(on attachment: AviationEngineAttachment) {
        attachment.installCommandGuardBlock = { [weak self] owner, block in
            self?.installCommandGuard(owner: owner, block: block)
        }
        attachment.clearCommandGuardBlock = { [weak self] owner in
            self?.clearCommandGuard(owner: owner)
        }
        attachment.domainEventSubscribeBlock = { [weak self] callback in
            self?.subscribeToDomainEvents(callback: callback) ?? 0
        }
        attachment.domainEventUnsubscribeBlock = { [weak self] subscriptionId in
            self?.unsubscribeFromDomainEvents(subscriptionId)
        }
        attachment.stateChangeSubscribeBlock = { [weak self] callback in
            self?.subscribeToStateChanges(callback: callback)
        }
        attachment.installPlaybackOutputBlock = { [weak self] owner, output in
            self?.installPlaybackOutput(owner: owner, objcOutput: output)
        }
        attachment.clearPlaybackOutputBlock = { [weak self] owner in
            self?.clearPlaybackOutput(owner: owner)
        }
        attachment.reportOutputTimeBlock = { [weak self] currentMs, durationMs, seekStart, seekEnd, isLive in
            self?.playbackOutput?.callbacks.onTimeUpdate?(currentMs, durationMs, seekStart, seekEnd, isLive)
        }
        attachment.reportOutputStateBlock = { [weak self] rawState in
            guard let state = DecoderState(rawValue: Int(rawState)) else { return }
            self?.playbackOutput?.callbacks.onStateChanged?(state)
        }
        attachment.reportOutputErrorBlock = { [weak self] message, code in
            self?.playbackOutput?.callbacks.onError?(message, code)
        }
        // Recorder integration: policy is snapshotted INTO the receipt at
        // pause time (interruptionAutoResumes); resume stays mechanical.
        attachment.recorderPauseBlock = { [weak self] in
            guard let self,
                  let coord = coordinator,
                  aviation_coordinator_get_state(coord) == AVIATION_STATE_PLAYING
            else { return nil }
            aviation_coordinator_pause(coord)
            return NSNumber(value: interruptionAutoResumes)
        }
        attachment.recorderResumeBlock = { [weak self] in
            guard let self,
                  let coord = coordinator,
                  aviation_coordinator_get_state(coord) == AVIATION_STATE_PAUSED
            else { return }
            aviation_coordinator_play(coord)
        }
    }

    func registerEngineAttachment() {
        let attachment = AviationEngineAttachment(engineId: engineId)
        configureEngineHooks(on: attachment)
        AviationEngineRegistry.register(engineAttachment: attachment)
    }

    func unregisterEngineAttachment() {
        AviationEngineRegistry.unregisterEngineAttachment(engineId: engineId)
    }

    /// Expose the event bus pointer for cross-module access (e.g., IMA pipeline integration).
    var eventBus: OpaquePointer? {
        guard let coord = coordinator else { return nil }
        return aviation_coordinator_get_event_bus(coord)
    }

    /// Expose the underlying AVPlayer so VideoView can attach its AVPlayerLayer
    /// and ads adapters (e.g., IMA) can create content playhead trackers.
    var underlyingPlayer: AVPlayer? { avDecoder?.avPlayer }

    /// The concrete AVFoundation decoder when the default adapter is
    /// installed. The composition root may couple to it; the DecoderPort
    /// protocol stays platform-free.
    var avDecoder: AVPlayerDecoderAdapter? { decoder as? AVPlayerDecoderAdapter }

    /// Expose the coordinator pointer for ads adapters that need direct C API access.
    var coordinatorPtr: OpaquePointer? { coordinator }

    /// Notify the coordinator that an ad break requires content to pause.
    /// Use this from plugin packages that can't import the C headers directly.
    func contentPauseRequested() {
        guard let coord = coordinator else { return }
        aviation_coordinator_content_pause_requested(coord)
    }

    /// Notify the coordinator that an ad break ended and content should resume.
    func contentResumeRequested() {
        guard let coord = coordinator else { return }
        aviation_coordinator_content_resume_requested(coord)
        // Force refresh now playing metadata — the IMA SDK may have
        // overwritten it with ad metadata (e.g., "Advertisement").
        nowPlayingAdapter.forceRefreshMetadata()
    }

    /// Set the now playing info to show ad metadata (title + duration).
    /// Called by the IMA adapter when an ad starts playing.
    func setNowPlayingAdMode(title: String, durationSeconds: Double) {
        nowPlayingAdapter.setAdMode(title: title, durationSeconds: durationSeconds)
    }

    /// Clear ad mode and restore content metadata in now playing.
    /// Called by the IMA adapter when an ad break ends.
    func clearNowPlayingAdMode() {
        nowPlayingAdapter.clearAdMode()
    }

    /// Emit an ad lifecycle event as adapter telemetry (AVIATION_EXT_AD_*
    /// family; slot conventions in aviation_extension_events.h).
    /// Called from the IMA attachment bridge.
    func emitAdDomainEvent(
        code: Int32, breakIndex: Int32, breakTimeMs: Double, totalAdsInBreak: Int32,
        adId: String?, title: String?, durationMs: Double, currentMs: Double,
        adIndexInBreak: Int32, adTotalAdsInBreak _: Int32,
        isSkippable: Bool, skipOffsetMs: Double, errorMessage: String?
    ) {
        guard let coord = coordinator else { return }
        let bus = aviation_coordinator_get_event_bus(coord)

        var flags: Int32 = 0
        if isSkippable { flags |= AviationExtCode.flagAdSkippable }
        let event = AviationExt.event(
            code: code,
            flags: flags,
            values: [
                Int64(breakIndex),
                AviationExt.bits(breakTimeMs),
                Int64(totalAdsInBreak),
                AviationExt.bits(durationMs),
                AviationExt.bits(currentMs),
                Int64(adIndexInBreak),
                AviationExt.bits(skipOffsetMs),
            ],
            text: adId ?? "",
            detail: title ?? "",
            aux: errorMessage ?? ""
        )
        AviationExt.emit(event, on: bus)
    }
}
