//! Azure Cognitive Speech Services provider
//!
//! This provider works with Azure Cognitive Speech Services:
//! - STT: https://{region}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1
//! - TTS: https://{region}.tts.speech.microsoft.com/cognitiveservices/v1
//!
//! Unlike OpenAI-compatible APIs, Azure Speech uses:
//! - Raw audio bytes for STT (no multipart)
//! - SSML format for TTS requests
//! - `Ocp-Apim-Subscription-Key` header for authentication

use crate::config::{AuthType, Modality, ModelConfig};
use async_trait::async_trait;
use enact_core::providers::{
    AudioTranscriptionRequest, AudioTranscriptionResponse, ChatRequest, ChatResponse,
    ModelCapabilities, ModelProvider, SpeechRequest, SpeechResponse,
};
use reqwest::Client;
use std::time::Duration;

/// Azure Cognitive Speech Services provider
#[allow(dead_code)]
pub struct AzureSpeech {
    client: Client,
    /// Model identifier
    model_id: String,
    /// API key for authentication
    api_key: String,
    /// Authentication type
    auth_type: AuthType,
    /// STT URL (speech-to-text)
    stt_url: Option<String>,
    /// TTS URL (text-to-speech)
    tts_url: Option<String>,
    /// Default voice for TTS
    default_voice: String,
    /// Request timeout
    timeout: Duration,
    /// Input modalities
    input_modalities: Vec<Modality>,
    /// Output modalities
    output_modalities: Vec<Modality>,
}

impl AzureSpeech {
    /// Create a new Azure Speech provider from model configuration.
    pub fn from_model_config(
        model_id: &str,
        config: &ModelConfig,
        api_key: &str,
    ) -> anyhow::Result<Self> {
        // Determine if this is STT or TTS based on modality
        let is_stt = config.input_modality.contains(&Modality::Audio)
            && config.output_modality.contains(&Modality::Text);
        let is_tts = config.input_modality.contains(&Modality::Text)
            && config.output_modality.contains(&Modality::Audio);

        let (stt_url, tts_url) = if is_stt {
            (Some(config.runtime.full_url.clone()), None)
        } else if is_tts {
            (None, Some(config.runtime.full_url.clone()))
        } else {
            // Default to using full_url for whichever is configured
            if config.runtime.full_url.contains("stt.speech") {
                (Some(config.runtime.full_url.clone()), None)
            } else {
                (None, Some(config.runtime.full_url.clone()))
            }
        };

        Ok(Self {
            client: Client::new(),
            model_id: model_id.to_string(),
            api_key: api_key.to_string(),
            auth_type: config.auth.auth_type.clone(),
            stt_url,
            tts_url,
            default_voice: "en-US-JennyNeural".to_string(),
            timeout: Duration::from_secs(config.runtime.timeout_seconds),
            input_modalities: config.input_modality.clone(),
            output_modalities: config.output_modality.clone(),
        })
    }

    fn build_stt_headers(&self, content_type: &str) -> Vec<(String, String)> {
        let mut headers = vec![("Content-Type".to_string(), content_type.to_string())];

        match &self.auth_type {
            AuthType::Header => {
                headers.push((
                    "Ocp-Apim-Subscription-Key".to_string(),
                    self.api_key.clone(),
                ));
            }
            AuthType::Bearer => {
                headers.push((
                    "Authorization".to_string(),
                    format!("Bearer {}", self.api_key),
                ));
            }
        }

        headers
    }

    fn build_tts_headers(&self, output_format: &str) -> Vec<(String, String)> {
        let mut headers = vec![
            (
                "Content-Type".to_string(),
                "application/ssml+xml".to_string(),
            ),
            (
                "X-Microsoft-OutputFormat".to_string(),
                output_format.to_string(),
            ),
            ("User-Agent".to_string(), "EnactGateway/1.0".to_string()),
        ];

        match &self.auth_type {
            AuthType::Header => {
                headers.push((
                    "Ocp-Apim-Subscription-Key".to_string(),
                    self.api_key.clone(),
                ));
            }
            AuthType::Bearer => {
                headers.push((
                    "Authorization".to_string(),
                    format!("Bearer {}", self.api_key),
                ));
            }
        }

        headers
    }

    /// Build SSML for TTS request
    fn build_ssml(&self, text: &str, voice: &str) -> String {
        format!(
            r#"<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US"><voice name="{}">{}</voice></speak>"#,
            voice,
            xml_escape(text)
        )
    }
}

/// Escape XML special characters
fn xml_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
}

#[async_trait]
impl ModelProvider for AzureSpeech {
    fn name(&self) -> &str {
        "azure-speech"
    }

    fn model(&self) -> &str {
        &self.model_id
    }

    fn capabilities(&self) -> ModelCapabilities {
        ModelCapabilities {
            max_tokens: 0,
            max_output_tokens: 0,
            supports_streaming: false,
            supports_tools: false,
            supports_reasoning: false,
            supports_vision: false,
            supports_json_mode: false,
            supports_embeddings: false,
            supports_image_generation: false,
            supports_audio_transcription: self.stt_url.is_some(),
            supports_speech: self.tts_url.is_some(),
            supports_video_generation: false,
            pii_safe: false,
            cost_per_1m_input: None,
            cost_per_1m_output: None,
            cost_per_1m_pixels: None,
        }
    }

    async fn chat(&self, _request: ChatRequest) -> anyhow::Result<ChatResponse> {
        anyhow::bail!("Chat not supported by Azure Speech provider. Use transcribe() for STT or speak() for TTS.")
    }

    async fn transcribe(
        &self,
        request: AudioTranscriptionRequest,
    ) -> anyhow::Result<AudioTranscriptionResponse> {
        let stt_url = self
            .stt_url
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("STT not configured for this Azure Speech model"))?;

        // Detect content type from filename
        let content_type = match request.filename.rsplit('.').next() {
            Some("wav") => "audio/wav",
            Some("mp3") => "audio/mpeg",
            Some("ogg") => "audio/ogg",
            Some("flac") => "audio/flac",
            _ => "audio/wav",
        };

        // Build URL with language parameter
        let language = request.language.as_deref().unwrap_or("en-US");
        let url = if stt_url.contains('?') {
            format!("{}&language={}", stt_url, language)
        } else {
            format!("{}?language={}", stt_url, language)
        };

        let headers = self.build_stt_headers(content_type);

        let mut request_builder = self.client.post(&url).timeout(self.timeout);

        for (name, value) in headers {
            request_builder = request_builder.header(&name, &value);
        }

        let response = request_builder.body(request.file).send().await?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            anyhow::bail!("Azure Speech STT error ({}): {}", status, error_text);
        }

        // Parse Azure Speech response format
        let json: serde_json::Value = response.json().await?;

        // Azure returns: {"RecognitionStatus": "Success", "DisplayText": "...", "Offset": ..., "Duration": ...}
        let text = json
            .get("DisplayText")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        let duration = json
            .get("Duration")
            .and_then(|v| v.as_i64())
            .map(|d| d as f64 / 10_000_000.0); // Convert from 100ns units to seconds

        Ok(AudioTranscriptionResponse {
            text,
            task: Some("transcribe".to_string()),
            language: Some(language.to_string()),
            duration,
            words: None,
            segments: None,
        })
    }

    async fn speak(&self, request: SpeechRequest) -> anyhow::Result<SpeechResponse> {
        let tts_url = self
            .tts_url
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("TTS not configured for this Azure Speech model"))?;

        // Map response format to Azure output format
        let output_format = match request.response_format.as_deref() {
            Some("mp3") | None => "audio-16khz-128kbitrate-mono-mp3",
            Some("opus") => "audio-48khz-96kbitrate-mono-opus",
            Some("aac") => "audio-16khz-64kbitrate-mono-mp3", // No AAC, use MP3
            Some("flac") => "audio-16khz-128kbitrate-mono-mp3", // No FLAC, use MP3
            Some("wav") => "riff-16khz-16bit-mono-pcm",
            Some("pcm") => "raw-16khz-16bit-mono-pcm",
            Some(fmt) => {
                anyhow::bail!("Unsupported audio format: {}", fmt);
            }
        };

        // Map voice name or use default
        let voice = if request.voice.is_empty() {
            &self.default_voice
        } else {
            // Map OpenAI-style voices to Azure voices if needed
            match request.voice.as_str() {
                "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer" => &self.default_voice,
                v => v,
            }
        };

        let ssml = self.build_ssml(&request.input, voice);
        let headers = self.build_tts_headers(output_format);

        let mut request_builder = self.client.post(tts_url).timeout(self.timeout);

        for (name, value) in headers {
            request_builder = request_builder.header(&name, &value);
        }

        let response = request_builder.body(ssml).send().await?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            anyhow::bail!("Azure Speech TTS error ({}): {}", status, error_text);
        }

        // Get content type from response
        let content_type = response
            .headers()
            .get("content-type")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("audio/mpeg")
            .to_string();

        let audio = response.bytes().await?.to_vec();

        Ok(SpeechResponse {
            audio,
            content_type,
        })
    }
}

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

    #[test]
    fn test_xml_escape() {
        assert_eq!(xml_escape("Hello & goodbye"), "Hello &amp; goodbye");
        assert_eq!(xml_escape("<tag>"), "&lt;tag&gt;");
        assert_eq!(xml_escape("\"quoted\""), "&quot;quoted&quot;");
    }

    #[test]
    fn test_build_ssml() {
        let provider = AzureSpeech {
            client: Client::new(),
            model_id: "test".to_string(),
            api_key: "key".to_string(),
            auth_type: AuthType::Header,
            stt_url: None,
            tts_url: Some("https://test.tts.speech.microsoft.com/cognitiveservices/v1".to_string()),
            default_voice: "en-US-JennyNeural".to_string(),
            timeout: Duration::from_secs(60),
            input_modalities: vec![Modality::Text],
            output_modalities: vec![Modality::Audio],
        };

        let ssml = provider.build_ssml("Hello world", "en-US-JennyNeural");
        assert!(ssml.contains("Hello world"));
        assert!(ssml.contains("en-US-JennyNeural"));
        assert!(ssml.contains("<speak"));
    }
}
