//
// SpeechSettings.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//

import AVFoundation
import Foundation

/// Shared configuration for spoken instructions. The Settings UI writes these via
/// `@AppStorage` on the same keys, and `SpeechAnnouncer` reads them from `UserDefaults`
/// at speak time — one source of truth, so the two never drift. Voice selection targets
/// English, since the exercise library is authored in English regardless of the device
/// language (a French voice reading English cues mispronounces everything).
enum SpeechSettings {
    // UserDefaults keys — used verbatim as the `@AppStorage` keys in Settings.
    static let voiceIdentifierKey = "speechVoiceIdentifier"
    static let rateKey = "speechRate"
    static let pitchKey = "speechPitch"
    static let volumeKey = "speechVolume"

    // Defaults, shared so a missing key resolves identically in the UI and the announcer.
    static let rateDefault = Double(AVSpeechUtteranceDefaultSpeechRate)
    static let pitchDefault = 1.0
    static let volumeDefault = 1.0

    // Slider ranges (also used to clamp what the announcer reads).
    static let rateRange = Double(AVSpeechUtteranceMinimumSpeechRate)...Double(AVSpeechUtteranceMaximumSpeechRate)
    static let pitchRange = 0.5...2.0
    static let volumeRange = 0.0...1.0

    /// The exercise library is English; match voices to the content, not the device locale.
    private static let languagePrefix = "en"

    // MARK: - Current values (read by the announcer)

    static var rate: Float {
        let v = UserDefaults.standard.object(forKey: rateKey) as? Double ?? rateDefault
        return Float(v.clamped(to: rateRange))
    }

    static var pitch: Float {
        let v = UserDefaults.standard.object(forKey: pitchKey) as? Double ?? pitchDefault
        return Float(v.clamped(to: pitchRange))
    }

    static var volume: Float {
        let v = UserDefaults.standard.object(forKey: volumeKey) as? Double ?? volumeDefault
        return Float(v.clamped(to: volumeRange))
    }

    /// The stored voice identifier, or `nil` for automatic best-available selection.
    static var voiceIdentifier: String? {
        let id = UserDefaults.standard.string(forKey: voiceIdentifierKey) ?? ""
        return id.isEmpty ? nil : id
    }

    // MARK: - Voice resolution

    /// The voice to speak with: the user's stored choice if it's still installed, otherwise
    /// the best available (an enhanced/premium English voice if one is downloaded, else the
    /// system default English voice).
    static func resolvedVoice() -> AVSpeechSynthesisVoice? {
        if let id = voiceIdentifier, let voice = AVSpeechSynthesisVoice(identifier: id) {
            return voice
        }
        return bestAvailableVoice()
    }

    /// English voices offered in the Settings picker, best quality first.
    static func availableVoices() -> [AVSpeechSynthesisVoice] {
        AVSpeechSynthesisVoice.speechVoices()
            .filter { $0.language.hasPrefix(languagePrefix) }
            .sorted { a, b in
                if a.quality.rank != b.quality.rank { return a.quality.rank > b.quality.rank }
                if a.name != b.name { return a.name < b.name }
                return a.language < b.language
            }
    }

    /// Prefer a downloaded enhanced/premium English voice; only such an upgrade is worth
    /// overriding the system default, which otherwise reads cues perfectly acceptably.
    static func bestAvailableVoice() -> AVSpeechSynthesisVoice? {
        let voices = availableVoices()
        if let upgraded = voices.first(where: { $0.quality.rank >= AVSpeechSynthesisVoiceQuality.enhanced.rank }) {
            return upgraded
        }
        let code = AVSpeechSynthesisVoice.currentLanguageCode()
        let language = code.hasPrefix(languagePrefix) ? code : "en-US"
        return AVSpeechSynthesisVoice(language: language) ?? voices.first
    }

    /// Human label for a voice's quality tier, shown next to its name in the picker.
    static func qualityLabel(_ quality: AVSpeechSynthesisVoiceQuality) -> String {
        switch quality {
        case .premium: "Premium"
        case .enhanced: "Enhanced"
        case .default: "Default"
        @unknown default: "Default"
        }
    }

    /// The picker label for a voice: its name, with the quality tier appended only when it
    /// adds information. Default voices get no suffix (labeling every one "Default" is noise),
    /// and enhanced/premium voices whose name already carries the tier — e.g. "Ava (Premium)" —
    /// aren't labeled twice.
    static func displayLabel(name: String, quality: AVSpeechSynthesisVoiceQuality) -> String {
        guard quality != .default else { return name }
        let label = qualityLabel(quality)
        return name.localizedCaseInsensitiveContains(label) ? name : "\(name) — \(label)"
    }
}

/// What the automatic workout narration says while a flow-mode routine runs, chosen in
/// Settings (only meaningful when "Speak Exercise Cues" is on). Stored as its raw string
/// under `spokenCueStyle`, read by both the Settings picker and the run flow.
enum SpokenCueStyle: String, CaseIterable, Identifiable {
    /// "Coming up, <exercise>" during the rest, then "in 1, 2, 3, GO!" before each set.
    case cues
    /// Reads each exercise's setup & form script aloud when it starts (the original behavior).
    case instructions
    /// Both — the concise coming-up/countdown cues plus the setup & form read.
    case both

    var id: String { rawValue }

    /// Picker label.
    var displayName: String {
        switch self {
        case .cues: "Coming Up & Countdown"
        case .instructions: "Setup & Form Read"
        case .both: "Both"
        }
    }

    /// Whether this style speaks the concise "coming up" / "1, 2, 3, GO!" cues.
    var speaksCues: Bool { self == .cues || self == .both }

    /// Whether this style reads each exercise's setup & form script on start.
    var readsInstructions: Bool { self == .instructions || self == .both }
}

private extension AVSpeechSynthesisVoiceQuality {
    /// Orders the quality tiers for "best available" selection and picker sorting.
    var rank: Int {
        switch self {
        case .premium: 3
        case .enhanced: 2
        case .default: 1
        @unknown default: 0
        }
    }
}

private extension Double {
    func clamped(to range: ClosedRange<Double>) -> Double {
        min(max(self, range.lowerBound), range.upperBound)
    }
}
