//! Claude (Anthropic) adapter.
//!
//! POST `https://api.anthropic.com/v1/messages`. Auth via `x-api-key`
//! header (not bearer) + required `anthropic-version` header. Payload
//! is NOT OpenAI-compatible — response text lives at
//! `content[0].text`. Phase 40 deliverable. System prompts travel in
//! a top-level `system` field, separate from the `messages` array.

use std::time::Duration;
use serde::{Deserialize, Serialize};

use super::{ChatRequest, ChatResponse, Choice, Message, UsageBlock};

const CLAUDE_BASE_URL: &str = "https://api.anthropic.com/v1";
const CLAUDE_API_VERSION: &str = "2023-06-01";
const CLAUDE_TIMEOUT_SECS: u64 = 180;

pub fn resolve_claude_key() -> Option<String> {
    if let Ok(k) = std::env::var("ANTHROPIC_API_KEY") {
        if !k.trim().is_empty() { return Some(k.trim().to_string()); }
    }
    for path in ["/home/profit/.env", "/root/.env"] {
        if let Ok(raw) = std::fs::read_to_string(path) {
            for line in raw.lines() {
                if let Some(rest) = line.strip_prefix("ANTHROPIC_API_KEY=") {
                    let k = rest.trim().trim_matches('"').trim_matches('\'');
                    if !k.is_empty() { return Some(k.to_string()); }
                }
            }
        }
    }
    None
}

pub async fn chat(
    key: &str,
    req: &ChatRequest,
) -> Result<ChatResponse, String> {
    // Strip the "claude/" prefix if the caller used the namespaced form.
    let model = req.model.strip_prefix("claude/").unwrap_or(&req.model).to_string();

    // Anthropic carries system prompts outside the messages array.
    // Concatenate any system-role messages into a single system string;
    // keep user + assistant messages in `messages`.
    let mut system_parts: Vec<String> = Vec::new();
    let mut msgs: Vec<AnMessage> = Vec::new();
    for m in &req.messages {
        if m.role == "system" {
            system_parts.push(m.content.clone());
        } else {
            // Anthropic expects strictly "user" or "assistant"; anything
            // else we normalize to "user".
            let role = if m.role == "assistant" { "assistant" } else { "user" };
            msgs.push(AnMessage { role: role.to_string(), content: m.content.clone() });
        }
    }
    let system = if system_parts.is_empty() {
        None
    } else {
        Some(system_parts.join("\n\n"))
    };

    let body = AnChatBody {
        model: model.clone(),
        messages: msgs,
        max_tokens: req.max_tokens.unwrap_or(800),
        temperature: req.temperature.unwrap_or(0.3),
        system,
    };

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(CLAUDE_TIMEOUT_SECS))
        .build()
        .map_err(|e| format!("build client: {e}"))?;

    let t0 = std::time::Instant::now();
    let resp = client
        .post(format!("{}/messages", CLAUDE_BASE_URL))
        .header("x-api-key", key)
        .header("anthropic-version", CLAUDE_API_VERSION)
        .json(&body)
        .send()
        .await
        .map_err(|e| format!("api.anthropic.com unreachable: {e}"))?;

    let status = resp.status();
    if !status.is_success() {
        let body = resp.text().await.unwrap_or_else(|_| "?".into());
        return Err(format!("claude {}: {}", status, body));
    }

    let parsed: AnChatResponse = resp.json().await
        .map_err(|e| format!("invalid claude response: {e}"))?;

    let latency_ms = t0.elapsed().as_millis();
    let text = parsed.content.into_iter()
        .find(|b| b.block_type == "text")
        .map(|b| b.text)
        .unwrap_or_default();

    let prompt_tokens = parsed.usage.as_ref().map(|u| u.input_tokens).unwrap_or_else(|| {
        let chars: usize = req.messages.iter().map(|m| m.content.chars().count()).sum();
        ((chars + 3) / 4) as u32
    });
    let completion_tokens = parsed.usage.as_ref().map(|u| u.output_tokens).unwrap_or_else(|| {
        ((text.chars().count() + 3) / 4) as u32
    });

    tracing::info!(
        target: "v1.chat",
        provider = "claude",
        model = %model,
        prompt_tokens,
        completion_tokens,
        latency_ms = latency_ms as u64,
        "claude chat completed",
    );

    Ok(ChatResponse {
        id: format!("chatcmpl-{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)),
        object: "chat.completion",
        created: chrono::Utc::now().timestamp(),
        model,
        choices: vec![Choice {
            index: 0,
            message: Message { role: "assistant".into(), content: text },
            finish_reason: parsed.stop_reason.unwrap_or_else(|| "stop".into()),
        }],
        usage: UsageBlock {
            prompt_tokens,
            completion_tokens,
            total_tokens: prompt_tokens + completion_tokens,
        },
    })
}

// -- Anthropic Messages API wire shapes --

#[derive(Serialize)]
struct AnChatBody {
    model: String,
    messages: Vec<AnMessage>,
    max_tokens: u32,
    temperature: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    system: Option<String>,
}

#[derive(Serialize)]
struct AnMessage { role: String, content: String }

#[derive(Deserialize)]
struct AnChatResponse {
    content: Vec<AnContentBlock>,
    #[serde(default, rename = "stop_reason")]
    stop_reason: Option<String>,
    #[serde(default)]
    usage: Option<AnUsage>,
}

#[derive(Deserialize)]
struct AnContentBlock {
    #[serde(rename = "type")]
    block_type: String,
    #[serde(default)]
    text: String,
}

#[derive(Deserialize)]
struct AnUsage { input_tokens: u32, output_tokens: u32 }

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

    #[test]
    fn resolve_claude_key_does_not_panic() {
        let _ = resolve_claude_key();
    }

    #[test]
    fn chat_body_serializes_with_separate_system() {
        let body = AnChatBody {
            model: "claude-3-5-sonnet-latest".into(),
            messages: vec![
                AnMessage { role: "user".into(), content: "hi".into() },
            ],
            max_tokens: 800,
            temperature: 0.3,
            system: Some("You are helpful.".into()),
        };
        let json = serde_json::to_string(&body).unwrap();
        assert!(json.contains("\"system\":\"You are helpful.\""));
        assert!(json.contains("\"messages\""));
        assert!(json.contains("\"max_tokens\":800"));
    }

    #[test]
    fn body_omits_system_when_none() {
        let body = AnChatBody {
            model: "claude-3-5-sonnet-latest".into(),
            messages: vec![AnMessage { role: "user".into(), content: "hi".into() }],
            max_tokens: 800,
            temperature: 0.3,
            system: None,
        };
        let json = serde_json::to_string(&body).unwrap();
        assert!(!json.contains("\"system\""), "system field should be skipped when None: {json}");
    }

    #[test]
    fn model_prefix_strip_preserves_bare_names() {
        let cases = [
            ("claude/claude-3-5-sonnet-latest", "claude-3-5-sonnet-latest"),
            ("claude-3-5-sonnet-latest", "claude-3-5-sonnet-latest"),
        ];
        for (input, expected) in cases {
            let out = input.strip_prefix("claude/").unwrap_or(input);
            assert_eq!(out, expected);
        }
    }
}
