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

import AVFoundation
import Observation

/// Speaks exercise instructions aloud with `AVSpeechSynthesizer` — on-device, offline,
/// no network. Two callers share one instance (via `AppServices`): the library detail
/// screen's Speak button (full reference read-through) and the active-workout flow's
/// hands-free cue when an exercise starts. A fresh `speak` cancels whatever's playing,
/// so a new exercise's cue never stacks on the last one.
///
/// The audio session ducks other audio (music, podcasts) rather than stopping it, and is
/// deactivated the moment nothing is speaking so that audio returns to full volume. iPhone
/// only: the watch's `HKWorkoutSession` owns its own audio session, so this stays out of
/// the watch target.
@MainActor
@Observable
final class SpeechAnnouncer: NSObject, AVSpeechSynthesizerDelegate {
    /// True while an utterance is being spoken — drives the Speak/Stop toggle in the UI.
    private(set) var isSpeaking = false

    private let synthesizer = AVSpeechSynthesizer()

    /// While true, `deactivateSession` won't hand audio focus back when an utterance ends —
    /// so a per-second spoken countdown ("in 1… 2… 3… Go") stays ducked throughout instead
    /// of pulsing other audio back to full in the gaps between counts. Cleared by the final
    /// (non-holding) utterance of a run and by `stop`.
    private var holdsSession = false

    override init() {
        super.init()
        synthesizer.delegate = self
    }

    /// Speak `text`, replacing anything currently playing. Empty/blank text is a no-op.
    /// Pass `holdSession: true` for all but the last of a rapid back-to-back sequence so
    /// other audio stays ducked between the utterances rather than un-ducking in each gap.
    func speak(_ text: String, holdSession: Bool = false) {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }

        holdsSession = holdSession
        activateSession()
        // Replace, don't queue: a new exercise's cue supersedes the previous one.
        if synthesizer.isSpeaking {
            synthesizer.stopSpeaking(at: .immediate)
        }

        // Voice and prosody come from SpeechSettings (the Settings › Voice controls), read
        // fresh each time so a just-changed slider or picker takes effect immediately.
        let utterance = AVSpeechUtterance(string: trimmed)
        utterance.voice = SpeechSettings.resolvedVoice()
        utterance.rate = SpeechSettings.rate
        utterance.pitchMultiplier = SpeechSettings.pitch
        utterance.volume = SpeechSettings.volume

        isSpeaking = true
        synthesizer.speak(utterance)
    }

    /// Stop immediately and hand audio focus back to other apps.
    func stop() {
        holdsSession = false
        if synthesizer.isSpeaking {
            synthesizer.stopSpeaking(at: .immediate)
        }
        isSpeaking = false
        deactivateSession()
    }

    // MARK: - Audio session

    /// Duck other audio while a prompt plays; `.voicePrompt` routes and mixes the way
    /// navigation/assistant prompts do. Activation is idempotent.
    private func activateSession() {
        let session = AVAudioSession.sharedInstance()
        try? session.setCategory(.playback, mode: .voicePrompt, options: [.duckOthers])
        try? session.setActive(true)
    }

    /// Return other audio to full volume. Skipped while something is still speaking, or while
    /// a countdown holds the session, so a cancel-then-speak (new exercise) or a per-second
    /// count doesn't briefly un-duck between utterances.
    private func deactivateSession() {
        guard !synthesizer.isSpeaking, !holdsSession else { return }
        try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
    }

    // MARK: - AVSpeechSynthesizerDelegate

    nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        Task { @MainActor in
            self.isSpeaking = self.synthesizer.isSpeaking
            self.deactivateSession()
        }
    }

    nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
        Task { @MainActor in
            self.isSpeaking = self.synthesizer.isSpeaking
            self.deactivateSession()
        }
    }
}
