import AVFoundation
import Foundation

enum TtsLang: String {
    case zh
    case en
}

enum TtsSource: String {
    case doubao
    case local
}

struct SpeakResult {
    let source: TtsSource
    var fallbackHint: String?
}

enum TtsClient {
    private static let doubaoPrefsKey = "studydeck.doubaoTtsVoicePrefs"
    private static var player: AVAudioPlayer?
    private static let synthesizer = AVSpeechSynthesizer()

    static func detectLang(_ text: String) -> TtsLang {
        text.range(of: #"[\u4e00-\u9fff]"#, options: .regularExpression) != nil ? .zh : .en
    }

    static func pickDoubaoSpeaker(for text: String) -> String {
        let prefs = loadDoubaoPrefs()
        return detectLang(text) == .zh ? prefs.zhSpeaker : prefs.enSpeaker
    }

    static func stopSpeaking() {
        player?.stop()
        player = nil
        synthesizer.stopSpeaking(at: .immediate)
    }

    @MainActor
    static func speakText(_ text: String, config: AiRuntimeConfig?) async throws -> SpeakResult {
        let cleaned = stripMarkdown(text)
        guard !cleaned.isEmpty else {
            throw NSError(domain: "Tts", code: 1, userInfo: [NSLocalizedDescriptionKey: "没有可朗读的内容"])
        }

        if let config, let ep = config.endpoint(for: "tts"), !ep.apiKey.trimmingCharacters(in: .whitespaces).isEmpty {
            do {
                let audio = try await synthesizeDoubao(text: cleaned, endpoint: ep)
                try await play(data: audio.data, mime: audio.mime)
                return SpeakResult(source: .doubao)
            } catch {
                try await speakLocal(cleaned)
                return SpeakResult(source: .local, fallbackHint: formatFallbackHint(error))
            }
        }

        try await speakLocal(cleaned)
        return SpeakResult(source: .local, fallbackHint: "豆包 TTS 未配置，已用本机朗读")
    }

    static func stripMarkdown(_ text: String) -> String {
        var s = text
        if let regex = try? NSRegularExpression(pattern: "```[\\s\\S]*?```", options: []) {
            s = regex.stringByReplacingMatches(in: s, range: NSRange(s.startIndex..., in: s), withTemplate: " ")
        }
        s = s.replacingOccurrences(of: "[#>*_`]", with: " ", options: .regularExpression)
        return s.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    private struct DoubaoAudio {
        let data: Data
        let mime: String
    }

    private static func synthesizeDoubao(text: String, endpoint: AiEndpoint) async throws -> DoubaoAudio {
        let clipped = String(text.prefix(1200))
        let speaker = pickDoubaoSpeaker(for: clipped)
        let apiKey = endpoint.apiKey.trimmingCharacters(in: .whitespaces)
        guard !apiKey.isEmpty else {
            throw NSError(domain: "Tts", code: 2, userInfo: [NSLocalizedDescriptionKey: "DOUBAO_TTS_NO_KEY"])
        }

        let base = endpoint.baseUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        let urlString: String
        if base.isEmpty {
            urlString = "https://openspeech.bytedance.com/api/v3/tts/unidirectional"
        } else if base.contains("/tts/") {
            urlString = base
        } else {
            urlString = base + "/tts/unidirectional"
        }
        guard let url = URL(string: urlString) else { throw URLError(.badURL) }

        let resourceId = resourceIdForSpeaker(speaker)
        let body: [String: Any] = [
            "req_params": [
                "text": clipped,
                "speaker": speaker,
                "audio_params": [
                    "format": "mp3",
                    "sample_rate": 24000,
                    "speed_ratio": 1.1,
                ],
            ],
        ]

        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.setValue(apiKey, forHTTPHeaderField: "X-Api-Key")
        req.setValue(resourceId, forHTTPHeaderField: "X-Api-Resource-Id")
        req.setValue(UUID().uuidString, forHTTPHeaderField: "X-Api-Request-Id")
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.httpBody = try JSONSerialization.data(withJSONObject: body)
        req.timeoutInterval = 60

        let (data, resp) = try await URLSession.shared.data(for: req)
        guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            let snippet = String(data: data, encoding: .utf8)?.prefix(200) ?? ""
            throw NSError(domain: "Tts", code: 3, userInfo: [NSLocalizedDescriptionKey: "DOUBAO_TTS_HTTP: \(snippet)"])
        }

        let raw = String(data: data, encoding: .utf8) ?? ""
        let audio = try parseOpenspeechChunks(raw)
        return DoubaoAudio(data: audio, mime: "audio/mpeg")
    }

    private static func resourceIdForSpeaker(_ speaker: String) -> String {
        let s = speaker.lowercased()
        if s.hasPrefix("icl_") || s.hasPrefix("s_") { return "seed-icl-2.0" }
        if s.contains("uranus") || s.contains("saturn") { return "seed-tts-2.0" }
        return "seed-tts-1.0"
    }

    private static func parseOpenspeechChunks(_ body: String) throws -> Data {
        var rest = body.trimmingCharacters(in: .whitespacesAndNewlines)
        var audio = Data()

        while !rest.isEmpty {
            guard let end = findJsonObjectEnd(rest) else {
                throw NSError(domain: "Tts", code: 4, userInfo: [NSLocalizedDescriptionKey: "DOUBAO_TTS_PARSE"])
            }
            let chunk = String(rest.prefix(end))
            rest = String(rest.dropFirst(end)).trimmingCharacters(in: .whitespacesAndNewlines)
            guard let data = chunk.data(using: .utf8),
                  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue }

            if let code = json["code"] as? Int, code == 20000000 { break }
            if let code = json["code"] as? Int, code != 0 && code != 20000000 {
                let msg = json["message"] as? String ?? "error"
                throw NSError(domain: "Tts", code: 5, userInfo: [NSLocalizedDescriptionKey: "DOUBAO_TTS_API: \(code) \(msg)"])
            }
            if let b64 = json["data"] as? String, !b64.isEmpty, let piece = Data(base64Encoded: b64) {
                audio.append(piece)
            }
        }

        guard !audio.isEmpty else {
            throw NSError(domain: "Tts", code: 6, userInfo: [NSLocalizedDescriptionKey: "DOUBAO_TTS_EMPTY"])
        }
        return audio
    }

    private static func findJsonObjectEnd(_ s: String) -> Int? {
        var depth = 0
        var inString = false
        var escape = false
        for (i, ch) in s.enumerated() {
            if inString {
                if escape { escape = false }
                else if ch == "\\" { escape = true }
                else if ch == "\"" { inString = false }
                continue
            }
            switch ch {
            case "\"": inString = true
            case "{": depth += 1
            case "}":
                depth -= 1
                if depth == 0 { return i + 1 }
            default: break
            }
        }
        return nil
    }

    @MainActor
    private static func play(data: Data, mime: String) async throws {
        stopSpeaking()
        try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
        try AVAudioSession.sharedInstance().setActive(true)
        let p = try AVAudioPlayer(data: data)
        player = p
        p.prepareToPlay()
        p.play()
        try await waitForPlayback(p)
        _ = mime
    }

    @MainActor
    private static func speakLocal(_ text: String) async throws {
        stopSpeaking()
        let lang = detectLang(text)
        let utterance = AVSpeechUtterance(string: text)
        utterance.rate = 0.48
        utterance.voice = AVSpeechSynthesisVoice(language: lang == .zh ? "zh-CN" : "en-US")
        synthesizer.speak(utterance)
    }

    @MainActor
    private static func waitForPlayback(_ player: AVAudioPlayer) async throws {
        while player.isPlaying {
            try await Task.sleep(nanoseconds: 100_000_000)
        }
    }

    private static func formatFallbackHint(_ error: Error) -> String {
        let msg = error.localizedDescription
        if msg.contains("DOUBAO_TTS_NO_KEY") || msg.contains("api key") {
            return "豆包 TTS 未配置 API Key，已用本机朗读"
        }
        let short = msg.count > 120 ? String(msg.prefix(120)) + "…" : msg
        return "豆包 TTS 失败，已用本机朗读（\(short)）"
    }

    private struct DoubaoPrefs {
        var zhSpeaker: String
        var enSpeaker: String
    }

    private static func loadDoubaoPrefs() -> DoubaoPrefs {
        let defaults = DoubaoPrefs(
            zhSpeaker: "zh_female_yingyujiaoxue_uranus_bigtts",
            enSpeaker: "en_female_dacey_uranus_bigtts"
        )
        guard let data = UserDefaults.standard.data(forKey: doubaoPrefsKey),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
            return defaults
        }
        return DoubaoPrefs(
            zhSpeaker: json["zhSpeaker"] ?? defaults.zhSpeaker,
            enSpeaker: json["enSpeaker"] ?? defaults.enSpeaker
        )
    }
}
