import AVFoundation
import NitroModules

/**
 * HybridAudioSession — AVAudioSession wrapper.
 *
 * Manages the app's audio environment: category, mode, activation,
 * interruption handling, and route change notifications.
 *
 * Threading:
 * configure() / activate() / deactivate() run inside Promise.async (background
 * thread). AVAudioSession methods are thread-safe per Apple docs. The readonly
 * state properties (isActive, currentCategory, currentMode) are mutated from
 * the Promise.async thread and read from the JS thread. This is a benign data
 * race on value types — worst case is a stale read. These properties are set
 * once during setup and rarely queried, so the risk is negligible.
 *
 * Observation: NotificationCenter only. No KVO.
 */
class HybridAudioSession: HybridAudioSessionSpec {
    /// Session category/mode mutations abort in-flight items spuriously;
    /// decoders use this to tell transient route churn from real failures.
    /// Consumers MUST gate recovery on item health (a failed item cannot
    /// be recovered by re-playing), so unrelated sessions' config calls
    /// never mask genuine errors. Process-wide by design: the category is
    /// an app-global resource.
    nonisolated(unsafe) static var lastCategoryChangeAt = Date.distantPast

    // MARK: - HybridObject

    var memorySize: Int {
        MemoryLayout<HybridAudioSession>.size
    }

    // MARK: - Private state

    private let session = AVAudioSession.sharedInstance()
    private var interruptionToken: NSNumber?
    private var routeChangeObserver: NSObjectProtocol?

    /// Callbacks registered from JS
    private var interruptionCallback: ((_ began: Bool, _ shouldResume: Bool) -> Void)?

    /// Route consumers are multiple by design (the engine's unplug pause and
    /// JS route wiring listen independently), so unlike interruptions this
    /// slot fans out.
    private let routeChangeCallbacksLock = NSLock()
    private var routeChangeCallbacks: [(AudioRouteChangeEvent) -> Void] = []

    // MARK: - Protocol properties (readonly)

    private(set) var isActive: Bool = false
    private(set) var currentCategory: AudioSessionCategory = .soloambient
    private(set) var currentMode: AudioSessionMode = .default

    override init() {
        // Observers live for the session's lifetime, independent of
        // activate/deactivate cycles: interruptions can begin or end while
        // inactive (a call arriving during a pause must still report its end),
        // and re-registering per cycle risks missing events across the seam.
        super.init()
        setupObservers()
    }

    // MARK: - Protocol methods

    func configure(config: AudioSessionConfig) throws -> Promise<Void> {
        Promise.async { [self] in
            var categoryOptions: AVAudioSession.CategoryOptions = []
            switch config.coexistence {
            case .mix:
                categoryOptions.insert(.mixWithOthers)
            case .duck:
                // Ducking is mixing with an instruction: others lower their
                // volume while this session is active.
                categoryOptions.insert(.mixWithOthers)
                categoryOptions.insert(.duckOthers)
            case .exclusive, nil:
                break
            }
            if config.allowBluetooth == true { categoryOptions.insert(.allowBluetoothA2DP) }
            if config.allowAirPlay == true { categoryOptions.insert(.allowAirPlay) }

            Self.lastCategoryChangeAt = Date()

            let avCategory = mapCategory(config.category ?? .playback)
            let avMode = mapMode(config.mode ?? .default)

            if avCategory == .playAndRecord {
                // Without this, iOS routes ALL output to the receiver
                // (earpiece) for as long as the session is record-capable —
                // playback becomes near-silent even for newly started
                // players. Apple's documented pairing for "record while the
                // user still hears the speaker".
                categoryOptions.insert(.defaultToSpeaker)
            }

            try session.setCategory(avCategory, mode: avMode, options: categoryOptions)
            Self.lastCategoryChangeAt = Date()
            currentCategory = config.category ?? .playback
            currentMode = config.mode ?? .default
        }
    }

    func activate() throws -> Promise<Void> {
        Promise.async { [self] in
            try session.setActive(true)
            isActive = true
        }
    }

    func deactivate() throws -> Promise<Void> {
        Promise.async { [self] in
            try session.setActive(false, options: .notifyOthersOnDeactivation)
            isActive = false
        }
    }

    func onInterruption(callback: @escaping (_ began: Bool, _ shouldResume: Bool) -> Void) throws {
        interruptionCallback = callback
    }

    func onRouteChange(callback: @escaping (AudioRouteChangeEvent) -> Void) throws {
        routeChangeCallbacksLock.lock()
        routeChangeCallbacks.append(callback)
        routeChangeCallbacksLock.unlock()
    }

    // MARK: - NotificationCenter observers (NO KVO)

    private func setupObservers() {
        teardownObservers()

        interruptionToken = AviationInterruptionBroadcaster.addObserver { [weak self] began, shouldResume in
            self?.interruptionCallback?(began, shouldResume)
        }

        routeChangeObserver = NotificationCenter.default.addObserver(
            forName: AVAudioSession.routeChangeNotification,
            object: session,
            queue: .main
        ) { [weak self] notification in
            self?.handleRouteChange(notification)
        }
    }

    private func teardownObservers() {
        if let token = interruptionToken {
            AviationInterruptionBroadcaster.removeObserver(token)
            interruptionToken = nil
        }
        if let observer = routeChangeObserver {
            NotificationCenter.default.removeObserver(observer)
            routeChangeObserver = nil
        }
    }

    // MARK: - Notification handlers

    private func handleRouteChange(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
              let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
        else {
            return
        }

        let nitroReason: RouteChangeReason = switch reason {
        case .newDeviceAvailable: .newdeviceavailable
        case .oldDeviceUnavailable: .olddeviceunavailable
        case .categoryChange: .categorychange
        case .override: .override
        default: .unknown
        }

        let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription

        let removedOutput = reason == .oldDeviceUnavailable
            ? previousRoute?.outputs.first.map {
                routeDescriptor(name: $0.portName, port: $0.portType)
            }
            : nil
        let output = session.currentRoute.outputs.first.map {
            routeDescriptor(name: $0.portName, port: $0.portType)
        }
        let event = AudioRouteChangeEvent(
            reason: nitroReason,
            removedOutput: removedOutput,
            output: output
        )

        routeChangeCallbacksLock.lock()
        let snapshot = routeChangeCallbacks
        routeChangeCallbacksLock.unlock()
        for callback in snapshot {
            callback(event)
        }
    }

    private func routeDescriptor(name: String?, port: AVAudioSession.Port) -> AudioRouteDescriptor {
        let portType: RoutePortType = switch port {
        case .headphones:
            .headphones
        case .bluetoothA2DP, .bluetoothLE, .bluetoothHFP:
            .bluetooth
        case .airPlay:
            .airplay
        case .carAudio:
            .car
        case .usbAudio:
            .usb
        case .builtInSpeaker:
            .speaker
        case .builtInReceiver:
            .receiver
        default:
            .unknown
        }
        return AudioRouteDescriptor(
            name: name.flatMap { $0.isEmpty ? nil : $0 },
            portType: portType
        )
    }

    // MARK: - Enum mapping

    private func mapCategory(_ category: AudioSessionCategory) -> AVAudioSession.Category {
        switch category {
        case .ambient: .ambient
        case .soloambient: .soloAmbient
        case .playback: .playback
        case .playandrecord: .playAndRecord
        }
    }

    private func mapMode(_ mode: AudioSessionMode) -> AVAudioSession.Mode {
        switch mode {
        case .default: .default
        case .movieplayback: .moviePlayback
        case .spokenaudio: .spokenAudio
        case .voicechat: .voiceChat
        case .videochat: .videoChat
        }
    }

    // MARK: - Lifecycle

    deinit {
        teardownObservers()
    }
}
