import ReplayKit
import AVFoundation

// SampleHandler — the ReplayKit broadcast upload extension entry point (docs/voice.md §9, iOS).
//
// This runs in a SEPARATE process with a ~50 MB memory cap. It captures system/app audio
// (`.audioApp`), drops video and mic buffers, converts each chunk to the core's canonical
// format (48 kHz, int16, stereo interleaved) with AVAudioConverter, and writes it into the
// App Group shared-memory ring. The host app's BroadcastAudioPump drains the ring and feeds the
// already-connected VoiceCatClient — so all Opus/AEAD/UDP work happens in the host, and the
// extension stays tiny and well inside the memory budget (no libvoicecat here).
class SampleHandler: RPBroadcastSampleHandler {

    private var ring: BroadcastAudioRing?
    private var converter: AVAudioConverter?
    private var inputFormat: AVAudioFormat?
    private let outputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                             sampleRate: 48_000, channels: 2, interleaved: true)!

    override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
        ring = try? BroadcastAudioRing()
        ring?.setActive(true, channels: 2, sampleRate: 48_000)
        postDarwin(BroadcastNotification.started)
    }

    override func broadcastFinished() {
        ring?.setActive(false)
        postDarwin(BroadcastNotification.finished)
        ring = nil
    }

    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer,
                                      with sampleBufferType: RPSampleBufferType) {
        // App/system audio only — drop video (the memory hog) and the device mic (the host
        // already captures and sends the user's voice).
        guard sampleBufferType == .audioApp, let ring else { return }
        guard let input = makeInputBuffer(sampleBuffer),
              let conv = converter(for: input.format) else { return }

        let ratio = outputFormat.sampleRate / input.format.sampleRate
        let capacity = AVAudioFrameCount(Double(input.frameLength) * ratio) + 1024
        guard let output = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: capacity) else { return }

        var supplied = false
        var error: NSError?
        let status = conv.convert(to: output, error: &error) { _, outStatus in
            if supplied { outStatus.pointee = .noDataNow; return nil }
            supplied = true
            outStatus.pointee = .haveData
            return input
        }
        guard status != .error, output.frameLength > 0,
              let mData = output.audioBufferList.pointee.mBuffers.mData else { return }

        // Interleaved int16 → one buffer of frameLength * channels samples.
        let count = Int(output.frameLength) * Int(outputFormat.channelCount)
        ring.push(UnsafeBufferPointer(start: mData.assumingMemoryBound(to: Int16.self), count: count))
    }

    // MARK: - Helpers

    /// Wrap the ReplayKit CMSampleBuffer's PCM in an AVAudioPCMBuffer matching its native format.
    private func makeInputBuffer(_ sb: CMSampleBuffer) -> AVAudioPCMBuffer? {
        guard let fmtDesc = CMSampleBufferGetFormatDescription(sb),
              var asbd = CMAudioFormatDescriptionGetStreamBasicDescription(fmtDesc)?.pointee,
              let fmt = AVAudioFormat(streamDescription: &asbd) else { return nil }
        let frames = AVAudioFrameCount(CMSampleBufferGetNumSamples(sb))
        guard frames > 0, let buf = AVAudioPCMBuffer(pcmFormat: fmt, frameCapacity: frames) else { return nil }
        buf.frameLength = frames
        let status = CMSampleBufferCopyPCMDataIntoAudioBufferList(
            sb, at: 0, frameCount: Int32(frames), into: buf.mutableAudioBufferList)
        return status == noErr ? buf : nil
    }

    /// Reuse the converter while the input format is stable; rebuild if ReplayKit changes it.
    private func converter(for inFmt: AVAudioFormat) -> AVAudioConverter? {
        if let converter, inputFormat == inFmt { return converter }
        inputFormat = inFmt
        converter = AVAudioConverter(from: inFmt, to: outputFormat)
        return converter
    }

    private func postDarwin(_ name: String) {
        CFNotificationCenterPostNotification(
            CFNotificationCenterGetDarwinNotifyCenter(),
            CFNotificationName(name as CFString), nil, nil, true)
    }
}
