import AVFoundation
import CoreAudio

// InputDeviceCapture — captures a single hardware INPUT device (mic / line-in / aux) on macOS and
// emits 20 ms (960 samples/channel @ 48 kHz, interleaved int16) frames for the aux outgoing stream.
//
// The input-device analogue of ScreenAudioCapture (which captures system audio via
// ScreenCaptureKit). The core already owns ONE capture device (the mic) and can't open a second
// arbitrary input, so for the aux stream the client captures the chosen device here and feeds PCM
// into the core via `vc_stream_feed_pcm` — the same external-feed pipeline screen audio uses.
//
// Device selection: an AVAudioEngine's input node wraps an AUHAL audio unit; setting
// kAudioOutputUnitProperty_CurrentDevice on it pins capture to a specific Core Audio device. We
// pass the device's *UID* (stable across reboots/replugs, unlike AudioDeviceID) and resolve it at
// start. The tap delivers Float32; AVAudioConverter resamples/quantises to 48 kHz int16.
final class InputDeviceCapture {

    /// Receives a full 20 ms frame: (interleaved int16 PCM, samplesPerChannel = 960, channels).
    typealias PcmHandler = (UnsafePointer<Int16>, Int, UInt32) -> Void

    enum CaptureError: Error { case deviceNotFound, engineStartFailed(OSStatus) }

    private static let frameSamplesPerChannel = 960  // 20 ms @ 48 kHz

    private let deviceUID: String?  // nil = system default input device
    private let onPcm: PcmHandler
    private let engine = AVAudioEngine()
    private var converter: AVAudioConverter?
    private var outFormat: AVAudioFormat?
    private var channels = 1

    /// Interleaved int16 carry-over between tap callbacks (the tap buffer doesn't align to 20 ms),
    /// drained in whole `frameSamplesPerChannel * channels` chunks. Only touched on the tap queue.
    private var pending: [Int16] = []

    init(deviceUID: String?, onPcm: @escaping PcmHandler) {
        self.deviceUID = deviceUID
        self.onPcm = onPcm
    }

    /// Begin capture. Throws if the device can't be resolved or the engine fails to start.
    func start() throws {
        let input = engine.inputNode

        // Pin the engine's AUHAL to the chosen device (skip for default — the engine already uses
        // the system default input). Must happen before reading inputFormat, which changes with
        // the selected device.
        if let deviceUID, let devId = Self.deviceID(forUID: deviceUID) {
            var dev = devId
            let status = AudioUnitSetProperty(input.audioUnit!,
                                              kAudioOutputUnitProperty_CurrentDevice,
                                              kAudioUnitScope_Global, 0,
                                              &dev, UInt32(MemoryLayout<AudioDeviceID>.size))
            if status != noErr { throw CaptureError.engineStartFailed(status) }
        } else if deviceUID != nil {
            throw CaptureError.deviceNotFound
        }

        let inFormat = input.inputFormat(forBus: 0)
        channels = max(1, min(2, Int(inFormat.channelCount)))
        guard let out = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 48000,
                                      channels: AVAudioChannelCount(channels), interleaved: true)
        else { throw CaptureError.deviceNotFound }
        outFormat = out
        converter = AVAudioConverter(from: inFormat, to: out)

        input.installTap(onBus: 0, bufferSize: 960, format: inFormat) { [weak self] buf, _ in
            self?.process(buf)
        }
        engine.prepare()
        do { try engine.start() }
        catch { throw CaptureError.engineStartFailed(-1) }
    }

    /// Stop capture and tear down the engine. Safe to call multiple times.
    func stop() {
        engine.inputNode.removeTap(onBus: 0)
        if engine.isRunning { engine.stop() }
        converter = nil
        pending.removeAll(keepingCapacity: false)
    }

    // MARK: - Conversion (called on the tap's realtime thread)

    private func process(_ inBuf: AVAudioPCMBuffer) {
        guard let converter, let outFormat else { return }

        // Output capacity must cover up-sampling (e.g. 44.1 → 48 kHz) plus slack.
        let ratio = outFormat.sampleRate / inBuf.format.sampleRate
        let cap = AVAudioFrameCount(Double(inBuf.frameLength) * ratio + 32)
        guard cap > 0, let outBuf = AVAudioPCMBuffer(pcmFormat: outFormat, frameCapacity: cap)
        else { return }

        var fed = false
        var err: NSError?
        let status = converter.convert(to: outBuf, error: &err) { _, outStatus in
            if fed { outStatus.pointee = .noDataNow; return nil }
            fed = true
            outStatus.pointee = .haveData
            return inBuf
        }
        guard status != .error, outBuf.frameLength > 0,
              let ch = outBuf.int16ChannelData else { return }

        // Interleaved int16: all channels live in the first buffer (ch[0]).
        let n = Int(outBuf.frameLength) * channels
        pending.append(contentsOf: UnsafeBufferPointer(start: ch[0], count: n))
        emit()
    }

    /// Fire `onPcm` for every whole 20 ms frame accumulated.
    private func emit() {
        let full = Self.frameSamplesPerChannel * channels
        while pending.count >= full {
            pending.withUnsafeBufferPointer { buf in
                onPcm(buf.baseAddress!, Self.frameSamplesPerChannel, UInt32(channels))
            }
            pending.removeFirst(full)
        }
    }

    // MARK: - UID → AudioDeviceID resolution

    private static func deviceID(forUID uid: String) -> AudioDeviceID? {
        var addr = AudioObjectPropertyAddress(
            mSelector: kAudioHardwarePropertyTranslateUIDToDevice,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMain)
        var deviceID = AudioDeviceID(0)
        var cfUID = uid as CFString
        var size = UInt32(MemoryLayout<AudioDeviceID>.size)
        let status = withUnsafeMutablePointer(to: &cfUID) { uidPtr -> OSStatus in
            AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &addr,
                                       UInt32(MemoryLayout<CFString>.size), uidPtr,
                                       &size, &deviceID)
        }
        return (status == noErr && deviceID != 0) ? deviceID : nil
    }
}

// InputDeviceInfo / InputDeviceEnumerator — Core Audio input-device enumeration for the aux-stream
// picker. Separate from the core's vc_list_devices (whose ids are miniaudio-opaque and can't be
// passed to Core Audio); the aux device is client-captured, so the picker uses device UIDs.
struct InputDeviceInfo: Equatable {
    let uid: String       // stable across reboots/replugs — what we persist
    let name: String
    let isDefault: Bool
}

enum InputDeviceEnumerator {

    /// All Core Audio devices that expose at least one input channel.
    static func list() -> [InputDeviceInfo] {
        let defaultUID = defaultInputUID()
        var result: [InputDeviceInfo] = []

        for devID in allDeviceIDs() {
            guard inputChannelCount(devID) > 0 else { continue }
            guard let uid = stringProperty(devID, kAudioDevicePropertyDeviceUID) else { continue }
            let name = stringProperty(devID, kAudioObjectPropertyName)
                ?? stringProperty(devID, kAudioDevicePropertyDeviceNameCFString)
                ?? "Unknown input device"
            result.append(InputDeviceInfo(uid: uid, name: name, isDefault: uid == defaultUID))
        }
        return result.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
    }

    // MARK: - Core Audio helpers

    private static func allDeviceIDs() -> [AudioDeviceID] {
        var addr = AudioObjectPropertyAddress(
            mSelector: kAudioHardwarePropertyDevices,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMain)
        var size = UInt32(0)
        guard AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &addr, 0, nil,
                                             &size) == noErr, size > 0 else { return [] }
        let count = Int(size) / MemoryLayout<AudioDeviceID>.size
        var ids = [AudioDeviceID](repeating: 0, count: count)
        let status = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &addr, 0,
                                                nil, &size, &ids)
        return status == noErr ? ids : []
    }

    private static func inputChannelCount(_ devID: AudioDeviceID) -> Int {
        var addr = AudioObjectPropertyAddress(
            mSelector: kAudioDevicePropertyStreamConfiguration,
            mScope: kAudioObjectPropertyScopeInput,
            mElement: kAudioObjectPropertyElementMain)
        var size = UInt32(0)
        guard AudioObjectGetPropertyDataSize(devID, &addr, 0, nil, &size) == noErr, size > 0
        else { return 0 }
        let bufList = UnsafeMutableRawPointer.allocate(byteCount: Int(size),
                                                       alignment: MemoryLayout<AudioBufferList>.alignment)
        defer { bufList.deallocate() }
        guard AudioObjectGetPropertyData(devID, &addr, 0, nil, &size, bufList) == noErr
        else { return 0 }
        let abl = UnsafeMutableAudioBufferListPointer(
            bufList.assumingMemoryBound(to: AudioBufferList.self))
        return abl.reduce(0) { $0 + Int($1.mNumberChannels) }
    }

    private static func defaultInputUID() -> String? {
        var addr = AudioObjectPropertyAddress(
            mSelector: kAudioHardwarePropertyDefaultInputDevice,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMain)
        var devID = AudioDeviceID(0)
        var size = UInt32(MemoryLayout<AudioDeviceID>.size)
        guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &addr, 0, nil,
                                         &size, &devID) == noErr, devID != 0 else { return nil }
        return stringProperty(devID, kAudioDevicePropertyDeviceUID)
    }

    private static func stringProperty(_ devID: AudioDeviceID,
                                       _ selector: AudioObjectPropertySelector) -> String? {
        var addr = AudioObjectPropertyAddress(
            mSelector: selector,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMain)
        var cf: CFString? = nil
        var size = UInt32(MemoryLayout<CFString?>.size)
        let status = withUnsafeMutablePointer(to: &cf) { ptr in
            AudioObjectGetPropertyData(devID, &addr, 0, nil, &size, ptr)
        }
        guard status == noErr else { return nil }
        return cf as String?
    }
}
