//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Copyright (c) 2026 everettjf. All rights reserved.
//

import AVFoundation
import Foundation

/// Single observer for AVAudioSession route-change and interruption events.
///
/// Voice bridges subscribe via `addHandler(_:)` and receive events on the main actor.
/// The underlying AVFoundation notifications fire on an unspecified queue and are
/// funneled here so that every bridge gets a consistent view without duplicating observers.
@MainActor
final class OpenRockyAudioSessionObserver {
    static let shared = OpenRockyAudioSessionObserver()

    enum Event: Sendable {
        case routeChanged(reason: AVAudioSession.RouteChangeReason, currentOutputPortType: AVAudioSession.Port?)
        case interruptionBegan
        case interruptionEnded(shouldResume: Bool)
    }

    typealias Handler = @MainActor (Event) -> Void

    private var handlers: [UUID: Handler] = [:]
    private var routeToken: NSObjectProtocol?
    private var interruptionToken: NSObjectProtocol?

    private init() {
        installObservers()
    }

    func addHandler(_ handler: @escaping Handler) -> UUID {
        let id = UUID()
        handlers[id] = handler
        return id
    }

    func removeHandler(_ id: UUID) {
        handlers.removeValue(forKey: id)
    }

    private func installObservers() {
        let center = NotificationCenter.default
        routeToken = center.addObserver(
            forName: AVAudioSession.routeChangeNotification,
            object: nil,
            queue: .main
        ) { [weak self] note in
            // Extract only Sendable primitives from the Notification on this queue,
            // then hop to MainActor — Notification itself is not Sendable.
            let reasonValue = (note.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt) ?? 0
            MainActor.assumeIsolated { self?.handleRouteChange(reasonValue: reasonValue) }
        }
        interruptionToken = center.addObserver(
            forName: AVAudioSession.interruptionNotification,
            object: nil,
            queue: .main
        ) { [weak self] note in
            let typeValue = (note.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt) ?? 0
            let optionsValue = (note.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt) ?? 0
            MainActor.assumeIsolated { self?.handleInterruption(typeValue: typeValue, optionsValue: optionsValue) }
        }
    }

    private func handleRouteChange(reasonValue: UInt) {
        let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) ?? .unknown
        let currentOutput = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType
        rlog.info("AudioSession route changed: reason=\(reason.rawValue) output=\(currentOutput?.rawValue ?? "none")", category: "Audio")
        emit(.routeChanged(reason: reason, currentOutputPortType: currentOutput))
    }

    private func handleInterruption(typeValue: UInt, optionsValue: UInt) {
        guard let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
        switch type {
        case .began:
            rlog.info("AudioSession interruption began", category: "Audio")
            emit(.interruptionBegan)
        case .ended:
            let shouldResume = AVAudioSession.InterruptionOptions(rawValue: optionsValue).contains(.shouldResume)
            rlog.info("AudioSession interruption ended: shouldResume=\(shouldResume)", category: "Audio")
            emit(.interruptionEnded(shouldResume: shouldResume))
        @unknown default:
            break
        }
    }

    private func emit(_ event: Event) {
        for handler in handlers.values {
            handler(event)
        }
    }
}
