//! Kylin AI Proxy provider.
//!
//! Integrates with the Kylin AI Cloud service (`aicloud.kylinos.cn`) which provides
//! a trial account API with MD5-signed authentication.
//!
//! # Authentication
//!
//! The Kylin API requires three authentication fields:
//! - `userId`: A unique user identifier (e.g., `"0722ac293cde5182bdb1f2282d1d707e"`)
//! - `st`: Unix timestamp in seconds (e.g., `1775450456`)
//! - `skey`: MD5 hash of `{userId_short}{st_short}`, where `_short` means dropping the first 3 chars
//!
//! # API Endpoints
//!
//! - Streaming: `POST https://aicloud.kylinos.cn/client/chat/stream`
//! - Non-streaming: `POST https://aicloud.kylinos.cn/client/chat`
//!
//! Request body format (both endpoints):
//! ```json
//! {
//!   "content": {
//!     "messages": [...],
//!     "model": "qwen-plus",
//!     "stream": true|false
//!   },
//!   "skey": "...",
//!   "st": "...",
//!   "userId": "..."
//! }
//!
//! Streaming response format (SSE-like, one JSON per line):
//! ```json
//! {"code":9000,"message":"success","data":{"finishReason":"NULL","reasoningContent":"","content":"部分内容"}}
//! {"code":9000,"message":"success","data":{"finishReason":"STOP","reasoningContent":"","content":""}}
//! ```

use crate::traits::{
    ChatMessage, ChatRequest, ChatResponse, Provider, StreamChunk, StreamError, StreamOptions,
    StreamResult,
};
use async_trait::async_trait;
use futures_util::{StreamExt, stream};
use md5;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{error, info, warn};

const KYLIN_BASE_URL: &str = "https://aicloud.kylinos.cn";
const KYLIN_STREAM_ENDPOINT: &str = "/client/chat/stream";
const KYLIN_CHAT_ENDPOINT: &str = "/client/chat";

/// Build the MD5 skey for Kylin API authentication.
///
/// Takes `userId` and `st` (Unix timestamp), drops the first 3 chars from each,
/// concatenates them, and returns the MD5 hex string.
fn build_kylin_skey(user_id: &str, st: u64) -> String {
    let st_str = st.to_string();
    // Use `get` so empty/short strings (e.g. missing config user id, small timestamps) never panic.
    let user_short = user_id.get(3..).unwrap_or("");
    let st_short = st_str.get(3..).unwrap_or("");
    let input = format!("{user_short}{st_short}");
    let digest = md5::compute(input.as_bytes());
    format!("{:x}", digest)
}

// ── Request / Response types ────────────────────────────────────────

#[derive(Debug, Serialize)]
struct KylinRequest {
    content: KylinContent,
    skey: String,
    st: String,
    #[serde(rename = "userId")]
    user_id: String,
}

#[derive(Debug, Serialize)]
struct KylinContent {
    #[serde(rename = "enable_search")]
    enable_search: bool,
    #[serde(rename = "enable_thinking")]
    enable_thinking: bool,
    messages: Vec<ChatMessage>,
    model: String,
    stream: bool,
    #[serde(rename = "stream_options", skip_serializing_if = "Option::is_none")]
    stream_options: Option<KylinStreamOptions>,
}

#[derive(Debug, Serialize)]
struct KylinStreamOptions {
    #[serde(rename = "include_usage")]
    include_usage: bool,
}

#[derive(Debug, Deserialize)]
struct KylinResponse {
    code: i32,
    message: String,
    #[serde(default)]
    data: KylinData,
}

#[derive(Debug, Deserialize, Default)]
struct KylinData {
    #[serde(default)]
    content: Option<String>,
    #[serde(rename = "finishReason", default)]
    finish_reason: Option<String>,
    #[serde(rename = "reasoningContent", default)]
    reasoning_content: Option<String>,
    #[serde(default)]
    usage: Option<KylinUsage>,
}

#[derive(Debug, Deserialize, Default)]
struct KylinUsage {
    #[serde(rename = "input_tokens", default)]
    input_tokens: Option<u64>,
    #[serde(rename = "output_tokens", default)]
    output_tokens: Option<u64>,
}

// ── Provider ───────────────────────────────────────────────────────

pub struct KylinProxyProvider {
    base_url: String,
    user_id: String,
}

impl KylinProxyProvider {
    pub fn new(user_id: String) -> Self {
        Self {
            base_url: KYLIN_BASE_URL.to_string(),
            user_id,
        }
    }

    fn now_ts(&self) -> u64 {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
    }

    fn build_request(&self, messages: &[ChatMessage], model: &str, stream: bool) -> KylinRequest {
        let st = self.now_ts();
        let skey = build_kylin_skey(&self.user_id, st);
        let request = KylinRequest {
            content: KylinContent {
                enable_search: false,
                enable_thinking: false,
                messages: messages.to_vec(),
                model: model.to_string(),
                stream,
                stream_options: if stream {
                    Some(KylinStreamOptions {
                        include_usage: true,
                    })
                } else {
                    None
                },
            },
            skey: skey.clone(),
            st: st.to_string(),
            user_id: self.user_id.clone(),
        };

        info!(
            kylin_build_request = true,
            user_id = %self.user_id,
            st = %st,
            skey = %skey,
            model = %model,
            stream = %stream,
            num_messages = %messages.len(),
            "Kylin: built request"
        );

        request
    }

    fn non_streaming_url(&self) -> String {
        format!("{}{}", self.base_url, KYLIN_CHAT_ENDPOINT)
    }

    fn streaming_url(&self) -> String {
        format!("{}{}", self.base_url, KYLIN_STREAM_ENDPOINT)
    }
}

impl Default for KylinProxyProvider {
    fn default() -> Self {
        Self::new(String::new())
    }
}

#[async_trait]
impl Provider for KylinProxyProvider {
    async fn chat_with_system(
        &self,
        system_prompt: Option<&str>,
        message: &str,
        model: &str,
        _temperature: Option<f64>,
    ) -> anyhow::Result<String> {
        let mut messages = Vec::new();
        if let Some(sys) = system_prompt {
            messages.push(ChatMessage::system(sys));
        }
        messages.push(ChatMessage::user(message));

        let request = self.build_request(&messages, model, false);
        let client = reqwest::Client::new();

        let resp = client
            .post(self.non_streaming_url())
            .json(&request)
            .send()
            .await
            .map_err(|e| anyhow::anyhow!("Kylin API request failed: {e}"))?;

        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            anyhow::bail!("Kylin API error ({}): {}", status, body);
        }

        let result: KylinResponse = resp
            .json()
            .await
            .map_err(|e| anyhow::anyhow!("Failed to parse Kylin response: {e}"))?;

        if result.code != 9000 {
            anyhow::bail!(
                "Kylin API error: code={}, message={}",
                result.code,
                result.message
            );
        }

        Ok(result.data.content.unwrap_or_default())
    }

    async fn chat(
        &self,
        request: ChatRequest<'_>,
        model: &str,
        temperature: Option<f64>,
    ) -> anyhow::Result<ChatResponse> {
        let text = self
            .chat_with_history(request.messages, model, temperature)
            .await?;

        Ok(ChatResponse {
            text: Some(text),
            tool_calls: Vec::new(),
            usage: None,
            reasoning_content: None,
        })
    }

    fn supports_streaming(&self) -> bool {
        true
    }

    fn stream_chat_with_system(
        &self,
        system_prompt: Option<&str>,
        message: &str,
        model: &str,
        _temperature: Option<f64>,
        _options: StreamOptions,
    ) -> stream::BoxStream<'static, StreamResult<StreamChunk>> {
        let mut messages = Vec::new();
        if let Some(sys) = system_prompt {
            messages.push(ChatMessage::system(sys));
        }
        messages.push(ChatMessage::user(message));

        let request = self.build_request(&messages, model, true);
        let url = self.streaming_url();
        let client = reqwest::Client::new();
        let model = model.to_string();

        let (tx, rx) = tokio::sync::mpsc::channel::<StreamResult<StreamChunk>>(100);

        tokio::spawn(async move {
            info!(url = %url, user_id = %request.user_id, model = %model, "Kylin: starting stream request");

            let resp = match client.post(&url).json(&request).send().await {
                Ok(r) => {
                    info!(status = %r.status(), "Kylin: received response status");
                    r
                }
                Err(e) => {
                    error!(error = %e, "Kylin: HTTP request failed");
                    let _ = tx.send(Err(StreamError::Http(e.to_string()))).await;
                    return;
                }
            };

            if !resp.status().is_success() {
                let status = resp.status();
                let error_body = match resp.text().await {
                    Ok(e) => e,
                    Err(_) => format!("HTTP error: {}", status),
                };
                error!(status = %status, body = %error_body, "Kylin: non-success HTTP response");
                let _ = tx
                    .send(Err(StreamError::Provider(format!(
                        "{}: {}",
                        status, error_body
                    ))))
                    .await;
                return;
            }

            // Parse SSE-like response: each line is a JSON object
            let mut byte_buffer = Vec::new();
            let mut line_buffer = String::new();
            let mut chunk_count = 0usize;
            let mut total_content_len = 0usize;

            let mut stream = resp.bytes_stream();

            while let Some(item) = stream.next().await {
                match item {
                    Ok(bytes) => {
                        info!(bytes_len = %bytes.len(), "Kylin: received bytes");
                        byte_buffer.extend_from_slice(&bytes);
                        let text = match String::from_utf8(byte_buffer.clone()) {
                            Ok(s) => {
                                byte_buffer.clear();
                                s
                            }
                            Err(e) => {
                                let valid_up_to = e.utf8_error().valid_up_to();
                                if valid_up_to == 0 && byte_buffer.len() < 4 {
                                    continue;
                                }
                                let valid = String::from_utf8_lossy(&byte_buffer[..valid_up_to])
                                    .into_owned();
                                byte_buffer.drain(..valid_up_to);
                                valid
                            }
                        };

                        line_buffer.push_str(&text);

                        while let Some(pos) = line_buffer.find('\n') {
                            let line = line_buffer[..pos].trim().to_string();
                            line_buffer.drain(..=pos);

                            if line.is_empty() {
                                continue;
                            }

                            info!(line = %line, "Kylin: parsed line");
                            match parse_kylin_sse_line(&line) {
                                Ok(Some(event)) => {
                                    chunk_count += 1;
                                    if !event.delta.is_empty() {
                                        total_content_len += event.delta.len();
                                        info!(
                                            chunk = %chunk_count,
                                            delta_len = %event.delta.len(),
                                            is_final = %event.is_final,
                                            "Kylin: sending chunk"
                                        );
                                    }
                                    if tx.send(Ok(event)).await.is_err() {
                                        warn!("Kylin: channel closed, stopping stream");
                                        return;
                                    }
                                }
                                Ok(None) => {
                                    info!("Kylin: line parsed to None (skipped)");
                                }
                                Err(e) => {
                                    error!(error = %e, line = %line, "Kylin: parse error");
                                    let _ = tx.send(Err(e)).await;
                                    return;
                                }
                            }
                        }
                    }
                    Err(e) => {
                        error!(error = %e, "Kylin: stream read error");
                        let _ = tx.send(Err(StreamError::Http(e.to_string()))).await;
                        return;
                    }
                }
            }

            // Drain any remaining content
            if !line_buffer.trim().is_empty() {
                info!(remaining = %line_buffer.trim(), "Kylin: draining remaining line buffer");
                if let Ok(Some(event)) = parse_kylin_sse_line(line_buffer.trim()) {
                    let _ = tx.send(Ok(event)).await;
                }
            }

            info!(
                total_chunks = %chunk_count,
                total_content_len = %total_content_len,
                "Kylin: stream completed"
            );
        });

        stream::unfold(rx, |mut rx| async move {
            rx.recv().await.map(|chunk| (chunk, rx))
        })
        .boxed()
    }
}

/// Parse a single line of Kylin SSE response.
///
/// Returns `Ok(Some(StreamChunk))` for content chunks,
/// `Ok(None)` for lines that should be skipped (empty, non-data lines).
fn parse_kylin_sse_line(line: &str) -> StreamResult<Option<StreamChunk>> {
    let line = line.trim();
    if line.is_empty() {
        return Ok(None);
    }

    // Strip SSE `data:` prefix if present
    let json_line = if let Some(stripped) = line.strip_prefix("data:") {
        stripped.trim()
    } else {
        line
    };

    if json_line.is_empty() {
        return Ok(None);
    }

    // Kylin sends SSE `data:` prefixed JSON lines
    let resp: KylinResponse = match serde_json::from_str(json_line) {
        Ok(r) => r,
        Err(e) => {
            warn!(error = %e, line = %json_line, "Kylin: failed to parse JSON, skipping");
            return Ok(None);
        }
    };

    info!(
        code = %resp.code,
        message = %resp.message,
        "Kylin: parsed response"
    );

    if resp.code != 9000 {
        return Err(StreamError::Provider(format!(
            "Kylin API error: code={}, message={}",
            resp.code, resp.message
        )));
    }

    let data = resp.data;
    let is_final = data
        .finish_reason
        .as_deref()
        .map(|r| r == "STOP")
        .unwrap_or(false);

    let content = data.content.clone().unwrap_or_default();
    let reasoning = data.reasoning_content.clone().filter(|s| !s.is_empty());
    let api_usage = data.usage.as_ref().map(|u| crate::traits::TokenUsage {
        input_tokens: u.input_tokens,
        output_tokens: u.output_tokens,
        cached_input_tokens: None,
    });

    info!(
        is_final = %is_final,
        finish_reason = ?data.finish_reason,
        content_len = %content.len(),
        content_preview = %content.chars().take(50).collect::<String>(),
        reasoning_len = %reasoning.as_ref().map(|s| s.len()).unwrap_or(0),
        has_usage = %api_usage.is_some(),
        "Kylin: parsed chunk data"
    );

    if is_final {
        // Even on final chunk, return content if present (some providers send all content in final)
        if !content.is_empty() {
            info!(final_content_len = %content.len(), "Kylin: final chunk has content");
            Ok(Some(StreamChunk {
                delta: content,
                reasoning: reasoning.filter(|s| !s.is_empty()),
                is_final: true,
                token_count: 0,
                usage: api_usage,
            }))
        } else {
            info!("Kylin: final chunk (no content)");
            Ok(Some(StreamChunk {
                usage: api_usage,
                ..StreamChunk::final_chunk()
            }))
        }
    } else if !content.is_empty() || reasoning.is_some() {
        info!(delta_len = %content.len(), "Kylin: content chunk");
        Ok(Some(StreamChunk {
            delta: content,
            reasoning,
            is_final: false,
            token_count: 0,
            usage: None,
        }))
    } else {
        info!("Kylin: empty chunk (skipped)");
        Ok(None)
    }
}

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

    #[test]
    fn build_kylin_skey_generates_consistent_hash() {
        let user_id = "0722ac293cde5182bdb1f2282d1d707e";
        let st: u64 = 1775450456;
        let skey = build_kylin_skey(user_id, st);
        assert_eq!(skey, "218816af5e1ea6fe53d1bfdab3e7e26c");
    }

    #[test]
    fn build_kylin_skey_drops_first_3_chars() {
        // user_id[3..] = "2ac293cde5182bdb1f2282d1d707e"
        // st[3..] = "75450456"
        // input = "2ac293cde5182bdb1f2282d1d707e75450456"
        let user_id = "abc123456789";
        let st: u64 = 1234567890;
        let skey = build_kylin_skey(user_id, st);
        assert_eq!(skey.len(), 32); // MD5 hex is always 32 chars
    }

    #[test]
    fn build_kylin_skey_empty_user_id_no_panic() {
        let skey = build_kylin_skey("", 1775450456);
        assert_eq!(skey.len(), 32);
    }

    #[test]
    fn build_kylin_skey_short_timestamp_no_panic() {
        let skey = build_kylin_skey("0722ac293cde5182bdb1f2282d1d707e", 42);
        assert_eq!(skey.len(), 32);
    }

    #[test]
    fn parse_kylin_sse_content_chunk() {
        let line = r#"{"code":9000,"message":"success","data":{"finishReason":"NULL","reasoningContent":"","content":"hello"}}"#;
        let result = parse_kylin_sse_line(line).unwrap();
        assert!(result.is_some());
        let chunk = result.unwrap();
        assert_eq!(chunk.delta, "hello");
        assert!(!chunk.is_final);
    }

    #[test]
    fn parse_kylin_sse_final_chunk() {
        let line = r#"{"code":9000,"message":"success","data":{"finishReason":"STOP","reasoningContent":"","content":""}}"#;
        let result = parse_kylin_sse_line(line).unwrap();
        assert!(result.is_some());
        let chunk = result.unwrap();
        assert!(chunk.is_final);
    }

    #[test]
    fn parse_kylin_sse_error_chunk() {
        let line = r#"{"code":9001,"message":"invalid signature","data":{}}"#;
        let result = parse_kylin_sse_line(line);
        assert!(result.is_err());
    }

    #[test]
    fn parse_kylin_sse_empty_line() {
        assert!(parse_kylin_sse_line("").unwrap().is_none());
        assert!(parse_kylin_sse_line("   ").unwrap().is_none());
    }

    #[test]
    fn parse_kylin_sse_invalid_json() {
        let result = parse_kylin_sse_line("not json");
        assert!(result.unwrap().is_none());
    }

    #[tokio::test]
    async fn provider_defaults_to_empty_user_id() {
        let provider = KylinProxyProvider::default();
        assert!(provider.user_id.is_empty());
        assert!(provider.supports_streaming());
    }
}
