//! OpenAI Codex CLI framework adapter.
//!
//! This adapter handles integration with OpenAI's Codex CLI tool.
//! Codex has a unique integration model compared to other frameworks:
//!
//! - **Limited notify hooks**: Only 2 events (`agent-turn-complete`, `approval-requested`)
//! - **TOML configuration**: Config is in TOML format, not JSON
//! - **CLI argument JSON**: JSON payload passed as command-line argument, not stdin
//! - **Rich OTEL telemetry**: Built-in OTLP support for detailed API request tracking
//!
//! # Integration Strategy
//!
//! Due to limited notify hook coverage (only 2 events), OTEL telemetry is the
//! primary data source for Codex. The notify hooks provide basic events, while
//! OTEL captures rich telemetry including token usage, cost, and model info.
//!
//! The recommended integration approach is dual:
//! 1. notify hooks for the 2 available events (turn completion, approval requests)
//! 2. OTEL ingestion for rich telemetry data (`codex.api_request`, etc.)
//!
//! Users cannot track session lifecycle, tool execution, or user prompts via
//! notify hooks - this data is only available through OTEL telemetry.

use super::{ConfigFormat, FrameworkAdapter, ParsedHookInput, ParsedHookInputBuilder, common};
use crate::model::EventType;
use std::path::PathBuf;

/// OpenAI Codex CLI framework adapter.
///
/// Handles hook generation and event parsing for Codex CLI.
pub struct CodexAdapter;

impl FrameworkAdapter for CodexAdapter {
    fn name(&self) -> &'static str {
        "codex"
    }

    fn display_name(&self) -> &'static str {
        "Codex CLI"
    }

    fn project_config_path(&self) -> PathBuf {
        PathBuf::from(".codex/config.toml")
    }

    fn user_config_path(&self) -> Option<PathBuf> {
        dirs::home_dir().map(|h| h.join(".codex/config.toml"))
    }

    fn config_format(&self) -> ConfigFormat {
        ConfigFormat::Toml
    }

    fn generate_hooks_config(
        &self,
        _enabled_events: &[EventType],
        mi6_bin: &str,
        otel_enabled: bool,
        otel_port: u16,
    ) -> serde_json::Value {
        // Note: _enabled_events is intentionally ignored because Codex uses a single
        // notify command for all events, rather than per-event hooks like other frameworks.
        //
        // Codex uses TOML config, so we generate a JSON representation
        // that will be converted to TOML by the init command.
        //
        // Codex notify hook format: notify = ["command", "arg1", "arg2"]
        // The hook receives JSON as the last CLI argument, not via stdin.
        let notify_command = vec![
            mi6_bin.to_string(),
            "ingest".to_string(),
            "event".to_string(),
            "--framework".to_string(),
            "codex".to_string(),
        ];

        let mut config = serde_json::json!({
            "notify": notify_command
        });

        // Add OTEL configuration if enabled
        // Codex expects: exporter = { otlp-http = { endpoint = "...", protocol = "json" } }
        if otel_enabled {
            config["otel"] = serde_json::json!({
                "exporter": {
                    "otlp-http": {
                        "endpoint": format!("http://127.0.0.1:{}", otel_port),
                        "protocol": "json"
                    }
                }
            });
        }

        config
    }

    fn merge_config(
        &self,
        generated: serde_json::Value,
        existing: Option<serde_json::Value>,
    ) -> serde_json::Value {
        let mut settings = existing.unwrap_or_else(|| serde_json::json!({}));

        // Set notify command
        if let Some(notify) = generated.get("notify") {
            settings["notify"] = notify.clone();
        }

        // Merge otel settings
        if let Some(new_otel) = generated.get("otel") {
            if let Some(existing_otel) = settings.get_mut("otel") {
                if let (Some(existing_obj), Some(new_obj)) =
                    (existing_otel.as_object_mut(), new_otel.as_object())
                {
                    for (key, value) in new_obj {
                        existing_obj.insert(key.clone(), value.clone());
                    }
                }
            } else {
                settings["otel"] = new_otel.clone();
            }
        }

        settings
    }

    fn parse_hook_input(&self, _event_type: &str, json: &serde_json::Value) -> ParsedHookInput {
        // Codex JSON payload schema (from notify hook):
        // {
        //   "type": "agent-turn-complete" | "approval-requested",
        //   "thread-id": "abc123",
        //   "turn-id": "turn-1",
        //   "cwd": "/path/to/project",
        //   ...
        // }
        ParsedHookInputBuilder::new(json)
            .session_id("thread-id")
            .tool_use_id("turn-id")
            .cwd("cwd")
            .build()
        // Note: Codex only provides basic fields via notify hooks;
        // rich telemetry comes from OTEL instead
    }

    fn map_event_type(&self, framework_event: &str) -> EventType {
        match framework_event {
            // approval-requested maps to PermissionRequest
            "approval-requested" => EventType::PermissionRequest,
            // agent-turn-complete has no direct canonical equivalent
            // Store as Custom for now
            "agent-turn-complete" => EventType::Custom("TurnComplete".to_string()),
            // Try canonical parsing, fall back to custom
            other => other
                .parse()
                .unwrap_or_else(|_| EventType::Custom(other.to_string())),
        }
    }

    fn supported_events(&self) -> Vec<&'static str> {
        // Codex only supports 2 notify hook events
        vec!["agent-turn-complete", "approval-requested"]
    }

    fn framework_specific_events(&self) -> Vec<&'static str> {
        // agent-turn-complete has no canonical equivalent
        vec!["agent-turn-complete"]
    }

    fn detection_env_vars(&self) -> &[&'static str] {
        // Environment variables that Codex CLI may set.
        // Note: These are inferred from Codex documentation and need verification
        // with actual Codex CLI behavior. If detection fails, users can explicitly
        // specify --framework codex.
        &["CODEX_SESSION_ID", "CODEX_PROJECT_DIR", "CODEX_THREAD_ID"]
    }

    fn is_installed(&self) -> bool {
        common::is_framework_installed(self.user_config_path(), "codex")
    }

    fn otel_support(&self) -> super::OtelSupport {
        use super::OtelSupport;
        // Codex supports OTEL via [otel] section in config.toml
        let Some(config_path) = self.user_config_path() else {
            return OtelSupport::Disabled;
        };
        let Ok(contents) = std::fs::read_to_string(&config_path) else {
            return OtelSupport::Disabled;
        };
        let Ok(toml_val) = toml::from_str::<toml::Value>(&contents) else {
            return OtelSupport::Disabled;
        };

        // Check for [otel] section with exporter configuration
        if let Some(otel) = toml_val.get("otel") {
            // Codex uses nested format: otel.exporter.otlp-http.endpoint
            if otel.get("exporter").is_some() {
                return OtelSupport::Enabled;
            }
        }
        OtelSupport::Disabled
    }

    fn remove_hooks(&self, existing: serde_json::Value) -> Option<serde_json::Value> {
        let mut settings = existing;
        let mut modified = false;

        // Remove notify command if it contains "mi6"
        if let Some(notify) = settings.get("notify")
            && let Some(arr) = notify.as_array()
        {
            // Check if any element contains "mi6"
            let has_mi6_hook = arr
                .iter()
                .any(|v| v.as_str().is_some_and(|s| s.contains("mi6")));
            if has_mi6_hook && let Some(obj) = settings.as_object_mut() {
                obj.remove("notify");
                modified = true;
            }
        }

        // Remove otel section
        if let Some(obj) = settings.as_object_mut()
            && obj.remove("otel").is_some()
        {
            modified = true;
        }

        if modified { Some(settings) } else { None }
    }
}

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

    #[test]
    fn test_name() {
        let adapter = CodexAdapter;
        assert_eq!(adapter.name(), "codex");
        assert_eq!(adapter.display_name(), "Codex CLI");
    }

    #[test]
    fn test_config_format() {
        let adapter = CodexAdapter;
        assert_eq!(adapter.config_format(), ConfigFormat::Toml);
    }

    #[test]
    fn test_project_config_path() {
        let adapter = CodexAdapter;
        assert_eq!(
            adapter.project_config_path(),
            PathBuf::from(".codex/config.toml")
        );
    }

    #[test]
    fn test_map_event_type() {
        let adapter = CodexAdapter;

        // Codex-specific mappings
        assert_eq!(
            adapter.map_event_type("approval-requested"),
            EventType::PermissionRequest
        );
        assert_eq!(
            adapter.map_event_type("agent-turn-complete"),
            EventType::Custom("TurnComplete".to_string())
        );

        // Unknown events become Custom
        assert_eq!(
            adapter.map_event_type("unknown-event"),
            EventType::Custom("unknown-event".to_string())
        );
    }

    #[test]
    fn test_parse_hook_input() {
        let adapter = CodexAdapter;
        let input = serde_json::json!({
            "type": "agent-turn-complete",
            "thread-id": "codex-thread-123",
            "turn-id": "turn-456",
            "cwd": "/projects/test"
        });

        let parsed = adapter.parse_hook_input("agent-turn-complete", &input);

        assert_eq!(parsed.session_id, Some("codex-thread-123".to_string()));
        assert_eq!(parsed.tool_use_id, Some("turn-456".to_string()));
        assert_eq!(parsed.cwd, Some("/projects/test".to_string()));
        // Codex doesn't provide these
        assert_eq!(parsed.tool_name, None);
        assert_eq!(parsed.permission_mode, None);
        assert_eq!(parsed.transcript_path, None);
    }

    #[test]
    fn test_generate_hooks_config() -> Result<(), String> {
        let adapter = CodexAdapter;
        let events = vec![EventType::PermissionRequest];

        let config = adapter.generate_hooks_config(&events, "mi6", false, 4318);

        // Should have notify array
        let notify = config
            .get("notify")
            .ok_or("missing notify")?
            .as_array()
            .ok_or("notify not an array")?;
        assert_eq!(notify[0], "mi6");
        assert_eq!(notify[1], "ingest");
        assert_eq!(notify[2], "event");
        assert_eq!(notify[3], "--framework");
        assert_eq!(notify[4], "codex");

        // No otel section when disabled
        assert!(config.get("otel").is_none());
        Ok(())
    }

    #[test]
    fn test_generate_hooks_config_with_otel() -> Result<(), String> {
        let adapter = CodexAdapter;
        let events = vec![];

        let config = adapter.generate_hooks_config(&events, "mi6", true, 4318);

        // Should have otel section with nested exporter format
        let otel = config.get("otel").ok_or("missing otel")?;
        assert_eq!(
            otel["exporter"]["otlp-http"]["endpoint"],
            "http://127.0.0.1:4318"
        );
        assert_eq!(otel["exporter"]["otlp-http"]["protocol"], "json");
        Ok(())
    }

    #[test]
    fn test_merge_config_new() -> Result<(), String> {
        let adapter = CodexAdapter;
        let generated = serde_json::json!({
            "notify": ["mi6", "ingest", "event", "--framework", "codex"]
        });

        let merged = adapter.merge_config(generated, None);

        let notify = merged
            .get("notify")
            .ok_or("missing notify")?
            .as_array()
            .ok_or("notify not an array")?;
        assert_eq!(notify.len(), 5);
        Ok(())
    }

    #[test]
    fn test_merge_config_existing() {
        let adapter = CodexAdapter;
        let generated = serde_json::json!({
            "notify": ["mi6", "ingest", "event", "--framework", "codex"],
            "otel": {
                "exporter": {
                    "otlp-http": {
                        "endpoint": "http://127.0.0.1:4318",
                        "protocol": "json"
                    }
                }
            }
        });
        let existing = serde_json::json!({
            "model": "gpt-4",
            "otel": {
                "headers": { "x-api-key": "secret" }
            }
        });

        let merged = adapter.merge_config(generated, Some(existing));

        // Should preserve existing settings
        assert_eq!(merged["model"], "gpt-4");
        // Should have notify
        assert!(merged.get("notify").is_some());
        // Should merge otel - nested exporter format
        assert_eq!(
            merged["otel"]["exporter"]["otlp-http"]["endpoint"],
            "http://127.0.0.1:4318"
        );
        // Existing otel keys are preserved
        assert_eq!(merged["otel"]["headers"]["x-api-key"], "secret");
    }

    #[test]
    fn test_supported_events() {
        let adapter = CodexAdapter;
        let events = adapter.supported_events();

        assert!(events.contains(&"agent-turn-complete"));
        assert!(events.contains(&"approval-requested"));
        assert_eq!(events.len(), 2);
    }

    #[test]
    fn test_framework_specific_events() {
        let adapter = CodexAdapter;
        let events = adapter.framework_specific_events();

        assert!(events.contains(&"agent-turn-complete"));
        assert_eq!(events.len(), 1);
    }

    #[test]
    fn test_detection_env_vars() {
        let adapter = CodexAdapter;
        let vars = adapter.detection_env_vars();

        assert!(vars.contains(&"CODEX_SESSION_ID"));
        assert!(vars.contains(&"CODEX_PROJECT_DIR"));
        assert!(vars.contains(&"CODEX_THREAD_ID"));
    }
}
