//! Codex activity event parsing
//!
//! This module handles parsing of Codex CLI activity events (non-token events)
//! such as conversation_starts, user_prompt, tool_decision, and tool_result.

use chrono::{DateTime, Utc};
use serde_json::json;

use super::attributes::{get_i64, get_string};
use super::otlp_types::{KeyValue, LogRecord};
use crate::model::{Event, EventBuilder, EventType};

/// Codex event types for activity tracking
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodexEventType {
    /// Session start with configuration
    ConversationStarts,
    /// User submitted a prompt
    UserPrompt,
    /// Tool approval decision
    ToolDecision,
    /// Tool execution result
    ToolResult,
    /// API request metadata (cf_ray, attempt, status_code)
    ApiRequest,
}

impl CodexEventType {
    /// Returns the string representation of the event type
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::ConversationStarts => "conversation_starts",
            Self::UserPrompt => "user_prompt",
            Self::ToolDecision => "tool_decision",
            Self::ToolResult => "tool_result",
            Self::ApiRequest => "api_request",
        }
    }
}

/// Parsed Codex activity event (non-token events)
#[derive(Debug)]
pub struct ParsedCodexEvent {
    pub event_type: CodexEventType,
    pub session_id: String,
    pub timestamp: Option<DateTime<Utc>>,
    pub model: Option<String>,
    /// Event-specific metadata as JSON
    pub metadata: serde_json::Value,
}

impl ParsedCodexEvent {
    /// Parse a LogRecord into a Codex activity event
    pub fn from_log_record(record: &LogRecord) -> Option<Self> {
        let attributes = record.attributes.as_ref()?;

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

        // Determine event type
        let event_type = match event_name.as_str() {
            "codex.conversation_starts" => CodexEventType::ConversationStarts,
            "codex.user_prompt" => CodexEventType::UserPrompt,
            "codex.tool_decision" => CodexEventType::ToolDecision,
            "codex.tool_result" => CodexEventType::ToolResult,
            "codex.api_request" => CodexEventType::ApiRequest,
            _ => return None,
        };

        // Get session ID (Codex uses conversation.id)
        let session_id = get_string(attributes, "conversation.id")?;

        // Parse timestamp from event.timestamp (ISO 8601)
        let timestamp = get_string(attributes, "event.timestamp").and_then(|ts| {
            chrono::DateTime::parse_from_rfc3339(&ts)
                .ok()
                .map(|dt| dt.with_timezone(&chrono::Utc))
        });

        // Get model
        let model = get_string(attributes, "model");

        // Build event-specific metadata
        let metadata = build_metadata(&event_type, attributes);

        Some(Self {
            event_type,
            session_id,
            timestamp,
            model,
            metadata,
        })
    }

    /// Convert to an Event with ApiRequest event type (0 tokens for activity events)
    pub fn into_event(self, machine_id: String) -> Event {
        let mut metadata_obj = self.metadata.as_object().cloned().unwrap_or_default();
        metadata_obj.insert("event_type".to_string(), json!(self.event_type.as_str()));

        EventBuilder::new(machine_id, EventType::ApiRequest, self.session_id)
            .framework("codex")
            .tokens(0, 0)
            .metadata(serde_json::Value::Object(metadata_obj).to_string())
            .timestamp_opt(self.timestamp)
            .model_opt(self.model)
            .source("otel")
            .build()
    }
}

/// Build event-specific metadata based on event type
fn build_metadata(event_type: &CodexEventType, attrs: &[KeyValue]) -> serde_json::Value {
    let mut obj = serde_json::Map::new();

    match event_type {
        CodexEventType::ConversationStarts => {
            insert_if_present(&mut obj, attrs, "approval_policy", "approval_policy");
            insert_if_present(&mut obj, attrs, "sandbox_policy", "sandbox_policy");
            insert_if_present(&mut obj, attrs, "reasoning_summary", "reasoning_summary");
            insert_if_present(&mut obj, attrs, "reasoning_effort", "reasoning_effort");
            insert_if_present(&mut obj, attrs, "mcp_servers", "mcp_servers");
            insert_if_present(&mut obj, attrs, "provider_name", "provider");
            insert_if_present(&mut obj, attrs, "app.version", "app_version");
            insert_if_present(&mut obj, attrs, "auth_mode", "auth_mode");
            insert_if_present(&mut obj, attrs, "active_profile", "active_profile");
            insert_i64_if_present(&mut obj, attrs, "context_window", "context_window");
            insert_i64_if_present(&mut obj, attrs, "max_output_tokens", "max_output_tokens");
            insert_i64_if_present(
                &mut obj,
                attrs,
                "auto_compact_token_limit",
                "auto_compact_token_limit",
            );
        }
        CodexEventType::UserPrompt => {
            insert_i64_if_present(&mut obj, attrs, "prompt_length", "prompt_length");
        }
        CodexEventType::ToolDecision => {
            insert_if_present(&mut obj, attrs, "tool_name", "tool_name");
            insert_if_present(&mut obj, attrs, "call_id", "call_id");
            insert_if_present(&mut obj, attrs, "decision", "decision");
            insert_if_present(&mut obj, attrs, "decision_type", "decision_type");
            // Try both "source" (docs) and "decision_source" (observed in logs)
            if let Some(v) =
                get_string(attrs, "source").or_else(|| get_string(attrs, "decision_source"))
            {
                obj.insert("source".into(), json!(v));
            }
        }
        CodexEventType::ToolResult => {
            insert_if_present(&mut obj, attrs, "tool_name", "tool_name");
            insert_if_present(&mut obj, attrs, "call_id", "call_id");
            if let Some(v) = get_string(attrs, "success") {
                obj.insert("success".into(), json!(v == "true"));
            }
            insert_i64_if_present(&mut obj, attrs, "duration_ms", "duration_ms");
            insert_i64_if_present(
                &mut obj,
                attrs,
                "tool_result_size_bytes",
                "result_size_bytes",
            );
            // Don't include 'output' or 'arguments' as they can be huge
        }
        CodexEventType::ApiRequest => {
            insert_if_present(&mut obj, attrs, "cf_ray", "cf_ray");
            insert_i64_if_present(&mut obj, attrs, "attempt", "attempt");
            insert_i64_if_present(&mut obj, attrs, "duration_ms", "duration_ms");
            insert_i64_if_present(&mut obj, attrs, "http.response.status_code", "status_code");
            insert_if_present(&mut obj, attrs, "error.message", "error_message");
        }
    }

    serde_json::Value::Object(obj)
}

/// Insert a string value into the metadata object if present
fn insert_if_present(
    obj: &mut serde_json::Map<String, serde_json::Value>,
    attrs: &[KeyValue],
    attr_key: &str,
    json_key: &str,
) {
    if let Some(v) = get_string(attrs, attr_key) {
        obj.insert(json_key.into(), json!(v));
    }
}

/// Insert an i64 value into the metadata object if present
fn insert_i64_if_present(
    obj: &mut serde_json::Map<String, serde_json::Value>,
    attrs: &[KeyValue],
    attr_key: &str,
    json_key: &str,
) {
    if let Some(v) = get_i64(attrs, attr_key) {
        obj.insert(json_key.into(), json!(v));
    }
}

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

    #[test]
    fn test_parse_codex_conversation_starts() {
        let record = make_log_record(
            "codex.conversation_starts",
            vec![
                make_kv("conversation.id", "codex-conv-start"),
                make_kv("event.timestamp", "2025-12-17T06:11:18.219Z"),
                make_kv("model", "gpt-5.1-codex-max"),
                make_kv("approval_policy", "on-request"),
                make_kv("sandbox_policy", "workspace-write"),
                make_kv("reasoning_summary", "auto"),
                make_kv("provider_name", "OpenAI"),
                make_kv("app.version", "0.73.0"),
                make_kv("auth_mode", "ChatGPT"),
            ],
        );

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

        assert_eq!(parsed.event_type, CodexEventType::ConversationStarts);
        assert_eq!(parsed.session_id, "codex-conv-start");
        assert_eq!(parsed.model, Some("gpt-5.1-codex-max".to_string()));
        assert!(parsed.timestamp.is_some());

        // Check metadata
        assert_eq!(parsed.metadata["approval_policy"], "on-request");
        assert_eq!(parsed.metadata["sandbox_policy"], "workspace-write");
        assert_eq!(parsed.metadata["reasoning_summary"], "auto");
        assert_eq!(parsed.metadata["provider"], "OpenAI");
        assert_eq!(parsed.metadata["app_version"], "0.73.0");
        assert_eq!(parsed.metadata["auth_mode"], "ChatGPT");

        // Test conversion to Event
        let event = parsed.into_event("machine-1".to_string());
        assert_eq!(event.tokens_input, Some(0));
        assert_eq!(event.tokens_output, Some(0));
        assert_eq!(event.framework, Some("codex".to_string()));

        let metadata: serde_json::Value =
            serde_json::from_str(event.metadata.as_ref().unwrap()).unwrap();
        assert_eq!(metadata["event_type"], "conversation_starts");
    }

    #[test]
    fn test_parse_codex_user_prompt() {
        let record = make_log_record(
            "codex.user_prompt",
            vec![
                make_kv("conversation.id", "codex-conv-prompt"),
                make_kv("event.timestamp", "2025-12-17T06:11:20.000Z"),
                make_kv("model", "gpt-5.1-codex-max"),
                make_kv("prompt_length", "150"),
            ],
        );

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

        assert_eq!(parsed.event_type, CodexEventType::UserPrompt);
        assert_eq!(parsed.session_id, "codex-conv-prompt");
        assert_eq!(parsed.metadata["prompt_length"], 150);
    }

    #[test]
    fn test_parse_codex_tool_decision() {
        let record = make_log_record(
            "codex.tool_decision",
            vec![
                make_kv("conversation.id", "codex-conv-tool"),
                make_kv("event.timestamp", "2025-12-17T06:11:25.000Z"),
                make_kv("model", "gpt-5.1-codex-max"),
                make_kv("tool_name", "shell_command"),
                make_kv("call_id", "call_abc123"),
                make_kv("decision", "approved"),
                make_kv("decision_type", "auto"),
                make_kv("decision_source", "policy"),
            ],
        );

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

        assert_eq!(parsed.event_type, CodexEventType::ToolDecision);
        assert_eq!(parsed.session_id, "codex-conv-tool");
        assert_eq!(parsed.metadata["tool_name"], "shell_command");
        assert_eq!(parsed.metadata["call_id"], "call_abc123");
        assert_eq!(parsed.metadata["decision"], "approved");
        assert_eq!(parsed.metadata["decision_type"], "auto");
        assert_eq!(parsed.metadata["source"], "policy");
    }

    #[test]
    fn test_parse_codex_tool_result() {
        let record = make_log_record(
            "codex.tool_result",
            vec![
                make_kv("conversation.id", "codex-conv-result"),
                make_kv("event.timestamp", "2025-12-17T06:11:30.000Z"),
                make_kv("model", "gpt-5.1-codex-max"),
                make_kv("tool_name", "shell_command"),
                make_kv("call_id", "call_abc123"),
                make_kv("success", "true"),
                make_kv("duration_ms", "500"),
                make_kv("tool_result_size_bytes", "1024"),
            ],
        );

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

        assert_eq!(parsed.event_type, CodexEventType::ToolResult);
        assert_eq!(parsed.session_id, "codex-conv-result");
        assert_eq!(parsed.metadata["tool_name"], "shell_command");
        assert_eq!(parsed.metadata["call_id"], "call_abc123");
        assert_eq!(parsed.metadata["success"], true);
        assert_eq!(parsed.metadata["duration_ms"], 500);
        assert_eq!(parsed.metadata["result_size_bytes"], 1024);
    }

    #[test]
    fn test_parse_codex_api_request_event() {
        let record = make_log_record(
            "codex.api_request",
            vec![
                make_kv("conversation.id", "codex-conv-api"),
                make_kv("event.timestamp", "2025-12-17T06:11:27.000Z"),
                make_kv("model", "gpt-5.1-codex-max"),
                make_kv("attempt", "1"),
                make_kv("duration_ms", "2500"),
                make_kv("http.response.status_code", "200"),
            ],
        );

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

        assert_eq!(parsed.event_type, CodexEventType::ApiRequest);
        assert_eq!(parsed.session_id, "codex-conv-api");
        assert_eq!(parsed.metadata["attempt"], 1);
        assert_eq!(parsed.metadata["duration_ms"], 2500);
        assert_eq!(parsed.metadata["status_code"], 200);
    }

    #[test]
    fn test_codex_event_rejects_unknown_event() {
        let record = make_log_record(
            "codex.unknown_event",
            vec![make_kv("conversation.id", "codex-conv-123")],
        );

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

    #[test]
    fn test_codex_event_rejects_without_conversation_id() {
        let record = make_log_record(
            "codex.conversation_starts",
            vec![make_kv("model", "gpt-5.1-codex-max")],
        );

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