import AVFoundation
import NitroModules

// Imperative preload, preload/buffer/cache config, cache management, audio session.
// Split out of HybridPlaybackEngine.swift; shared state and helpers
// remain on the main type.
extension HybridPlaybackEngine {
    // MARK: - Imperative Preload

    func preload(items: [any HybridMediaItemSpec]) throws {
        preloadAdapter?.preloadItems(items)
    }

    // MARK: - Preload Configuration

    func setPreloadConfig(config: PreloadConfig) throws {
        if let count = config.itemCount {
            let clamped = max(1, min(5, Int(count)))
            preloadAdapter?.itemCount = clamped
        }
        if let ttl = config.ttlSeconds {
            preloadAdapter?.ttlSeconds = TimeInterval(max(30, ttl))
        }
    }

    // MARK: - Buffer Configuration

    func setBufferConfig(config: BufferConfig) throws {
        // maxBufferMs applies to iOS (preferredForwardBufferDuration)
        if let maxMs = config.maxBufferMs {
            decoder?.maxBufferDuration = maxMs / 1000.0
        }
        // minBufferMs and playBufferMs are Android-only, no-op on iOS
    }

    // MARK: - Cache Max Size

    func setCacheMaxSize(sizeBytes: Double) throws {
        cacheAdapter.setMaxSizeBytes(Int64(sizeBytes))
    }

    // MARK: - Preload Enable/Disable

    func setPreloadEnabled(enabled: Bool) throws {
        preloadAdapter?.enabled = enabled
    }

    // MARK: - Cache Management

    func clearCache() throws -> Promise<Void> {
        let promise = Promise<Void>()
        DispatchQueue.global(qos: .utility).async { [weak self] in
            self?.cacheAdapter.clearCache()
            // Evict preloaded items — they may hold cache-aware assets
            // referencing files that were just deleted.
            self?.preloadAdapter?.clearWarmPool()
            DispatchQueue.main.async { [weak self] in
                self?.safeResolve(promise)
            }
        }
        return promise
    }

    func getCacheSize() throws -> Double {
        Double(cacheAdapter.getCacheSizeBytes())
    }

    func isCached(uri: String) throws -> Bool {
        cacheAdapter.isCached(uri: uri)
    }

    func setCacheEnabled(enabled: Bool) throws {
        cacheAdapter.setCacheEnabled(enabled)
    }

    // MARK: - Audio Session

    func setAudioSession(session: any HybridAudioSessionSpec) throws {
        guard let audioSession = session as? HybridAudioSession else { return }
        boundAudioSession = audioSession
        // The engine is the interruption sensor's canonical consumer. It
        // forwards into the C coordinator, so the default pause/resume path
        // works headlessly and flows through the same state machine — with
        // command guards and output takeover applying to the resulting
        // transitions like any other transport change.
        try audioSession.onInterruption(callback: { [weak self] began, shouldResume in
            self?.forwardInterruption(began: began, shouldResume: shouldResume)
        })
        // Route-loss policy — this handler is authoritative over the decoder
        // for headphone removal. AVPlayer auto-pauses on disconnect at the
        // platform level (see Apple's "Responding to audio route changes"),
        // so aviation must act on the SAME event to honor its own contract:
        //
        //  - 'pause' (default): pause explicitly and remember it was
        //    route-caused, so a later replug can resume under 'resume' mode.
        //  - 'ignore': counteract the platform pause by resuming once, since
        //    AVPlayer has no API to opt out of it.
        try audioSession.onRouteChange(callback: { [weak self] event in
            guard let self else { return }

            // Bluetooth removals arrive as domain portType .bluetooth
            // (HybridAudioSession folds .bluetoothA2DP/.bluetoothLE/.bluetoothHFP
            // into it); treating only wired .headphones left those users
            // without route-pause and therefore without replug-resume. The
            // guard must admit every port type the switch below treats as
            // headphone-class.
            let removedPort = event.removedOutput?.portType
            let removedIsHeadphoneOutput = removedPort == .headphones || removedPort == .bluetooth
            guard let coord = coordinator,
                  removedIsHeadphoneOutput || event.reason == .newdeviceavailable
            else { return }

            switch (event.reason, becomingNoisyPause) {
            case (.olddeviceunavailable, true)
                where !interruptionPolicyIgnoresTransport && removedIsHeadphoneOutput:
                pausedByRouteChange = true
                aviation_coordinator_pause(coord)

            case (.olddeviceunavailable, false):
                // 'ignore': AVPlayer already paused itself; resume over it.
                // The platform pause lands via KVO asynchronously relative to
                // this notification, so a PLAYING state here means the user
                // had NOT paused before the unplug. A pause issued before the
                // unplug (or during the stagger window) is user intent and
                // wins over the counterattack. Re-checks the policy each time
                // so a runtime switch to 'pause' stops the counterattack.
                let wasPlayingBeforeRouteLoss =
                    aviation_coordinator_get_state(coord) == AVIATION_STATE_PLAYING
                let transportGenerationAtLoss = transportCommandGeneration
                for delayMs in [100.0, 300.0, 700.0] {
                    DispatchQueue.main.asyncAfter(deadline: .now() + delayMs / 1000) { [weak self] in
                        guard let self,
                              !AviationEngineRegistry.isRecorderPauseActive(),
                              !self.becomingNoisyPause,
                              wasPlayingBeforeRouteLoss,
                              transportCommandGeneration == transportGenerationAtLoss,
                              let coord = coordinator,
                              aviation_coordinator_get_state(coord) == AVIATION_STATE_PAUSED
                        else { return }
                        aviation_coordinator_play(coord)
                    }
                }

            case (.newdeviceavailable, _):
                // Replug resumes only what WE paused, and only when the
                // user's interruption policy allows auto-resume.
                guard pausedByRouteChange,
                      interruptionAutoResumes,
                      aviation_coordinator_get_state(coord) == AVIATION_STATE_PAUSED,
                      event.output?.portType != .speaker,
                      event.output?.portType != .receiver
                else { return }
                pausedByRouteChange = false
                aviation_coordinator_play(coord)

            default:
                break
            }
        })
    }

    /// Interruption events land on the main queue (NotificationCenter) and the
    /// coordinator is pthread-locked internally, so a direct call is safe.
    func forwardInterruption(began: Bool, shouldResume: Bool) {
        guard let coord = coordinator else { return }
        aviation_coordinator_on_interruption(coord, began, shouldResume)
    }

    func setInterruptionMode(mode: AviationInterruptionMode) throws {
        interruptionPolicyIgnoresTransport = mode == .ignore
        interruptionAutoResumes = mode == .resume
        guard let coord = coordinator else { return }
        let policy: AviationInterruptionPolicy = switch mode {
        case .resume: AVIATION_INTERRUPTION_RESUME
        case .pause: AVIATION_INTERRUPTION_PAUSE
        case .ignore: AVIATION_INTERRUPTION_IGNORE
        }
        aviation_coordinator_set_interruption_mode(coord, policy)
    }

    func setBecomingNoisyBehavior(behavior: BecomingNoisyBehavior) throws {
        becomingNoisyPause = behavior == .pause
    }
}
