//! `voice_set` — turn voice mode ON + persist voice/speed/pitch.
//!
//! Gated by `policy.allow_toggle` when `source = "llm"`. UI/admin
//! callers bypass policy (operator's intent is explicit).

use serde::Deserialize;
use serde_json::{json, Value};

use super::VoiceToolContext;
use crate::audit::TOPIC_TOGGLE_CHANGED;
use crate::catalog::find_by_id;
use crate::error::{Result, VoiceError};
use crate::policy::PolicyDimension;

#[derive(Debug, Deserialize)]
struct Args {
    context: SetContext,
    voice_id: String,
    #[serde(default)]
    speed: Option<f32>,
    #[serde(default)]
    pitch_pct: Option<i32>,
}

#[derive(Debug, Deserialize)]
struct SetContext {
    conversation_key: String,
    #[serde(default = "default_source")]
    source: String,
}

fn default_source() -> String {
    "ui".to_string()
}

pub async fn handle(ctx: &VoiceToolContext, args: Value) -> Result<Value> {
    let parsed: Args = serde_json::from_value(args)
        .map_err(|e| VoiceError::InvalidParam(format!("voice_set: {e}")))?;

    // LLM-sourced calls go through the policy gate.
    if parsed.context.source == "llm"
        && !ctx.policy.dimension_allowed(PolicyDimension::Toggle)
    {
        return Err(VoiceError::InvalidParam(
            "voice_set: LLM toggle denied by policy.allow_toggle=false".into(),
        ));
    }

    // Voice must exist in catalog.
    let entry = find_by_id(&parsed.voice_id)
        .ok_or_else(|| VoiceError::UnknownVoice(parsed.voice_id.clone()))?;

    // Range checks.
    if let Some(s) = parsed.speed {
        if !(0.5..=2.0).contains(&s) {
            return Err(VoiceError::InvalidParam(format!(
                "speed {s} out of range 0.5..=2.0"
            )));
        }
    }
    if let Some(p) = parsed.pitch_pct {
        if !(-50..=50).contains(&p) {
            return Err(VoiceError::InvalidParam(format!(
                "pitch_pct {p} out of range -50..=50"
            )));
        }
    }

    let row = ctx
        .store
        .upsert(
            &parsed.context.conversation_key,
            true,
            Some(&entry.id),
            Some(&entry.provider),
            parsed.speed,
            parsed.pitch_pct,
            None,
            None,
            None,
        )
        .await?;

    Ok(json!({
        "ok": true,
        "row": row,
        "audit_topic": TOPIC_TOGGLE_CHANGED,
        "source": parsed.context.source,
    }))
}
