// EventFeedback — shared audible + spoken feedback for session events, used by both the macOS
// (AppKit) and iOS (SwiftUI) clients. Mirrors the Windows client's EventFeedback policy
// (clients/windows/.../Notifications/EventFeedback.cs): the platform event handlers decide WHAT
// to play (they own the model/nickname/channel context); this type owns the "should I, and how"
// policy plus the AVFoundation playback/synthesis.
//
// Sound effects use AVAudioPlayer; spoken announcements use the OS-native AVSpeechSynthesizer.
//
// NOTE (iOS): on the voice path the app runs a play-and-record AVAudioSession (VPIO). Playing
// these cues / speech over that session can interact with the live call (ducking, route, or the
// mute switch). The session category should allow mixing — verify on device. This is the most
// likely place for platform bugs to surface.

import Foundation
import AVFoundation

/// User preferences for event sounds and spoken feedback, backed by UserDefaults so the macOS
/// and iOS settings screens and this player share one source of truth.
public struct FeedbackSettings: Sendable {
    public var sounds: Bool
    public var speech: Bool
    public var volume: Float
    public var selfTalkSounds: Bool
    public var pttSound: Bool

    static let keySounds = "feedback.sounds"
    static let keySpeech = "feedback.speech"
    static let keyVolume = "feedback.volume"
    static let keySelfTalk = "feedback.selfTalk"
    static let keyPtt = "feedback.ptt"

    /// Default values registered with UserDefaults (so "unset" reads as the intended default
    /// rather than false/0).
    static let defaults: [String: Any] = [
        keySounds: true,
        keySpeech: false,
        keyVolume: 1.0,
        keySelfTalk: false,
        keyPtt: false,
    ]

    /// The current settings, read live from UserDefaults.
    public static var current: FeedbackSettings {
        let d = UserDefaults.standard
        d.register(defaults: defaults)  // idempotent — ensures "unset" reads as the intended default
        return FeedbackSettings(
            sounds: d.bool(forKey: keySounds),
            speech: d.bool(forKey: keySpeech),
            volume: d.float(forKey: keyVolume),
            selfTalkSounds: d.bool(forKey: keySelfTalk),
            pttSound: d.bool(forKey: keyPtt))
    }
}

@MainActor
public final class EventFeedback {
    public static let shared = EventFeedback()

    private var players: [SoundEvent: AVAudioPlayer] = [:]
    private let synthesizer = AVSpeechSynthesizer()

    private init() {
        UserDefaults.standard.register(defaults: FeedbackSettings.defaults)
    }

    // MARK: - Sounds

    /// Play an event cue, honouring the user's settings. The two opt-in categories (your own
    /// voice-activity, and the PTT cue) are gated by their own flags.
    public func play(_ event: SoundEvent) {
        let s = FeedbackSettings.current
        guard s.sounds, s.volume > 0 else { return }
        if (event == .vaStart || event == .vaStop), !s.selfTalkSounds { return }
        if event == .ptt, !s.pttSound { return }

        guard let player = player(for: event) else { return }
        player.volume = s.volume
        player.currentTime = 0
        player.play()
    }

    /// Lazily load and cache an AVAudioPlayer for the event's bundled WAV. Returns nil (silent)
    /// if the resource is missing or fails to load.
    private func player(for event: SoundEvent) -> AVAudioPlayer? {
        if let cached = players[event] { return cached }
        guard let url = Bundle.module.url(forResource: event.resourceName, withExtension: "wav"),
              let player = try? AVAudioPlayer(contentsOf: url) else {
            return nil
        }
        player.prepareToPlay()
        players[event] = player
        return player
    }

    // MARK: - Speech

    /// Speak `text` when spoken feedback is enabled. Utterances queue (do not interrupt prior
    /// speech) so a burst of events is read in order.
    public func speak(_ text: String) {
        guard FeedbackSettings.current.speech else { return }
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        synthesizer.speak(AVSpeechUtterance(string: trimmed))
    }
}
