//
//  ScenicQueueTTSManager.swift
//  suixinkan
//

import AVFoundation
import Foundation

/// 排队语音播报服务协议，隔离具体 TTS SDK。
protocol ScenicQueueTTSManaging: AnyObject {
    /// 自定义文案是否正在循环播报。
    var customTextLooping: Bool { get }

    /// 是否支持离线语音合成。
    var supportsOfflineTTS: Bool { get }

    /// 预热排队 TTS 引擎。
    func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async

    /// 开始循环播报指定排号。
    func startQueueCallLoop(queueNo: String)

    /// 停止叫号循环。
    func stopQueueCallLoop()

    /// 单次播报普通文本。
    @discardableResult
    func speakPlainTextOnce(_ text: String) async -> Bool

    /// 开始循环播报自定义文本。
    @discardableResult
    func startCustomTextLoop(_ text: String) async -> Bool

    /// 停止自定义文本循环。
    func stopCustomTextLoop()

    /// 中断拍摄倒计时等临时播报。
    func interruptShootingAnnouncement()

    /// 估算播报耗时。
    func estimateAnnouncementDurationMillis(_ text: String) -> Int64
}

/// 主线程隔离的系统语音合成器，统一管理播放状态与异步 continuation。
@MainActor
private final class SystemVoiceSynthesizer: NSObject, @preconcurrency AVSpeechSynthesizerDelegate {
    private let synthesizer = AVSpeechSynthesizer()
    private var currentContinuation: CheckedContinuation<Void, Never>?
    private var currentUtterance: AVSpeechUtterance?

    nonisolated override init() {
        super.init()
    }

    /// 播放一段中文文本，并在完成或取消后结束等待。
    func speak(_ text: String) async {
        synthesizer.delegate = self
        finishCurrentSpeech()
        if synthesizer.isSpeaking {
            synthesizer.stopSpeaking(at: .immediate)
        }

        await withCheckedContinuation { continuation in
            let utterance = AVSpeechUtterance(string: text)
            utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
            utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.86
            utterance.volume = 1.0
            currentContinuation = continuation
            currentUtterance = utterance
            synthesizer.speak(utterance)
        }
    }

    /// 停止当前播放并释放等待中的调用方。
    func stop() {
        finishCurrentSpeech()
        if synthesizer.isSpeaking {
            synthesizer.stopSpeaking(at: .immediate)
        }
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        finishCurrentSpeech(matching: utterance)
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
        finishCurrentSpeech(matching: utterance)
    }

    private func finishCurrentSpeech(matching utterance: AVSpeechUtterance? = nil) {
        if let utterance, utterance !== currentUtterance {
            return
        }
        currentUtterance = nil
        currentContinuation?.resume()
        currentContinuation = nil
    }
}

/// 阿里云 NLS TTS 管理器，优先使用 NUI SDK，失败时回退到系统中文语音。
///
/// 线程安全性：循环状态只在 `loopQueue` 读写，原生引擎状态只在 `nativeQueue` 读写，
/// 系统语音合成由 `SystemVoiceSynthesizer` 的主 Actor 隔离保护。
final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, @unchecked Sendable {
    static let shared = AliyunNLSTTSManager()

    private enum LoopKind {
        case queueCall(String)
        case custom(String)
    }

    /// 当前已初始化的阿里云原生 TTS 引擎类型。
    private enum NativeEngineKind {
        case online
        case offline
    }

    /// 离线 TTS 工作目录及默认发音人资源。
    private struct OfflineResources {
        let workspace: String
        let voicePath: String
    }

    private let systemVoiceSynthesizer = SystemVoiceSynthesizer()
    private let loopQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.tts")
    private let nativeQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.aliyunNLS")
    private let appStore: AppStore
    private let tokenProvider: AliyunNLSTokenProviding
    private let bridge: AliyunNuiTTSBridging
    private let streamingPlayer: AliyunPCMStreamingPlaying
    private let workspacePathProvider: () -> String?
    private var activeLoop: LoopKind?
    private var nativeContinuation: CheckedContinuation<Bool, Never>?
    private var nativeReady = false
    private var nativeEngineKind: NativeEngineKind?
    private var nativeSampleRate: Double
    private var customTextLoopingStorage = false

    var customTextLooping: Bool {
        loopQueue.sync { customTextLoopingStorage }
    }

    var supportsOfflineTTS: Bool {
        bridge.sdkAvailable && offlineResources() != nil
    }

    init(
        appStore: AppStore = .shared,
        tokenProvider: AliyunNLSTokenProviding = AliyunNLSTokenProvider(),
        bridge: AliyunNuiTTSBridging = AliyunNuiTTSBridgeAdapter(),
        streamingPlayer: AliyunPCMStreamingPlaying = AliyunPCMStreamingPlayer(),
        workspacePathProvider: (() -> String?)? = nil
    ) {
        self.appStore = appStore
        self.tokenProvider = tokenProvider
        self.bridge = bridge
        self.streamingPlayer = streamingPlayer
        self.workspacePathProvider = workspacePathProvider ?? Self.bundledWorkspacePath
        nativeSampleRate = AliyunNLSTTSManager.onlineSampleRate
        super.init()
        self.bridge.delegate = self
    }

    func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
        guard let api else { return }
        guard !appStore.scenicQueue.useOfflineTTS || supportsOfflineTTS else {
            await setNativeReady(false)
            return
        }
        do {
            let sts = try await api.aliyunSTS(bucket: "vipsky")
            let token = try await tokenProvider.createToken(credentials: sts.credentials)
            let isReady = await prepareNativeEngineIfAvailable(token: token, credentials: sts.credentials)
            await setNativeReady(isReady)
        } catch {
            await setNativeReady(false)
        }
    }

    func startQueueCallLoop(queueNo: String) {
        let text = Self.callAnnouncementText(queueNo: queueNo)
        loopQueue.async { [weak self] in
            guard let self else { return }
            self.activeLoop = .queueCall(queueNo)
            self.customTextLoopingStorage = false
            Task { await self.runLoop(text: text, expected: .queueCall(queueNo)) }
        }
    }

    func stopQueueCallLoop() {
        loopQueue.async { [weak self] in
            guard let self else { return }
            if case .queueCall = self.activeLoop {
                self.activeLoop = nil
            }
            self.stopSpeaking()
        }
    }

    @discardableResult
    func speakPlainTextOnce(_ text: String) async -> Bool {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return false }
        await speak(trimmed)
        return true
    }

    @discardableResult
    func startCustomTextLoop(_ text: String) async -> Bool {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return false }
        loopQueue.async { [weak self] in
            guard let self else { return }
            self.activeLoop = .custom(trimmed)
            self.customTextLoopingStorage = true
            Task { await self.runLoop(text: trimmed, expected: .custom(trimmed)) }
        }
        return true
    }

    func stopCustomTextLoop() {
        loopQueue.async { [weak self] in
            guard let self else { return }
            if case .custom = self.activeLoop {
                self.activeLoop = nil
            }
            self.customTextLoopingStorage = false
            self.stopSpeaking()
        }
    }

    func interruptShootingAnnouncement() {
        stopSpeaking()
    }

    func estimateAnnouncementDurationMillis(_ text: String) -> Int64 {
        let count = max(1, text.count)
        return Int64(min(max(850 + count * 200, 900), 12_000))
    }

    /// 排队号 TTS 格式化，降低字母数字连读概率。
    static func formatQueueNoForTTS(_ queueNo: String) -> String {
        let trimmed = queueNo.trimmingCharacters(in: .whitespacesAndNewlines)
        let core = trimmed.hasSuffix("号") ? String(trimmed.dropLast()) : trimmed
        guard !core.isEmpty else { return trimmed.isEmpty ? "请" : trimmed }
        let pattern = #"^([A-Za-z]+)([0-9]+)$"#
        if let regex = try? NSRegularExpression(pattern: pattern),
           let match = regex.firstMatch(in: core, range: NSRange(core.startIndex..., in: core)),
           let lettersRange = Range(match.range(at: 1), in: core),
           let digitsRange = Range(match.range(at: 2), in: core) {
            return "\(core[lettersRange].uppercased())，\(core[digitsRange])号"
        }
        return "\(core)号"
    }

    /// 叫号播报文案。
    static func callAnnouncementText(queueNo: String) -> String {
        "\(formatQueueNoForTTS(queueNo))，请到拍摄区域，准备拍摄。"
    }

    private func runLoop(text: String, expected: LoopKind) async {
        while isLoopStillActive(expected) {
            await speak(text)
            guard isLoopStillActive(expected) else { break }
            try? await Task.sleep(nanoseconds: 1_000_000_000)
        }
        if case .custom = expected {
            loopQueue.async { [weak self] in
                self?.customTextLoopingStorage = false
            }
        }
    }

    private func isLoopStillActive(_ expected: LoopKind) -> Bool {
        loopQueue.sync {
            switch (activeLoop, expected) {
            case (.queueCall(let lhs), .queueCall(let rhs)):
                lhs == rhs
            case (.custom(let lhs), .custom(let rhs)):
                lhs == rhs
            default:
                false
            }
        }
    }

    private func speak(_ text: String) async {
        if await speakWithNativeEngine(text) {
            return
        }
        await speakWithSystemVoice(text)
    }

    private func speakWithNativeEngine(_ text: String) async -> Bool {
        await withCheckedContinuation { continuation in
            nativeQueue.async { [weak self] in
                guard let self, self.nativeReady, self.bridge.sdkAvailable else {
                    continuation.resume(returning: false)
                    return
                }
                self.cancelNativeSpeakLocked(resumeExisting: true)
                self.nativeContinuation = continuation
                self.streamingPlayer.stop()
                self.streamingPlayer.start(sampleRate: self.nativeSampleRate)
                self.applyNativePlaybackParams(text: text)
                let code = self.bridge.play(priority: "1", taskId: Self.nuiTaskId, text: text)
                if code != 0 {
                    self.streamingPlayer.stop()
                    self.nativeContinuation = nil
                    continuation.resume(returning: false)
                }
            }
        }
    }

    private func setNativeReady(_ isReady: Bool) async {
        await withCheckedContinuation { continuation in
            nativeQueue.async { [weak self] in
                self?.nativeReady = isReady
                continuation.resume()
            }
        }
    }

    private func speakWithSystemVoice(_ text: String) async {
        await systemVoiceSynthesizer.speak(text)
    }

    private func stopSpeaking() {
        nativeQueue.async { [weak self] in
            self?.cancelNativeSpeakLocked(resumeExisting: true)
        }
        Task { @MainActor [systemVoiceSynthesizer] in
            systemVoiceSynthesizer.stop()
        }
    }

    private func prepareNativeEngineIfAvailable(
        token: AliyunNLSToken,
        credentials: AliyunOSSCredentials
    ) async -> Bool {
        guard bridge.sdkAvailable else {
            return false
        }
        let wantsOffline = appStore.scenicQueue.useOfflineTTS
        let offlineResources = wantsOffline ? offlineResources() : nil
        guard !wantsOffline || offlineResources != nil else { return false }
        guard let ticket = wantsOffline
            ? nativeOfflineTicket(token: token.id, credentials: credentials, resources: offlineResources!)
            : nativeOnlineTicket(token: token.id) else { return false }

        return await withCheckedContinuation { continuation in
            nativeQueue.async { [weak self] in
                guard let self else {
                    continuation.resume(returning: false)
                    return
                }
                self.cancelNativeSpeakLocked(resumeExisting: true)
                _ = self.bridge.releaseEngine()
                self.nativeEngineKind = nil
                let code = self.bridge.initialize(ticket: ticket, logLevel: 0, saveLog: true)
                guard code == 0 else {
                    continuation.resume(returning: false)
                    return
                }
                let configured: Bool
                if let offlineResources {
                    configured = self.configureOfflineEngine(resources: offlineResources)
                } else {
                    self.nativeEngineKind = .online
                    self.nativeSampleRate = Self.onlineSampleRate
                    self.applyNativeOnlineQueueProsodyParams()
                    configured = true
                }
                guard configured else {
                    _ = self.bridge.releaseEngine()
                    self.nativeEngineKind = nil
                    continuation.resume(returning: false)
                    return
                }
                continuation.resume(returning: true)
            }
        }
    }

    private func nativeOnlineTicket(token: String) -> String? {
        guard let workspace = workspacePathProvider() else { return nil }
        let ticket: [String: String] = [
            "app_key": Self.onlineAppKey,
            "token": token,
            "workspace": workspace,
            "debug_path": nativeDebugPath(),
            "url": "wss://nls-gateway.cn-shanghai.aliyuncs.com:443/ws/v1",
            "device_id": Self.deviceId(),
            "mode_type": "2",
            "tts_version": "0",
        ]
        guard let data = try? JSONSerialization.data(withJSONObject: ticket) else { return nil }
        return String(data: data, encoding: .utf8)
    }

    private func nativeOfflineTicket(
        token: String,
        credentials: AliyunOSSCredentials,
        resources: OfflineResources
    ) -> String? {
        let ticket: [String: String] = [
            "app_key": Self.offlineAppKey,
            "ak_id": credentials.accessKeyId,
            "ak_secret": credentials.accessKeySecret,
            "sts_token": credentials.securityToken,
            "token": token,
            "sdk_code": Self.offlineSDKCode,
            "save_wav": "false",
            "debug_path": nativeDebugPath(),
            "workspace": resources.workspace,
            "log_track_level": "5",
            "mode_type": "0",
            "device_id": Self.deviceId(),
        ]
        guard let data = try? JSONSerialization.data(withJSONObject: ticket) else { return nil }
        return String(data: data, encoding: .utf8)
    }

    private static func bundledWorkspacePath() -> String? {
        if let privateFrameworksPath = Bundle.main.privateFrameworksPath {
            let frameworkPath = (privateFrameworksPath as NSString).appendingPathComponent("nuisdk.framework")
            if let frameworkBundle = Bundle(path: frameworkPath),
               let path = frameworkBundle.path(forResource: "Resources", ofType: "bundle") {
                return path
            }
        }
        return Bundle.main.path(forResource: "Resources", ofType: "bundle")
    }

    private func offlineResources() -> OfflineResources? {
        guard let workspace = workspacePathProvider() else { return nil }
        let ttsPath = (workspace as NSString).appendingPathComponent("tts")
        let languagePath = (ttsPath as NSString).appendingPathComponent("languagedata_embedded.bin")
        let parameterPath = (ttsPath as NSString).appendingPathComponent("parameter.cfg")
        let voicePath = (ttsPath as NSString).appendingPathComponent("voices/\(Self.offlineVoiceName)")
        let fileManager = FileManager.default
        guard fileManager.fileExists(atPath: languagePath),
              fileManager.fileExists(atPath: parameterPath),
              fileManager.fileExists(atPath: voicePath) else { return nil }
        return OfflineResources(workspace: workspace, voicePath: voicePath)
    }

    private func nativeDebugPath() -> String {
        let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
        let url = (caches ?? URL(fileURLWithPath: NSTemporaryDirectory())).appendingPathComponent("aliyun_nls_tts_debug", isDirectory: true)
        try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url.path
    }

    private func applyNativePlaybackParams(text: String) {
        switch nativeEngineKind {
        case .online:
            _ = bridge.setParam("mode_type", value: "2")
            _ = bridge.setParam("tts_version", value: text.count > 300 ? "1" : "0")
            applyNativeOnlineQueueProsodyParams()
        case .offline:
            applyNativeOfflineQueueProsodyParams()
        case nil:
            break
        }
    }

    private func applyNativeOnlineQueueProsodyParams() {
        _ = bridge.setParam("speech_rate", value: Self.onlineSpeechRate)
        _ = bridge.setParam("volume", value: Self.playbackVolume)
    }

    private func configureOfflineEngine(resources: OfflineResources) -> Bool {
        guard bridge.setParam("extend_font_name", value: resources.voicePath) == 0 else { return false }
        guard bridge.setParam("encode_type", value: "pcm") == 0 else { return false }
        guard bridge.setParam("sample_rate", value: Self.offlineSampleRateText) == 0 else { return false }
        nativeEngineKind = .offline
        nativeSampleRate = Double(bridge.paramValue(for: "model_sample_rate") ?? "")
            .flatMap { $0 > 0 ? $0 : nil } ?? Self.offlineSampleRate
        applyNativeOfflineQueueProsodyParams()
        return true
    }

    private func applyNativeOfflineQueueProsodyParams() {
        _ = bridge.setParam("speed_level", value: Self.offlineSpeedLevel)
        _ = bridge.setParam("volume", value: Self.playbackVolume)
    }

    private func cancelNativeSpeakLocked(resumeExisting: Bool) {
        _ = bridge.cancel(taskId: Self.nuiTaskId)
        streamingPlayer.stop()
        if resumeExisting {
            finishNativeSpeakLocked(success: false)
        } else {
            nativeContinuation = nil
        }
    }

    private func finishNativeSpeakLocked(success: Bool) {
        let continuation = nativeContinuation
        nativeContinuation = nil
        continuation?.resume(returning: success)
    }

    private static func deviceId() -> String {
        let key = "aliyun_nls_tts_device_id_v1"
        if let value = UserDefaults.standard.string(forKey: key), !value.isEmpty {
            return value
        }
        let value = UUID().uuidString
        UserDefaults.standard.set(value, forKey: key)
        return value
    }

    private static let onlineAppKey = "3WX8cD04JCGkbRLi"
    private static let offlineAppKey = "gfWimmOuTku1AbIV"
    private static let offlineSDKCode = "software_nls_tts_offline_standard"
    private static let offlineVoiceName = "aijia"
    private static let nuiTaskId = "1"
    private static let onlineSampleRate = 16_000.0
    private static let offlineSampleRate = 24_000.0
    private static let offlineSampleRateText = "24000"
    private static let onlineSpeechRate = "-120"
    private static let offlineSpeedLevel = "1.0"
    private static let playbackVolume = "2.0"
}

/// 阿里云 NUI TTS Swift 桥接协议。
protocol AliyunNuiTTSBridging: AnyObject {
    /// SDK 回调代理。
    var delegate: AliyunNuiTTSBridgeDelegate? { get set }

    /// 当前平台是否可使用 SDK。
    var sdkAvailable: Bool { get }

    /// 初始化引擎。
    func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int

    /// 设置参数。
    func setParam(_ param: String, value: String) -> Int

    /// 开始播放。
    func play(priority: String, taskId: String, text: String) -> Int

    /// 取消任务。
    func cancel(taskId: String?) -> Int

    /// 释放引擎。
    func releaseEngine() -> Int

    /// 读取 SDK 参数。
    func paramValue(for key: String) -> String?
}

/// 阿里云 NUI TTS Objective-C 桥接器的 Swift 适配器。
final class AliyunNuiTTSBridgeAdapter: AliyunNuiTTSBridging {
    private let bridge: AliyunNuiTTSBridge

    var delegate: AliyunNuiTTSBridgeDelegate? {
        get { bridge.delegate }
        set { bridge.delegate = newValue }
    }

    var sdkAvailable: Bool { bridge.sdkAvailable }

    init(bridge: AliyunNuiTTSBridge = AliyunNuiTTSBridge()) {
        self.bridge = bridge
    }

    func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
        bridge.initialize(withTicket: ticket, logLevel: logLevel, saveLog: saveLog)
    }

    func setParam(_ param: String, value: String) -> Int {
        bridge.setParam(param, value: value)
    }

    func play(priority: String, taskId: String, text: String) -> Int {
        bridge.play(withPriority: priority, taskId: taskId, text: text)
    }

    func cancel(taskId: String?) -> Int {
        bridge.cancel(withTaskId: taskId)
    }

    func releaseEngine() -> Int {
        bridge.releaseEngine()
    }

    func paramValue(for key: String) -> String? {
        bridge.paramValue(forKey: key)
    }
}

extension AliyunNLSTTSManager: AliyunNuiTTSBridgeDelegate {
    func aliyunNuiTTSBridge(_ bridge: AliyunNuiTTSBridge, didReceiveEvent event: Int, taskId: String?, code: Int) {
        nativeQueue.async { [weak self] in
            guard let self else { return }
            switch event {
            case 1:
                self.streamingPlayer.drain { [weak self] in
                    self?.nativeQueue.async {
                        self?.finishNativeSpeakLocked(success: true)
                    }
                }
            case 2, 5:
                self.streamingPlayer.stop()
                self.finishNativeSpeakLocked(success: false)
            default:
                break
            }
            _ = taskId
            _ = code
        }
    }

    func aliyunNuiTTSBridge(_ bridge: AliyunNuiTTSBridge, didReceiveAudioData data: Data, taskId: String?, info: String?) {
        streamingPlayer.write(data)
        _ = taskId
        _ = info
    }
}
