import Foundation
import Speech

/// One language's showing in the sampling pass: how well `SpeechTranscriber`
/// transcribed the first few seconds of audio in that locale.
struct LanguageCandidate: Equatable {
    /// BCP-47 identifier of the locale (e.g. `en-US`, `ru-RU`).
    let localeID: String
    /// Character-count-weighted mean of the per-run transcription confidence,
    /// 0…1. A locale that produced no text scores 0.
    let meanConfidence: Double
    /// Number of characters the transcript came out to — the tie-breaker and the
    /// reason an empty transcript can't win.
    let transcriptLength: Int
}

/// Pure, testable language selection for a voice note. No `SpeechAnalyzer` calls
/// live here: the service transcribes a ~15 s sample in each enabled language,
/// turns each into a `LanguageCandidate`, and this picks the winner. Keeping it
/// side-effect-free is what makes the (undocumented, cross-locale) confidence
/// calibration unit-testable.
enum LanguagePicker {
    /// The confidence assigned to a transcript that carried text but no
    /// `transcriptionConfidence` attribute at all — the graceful-degradation case
    /// for `DictationTranscriber`, which need not emit per-run confidence. A
    /// mid-scale value so such a candidate is ranked by transcript length against
    /// its peers rather than being zeroed out and always losing to a confidence-
    /// reporting engine. Two confidence-less candidates then tie here and fall
    /// through to the length tie-break in `pick`.
    static let neutralConfidence = 0.5

    /// Character-count-weighted mean confidence over an attributed transcript.
    /// Only runs that actually carry a `transcriptionConfidence` attribute
    /// contribute, weighted by their character length; a transcript with no
    /// confidence anywhere (or no characters) scores 0.
    static func meanConfidence(of attributed: AttributedString) -> Double {
        var weighted = 0.0
        var totalCharacters = 0
        for run in attributed.runs {
            guard let confidence = run.transcriptionConfidence else { continue }
            let length = attributed[run.range].characters.count
            guard length > 0 else { continue }
            weighted += confidence * Double(length)
            totalCharacters += length
        }
        return totalCharacters > 0 ? weighted / Double(totalCharacters) : 0
    }

    /// Whether any run in the transcript carries a `transcriptionConfidence`
    /// attribute — i.e. whether `meanConfidence` measured anything real.
    static func hasConfidence(of attributed: AttributedString) -> Bool {
        attributed.runs.contains { $0.transcriptionConfidence != nil }
    }

    /// The confidence used to *rank* a candidate. Prefers the real weighted mean;
    /// when the transcript has text but no confidence attribute anywhere (an engine
    /// that doesn't emit it), falls back to `neutralConfidence` so the candidate
    /// competes on length instead of being zeroed. An empty transcript scores 0 —
    /// it can never win against real text.
    static func rankingConfidence(of attributed: AttributedString) -> Double {
        if hasConfidence(of: attributed) { return meanConfidence(of: attributed) }
        return attributed.characters.isEmpty ? 0 : neutralConfidence
    }

    /// The best candidate, or nil when given nothing. Argmax on `meanConfidence`;
    /// a zero-length transcript can only win if every candidate is empty. Ties
    /// break deterministically — higher confidence, then longer transcript, then
    /// lexicographically smaller `localeID` — so the same samples always yield the
    /// same pick.
    static func pick(from candidates: [LanguageCandidate]) -> LanguageCandidate? {
        candidates.max { lhs, rhs in
            if lhs.meanConfidence != rhs.meanConfidence {
                return lhs.meanConfidence < rhs.meanConfidence
            }
            if lhs.transcriptLength != rhs.transcriptLength {
                return lhs.transcriptLength < rhs.transcriptLength
            }
            // `max` keeps the greater element; invert so the smaller localeID wins.
            return lhs.localeID > rhs.localeID
        }
    }
}
