use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;

pub use agent_types::LlmError;
use chrono::Local;
use regex::Regex;
use reqwest::StatusCode;
use serde_json::Value;

pub(crate) fn map_reqwest_error(err: reqwest::Error) -> LlmError {
    if err.is_timeout() {
        LlmError::HttpError(format!("Request timeout: {}", err))
    } else if err.is_connect() {
        LlmError::HttpError(format!("Connection failed: {}", err))
    } else {
        LlmError::HttpError(err.to_string())
    }
}

pub(crate) fn map_serde_error(err: serde_json::Error) -> LlmError {
    LlmError::ParseError(err.to_string())
}

pub(crate) fn map_api_status_error(
    status: StatusCode,
    response_body: &str,
    _request_body: &str,
    headers: Option<&reqwest::header::HeaderMap>,
) -> LlmError {
    if status.as_u16() == 529 {
        let message = if response_body.trim().is_empty() {
            "no response body".to_string()
        } else {
            response_body.to_string()
        };
        return LlmError::RateLimited {
            retry_after_ms: 5000,
            message,
        };
    }

    if status == StatusCode::TOO_MANY_REQUESTS {
        let retry_after_ms = headers
            .and_then(|h| h.get("retry-after"))
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.parse::<u64>().ok())
            .map(|secs| secs * 1000)
            .unwrap_or(0);
        let message = if response_body.trim().is_empty() {
            "no response body".to_string()
        } else {
            response_body.to_string()
        };
        return LlmError::RateLimited {
            retry_after_ms,
            message,
        };
    }

    let message = format_api_status_message(status, response_body);

    if is_context_overflow(status, response_body) {
        return LlmError::ContextLengthExceeded { message };
    }

    LlmError::ApiError(message)
}

fn format_api_status_message(status: StatusCode, response_body: &str) -> String {
    let body = if response_body.trim().is_empty() {
        "no response body"
    } else {
        response_body
    };
    format!("HTTP {status}: {body}")
}

pub(crate) fn parse_stream_error(data: &str) -> Option<LlmError> {
    let body = parse_json_object(data)?;
    if body.get("type").and_then(Value::as_str) != Some("error") {
        return None;
    }

    let response_body = serde_json::to_string(&body).ok()?;

    if error_code(&body) == Some("context_length_exceeded")
        || is_overflow_message(&extract_error_message(&body))
    {
        return Some(LlmError::ContextLengthExceeded {
            message: format!("stream error: {response_body}"),
        });
    }

    Some(LlmError::ApiError(format!("stream error: {response_body}")))
}

fn is_context_overflow(status: StatusCode, response_body: &str) -> bool {
    if status == StatusCode::PAYLOAD_TOO_LARGE {
        return true;
    }

    if let Some(body) = parse_json_object(response_body) {
        if error_code(&body) == Some("context_length_exceeded") {
            return true;
        }

        let message = extract_error_message(&body);
        if is_overflow_message(&message) {
            return true;
        }
    }

    is_overflow_message(response_body)
}

fn parse_json_object(input: &str) -> Option<Value> {
    let value: Value = serde_json::from_str(input).ok()?;
    value.is_object().then_some(value)
}

fn extract_error_message(body: &Value) -> String {
    body.get("message")
        .and_then(Value::as_str)
        .or_else(|| body.get("error").and_then(Value::as_str))
        .or_else(|| {
            body.get("error")
                .and_then(|error| error.get("message"))
                .and_then(Value::as_str)
        })
        .unwrap_or_default()
        .trim()
        .to_string()
}

fn error_code(body: &Value) -> Option<&str> {
    body.get("error")
        .and_then(|error| error.get("code"))
        .and_then(Value::as_str)
}

fn is_overflow_message(message: &str) -> bool {
    if message.trim().is_empty() {
        return false;
    }

    overflow_patterns()
        .iter()
        .any(|pattern| pattern.is_match(message))
        || NO_BODY_OVERFLOW_RE
            .get_or_init(|| {
                Regex::new(r"(?i)^4(00|13)\s*(status code)?\s*\(no body\)")
                    .expect("valid no-body overflow regex")
            })
            .is_match(message)
}

fn overflow_patterns() -> &'static [Regex] {
    OVERFLOW_PATTERNS.get_or_init(|| {
        [
            r"prompt is too long",
            r"input is too long for requested model",
            r"exceeds the context window",
            r"input token count.*exceeds the maximum",
            r"maximum prompt length is \d+",
            r"reduce the length of the messages",
            r"maximum context length is \d+ tokens",
            r"exceeds the limit of \d+",
            r"exceeds the available context size",
            r"greater than the context length",
            r"context window exceeds limit",
            r"exceeded model token limit",
            r"context[_ ]length[_ ]exceeded",
            r"request entity too large",
            r"context length is only \d+ tokens",
            r"input length.*exceeds.*context length",
            r"prompt too long; exceeded (?:max )?context length",
            r"too large for model with \d+ maximum context length",
            r"model_context_window_exceeded",
        ]
        .into_iter()
        .map(|pattern| Regex::new(&format!("(?i){pattern}")).expect("valid overflow regex"))
        .collect()
    })
}

static OVERFLOW_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();
static NO_BODY_OVERFLOW_RE: OnceLock<Regex> = OnceLock::new();

/// 记录 LLM 流式请求失败的详细信息到 error.log(完整记录,不截断)
pub(crate) fn write_stream_error_log(
    url: &str,
    response_headers: Option<&reqwest::header::HeaderMap>,
    partial_response_buffer: &str,
    error_message: &str,
    http_status: Option<u16>,
) {
    let log_path = get_error_log_path();

    if let Some(parent) = log_path.parent() {
        if let Err(e) = std::fs::create_dir_all(parent) {
            tracing::error!("Failed to create error log directory: {}", e);
            return;
        }
    }

    match OpenOptions::new().create(true).append(true).open(&log_path) {
        Ok(mut file) => {
            let timestamp = Local::now().to_rfc3339();

            writeln!(file).ok();
            writeln!(file, "{}", "=".repeat(60)).ok();
            writeln!(file, "===== {} source=llm_stream_error =====", timestamp).ok();
            writeln!(file, "{}", "=".repeat(60)).ok();

            writeln!(file).ok();
            writeln!(file, "[Basic Info]").ok();
            writeln!(file, "URL: {}", url).ok();
            if let Some(status) = http_status {
                writeln!(file, "HTTP Status: {}", status).ok();
            }
            writeln!(file, "Error: {}", error_message).ok();

            if let Some(headers) = response_headers {
                writeln!(file).ok();
                writeln!(file, "[Key Headers]").ok();

                let essential_keys = ["content-type", "transfer-encoding"];
                let debug_keys = [
                    "x-request-id",
                    "retry-after",
                    "x-ratelimit-limit",
                    "x-ratelimit-remaining",
                    "x-ratelimit-reset",
                    "openai-model",
                    "x-api-key",
                ];

                for (key, value) in headers.iter() {
                    let key_str = key.as_str();
                    if essential_keys.contains(&key_str) || debug_keys.contains(&key_str) {
                        let value_str = value.to_str().unwrap_or("<binary>");
                        writeln!(file, "  {}: {}", key, value_str).ok();
                    }
                }
            }

            writeln!(file).ok();
            writeln!(file, "[Response Body]").ok();
            writeln!(file, "Length: {} bytes", partial_response_buffer.len()).ok();
            if !partial_response_buffer.is_empty() {
                writeln!(file, "{}", partial_response_buffer).ok();
            }

            writeln!(file).ok();
            writeln!(file, "{}", "=".repeat(60)).ok();
            writeln!(file).ok();

            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                if let Err(e) =
                    std::fs::set_permissions(&log_path, std::fs::Permissions::from_mode(0o600))
                {
                    tracing::warn!("Failed to set error.log permissions: {}", e);
                }
            }
        }
        Err(e) => {
            tracing::error!("Failed to write stream error log: {}", e);
        }
    }
}

fn get_error_log_path() -> PathBuf {
    dirs::home_dir()
        .map(|h| h.join(".xiaoo").join("log").join("error.log"))
        .unwrap_or_else(|| PathBuf::from(".xiaoo_error.log"))
}

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

    #[test]
    fn map_api_status_error_classifies_context_limit_failures() {
        let error = map_api_status_error(
            StatusCode::BAD_REQUEST,
            r#"{"error":{"message":"This model's maximum context length is 128000 tokens, however you requested 128500 tokens."}}"#,
            r#"{"messages":[{"content":"secret prompt"}]}"#,
            None,
        );

        assert!(matches!(error, LlmError::ContextLengthExceeded { .. }));
        assert!(!error.to_string().contains("Request body"));
        assert!(!error.to_string().contains("secret prompt"));
    }

    #[test]
    fn map_api_status_error_classifies_413_without_body_as_context_limit() {
        let error = map_api_status_error(StatusCode::PAYLOAD_TOO_LARGE, "", "{}", None);

        assert!(matches!(error, LlmError::ContextLengthExceeded { .. }));
    }

    #[test]
    fn map_api_status_error_classifies_error_code_context_length_exceeded() {
        let error = map_api_status_error(
            StatusCode::BAD_REQUEST,
            r#"{"error":{"code":"context_length_exceeded","message":"overflow"}}"#,
            "{}",
            None,
        );

        assert!(matches!(error, LlmError::ContextLengthExceeded { .. }));
    }

    #[test]
    fn map_api_status_error_keeps_non_context_failures_as_api_errors() {
        let error = map_api_status_error(
            StatusCode::INTERNAL_SERVER_ERROR,
            r#"{"error":{"message":"provider unavailable"}}"#,
            r#"{"messages":[{"content":"secret prompt"}]}"#,
            None,
        );

        assert!(matches!(error, LlmError::ApiError(_)));
        assert!(!error.to_string().contains("Request body"));
        assert!(!error.to_string().contains("secret prompt"));
    }

    #[test]
    fn map_api_status_error_classifies_529_as_rate_limited_with_retry() {
        let status = StatusCode::from_u16(529).expect("529 should be a valid status code");
        let error = map_api_status_error(
            status,
            r#"{"error":{"type":"overloaded_error","message":"busy"}}"#,
            "{}",
            None,
        );

        match error {
            LlmError::RateLimited {
                retry_after_ms,
                message,
            } => {
                assert_eq!(retry_after_ms, 5000);
                assert!(message.contains("overloaded_error"));
            }
            other => panic!("expected RateLimited for 529, got {:?}", other),
        }
    }

    #[test]
    fn parse_stream_error_classifies_context_length_exceeded() {
        let error = parse_stream_error(
            r#"{"type":"error","error":{"code":"context_length_exceeded","message":"overflow"}}"#,
        );

        assert!(matches!(
            error,
            Some(LlmError::ContextLengthExceeded { .. })
        ));
    }

    #[test]
    fn parse_stream_error_keeps_other_stream_errors_as_api_errors() {
        let error = parse_stream_error(
            r#"{"type":"error","error":{"code":"invalid_prompt","message":"bad prompt"}}"#,
        );

        assert!(matches!(error, Some(LlmError::ApiError(_))));
    }
}