//! Route-to-processing policy for P1 iOS.
//!
//! Maps the current [`AudioRoute`] to the recommended
//! [`AudioProcessingConfig`] for that route. The policy enforces
//! INV_009 (never enable platform AEC and Rust AEC simultaneously)
//! and INV_010 (never enable VoiceProcessingIO and Sonora AEC3
//! simultaneously).
//!
//! The returned config is a *recommendation*; the engine may override
//! individual fields (e.g. to keep the user's explicit VAD backend
//! choice) but must not violate the hard invariants.

use crate::audio_processing::{
    AudioBackend, AudioProcessingConfig, AudioRoute, EffectOwner, IosVoiceProcessingMode,
    VadBackend,
};

/// Compute the recommended [`AudioProcessingConfig`] for a given
/// iOS audio route. The returned config always satisfies the P1
/// hard invariants for iOS.
///
/// * Speaker / Earpiece → platform VPIO (AEC/NS/AGC owned by platform).
/// * Wired headset → noop AEC, conservative NS/AGC optional.
/// * Bluetooth HFP → route-managed (app-side AEC off, NS/AGC conservative).
/// * Bluetooth A2DP → invalid for duplex; transmit blocked at selector level.
/// * Unknown → safe fallback (AEC off until classified).
pub fn ios_route_policy(route: AudioRoute) -> AudioProcessingConfig {
    match route {
        AudioRoute::Speaker | AudioRoute::Earpiece => AudioProcessingConfig {
            route,
            ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
            processing_backend: AudioBackend::PlatformVoiceProcessing,
            vad_backend: VadBackend::SileroOnnx,
            aec: EffectOwner::Platform,
            // VPIO owns NS and AGC on the shipping default path (IOSP_002/003).
            ns: EffectOwner::Platform,
            agc: EffectOwner::Platform,
            hpf_enabled: true,
            limiter_enabled: true,
            ..AudioProcessingConfig::default()
        },
        AudioRoute::WiredHeadset => AudioProcessingConfig {
            route,
            ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
            processing_backend: AudioBackend::Noop,
            vad_backend: VadBackend::SileroOnnx,
            // No AEC needed for wired headset (no acoustic echo path).
            aec: EffectOwner::Off,
            // Conservative NS/AGC: optional, not forced.
            ns: EffectOwner::Conservative,
            agc: EffectOwner::Conservative,
            hpf_enabled: true,
            limiter_enabled: true,
            ..AudioProcessingConfig::default()
        },
        AudioRoute::BluetoothHfp => AudioProcessingConfig {
            route,
            ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
            processing_backend: AudioBackend::PlatformVoiceProcessing,
            vad_backend: VadBackend::SileroOnnx,
            // BT HFP manages its own AEC in the headset firmware.
            aec: EffectOwner::Off,
            ns: EffectOwner::Conservative,
            agc: EffectOwner::Conservative,
            hpf_enabled: true,
            limiter_enabled: true,
            ..AudioProcessingConfig::default()
        },
        AudioRoute::BluetoothA2dp => {
            // A2DP is output-only; duplex voice is invalid on this route.
            // Return a config that disables all processing and VAD.
            // The transmit selector will block transmit via the route check.
            AudioProcessingConfig {
                route,
                ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
                processing_backend: AudioBackend::Noop,
                vad_backend: VadBackend::Disabled,
                aec: EffectOwner::Off,
                ns: EffectOwner::Off,
                agc: EffectOwner::Off,
                hpf_enabled: false,
                limiter_enabled: false,
                ..AudioProcessingConfig::default()
            }
        }
        AudioRoute::Unknown => AudioProcessingConfig {
            route,
            ios_mode: IosVoiceProcessingMode::PlatformVoiceProcessing,
            processing_backend: AudioBackend::Noop,
            vad_backend: VadBackend::SileroOnnx,
            // Safe fallback: AEC off until route is classified.
            aec: EffectOwner::Off,
            ns: EffectOwner::Off,
            agc: EffectOwner::Off,
            hpf_enabled: true,
            limiter_enabled: true,
            ..AudioProcessingConfig::default()
        },
    }
}

/// Apply a route change to an existing config, preserving user-chosen
/// VAD backend, timing, and debug settings while updating the
/// route-dependent policy fields.
pub fn apply_route_change(
    existing: &AudioProcessingConfig,
    new_route: AudioRoute,
) -> AudioProcessingConfig {
    let policy = ios_route_policy(new_route);
    AudioProcessingConfig {
        // Route-policy fields from the new route.
        route: policy.route,
        ios_mode: policy.ios_mode,
        processing_backend: policy.processing_backend,
        aec: policy.aec,
        ns: policy.ns,
        agc: policy.agc,
        hpf_enabled: policy.hpf_enabled,
        limiter_enabled: policy.limiter_enabled,
        // Preserve user-chosen VAD backend and timing.
        vad_backend: existing.vad_backend,
        vad_hangover_ms: existing.vad_hangover_ms,
        vad_pre_roll_ms: existing.vad_pre_roll_ms,
        vad_min_tx_ms: existing.vad_min_tx_ms,
        // Preserve debug settings.
        debug_wav_dump_enabled: existing.debug_wav_dump_enabled,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn speaker_uses_platform_vpio() {
        let cfg = ios_route_policy(AudioRoute::Speaker);
        assert_eq!(
            cfg.processing_backend,
            AudioBackend::PlatformVoiceProcessing
        );
        assert_eq!(cfg.aec, EffectOwner::Platform);
        // VPIO owns NS and AGC on the default path.
        assert_eq!(cfg.ns, EffectOwner::Platform);
        assert_eq!(cfg.agc, EffectOwner::Platform);
        assert_eq!(
            cfg.ios_mode,
            IosVoiceProcessingMode::PlatformVoiceProcessing
        );
        assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
    }

    #[test]
    fn earpiece_uses_platform_vpio() {
        let cfg = ios_route_policy(AudioRoute::Earpiece);
        assert_eq!(
            cfg.processing_backend,
            AudioBackend::PlatformVoiceProcessing
        );
        assert_eq!(cfg.aec, EffectOwner::Platform);
        assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
    }

    #[test]
    fn wired_headset_disables_aec() {
        let cfg = ios_route_policy(AudioRoute::WiredHeadset);
        assert_eq!(cfg.aec, EffectOwner::Off);
        assert_eq!(cfg.processing_backend, AudioBackend::Noop);
        assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
    }

    #[test]
    fn bluetooth_hfp_disables_app_aec() {
        let cfg = ios_route_policy(AudioRoute::BluetoothHfp);
        assert_eq!(cfg.aec, EffectOwner::Off);
        assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
    }

    #[test]
    fn bluetooth_a2dp_disables_all_processing_and_vad() {
        let cfg = ios_route_policy(AudioRoute::BluetoothA2dp);
        assert_eq!(cfg.aec, EffectOwner::Off);
        assert_eq!(cfg.vad_backend, VadBackend::Disabled);
        assert_eq!(cfg.processing_backend, AudioBackend::Noop);
    }

    #[test]
    fn unknown_route_safe_fallback_no_aec() {
        let cfg = ios_route_policy(AudioRoute::Unknown);
        assert_eq!(cfg.aec, EffectOwner::Off);
        assert_eq!(cfg.vad_backend, VadBackend::SileroOnnx);
    }

    #[test]
    fn apply_route_change_preserves_vad_timing_and_debug() {
        let existing = AudioProcessingConfig {
            vad_backend: VadBackend::WebrtcVad,
            vad_hangover_ms: 600,
            vad_pre_roll_ms: 200,
            vad_min_tx_ms: 300,
            debug_wav_dump_enabled: true,
            ..AudioProcessingConfig::default()
        };
        let updated = apply_route_change(&existing, AudioRoute::WiredHeadset);
        assert_eq!(updated.vad_backend, VadBackend::WebrtcVad);
        assert_eq!(updated.vad_hangover_ms, 600);
        assert_eq!(updated.vad_pre_roll_ms, 200);
        assert_eq!(updated.vad_min_tx_ms, 300);
        assert!(updated.debug_wav_dump_enabled);
        assert_eq!(updated.route, AudioRoute::WiredHeadset);
        assert_eq!(updated.aec, EffectOwner::Off);
    }

    #[test]
    fn no_sonora_aec_in_platform_vpio_policy() {
        // INV_009: AEC must never be Sonora in the VPIO path.
        for route in [
            AudioRoute::Speaker,
            AudioRoute::Earpiece,
            AudioRoute::WiredHeadset,
            AudioRoute::BluetoothHfp,
            AudioRoute::BluetoothA2dp,
            AudioRoute::Unknown,
        ] {
            let cfg = ios_route_policy(route);
            assert_ne!(
                cfg.processing_backend,
                AudioBackend::Sonora,
                "route {:?} must not use Sonora backend in platform policy",
                route
            );
            assert_ne!(
                cfg.aec,
                EffectOwner::Sonora,
                "route {:?}: AEC must not be Sonora in VPIO path (INV_009)",
                route
            );
            assert_ne!(
                cfg.ns,
                EffectOwner::Sonora,
                "route {:?}: NS must not be Sonora in VPIO path (IOSP_003)",
                route
            );
            assert_ne!(
                cfg.agc,
                EffectOwner::Sonora,
                "route {:?}: AGC must not be Sonora in VPIO path (IOSP_003)",
                route
            );
        }
    }
}
