import Foundation
import AVFoundation
import React

@objc(ToneListenAudioModule)
class ToneListenAudioModule: RCTEventEmitter {
  private var engine: AVAudioEngine?
  private var isRunning: Bool = false
  private var sampleRate: Double = 44100
  private var frameSize: AVAudioFrameCount = 4800
  private var inputNode: AVAudioInputNode?
  private var playbackReader: AVAssetReader?
  private var playbackOutput: AVAssetReaderTrackOutput?
  private var playbackDecodeQueue: DispatchQueue?
  private var isDecodingPlayback: Bool = false
  private var isPreparedForMediaPlayback: Bool = false

  override static func requiresMainQueueSetup() -> Bool { return false }

  override func supportedEvents() -> [String]! {
    return ["TLRN_AudioFrame"]
  }

  @objc(requestMicrophonePermission:rejecter:)
  func requestMicrophonePermission(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    AVAudioSession.sharedInstance().requestRecordPermission { granted in
      DispatchQueue.main.async {
        resolver(granted)
      }
    }
  }

  @objc(start:bufferSize:resolver:rejecter:)
  func start(_ rate: NSNumber, bufferSize: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    if isRunning {
      resolver(true)
      return
    }
    
    sampleRate = rate.doubleValue > 0 ? rate.doubleValue : 44100
    frameSize = AVAudioFrameCount(truncating: bufferSize) > 0 ? AVAudioFrameCount(truncating: bufferSize) : 4800

    // First check microphone permission
    AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in
      guard let self = self else { return }
      
      if !granted {
        DispatchQueue.main.async {
          rejecter("E_PERMISSION", "Microphone permission denied", nil)
        }
        return
      }
      
      DispatchQueue.main.async {
        self.setupAudioSessionAndStartEngine(resolver: resolver, rejecter: rejecter)
      }
    }
  }

  private func setupAudioSessionAndStartEngine(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    do {
      let session = AVAudioSession.sharedInstance()

      // Configure audio session with comprehensive options (matching working Swift SDK)
      try session.setCategory(.playAndRecord,
                             mode: .measurement,
                             options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP, .allowAirPlay])

      // Set preferred sample rate for better tone detection
      try session.setPreferredSampleRate(sampleRate)

      // Set preferred buffer duration for real-time processing (matching working SDK)
      try session.setPreferredIOBufferDuration(0.005) // 5ms buffer for low latency

      try session.setActive(true)
      print("AVAudioSession configured and activated successfully")

      // Start the audio engine
    let eng = AVAudioEngine()
    let input = eng.inputNode
    let format = input.inputFormat(forBus: 0)

    // Use the hardware's native sample rate to avoid format mismatch
    let actualSampleRate = format.sampleRate
    print("Hardware sample rate: \(actualSampleRate), requested: \(sampleRate)")
    
    // Use nil format to let the system use the node's native format (like Swift framework)
    input.installTap(onBus: 0, bufferSize: frameSize, format: nil) { [weak self] buffer, _ in
      guard let self = self else { return }
      let channelData = buffer.floatChannelData?[0]
      let count = Int(buffer.frameLength)
      if let data = channelData, count > 0 {
        var arr = [Float](repeating: 0, count: count)
        arr.withUnsafeMutableBufferPointer { ptr in
          ptr.baseAddress?.update(from: data, count: count)
        }
        // Send as array of numbers (Float -> Double for JS)
        self.sendEvent(withName: "TLRN_AudioFrame", body: [
          "sampleRate": actualSampleRate,
          "buffer": arr,
          "source": "mic"
        ])
      }
    }

      try eng.start()
      engine = eng
      inputNode = input
      isRunning = true
      
      // Set up interruption handling
      setupAudioSessionInterruptionHandling()
      
      resolver(true)
      print("Audio engine started successfully")

    } catch let sessionError {
      print("Error setting up audio session:", sessionError.localizedDescription)

      // Try fallback configuration (matching working SDK)
      do {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.record, mode: .measurement, options: [])
        try session.setActive(true)
        print("Fallback audio session configuration applied")

        let eng = AVAudioEngine()
        let input = eng.inputNode
        let format = input.inputFormat(forBus: 0)

        // Use the hardware's native sample rate to avoid format mismatch
        let actualSampleRate = format.sampleRate
        print("Fallback - Hardware sample rate: \(actualSampleRate), requested: \(sampleRate)")

        let targetFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32,
                                         sampleRate: actualSampleRate,
                                         channels: 1,
                                         interleaved: false)!

        input.installTap(onBus: 0, bufferSize: frameSize, format: targetFormat) { [weak self] buffer, _ in
          guard let self = self else { return }
          let channelData = buffer.floatChannelData?[0]
          let count = Int(buffer.frameLength)
          if let data = channelData, count > 0 {
            var arr = [Float](repeating: 0, count: count)
            arr.withUnsafeMutableBufferPointer { ptr in
          ptr.baseAddress?.update(from: data, count: count)
        }
            arr.withUnsafeMutableBufferPointer { ptr in
              ptr.baseAddress?.update(from: data, count: count)
            }
            self.sendEvent(withName: "TLRN_AudioFrame", body: [
              "sampleRate": actualSampleRate,
              "buffer": arr,
              "source": "mic"
            ])
          }
        }

        try eng.start()
        engine = eng
        inputNode = input
        isRunning = true
        setupAudioSessionInterruptionHandling()
        resolver(true)
        print("Audio engine started with fallback configuration")

      } catch let fallbackError {
        print("Error with fallback audio configuration:", fallbackError.localizedDescription)
        rejecter("E_ENGINE", "Failed to start AVAudioEngine", fallbackError)
      }
    }
  }

  private func setupAudioSessionInterruptionHandling() {
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(handleAudioSessionInterruption(_:)),
      name: AVAudioSession.interruptionNotification,
      object: nil
    )
  }

  @objc private func handleAudioSessionInterruption(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
      return
    }

    switch type {
    case .began:
      print("Audio session interruption began - stopping engine")
      stopEngine()

    case .ended:
      print("Audio session interruption ended - attempting restart")
      
      // Check interruption reason
      let shouldResume: Bool
      if let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt,
         let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue) {
        switch reason {
        case .default, .routeDisconnected:
          shouldResume = true
        @unknown default:
          shouldResume = true
        }
      } else {
        shouldResume = true
      }

      guard shouldResume else {
        print("Not resuming audio engine - interruption indicates we should not resume")
        return
      }

      // Add a delay to ensure system audio resources are fully released
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
        self?.restartAudioEngineAfterInterruption()
      }
    default:
      break
    }
  }

  private func restartAudioEngineAfterInterruption() {
    do {
      let session = AVAudioSession.sharedInstance()
      
      // First, deactivate the session completely
      try session.setActive(false)
      
      // Wait a bit for deactivation to complete
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        do {
          // Set the audio session without mixing to avoid ducking/quiet output later
          try session.setCategory(.playAndRecord,
                                 mode: .default,
                                 options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP, .allowAirPlay])
          try session.setActive(true)

          // Restart the audio engine
          if let eng = self.engine {
            try eng.start()
            self.isRunning = true
            print("Audio engine restarted successfully after interruption")
          }

        } catch let restartError {
          print("Error restarting audio engine: \(restartError.localizedDescription)")
          self.isRunning = false
        }
      }

    } catch let deactivationError {
      print("Error deactivating audio session: \(deactivationError.localizedDescription)")
      self.isRunning = false
    }
  }

  private func stopEngine() {
    if let eng = engine {
      eng.inputNode.removeTap(onBus: 0)
      eng.stop()
    }
    isRunning = false
  }

  // MARK: - Playback decode using AVAssetReader (tap-like)

  @objc(startPlaybackDecode:resolver:rejecter:)
  func startPlaybackDecode(_ urlString: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    // Stop mic engine to avoid iOS output attenuation during playback
    if isRunning {
      print("PlaybackDecode: stopping mic engine before starting decode")
      stopEngine()
    }
    if isDecodingPlayback {
      resolver(true)
      return
    }
    guard let url = URL(string: urlString) else {
      rejecter("E_URL", "Invalid URL", nil)
      return
    }

    // Configure session for loud playback (no mic) while decoding media for detection
    do {
      let session = AVAudioSession.sharedInstance()
      // Use only options valid for `.playback` to avoid '!pri' failures
      try session.setCategory(.playback, mode: .moviePlayback, options: [])
      try? session.setPreferredSampleRate(48000)
      try? session.setPreferredIOBufferDuration(0.01)
      try session.setActive(true)
      print("PlaybackDecode: audio session set to category=playback, mode=moviePlayback")
    } catch {
      // best-effort
    }

    // If remote URL, download to temp file first (AVAssetReader requires local file)
    if let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" {
      self.playbackDecodeQueue = DispatchQueue(label: "com.tonelisten.playbackDecode")
      self.isDecodingPlayback = true
      resolver(true)
      print("PlaybackDecode: downloading remote media for decode → \(url.absoluteString)")
      let task = URLSession.shared.downloadTask(with: url) { [weak self] tempURL, _, error in
        guard let self = self else { return }
        if let error = error {
          self.isDecodingPlayback = false
          rejecter("E_DOWNLOAD", "Failed to download media for decode", error)
          return
        }
        guard let tempURL = tempURL else {
          self.isDecodingPlayback = false
          rejecter("E_DOWNLOAD", "No temp file URL returned", nil)
          return
        }
        let dst = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "." + (url.pathExtension.isEmpty ? "mp4" : url.pathExtension))
        do {
          try? FileManager.default.removeItem(at: dst)
          try FileManager.default.moveItem(at: tempURL, to: dst)
          print("PlaybackDecode: download complete → \(dst.lastPathComponent)")
        } catch {
          self.isDecodingPlayback = false
          rejecter("E_FILE", "Failed to move downloaded media", error)
          return
        }

        let asset = AVURLAsset(url: dst)
        asset.loadValuesAsynchronously(forKeys: ["tracks"]) { [weak self] in
          guard let self = self else { return }
          var error: NSError?
          let status = asset.statusOfValue(forKey: "tracks", error: &error)
          if status != .loaded {
            DispatchQueue.main.async { rejecter("E_ASSET", "Failed to load asset tracks", error) }
            self.isDecodingPlayback = false
            return
          }

          guard let track = asset.tracks(withMediaType: .audio).first else {
            DispatchQueue.main.async { rejecter("E_NO_AUDIO", "No audio track found", nil) }
            self.isDecodingPlayback = false
            return
          }

          let settings: [String: Any] = [
            AVFormatIDKey: kAudioFormatLinearPCM,
            AVLinearPCMIsFloatKey: true,
            AVLinearPCMBitDepthKey: 32,
            AVLinearPCMIsNonInterleaved: false,
            AVLinearPCMIsBigEndianKey: false
          ]

          do {
            let reader = try AVAssetReader(asset: asset)
            let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
            if reader.canAdd(output) { reader.add(output) }
            self.playbackReader = reader
            self.playbackOutput = output
          } catch let readerError {
            DispatchQueue.main.async { rejecter("E_READER", "Failed to create asset reader", readerError) }
            self.isDecodingPlayback = false
            return
          }

          guard let reader = self.playbackReader, let output = self.playbackOutput else {
            DispatchQueue.main.async { rejecter("E_READER_INIT", "Reader not initialized", nil) }
            self.isDecodingPlayback = false
            return
          }

          if reader.startReading() == false {
            self.isDecodingPlayback = false
            DispatchQueue.main.async { rejecter("E_READER_START", "Failed to start reading", reader.error) }
            return
          }

          self.playbackDecodeQueue?.async { [weak self] in
            guard let self = self else { return }
            var monoBuffer = [Float]()
            monoBuffer.reserveCapacity(Int(self.frameSize) * 2)
            var emittedSamples: Int64 = 0
            let startTime = Date()
            var currentSampleRate: Double = 48000.0

            while self.isDecodingPlayback, reader.status == .reading {
              guard let sampleBuffer = output.copyNextSampleBuffer() else { break }
              if let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer),
                 let formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer) {
                let asbdPtr = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc)
                let sampleRate = asbdPtr?.pointee.mSampleRate ?? 48000.0
                currentSampleRate = sampleRate
                let numChannels = Int(asbdPtr?.pointee.mChannelsPerFrame ?? 1)
                let frameCount = Int(CMSampleBufferGetNumSamples(sampleBuffer))
                if emittedSamples == 0 {
                  print("PlaybackDecode: reader started → rate=\(sampleRate)Hz, channels=\(numChannels), frameCount=\(frameCount)")
                }

                var lengthAtOffset: Int = 0
                var totalLength: Int = 0
                var dataPointer: UnsafeMutablePointer<Int8>?
                if CMBlockBufferGetDataPointer(blockBuffer, atOffset: 0, lengthAtOffsetOut: &lengthAtOffset, totalLengthOut: &totalLength, dataPointerOut: &dataPointer) == kCMBlockBufferNoErr,
                   let basePointer = dataPointer {
                  let totalFloats = totalLength / MemoryLayout<Float>.size
                  basePointer.withMemoryRebound(to: Float.self, capacity: totalFloats) { floatPtr in
                    if numChannels <= 1 {
                      let samples = UnsafeBufferPointer(start: floatPtr, count: min(totalFloats, frameCount))
                      monoBuffer.append(contentsOf: samples)
                    } else {
                      for i in stride(from: 0, to: min(totalFloats, frameCount * numChannels), by: numChannels) {
                        var sum: Float = 0
                        for c in 0..<numChannels { sum += floatPtr[i + c] }
                        monoBuffer.append(sum / Float(numChannels))
                      }
                    }
                  }

                  while monoBuffer.count >= Int(self.frameSize) {
                    let chunk = Array(monoBuffer.prefix(Int(self.frameSize)))
                    monoBuffer.removeFirst(Int(self.frameSize))
                    emittedSamples += Int64(chunk.count)
                    let elapsed = Date().timeIntervalSince(startTime)
                    let target = Double(emittedSamples) / sampleRate
                    if target > elapsed {
                      let sleepSec = target - elapsed
                      if sleepSec > 0 { usleep(useconds_t(min(sleepSec * 1_000_000.0, 200_000.0))) }
                    }
                    self.sendEvent(withName: "TLRN_AudioFrame", body: [
                      "sampleRate": sampleRate,
                      "buffer": chunk,
                      "source": "playback"
                    ])
                  }
                }
              }
              CMSampleBufferInvalidate(sampleBuffer)
            }

            if monoBuffer.count > 0 {
              let sampleRate = currentSampleRate
              self.sendEvent(withName: "TLRN_AudioFrame", body: [
                "sampleRate": sampleRate,
                "buffer": monoBuffer,
                "source": "playback"
              ])
              monoBuffer.removeAll(keepingCapacity: false)
            }

            print("PlaybackDecode: reader finished with status=\(reader.status.rawValue), error=\(String(describing: reader.error))")
            self.cleanupPlaybackReader()
          }
        }
      }
      task.resume()
      return
    }

    let asset = AVURLAsset(url: url)
    asset.loadValuesAsynchronously(forKeys: ["tracks"]) { [weak self] in
      guard let self = self else { return }
      var error: NSError?
      let status = asset.statusOfValue(forKey: "tracks", error: &error)
      if status != .loaded {
        DispatchQueue.main.async { rejecter("E_ASSET", "Failed to load asset tracks", error) }
        return
      }

      guard let track = asset.tracks(withMediaType: .audio).first else {
        DispatchQueue.main.async { rejecter("E_NO_AUDIO", "No audio track found", nil) }
        return
      }

      let settings: [String: Any] = [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVLinearPCMIsFloatKey: true,
        AVLinearPCMBitDepthKey: 32,
        AVLinearPCMIsNonInterleaved: false,
        AVLinearPCMIsBigEndianKey: false
      ]

      do {
        let reader = try AVAssetReader(asset: asset)
        let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
        if reader.canAdd(output) { reader.add(output) }
        self.playbackReader = reader
        self.playbackOutput = output
      } catch let readerError {
        DispatchQueue.main.async { rejecter("E_READER", "Failed to create asset reader", readerError) }
        return
      }

      guard let reader = self.playbackReader, let output = self.playbackOutput else {
        DispatchQueue.main.async { rejecter("E_READER_INIT", "Reader not initialized", nil) }
        return
      }

      self.playbackDecodeQueue = DispatchQueue(label: "com.tonelisten.playbackDecode")
      self.isDecodingPlayback = true

      if reader.startReading() == false {
        self.isDecodingPlayback = false
        DispatchQueue.main.async { rejecter("E_READER_START", "Failed to start reading", reader.error) }
        return
      }

      DispatchQueue.main.async { resolver(true) }

      self.playbackDecodeQueue?.async { [weak self] in
        guard let self = self else { return }
        var monoBuffer = [Float]()
        monoBuffer.reserveCapacity(Int(self.frameSize) * 2)
        var emittedSamples: Int64 = 0
        let startTime = Date()
        var currentSampleRate: Double = 48000.0

        while self.isDecodingPlayback, reader.status == .reading {
          guard let sampleBuffer = output.copyNextSampleBuffer() else { break }
          if let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer),
             let formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer) {
            let asbdPtr = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc)
            let sampleRate = asbdPtr?.pointee.mSampleRate ?? 48000.0
            currentSampleRate = sampleRate
            let numChannels = Int(asbdPtr?.pointee.mChannelsPerFrame ?? 1)
            let frameCount = Int(CMSampleBufferGetNumSamples(sampleBuffer))

            var lengthAtOffset: Int = 0
            var totalLength: Int = 0
            var dataPointer: UnsafeMutablePointer<Int8>?
            if CMBlockBufferGetDataPointer(blockBuffer, atOffset: 0, lengthAtOffsetOut: &lengthAtOffset, totalLengthOut: &totalLength, dataPointerOut: &dataPointer) == kCMBlockBufferNoErr,
               let basePointer = dataPointer {
              let totalFloats = totalLength / MemoryLayout<Float>.size
              basePointer.withMemoryRebound(to: Float.self, capacity: totalFloats) { floatPtr in
                if numChannels <= 1 {
                  // Mono
                  let samples = UnsafeBufferPointer(start: floatPtr, count: min(totalFloats, frameCount))
                  monoBuffer.append(contentsOf: samples)
                } else {
                  // Downmix interleaved stereo to mono
                  for i in stride(from: 0, to: min(totalFloats, frameCount * numChannels), by: numChannels) {
                    var sum: Float = 0
                    for c in 0..<numChannels { sum += floatPtr[i + c] }
                    monoBuffer.append(sum / Float(numChannels))
                  }
                }
              }

              // Emit fixed-size chunks to JS
              while monoBuffer.count >= Int(self.frameSize) {
                let chunk = Array(monoBuffer.prefix(Int(self.frameSize)))
                monoBuffer.removeFirst(Int(self.frameSize))
                emittedSamples += Int64(chunk.count)
                // Pace to real-time based on emitted sample count
                let elapsed = Date().timeIntervalSince(startTime)
                let target = Double(emittedSamples) / sampleRate
                if target > elapsed {
                  let sleepSec = target - elapsed
                  if sleepSec > 0 {
                    usleep(useconds_t(min(sleepSec * 1_000_000.0, 200_000.0)))
                  }
                }
                self.sendEvent(withName: "TLRN_AudioFrame", body: [
                  "sampleRate": sampleRate,
                  "buffer": chunk,
                  "source": "playback"
                ])
              }
            }
          }
          CMSampleBufferInvalidate(sampleBuffer)
        }

        // Flush remaining samples
        if monoBuffer.count > 0 {
          let sampleRate = currentSampleRate
          self.sendEvent(withName: "TLRN_AudioFrame", body: [
            "sampleRate": sampleRate,
            "buffer": monoBuffer,
            "source": "playback"
          ])
          monoBuffer.removeAll(keepingCapacity: false)
        }

        self.cleanupPlaybackReader()
      }
    }
  }

  @objc(stopPlaybackDecode:rejecter:)
  func stopPlaybackDecode(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    isDecodingPlayback = false
    cleanupPlaybackReader()
    // Restore session to measurement for mic capture readiness (without mixWithOthers)
    do {
      let session = AVAudioSession.sharedInstance()
      try session.setCategory(.playAndRecord, mode: .measurement, options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP, .allowAirPlay])
      try? session.setPreferredIOBufferDuration(0.005)
      try session.setActive(true)
    } catch {
      // ignore
    }
    resolver(true)
  }

  private func cleanupPlaybackReader() {
    playbackReader?.cancelReading()
    playbackReader = nil
    playbackOutput = nil
    playbackDecodeQueue = nil
    isDecodingPlayback = false
  }

  @objc(setAudioMode:resolver:rejecter:)
  func setAudioMode(_ mode: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    do {
      let session = AVAudioSession.sharedInstance()
      let audioMode: AVAudioSession.Mode
      
      switch mode.lowercased() {
      case "measurement":
        audioMode = .measurement
      case "default":
        audioMode = .default
      case "videoRecording":
        audioMode = .videoRecording
      case "moviePlayback":
        audioMode = .moviePlayback
      case "videoChat":
        audioMode = .videoChat
      case "gameChat":
        audioMode = .gameChat
      case "spokenAudio":
        audioMode = .spokenAudio
      default:
        rejecter("E_INVALID_MODE", "Invalid audio mode: \(mode)", nil)
        return
      }
      // Choose options based on mode to improve playback loudness while preserving capture
      let options: AVAudioSession.CategoryOptions = [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP, .allowAirPlay]

      try session.setCategory(.playAndRecord, mode: audioMode, options: options)
      // Force speaker when not in measurement to maximize output
      if audioMode != .measurement {
        try? session.overrideOutputAudioPort(.speaker)
      }
      try session.setActive(true)
      
      print("Audio mode switched to: \(audioMode.rawValue) with options: \(options)")
      resolver(true)
      
    } catch let error {
      print("Error switching audio mode: \(error.localizedDescription)")
      rejecter("E_MODE_SWITCH", "Failed to switch audio mode", error)
    }
  }
  
  @objc(getCurrentAudioMode:rejecter:)
  func getCurrentAudioMode(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    let session = AVAudioSession.sharedInstance()
    let currentMode = session.mode.rawValue
    resolver(currentMode)
  }
  
  @objc(isMediaPlaying:rejecter:)
  func isMediaPlaying(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    // Check if other audio is playing by looking at the audio session's secondary audio
    let session = AVAudioSession.sharedInstance()
    let isOtherAudioPlaying = session.isOtherAudioPlaying
    resolver(isOtherAudioPlaying)
  }

  // Prepare audio session for in-app media playback while preserving mic capture
  @objc(prepareForMediaPlayback:rejecter:)
  func prepareForMediaPlayback(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    let session = AVAudioSession.sharedInstance()
    do {
      if isPreparedForMediaPlayback {
        resolver(true)
        return
      }
      // Keep measurement mode for flat high-frequency capture and maximize output via speaker
      try session.setCategory(.playAndRecord,
                              mode: .measurement,
                              options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP])
      // Prefer hardware sample rate (48k) to avoid resampling during playback
      try? session.setPreferredSampleRate(48000)
      // Force speaker to avoid earpiece routing when in playAndRecord
      try? session.overrideOutputAudioPort(.speaker)
      // Slightly larger IO buffer during playback to reduce glitches under UI load
      try? session.setPreferredIOBufferDuration(0.01) // ~10ms
      try session.setActive(true)
      isPreparedForMediaPlayback = true
      print("Audio session prepared for media playback (mode: measurement, speaker, 10ms buffer)")
      resolver(true)
    } catch let error {
      print("Error preparing audio session for media playback: \(error.localizedDescription)")
      rejecter("E_PREPARE", "Failed to prepare media playback", error)
    }
  }

  // Restore audio session after media playback to measurement mode
  @objc(restoreAfterMediaPlayback:rejecter:)
  func restoreAfterMediaPlayback(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    let session = AVAudioSession.sharedInstance()
    do {
      try session.setCategory(.playAndRecord,
                              mode: .measurement,
                              options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP, .allowAirPlay])
      // Restore low-latency buffer for detection
      try? session.setPreferredIOBufferDuration(0.005)
      try session.setActive(true)
      isPreparedForMediaPlayback = false
      print("Audio session restored after media playback (mode: measurement, 5ms buffer)")
      resolver(true)
    } catch let error {
      print("Error restoring audio session after media playback: \(error.localizedDescription)")
      rejecter("E_RESTORE", "Failed to restore after media playback", error)
    }
  }

  @objc(stop:rejecter:)
  func stop(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
    stopEngine()
    engine = nil
    inputNode = nil
    
    // Remove interruption observer
    NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
    
    resolver(true)
  }
}
