//! Daemon-proxied HTTP routes mounted under `/api/voice`.
//!
//! Phase 81.33.b.real Stage 2 — daemon forwards every request
//! matching `mount_prefix` via broker JSON-RPC (`plugin.voice.
//! http.request`). The plugin's handler dispatches by path +
//! method and returns the response body + status as JSON.
//!
//! Three endpoints:
//!   - GET  /api/voice?conversation_key=…       → current state
//!   - PUT  /api/voice                          → toggle + voice
//!   - GET  /api/voice/voices                   → catalog
//!   - POST /api/voice/test                     → operator preview
//!
//! Wire shape (request side, what the daemon ships us):
//!   { method: "GET"|"PUT"|..., path: "/api/voice", query: "…",
//!     headers: {…}, body_b64: "…" }
//! Reply shape: { status: 200, body: <json>, content_type: "application/json" }

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

use crate::catalog::filter;
use crate::error::{Result, VoiceError};
use crate::tools::VoiceToolContext;

/// Wire shape the daemon's `pairing::plugin_http::forward_request`
/// publishes on `plugin.voice.http.request`. Field names match
/// the daemon contract bit-exact: `body_base64` (not `body_b64`),
/// `headers` as a sequence of `(key, value)` pairs (not a map).
#[derive(Debug, Deserialize)]
pub struct HttpRequest {
    pub method: String,
    pub path: String,
    #[serde(default)]
    pub query: String,
    #[serde(default)]
    pub headers: Vec<(String, String)>,
    #[serde(default)]
    pub body_base64: String,
}

/// Top-level router. Returns `(status, json_body)` for the
/// auto_discovery wrapper that builds `PluginHttpResponse` shape
/// the daemon's forwarder reads back (`{ status, headers,
/// body_base64 }`).
pub async fn dispatch(ctx: &VoiceToolContext, req: HttpRequest) -> Result<(u16, Value)> {
    let path = req.path.trim_end_matches('/');
    match (req.method.as_str(), path) {
        ("GET", "/api/voice") => get_state(ctx, &req.query).await,
        ("PUT", "/api/voice") => put_state(ctx, &req.body_base64).await,
        ("GET", "/api/voice/voices") => get_voices(&req.query).await,
        ("POST", "/api/voice/test") => post_test(ctx, &req.body_base64).await,
        _ => Ok((404, json!({ "error": "not_found", "path": req.path }))),
    }
}

async fn get_state(ctx: &VoiceToolContext, query: &str) -> Result<(u16, Value)> {
    let conv = parse_query_param(query, "conversation_key").ok_or_else(|| {
        VoiceError::InvalidParam("conversation_key query param required".into())
    })?;
    match ctx.store.get(&conv).await? {
        Some(row) => Ok((200, json!(row))),
        None => Ok((404, json!({ "error": "no_state" }))),
    }
}

#[derive(Debug, Deserialize)]
struct PutBody {
    conversation_key: String,
    enabled: bool,
    #[serde(default)]
    voice_id: Option<String>,
    #[serde(default)]
    provider_id: Option<String>,
    #[serde(default)]
    speed: Option<f32>,
    #[serde(default)]
    pitch_pct: Option<i32>,
}

async fn put_state(ctx: &VoiceToolContext, body_b64: &str) -> Result<(u16, Value)> {
    let body = decode_body(body_b64)?;
    let parsed: PutBody = serde_json::from_str(&body)
        .map_err(|e| VoiceError::InvalidParam(format!("PUT /api/voice body: {e}")))?;

    if let Some(ref v) = parsed.voice_id {
        if crate::catalog::find_by_id(v).is_none() {
            return Ok((
                400,
                json!({ "error": "invalid_voice", "voice_id": v }),
            ));
        }
    }
    if let Some(s) = parsed.speed {
        if !(0.5..=2.0).contains(&s) {
            return Ok((400, json!({ "error": "speed_out_of_range", "speed": s })));
        }
    }

    let row = ctx
        .store
        .upsert(
            &parsed.conversation_key,
            parsed.enabled,
            parsed.voice_id.as_deref(),
            parsed.provider_id.as_deref(),
            parsed.speed,
            parsed.pitch_pct,
            None,
            None,
            None,
        )
        .await?;
    Ok((200, json!({ "ok": true, "row": row })))
}

async fn get_voices(query: &str) -> Result<(u16, Value)> {
    let locale = parse_query_param(query, "locale");
    let provider = parse_query_param(query, "provider");
    let cost = parse_query_param(query, "cost_class");
    let voices = filter(locale.as_deref(), provider.as_deref(), cost.as_deref());
    Ok((200, json!({ "voices": voices })))
}

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

async fn post_test(ctx: &VoiceToolContext, body_b64: &str) -> Result<(u16, Value)> {
    use base64::Engine;
    let body = decode_body(body_b64)?;
    let parsed: TestBody = serde_json::from_str(&body)
        .map_err(|e| VoiceError::InvalidParam(format!("POST /api/voice/test body: {e}")))?;
    let outcome = crate::tools::invoke_pipeline(
        ctx,
        &parsed.text,
        &parsed.voice_id,
        parsed.provider.as_deref(),
        parsed.speed,
        parsed.pitch_pct,
        None,
        None,
    )
    .await?;
    // Extract audio if we got a VoiceNote variant; otherwise return
    // the text fallback inline.
    let (audio_b64, mimetype, transcript) = match &outcome.reply {
        nexo_tool_meta::reply_kind::OutboundReplyKind::VoiceNote {
            audio_bytes,
            mimetype,
            transcript,
        } => (
            Some(base64::engine::general_purpose::STANDARD.encode(audio_bytes)),
            Some(mimetype.clone()),
            transcript.clone(),
        ),
        nexo_tool_meta::reply_kind::OutboundReplyKind::Text { body } => {
            (None, None, Some(body.clone()))
        }
        _ => (None, None, None),
    };
    Ok((
        200,
        json!({
            "audio_b64": audio_b64,
            "mimetype": mimetype,
            "transcript": transcript,
            "winner": outcome.winner,
            "latency_ms": outcome.latency_ms,
            "reason_code": outcome.reason_code.as_metric_label(),
            "cache_hit": outcome.cache_hit,
        }),
    ))
}

fn parse_query_param(query: &str, key: &str) -> Option<String> {
    for pair in query.split('&') {
        let mut split = pair.splitn(2, '=');
        if let (Some(k), Some(v)) = (split.next(), split.next()) {
            if k == key {
                return Some(v.to_string());
            }
        }
    }
    None
}

fn decode_body(b64: &str) -> Result<String> {
    use base64::Engine;
    if b64.is_empty() {
        return Ok(String::new());
    }
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(b64)
        .map_err(|e| VoiceError::InvalidParam(format!("body_base64 decode: {e}")))?;
    String::from_utf8(bytes).map_err(|e| VoiceError::InvalidParam(format!("body utf-8: {e}")))
}

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

    #[test]
    fn parse_query_param_finds_value() {
        assert_eq!(
            parse_query_param("conversation_key=ana:s:1&locale=es-CO", "conversation_key"),
            Some("ana:s:1".into())
        );
        assert_eq!(
            parse_query_param("conversation_key=ana:s:1&locale=es-CO", "locale"),
            Some("es-CO".into())
        );
        assert_eq!(parse_query_param("a=b", "missing"), None);
    }

    #[test]
    fn parse_query_param_empty_string() {
        assert_eq!(parse_query_param("", "any"), None);
    }
}
