//! Parsed API request type, parsing logic, and Event conversion
//!
//! This module contains the `ParsedApiRequest` type which represents
//! a parsed API request from any supported framework (Claude, Codex, Gemini, OpenCode),
//! along with the parsing logic to extract it from OTLP log records.

use chrono::{DateTime, Utc};

use super::attributes::{get_string, has_attr};
use super::otlp_types::{KeyValue, LogRecord};
use crate::model::{EventBuilder, EventType};
use crate::time::parse_timestamp_millis;

/// Parsed API request event from OpenTelemetry
#[derive(Debug, Default)]
pub struct ParsedApiRequest {
    pub session_id: Option<String>,
    /// Framework that generated this request (claude, codex, etc.)
    pub framework: Option<String>,
    pub tokens_input: Option<i64>,
    pub tokens_output: Option<i64>,
    pub tokens_cache_read: Option<i64>,
    pub tokens_cache_write: Option<i64>,
    /// Reasoning tokens (used by OpenAI o1/o3 models via Codex)
    pub reasoning_tokens: Option<i64>,
    /// Tool tokens (Codex-specific: tokens used for tool definitions/calls)
    pub tool_tokens: Option<i64>,
    /// API provider name (e.g., "OpenAI", "Anthropic")
    pub provider_name: Option<String>,
    pub cost: Option<f64>,
    pub duration_ms: Option<i64>,
    pub model: Option<String>,
    pub timestamp: Option<DateTime<Utc>>,
}

impl ParsedApiRequest {
    /// Parse a LogRecord into a ParsedApiRequest if it's an api_request event
    pub fn from_log_record(record: &LogRecord) -> Option<Self> {
        let attributes = record.attributes.as_ref()?;

        let event_name = get_string(attributes, "event.name")?;

        // Accept token-bearing events from any framework:
        // - "api_request" (Claude Code - no prefix)
        // - "codex.api_request" (Codex CLI)
        // - "codex.sse_event" with event.kind="response.completed" (Codex CLI - token data)
        // - "gemini_cli.api_response" (Gemini CLI - token data is in response, not request)
        // - any other "*.api_request" or "*.api_response" pattern
        let is_api_request = event_name == "api_request" || event_name.ends_with(".api_request");
        let is_api_response = event_name.ends_with(".api_response");
        let is_codex_sse = event_name == "codex.sse_event";

        // For SSE events, only accept response.completed (that's where token counts are)
        if is_codex_sse {
            let event_kind = get_string(attributes, "event.kind");
            if event_kind.as_deref() != Some("response.completed") {
                return None;
            }
        } else if !is_api_request && !is_api_response {
            return None;
        }

        let framework = detect_framework(&event_name, attributes);
        let timestamp = parse_timestamp(record, attributes);

        let mut parsed = ParsedApiRequest {
            framework,
            timestamp,
            ..Default::default()
        };

        // Parse attribute values
        for kv in attributes {
            match kv.key.as_str() {
                // Session ID: Claude uses "session.id", Codex uses "conversation.id"
                "session.id" | "conversation.id" => {
                    parsed.session_id.clone_from(&kv.value.string_value);
                }
                // Input tokens: Claude uses "input_tokens", Codex uses "input_token_count"
                "input_tokens" | "tokens.input" | "input_token_count" => {
                    parsed.tokens_input = kv.value.as_i64();
                }
                // Output tokens: Claude uses "output_tokens", Codex uses "output_token_count"
                "output_tokens" | "tokens.output" | "output_token_count" => {
                    parsed.tokens_output = kv.value.as_i64();
                }
                // Cache read tokens: Claude uses "cache_read_tokens", Codex uses "cached_token_count"
                "cache_read_tokens" | "tokens.cache_read" | "cached_token_count" => {
                    parsed.tokens_cache_read = kv.value.as_i64();
                }
                // Cache write tokens (Claude only currently)
                "cache_creation_tokens" | "tokens.cache_creation" => {
                    parsed.tokens_cache_write = kv.value.as_i64();
                }
                // Reasoning tokens (Codex uses this for o1/o3 models)
                "reasoning_token_count" => {
                    parsed.reasoning_tokens = kv.value.as_i64();
                }
                // Tool tokens (Codex-specific: tokens used for tool definitions/calls)
                "tool_token_count" => {
                    parsed.tool_tokens = kv.value.as_i64();
                }
                // Provider name (e.g., "OpenAI", "Anthropic")
                "provider_name" => {
                    parsed.provider_name.clone_from(&kv.value.string_value);
                }
                // Cost (Claude only currently)
                "cost_usd" | "cost" => {
                    parsed.cost = kv.value.as_f64();
                }
                "duration_ms" => {
                    parsed.duration_ms = kv.value.as_i64();
                }
                "model" => {
                    parsed.model.clone_from(&kv.value.string_value);
                }
                _ => {}
            }
        }

        if parsed.session_id.is_some()
            && (parsed.tokens_input.is_some() || parsed.tokens_output.is_some())
        {
            Some(parsed)
        } else {
            None
        }
    }

    /// Convert to an Event with ApiRequest event type
    pub fn into_event(self, machine_id: String) -> Option<crate::Event> {
        let session_id = self.session_id?;
        let tokens_input = self.tokens_input.unwrap_or(0);
        let tokens_output = self.tokens_output.unwrap_or(0);

        // Build metadata JSON with extra fields (reasoning_tokens, tool_tokens, provider)
        let mut metadata_obj = serde_json::Map::new();
        if let Some(reasoning) = self.reasoning_tokens {
            metadata_obj.insert(
                "reasoning_tokens".to_string(),
                serde_json::Value::Number(reasoning.into()),
            );
        }
        if let Some(tool_tokens) = self.tool_tokens {
            metadata_obj.insert(
                "tool_tokens".to_string(),
                serde_json::Value::Number(tool_tokens.into()),
            );
        }
        if let Some(provider) = self.provider_name {
            metadata_obj.insert("provider".to_string(), serde_json::Value::String(provider));
        }

        let metadata = if metadata_obj.is_empty() {
            None
        } else {
            Some(serde_json::Value::Object(metadata_obj).to_string())
        };

        let event = EventBuilder::new(machine_id, EventType::ApiRequest, session_id)
            .tokens(tokens_input, tokens_output)
            .framework_opt(self.framework)
            .timestamp_opt(self.timestamp)
            .tokens_cache_read_opt(self.tokens_cache_read)
            .tokens_cache_write_opt(self.tokens_cache_write)
            .cost_opt(self.cost)
            .duration_ms_opt(self.duration_ms)
            .model_opt(self.model)
            .metadata_opt(metadata)
            .source("otel")
            .build();

        Some(event)
    }
}

/// Detect framework from event name and attributes
fn detect_framework(event_name: &str, attrs: &[KeyValue]) -> Option<String> {
    // Framework detection priority:
    // 1. Event name prefix (most reliable)
    // 2. Attribute-based detection (fallback)
    //
    // Event name prefixes:
    // - "codex." (Codex CLI)
    // - "gemini_cli." (Gemini CLI)
    // - "opencode." (OpenCode)
    // - No prefix (Claude Code)
    //
    // Attribute differences:
    // - Claude Code: "session.id"
    // - Codex: "conversation.id"
    // - Gemini: "session.id"
    // - OpenCode: "session.id"

    // Check event name prefix first (most reliable)
    if event_name.starts_with("codex.") {
        return Some("codex".to_string());
    }
    if event_name.starts_with("gemini_cli.") {
        return Some("gemini".to_string());
    }
    if event_name.starts_with("opencode.") {
        return Some("opencode".to_string());
    }

    // Fallback to attribute-based detection
    let has_session_id = has_attr(attrs, "session.id");
    let has_conversation_id = has_attr(attrs, "conversation.id");

    if has_conversation_id {
        // Codex uses conversation.id
        Some("codex".to_string())
    } else if has_session_id || event_name == "api_request" {
        // Claude Code uses session.id and unprefixed event names
        Some("claude".to_string())
    } else {
        None
    }
}

/// Parse timestamp from log record (nanoseconds or ISO 8601 fallback)
fn parse_timestamp(record: &LogRecord, attrs: &[KeyValue]) -> Option<DateTime<Utc>> {
    // Try time_unix_nano first, but if it's 0 (Codex sends 0), fall back to event.timestamp
    if let Some(ref ts) = record.time_unix_nano
        && let Ok(nanos) = ts.parse::<u64>()
        && nanos > 0
    {
        let millis = (nanos / 1_000_000) as i64;
        return parse_timestamp_millis(millis);
    }

    // Try to parse event.timestamp attribute (ISO 8601 format from Codex)
    get_string(attrs, "event.timestamp").and_then(|ts| {
        chrono::DateTime::parse_from_rfc3339(&ts)
            .ok()
            .map(|dt| dt.with_timezone(&chrono::Utc))
    })
}

#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::input::otel::test_utils::{make_kv, make_kv_int, make_log_record};

    #[test]
    fn test_parse_claude_api_request() {
        // Claude Code sends "api_request" without prefix, uses "session.id"
        let record = make_log_record(
            "api_request",
            vec![
                make_kv("session.id", "claude-session-123"),
                make_kv("input_tokens", "1000"),
                make_kv("output_tokens", "500"),
                make_kv("cache_read_tokens", "200"),
                make_kv("cost_usd", "0.05"),
                make_kv("model", "claude-3-opus"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();

        // Framework detected from session.id attribute (Claude Code uses session.id)
        assert_eq!(parsed.framework, Some("claude".to_string()));
        assert_eq!(parsed.session_id, Some("claude-session-123".to_string()));
        assert_eq!(parsed.tokens_input, Some(1000));
        assert_eq!(parsed.tokens_output, Some(500));
        assert_eq!(parsed.tokens_cache_read, Some(200));
        assert_eq!(parsed.cost, Some(0.05));
        assert_eq!(parsed.model, Some("claude-3-opus".to_string()));

        // Verify framework propagates to Event
        let event = parsed.into_event("machine-1".to_string()).unwrap();
        assert_eq!(event.framework, Some("claude".to_string()));
    }

    #[test]
    fn test_parse_codex_sse_event_response_completed() {
        // Codex emits token data in codex.sse_event with event.kind="response.completed"
        let record = make_log_record(
            "codex.sse_event",
            vec![
                make_kv("event.kind", "response.completed"),
                make_kv("conversation.id", "codex-conv-456"),
                make_kv("input_token_count", "2000"),
                make_kv("output_token_count", "800"),
                make_kv("cached_token_count", "500"),
                make_kv("reasoning_token_count", "1500"),
                make_kv("tool_token_count", "3000"),
                make_kv("provider_name", "OpenAI"),
                make_kv("model", "o3-mini"),
                make_kv("duration_ms", "1234"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();

        // Framework should be extracted from event name prefix
        assert_eq!(parsed.framework, Some("codex".to_string()));
        assert_eq!(parsed.session_id, Some("codex-conv-456".to_string()));
        assert_eq!(parsed.tokens_input, Some(2000));
        assert_eq!(parsed.tokens_output, Some(800));
        assert_eq!(parsed.tokens_cache_read, Some(500));
        assert_eq!(parsed.reasoning_tokens, Some(1500));
        assert_eq!(parsed.tool_tokens, Some(3000));
        assert_eq!(parsed.provider_name, Some("OpenAI".to_string()));
        assert_eq!(parsed.model, Some("o3-mini".to_string()));
        assert_eq!(parsed.duration_ms, Some(1234));

        // Verify framework propagates to Event
        let event = parsed.into_event("machine-1".to_string()).unwrap();
        assert_eq!(event.framework, Some("codex".to_string()));

        // Check metadata contains tool_tokens and provider
        let metadata: serde_json::Value =
            serde_json::from_str(event.metadata.as_ref().unwrap()).unwrap();
        assert_eq!(metadata["tool_tokens"], 3000);
        assert_eq!(metadata["provider"], "OpenAI");
    }

    #[test]
    fn test_rejects_sse_event_without_response_completed() {
        // Should reject SSE events that aren't response.completed
        let record = make_log_record(
            "codex.sse_event",
            vec![
                make_kv("event.kind", "response.in_progress"),
                make_kv("conversation.id", "codex-conv-456"),
                make_kv("input_token_count", "2000"),
                make_kv("output_token_count", "800"),
            ],
        );

        assert!(ParsedApiRequest::from_log_record(&record).is_none());
    }

    #[test]
    fn test_parse_codex_with_int_values() {
        // Codex sometimes sends cached_token_count and reasoning_token_count as int_value
        let record = make_log_record(
            "codex.sse_event",
            vec![
                make_kv("event.kind", "response.completed"),
                make_kv("conversation.id", "codex-conv-int"),
                make_kv("input_token_count", "2000"),
                make_kv("output_token_count", "800"),
                make_kv_int("cached_token_count", 3072),
                make_kv_int("reasoning_token_count", 1500),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();

        // Should parse int_value fields correctly
        assert_eq!(parsed.tokens_cache_read, Some(3072));
        assert_eq!(parsed.reasoning_tokens, Some(1500));
    }

    #[test]
    fn test_parse_codex_api_request() {
        // Codex also emits codex.api_request events
        let record = make_log_record(
            "codex.api_request",
            vec![
                make_kv("conversation.id", "codex-conv-789"),
                make_kv("input_token_count", "100"),
                make_kv("output_token_count", "50"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();

        assert_eq!(parsed.session_id, Some("codex-conv-789".to_string()));
        assert_eq!(parsed.tokens_input, Some(100));
        assert_eq!(parsed.tokens_output, Some(50));
    }

    #[test]
    fn test_rejects_non_token_events() {
        // Should reject events that don't match our patterns
        let record = make_log_record(
            "codex.user_prompt",
            vec![
                make_kv("conversation.id", "codex-conv-123"),
                make_kv("prompt_length", "100"),
            ],
        );

        assert!(ParsedApiRequest::from_log_record(&record).is_none());
    }

    #[test]
    fn test_rejects_event_without_session_id() {
        let record = make_log_record(
            "codex.sse_event",
            vec![
                make_kv("event.kind", "response.completed"),
                make_kv("input_token_count", "100"),
                make_kv("output_token_count", "50"),
            ],
        );

        assert!(ParsedApiRequest::from_log_record(&record).is_none());
    }

    #[test]
    fn test_rejects_event_without_token_data() {
        let record = make_log_record(
            "codex.sse_event",
            vec![
                make_kv("event.kind", "response.completed"),
                make_kv("conversation.id", "codex-conv-123"),
                make_kv("duration_ms", "100"),
            ],
        );

        assert!(ParsedApiRequest::from_log_record(&record).is_none());
    }

    #[test]
    fn test_into_event_with_reasoning_tokens() {
        let parsed = ParsedApiRequest {
            session_id: Some("test-session".to_string()),
            tokens_input: Some(1000),
            tokens_output: Some(500),
            reasoning_tokens: Some(2000),
            model: Some("o3".to_string()),
            ..Default::default()
        };

        let event = parsed.into_event("machine-1".to_string()).unwrap();

        assert_eq!(event.session_id, "test-session");
        assert_eq!(event.tokens_input, Some(1000));
        assert_eq!(event.tokens_output, Some(500));
        assert_eq!(event.model, Some("o3".to_string()));

        // Reasoning tokens should be in metadata
        let metadata: serde_json::Value =
            serde_json::from_str(event.metadata.as_ref().unwrap()).unwrap();
        assert_eq!(metadata["reasoning_tokens"], 2000);
    }

    #[test]
    fn test_into_event_with_all_metadata() {
        let parsed = ParsedApiRequest {
            session_id: Some("test-session".to_string()),
            tokens_input: Some(1000),
            tokens_output: Some(500),
            reasoning_tokens: Some(2000),
            tool_tokens: Some(3000),
            provider_name: Some("OpenAI".to_string()),
            model: Some("o3".to_string()),
            ..Default::default()
        };

        let event = parsed.into_event("machine-1".to_string()).unwrap();

        // All extra fields should be in metadata
        let metadata: serde_json::Value =
            serde_json::from_str(event.metadata.as_ref().unwrap()).unwrap();
        assert_eq!(metadata["reasoning_tokens"], 2000);
        assert_eq!(metadata["tool_tokens"], 3000);
        assert_eq!(metadata["provider"], "OpenAI");
    }

    #[test]
    fn test_accepts_generic_api_request() {
        let record = make_log_record(
            "api_request",
            vec![
                make_kv("session.id", "generic-session"),
                make_kv("input_tokens", "100"),
                make_kv("output_tokens", "50"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();
        assert_eq!(parsed.session_id, Some("generic-session".to_string()));
    }

    #[test]
    fn test_parse_gemini_api_response() {
        // Gemini CLI sends token data in gemini_cli.api_response (not api_request)
        let record = make_log_record(
            "gemini_cli.api_response",
            vec![
                make_kv("session.id", "gemini-session-123"),
                make_kv("input_token_count", "1500"),
                make_kv("output_token_count", "750"),
                make_kv("duration_ms", "2500"),
                make_kv("model", "gemini-2.0-flash"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();

        // Framework detected from event name prefix
        assert_eq!(parsed.framework, Some("gemini".to_string()));
        assert_eq!(parsed.session_id, Some("gemini-session-123".to_string()));
        assert_eq!(parsed.tokens_input, Some(1500));
        assert_eq!(parsed.tokens_output, Some(750));
        assert_eq!(parsed.duration_ms, Some(2500));
        assert_eq!(parsed.model, Some("gemini-2.0-flash".to_string()));

        // Verify framework propagates to Event
        let event = parsed.into_event("machine-1".to_string()).unwrap();
        assert_eq!(event.framework, Some("gemini".to_string()));
    }

    #[test]
    fn test_parse_opencode_api_response() {
        // OpenCode uses opencode. prefix
        let record = make_log_record(
            "opencode.api_response",
            vec![
                make_kv("session.id", "opencode-session-456"),
                make_kv("input_token_count", "500"),
                make_kv("output_token_count", "250"),
                make_kv("model", "gpt-4o"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();

        // Framework detected from event name prefix
        assert_eq!(parsed.framework, Some("opencode".to_string()));
        assert_eq!(parsed.session_id, Some("opencode-session-456".to_string()));
        assert_eq!(parsed.tokens_input, Some(500));
        assert_eq!(parsed.tokens_output, Some(250));

        // Verify framework propagates to Event
        let event = parsed.into_event("machine-1".to_string()).unwrap();
        assert_eq!(event.framework, Some("opencode".to_string()));
    }

    #[test]
    fn test_framework_detection_priority() {
        // Event name prefix should take priority over attributes
        // Even if conversation.id is present (Codex attribute), gemini_cli. prefix wins
        let record = make_log_record(
            "gemini_cli.api_response",
            vec![
                make_kv("conversation.id", "conv-123"), // Codex-style attribute
                make_kv("input_token_count", "100"),
                make_kv("output_token_count", "50"),
            ],
        );

        let parsed = ParsedApiRequest::from_log_record(&record).unwrap();
        assert_eq!(parsed.framework, Some("gemini".to_string()));
    }
}
