// 14D deep link 听写链路（主 App 进程）。
//
// 背景（1A spike 真机结论）：键盘扩展进程内无法录音 —— 即使"允许完全访问"开启，
// AVAudioSession setActive/start 也直接报 CoreAudio 错误 2003329396，iOS 平台硬限制。
// 因此语音主路径走设计文档预留的降级方案：键盘 MicBar → dudu://dictate → 本控制器
// 在主 App 进程录音推流 → 结果写剪贴板 + App Group pending_text → 用户切回原 App，
// dudu 键盘检出 pending_text 自动上屏。
//
// 链路：AVAudioEngine 采集（硬件原生格式，tap 写法参考 ios/spike-kb-mic）
//   → AVAudioConverter 重采样 16kHz/16bit/mono
//   → 100ms/3200B 分帧（DuduProtocol.frameBytes）
//   → WSS /v1/asr/stream 推流：上行 start → 二进制 PCM → stop/cancel，
//     下行 partial/final/usage/error（协议对齐 server/pkg/protocol/ws.go）。
// DEBUG + Api.offlineMock 时用 MockAsrDriver 假时序代替「录音 + WS」，无后端可演示。
import AVFoundation
import Foundation
import UIKit

// MARK: - 音频采集（AVAudioEngine → 16k/16bit/mono → 3200B 帧）

final class AsrAudioCapture {
    private let engine = AVAudioEngine()
    /// 21B：converter / outFormat / buffered / firstBufferSeen 由本串行队列保护。
    /// tap 回调在音频线程跑 consume()，stop() 在主线程置 nil —— 二者经此队列序列化，
    /// stop() removeTap 后再 q.sync 等在途回调跑完才置 nil，杜绝 EXC_BAD_ACCESS 数据竞争。
    private let q = DispatchQueue(label: "app.dudu.asr.capture")
    private var converter: AVAudioConverter?
    private var outFormat: AVAudioFormat?
    /// 重采样后字节缓冲（q 保护）
    private var buffered = Data()
    private var firstBufferSeen = false
    private var running = false

    /// 凑满 100ms/3200B 一帧回调一次（音频 tap 线程）
    var onFrame: ((Data) -> Void)?
    /// 首个音频缓冲到达（audio.start_ms 打点挂接，音频 tap 线程）
    var onFirstBuffer: (() -> Void)?

    func start() throws {
        guard !running else { return }
        // 会话配置与 1A spike 同款（spike 在键盘扩展进程报 2003329396，主 App 进程正常）
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playAndRecord, mode: .measurement, options: [.mixWithOthers])
        try session.setActive(true)

        let input = engine.inputNode
        let inFormat = input.outputFormat(forBus: 0)
        guard inFormat.sampleRate > 0, inFormat.channelCount > 0 else {
            throw ApiError(code: "AUDIO", message: "麦克风不可用")
        }
        guard let out = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                      sampleRate: Double(DuduProtocol.sampleRate),
                                      channels: 1, interleaved: true),
              let conv = AVAudioConverter(from: inFormat, to: out) else {
            throw ApiError(code: "AUDIO", message: "音频格式不支持")
        }
        q.sync {
            converter = conv
            outFormat = out
            buffered.removeAll()
            firstBufferSeen = false
        }

        input.removeTap(onBus: 0)
        // tap 回调在音频渲染线程串行执行；buffer 仅在回调期间有效，
        // 因此重采样必须在回调内同步完成，产出的 Data 才可跨线程传递
        input.installTap(onBus: 0, bufferSize: 2048, format: inFormat) { [weak self] buf, _ in
            self?.consume(buf)
        }
        engine.prepare()
        try engine.start()
        running = true
    }

    func stop() {
        guard running else { return }
        running = false
        // 先摘 tap，阻止新回调入队；再 q.sync 等在途 consume 跑完后才置 nil
        engine.inputNode.removeTap(onBus: 0)
        engine.stop()
        q.sync {
            converter = nil
            outFormat = nil
            buffered.removeAll()
        }
        // 残余不足一帧的尾巴（<100ms）直接丢弃，对识别影响可忽略
        try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
    }

    /// tap 回调（音频线程）：在 q 上序列化访问 converter/outFormat/buffered。
    /// buffer 仅在回调期间有效，故重采样与取帧都在 q.sync 内同步完成。
    private func consume(_ buf: AVAudioPCMBuffer) {
        var firstSeen = false
        var frames: [Data] = []
        q.sync {
            guard let converter, let outFormat else { return } // stop 已置 nil：丢弃本帧
            if !firstBufferSeen {
                firstBufferSeen = true
                firstSeen = true
            }
            let ratio = outFormat.sampleRate / buf.format.sampleRate
            let capacity = AVAudioFrameCount(Double(buf.frameLength) * ratio) + 64
            guard let out = AVAudioPCMBuffer(pcmFormat: outFormat, frameCapacity: capacity) else { return }
            var fed = false
            var convErr: NSError?
            // 单 buffer 喂入：本次给 buf，再要就 noDataNow（下个 tap 回调再来）
            converter.convert(to: out, error: &convErr) { _, status in
                if fed {
                    status.pointee = .noDataNow
                    return nil
                }
                fed = true
                status.pointee = .haveData
                return buf
            }
            guard convErr == nil, out.frameLength > 0, let ch = out.int16ChannelData else { return }
            buffered.append(Data(bytes: ch[0], count: Int(out.frameLength) * MemoryLayout<Int16>.size))
            while buffered.count >= DuduProtocol.frameBytes {
                frames.append(Data(buffered.prefix(DuduProtocol.frameBytes)))
                buffered.removeFirst(DuduProtocol.frameBytes)
            }
        }
        // 回调出口在 q 外触发（避免持锁回调引发的潜在重入 / 死锁）
        if firstSeen { onFirstBuffer?() }
        for frame in frames { onFrame?(frame) }
    }
}

// MARK: - WS 客户端（URLSessionWebSocketTask，wss /v1/asr/stream）

final class AsrStreamClient: NSObject, URLSessionWebSocketDelegate {
    /// 下行文本帧（主线程回调）
    var onMessage: ((DuduProtocol.ServerMsg) -> Void)?
    /// 握手完成（主线程，ws.connect_ms 打点挂接）
    var onOpen: (() -> Void)?
    /// 断开 / 发送失败（主线程，只回调一次）
    var onClose: ((String) -> Void)?

    private var session: URLSession?
    private var task: URLSessionWebSocketTask?
    private var closed = false
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    /// 握手：Authorization Bearer + X-Device-ID（对齐 gateway.go HandleWS）；
    /// URL 由 ApiConfig.baseURL 推导（http→ws / https→wss）。
    func connect(token: String, deviceID: String) {
        guard var comps = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
            emitClose("无效地址")
            return
        }
        comps.scheme = comps.scheme == "https" ? "wss" : "ws"
        comps.path = DuduProtocol.wsPath
        guard let url = comps.url else {
            emitClose("无效地址")
            return
        }
        var req = URLRequest(url: url)
        req.timeoutInterval = 10
        req.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
        req.setValue(deviceID, forHTTPHeaderField: "X-Device-ID")

        let cfg = URLSessionConfiguration.ephemeral
        cfg.timeoutIntervalForRequest = 10
        // delegateQueue = main：didOpen / didClose 直接回主线程
        let s = URLSession(configuration: cfg, delegate: self, delegateQueue: .main)
        session = s
        let t = s.webSocketTask(with: req)
        task = t
        receiveLoop(t)
        t.resume()
    }

    /// 上行控制帧（start / stop / cancel，文本帧）
    func send(_ msg: DuduProtocol.ClientMsg) {
        guard let task, let data = try? encoder.encode(msg),
              let text = String(data: data, encoding: .utf8) else { return }
        task.send(.string(text)) { [weak self] err in
            if err != nil {
                DispatchQueue.main.async { self?.emitClose("网络断开了") }
            }
        }
    }

    /// 二进制 PCM 帧（可从任意线程调用，URLSessionWebSocketTask 线程安全）。
    /// 帧级失败不单独上抛，由 receive 侧 / 控制帧失败收敛成一次 onClose。
    func send(frame: Data) {
        task?.send(.data(frame)) { _ in }
    }

    func close() {
        closed = true
        task?.cancel(with: .normalClosure, reason: nil)
        task = nil
        // URLSession 强持有 delegate（self），必须 invalidate 解环
        session?.invalidateAndCancel()
        session = nil
    }

    private func receiveLoop(_ t: URLSessionWebSocketTask) {
        t.receive { [weak self] result in
            guard let self else { return }
            switch result {
            case .failure:
                DispatchQueue.main.async { self.emitClose("网络断开了") }
            case .success(let message):
                if case .string(let text) = message,
                   let msg = try? self.decoder.decode(DuduProtocol.ServerMsg.self,
                                                      from: Data(text.utf8)) {
                    DispatchQueue.main.async { self.onMessage?(msg) }
                }
                self.receiveLoop(t)
            }
        }
    }

    private func emitClose(_ reason: String) {
        guard !closed else { return }
        closed = true
        onClose?(reason)
    }

    // MARK: URLSessionWebSocketDelegate（delegateQueue = main）

    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
                    didOpenWithProtocol protocolName: String?) {
        onOpen?()
    }

    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
                    didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        emitClose("连接已断开")
    }
}

// MARK: - 听写会话状态机（DictationView 的唯一状态源）

@MainActor
final class DictationController: ObservableObject {
    enum Phase: Equatable {
        case idle       // 待按
        case recording  // 按住中：采集 + 推流，partial/final 流式刷新
        case finishing  // 已松开：等 stop 后的结算 usage 帧（2.5s 兜底）
        case done       // 已定稿：已写剪贴板 + App Group pending_text
        case failed     // 错误态：errorText / errorCode
    }

    @Published private(set) var phase: Phase = .idle
    @Published private(set) var finalText = ""
    @Published private(set) var partialText = ""
    @Published private(set) var committedText = ""
    @Published private(set) var elapsedSeconds = 0
    @Published private(set) var errorText: String?
    @Published private(set) var errorCode: String?

    /// usage 帧实时回写余额 / 试用（由视图注入；App 持有其生命周期）
    weak var account: AccountStore?

    private let capture = AsrAudioCapture()
    private var ws: AsrStreamClient?
    #if DEBUG
    private let mock = MockAsrDriver()
    #endif

    private var sessionID = ""
    /// start 控制帧已发出；之前采到的帧先进 pendingFrames，发出后一次性补发
    private var started = false
    private var pendingFrames: [Data] = []
    private var timer: Timer?
    private var finishFallback: Task<Void, Never>?
    /// 21A：收尾阶段收到 final 后的短 grace 窗口，容纳连续尾部 final 再定稿
    private var finalGrace: Task<Void, Never>?

    // 打点（事件名严格对齐 MetricWhitelist，白名单外服务端直接丢弃）
    private var micDownAt: Date?
    private var connectAt: Date?
    private var firstPartialTracked = false
    private var audioStartTracked = false
    private var partialCount = 0

    /// 错误码 → 用户文案（对齐 errors.go ErrMessage，遵循文案规范：短句、无句号、"·"分隔）
    private static let errText: [String: String] = [
        "QUOTA_EXCEEDED": "今日试用已用完 · 充值后接着说",
        "UNAUTHORIZED": "登录失效了 · 去 dudu 重新登录",
        "ASR_UNAVAILABLE": "识别服务暂不可用 · 稍后再试",
        "SESSION_LIMIT": "单次最长 3 分钟 · 已自动定稿",
        "RATE_LIMITED": "说得太频繁了 · 稍后再试",
    ]

    // MARK: 按住开始

    func start(token: String) {
        guard phase != .recording, phase != .finishing else { return }
        resetSession()
        phase = .recording
        micDownAt = Date()
        startTimer()

        #if DEBUG
        if Api.offlineMock {
            // 离线演示：假时序代替「录音 + WS」，定稿 / 剪贴板 / App Group 流程不变
            mock.onMessage = { [weak self] msg in
                DispatchQueue.main.async { self?.handle(msg) }
            }
            mock.start()
            return
        }
        #endif

        sessionID = UUID().uuidString
        let client = AsrStreamClient()
        ws = client
        client.onOpen = { [weak self] in self?.wsDidOpen() }
        client.onMessage = { [weak self] msg in self?.handle(msg) }
        client.onClose = { [weak self] reason in self?.wsDidClose(reason) }
        connectAt = Date()
        client.connect(token: token, deviceID: MetricsQueue.shared.deviceID)

        // 录音立刻启动（不等握手）：帧先入 pendingFrames，start 帧发出后补发，
        // 把「按下 → 首个 partial」的延迟压到最低
        capture.onFirstBuffer = { [weak self] in
            DispatchQueue.main.async { self?.trackAudioStart() }
        }
        capture.onFrame = { [weak self] frame in
            DispatchQueue.main.async { self?.feed(frame) }
        }
        do {
            try capture.start()
        } catch {
            failWith(code: nil, message: "麦克风启动失败 · 再试一次")
        }
    }

    // MARK: 松开结束 / 上滑取消

    func stop(cancelled: Bool = false) {
        guard phase == .recording else { return }
        stopCaptureAndTimer()

        #if DEBUG
        if Api.offlineMock {
            mock.stop()
            finalize(cancelled: cancelled)
            return
        }
        #endif

        if cancelled {
            if started {
                ws?.send(DuduProtocol.ClientMsg(type: DuduProtocol.ClientMsgType.cancel,
                                                sessionID: sessionID))
            }
            finalize(cancelled: true)
            return
        }
        phase = .finishing
        if started {
            ws?.send(DuduProtocol.ClientMsg(type: DuduProtocol.ClientMsgType.stop,
                                            sessionID: sessionID))
        } // 未握手完成就松开：wsDidOpen 里补发 start + 帧 + stop
        armFinishFallback()
    }

    /// 视图消失时兜底清理（录音中离开按取消处理）
    func teardown() {
        if phase == .recording {
            stop(cancelled: true)
        }
        finishFallback?.cancel()
        finishFallback = nil
        finalGrace?.cancel()
        finalGrace = nil
        stopCaptureAndTimer()
        ws?.close()
        ws = nil
        #if DEBUG
        mock.stop()
        #endif
    }

    // MARK: WS 事件

    private func wsDidOpen() {
        if let t0 = connectAt {
            connectAt = nil
            MetricsQueue.shared.track(.wsConnectMs, props: [
                "ms": .number((Date().timeIntervalSince(t0) * 1000).rounded()),
            ])
        }
        guard phase == .recording || phase == .finishing else { return }
        ws?.send(DuduProtocol.ClientMsg(type: DuduProtocol.ClientMsgType.start,
                                        sessionID: sessionID,
                                        sampleRate: DuduProtocol.sampleRate,
                                        format: "pcm16"))
        started = true
        for frame in pendingFrames {
            ws?.send(frame: frame)
        }
        pendingFrames.removeAll()
        if phase == .finishing {
            // 极端时序：握手完成前用户已松开 —— 缓冲发完立刻 stop
            ws?.send(DuduProtocol.ClientMsg(type: DuduProtocol.ClientMsgType.stop,
                                            sessionID: sessionID))
        }
    }

    private func wsDidClose(_ reason: String) {
        switch phase {
        case .recording:
            failWith(code: nil, message: "网络断开了 · 稍后再试")
        case .finishing:
            finalize(cancelled: false) // 已收到的 final/partial 尽量不浪费
        default:
            break
        }
    }

    private func feed(_ frame: Data) {
        guard phase == .recording else { return }
        if started {
            ws?.send(frame: frame)
        } else if pendingFrames.count < 50 { // 握手前最多缓冲 5s，足够覆盖正常建连
            pendingFrames.append(frame)
        }
    }

    private func handle(_ msg: DuduProtocol.ServerMsg) {
        switch msg.type {
        case DuduProtocol.ServerMsgType.partial:
            partialCount += 1
            if !firstPartialTracked, let t0 = micDownAt {
                firstPartialTracked = true
                MetricsQueue.shared.track(.asrFirstPartialMs, props: [
                    "ms": .number((Date().timeIntervalSince(t0) * 1000).rounded()),
                ])
            }
            partialText = msg.text ?? "" // partial 为当前未定稿段全量文本，整帧替换

        case DuduProtocol.ServerMsgType.final:
            finalText += msg.text ?? "" // final 追加式定稿，随后 partial 清空
            partialText = ""
            // 21A：收尾阶段以 stop 后的 final 帧为主要结束依据。收到 final 后再开一个
            // 短 grace 窗口（容纳可能的连续尾部 final），窗口内无新 final 即定稿，
            // 避免被周期 usage 帧提前掐断而丢尾字。
            if phase == .finishing {
                armFinalGrace()
            }

        case DuduProtocol.ServerMsgType.usage:
            // 21A：usage 仅用于实时回写余额，不再当作收尾信号立即 finalize。
            // 服务端 usageLoop 每 2s 独立下发周期 usage，松开瞬间可能恰好落在 tick 窗口、
            // 早于尾部 final 到达——若据此 finalize 会丢字。收尾改以 final 帧 + grace 窗口
            // 为准，并由 armFinishFallback（2.5s）兜底服务端不补发 final 的极端情况。
            account?.applyUsage(balanceSeconds: msg.balanceSeconds,
                                trialRemaining: msg.trialRemaining)

        case DuduProtocol.ServerMsgType.error:
            let code = msg.code ?? "INTERNAL"
            // 21G：文案 server-first（errors.go 为真相源），本地表仅兜底未知码 / 服务端空文案
            let text = msg.message ?? Self.errText[code] ?? "出错了 · 再试一次"
            if code == "SESSION_LIMIT" {
                // 单会话超长：服务端已自动截断定稿，按完成处理（保留提示文案）
                errorText = text
                stopCaptureAndTimer()
                if phase == .recording {
                    phase = .finishing
                    armFinishFallback()
                }
            } else {
                failWith(code: code, message: text)
            }

        default:
            break
        }
    }

    // MARK: 收尾

    private func finalize(cancelled: Bool) {
        finishFallback?.cancel()
        finishFallback = nil
        finalGrace?.cancel()
        finalGrace = nil
        stopCaptureAndTimer()
        ws?.close()
        ws = nil
        started = false
        pendingFrames.removeAll()

        // 会话质量事件（白名单 asr.session；props.source 区分 deep link 听写链路，不含识别文本）
        MetricsQueue.shared.track(.asrSession, props: [
            "audio_seconds": .number(Double(elapsedSeconds)),
            "partial_count": .number(Double(partialCount)),
            "cancel": .bool(cancelled),
            "source": .string("dictate"),
        ])

        if cancelled {
            finalText = ""
            partialText = ""
            phase = .idle
            return
        }
        let committed = finalText + partialText
        guard !committed.isEmpty else {
            errorCode = nil
            errorText = "没听清 · 再说一次"
            phase = .failed
            return
        }
        committedText = committed
        // 21G：主通道 = App Group pending_text（dudu 键盘 120s 内自动上屏，且键盘 consume
        // 后立即清除，无一致性缝隙）。剪贴板降级为兜底：仅当 App Group 写失败（无签名调试 /
        // 键盘读不到）时才写剪贴板，避免每次 finalize 无条件污染用户剪贴板、造成重复粘贴。
        if !PendingTextStore.write(committed) {
            UIPasteboard.general.string = committed
        }
        phase = .done
    }

    private func failWith(code: String?, message: String) {
        finishFallback?.cancel()
        finishFallback = nil
        finalGrace?.cancel()
        finalGrace = nil
        stopCaptureAndTimer()
        ws?.close()
        ws = nil
        started = false
        pendingFrames.removeAll()
        errorCode = code
        errorText = message
        phase = .failed
    }

    // MARK: 杂项

    private func resetSession() {
        finalText = ""
        partialText = ""
        committedText = ""
        errorText = nil
        errorCode = nil
        elapsedSeconds = 0
        started = false
        pendingFrames.removeAll()
        firstPartialTracked = false
        audioStartTracked = false
        partialCount = 0
        connectAt = nil
    }

    private func startTimer() {
        timer?.invalidate()
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            Task { @MainActor in
                guard let self, self.phase == .recording else { return }
                self.elapsedSeconds += 1
                // 21E：单会话上限真相源是服务端 SESSION_LIMIT 帧（与 Android/桌面一致）。
                // 这里只保留更宽松的本地防御性安全上限（localSafetyCapSeconds=200s），
                // 仅在服务端异常不下发 SESSION_LIMIT 时兜底，避免无限录音。
                if self.elapsedSeconds >= DuduProtocol.localSafetyCapSeconds {
                    self.stop()
                }
            }
        }
    }

    private func stopCaptureAndTimer() {
        timer?.invalidate()
        timer = nil
        capture.stop()
    }

    private func armFinishFallback() {
        finishFallback?.cancel()
        finishFallback = Task { [weak self] in
            try? await Task.sleep(nanoseconds: 2_500_000_000)
            guard let self, !Task.isCancelled, self.phase == .finishing else { return }
            self.finalize(cancelled: false) // 尾部 final / 结算帧迟迟不到：用已有文本定稿
        }
    }

    /// 21A：收尾阶段收到 final 后开一个短 grace 窗口（600ms）。窗口内每来一帧 final
    /// 重置窗口，确保连续尾部 final 全部落袋；窗口静默到点即定稿。比 2.5s 兜底更快，
    /// 又不会被周期 usage 提前掐断。
    private func armFinalGrace() {
        finalGrace?.cancel()
        finalGrace = Task { [weak self] in
            try? await Task.sleep(nanoseconds: 600_000_000)
            guard let self, !Task.isCancelled, self.phase == .finishing else { return }
            self.finalize(cancelled: false)
        }
    }

    private func trackAudioStart() {
        guard !audioStartTracked, let t0 = micDownAt else { return }
        audioStartTracked = true
        MetricsQueue.shared.track(.audioStartMs, props: [
            "ms": .number((Date().timeIntervalSince(t0) * 1000).rounded()),
        ])
    }
}
