//! Gemini CLI framework adapter.
//!
//! This adapter handles integration with Google's Gemini CLI tool.
//! Gemini CLI uses a similar hook system to Claude Code with the same
//! JSON configuration structure in `.gemini/settings.json`.

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

/// OTel environment variable keys used by Gemini CLI.
const GEMINI_OTEL_KEYS: &[&str] = &[
    "OTEL_LOGS_EXPORTER",
    "OTEL_EXPORTER_OTLP_PROTOCOL",
    "OTEL_EXPORTER_OTLP_ENDPOINT",
];

/// Gemini CLI framework adapter.
///
/// Handles hook generation and event parsing for Gemini CLI.
pub struct GeminiAdapter;

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

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

    fn project_config_path(&self) -> PathBuf {
        PathBuf::from(".gemini/settings.json")
    }

    fn user_config_path(&self) -> Option<PathBuf> {
        dirs::home_dir().map(|h| h.join(".gemini/settings.json"))
    }

    fn generate_hooks_config(
        &self,
        enabled_events: &[EventType],
        mi6_bin: &str,
        otel_enabled: bool,
        otel_port: u16,
    ) -> serde_json::Value {
        let mut hooks = serde_json::Map::new();

        // Gemini-supported event names
        let gemini_events: std::collections::HashSet<&str> =
            self.supported_events().into_iter().collect();

        // Process canonical events that map to Gemini events
        for event in enabled_events {
            // Map canonical event types to Gemini CLI event names
            let gemini_event = canonical_to_gemini(event);

            // Skip events that Gemini doesn't support
            if !gemini_events.contains(gemini_event.as_ref()) {
                continue;
            }

            // For SessionStart with otel, also ensure the otel server is running
            // Always include --framework flag for explicit framework identification
            let command = if otel_enabled && *event == EventType::SessionStart {
                format!(
                    "{} otel start --port {} </dev/null >/dev/null 2>&1; {} ingest event {} --framework gemini",
                    mi6_bin, otel_port, mi6_bin, gemini_event
                )
            } else {
                format!(
                    "{} ingest event {} --framework gemini",
                    mi6_bin, gemini_event
                )
            };

            let hook_entry = serde_json::json!([{
                "hooks": [{
                    "type": "command",
                    "command": command,
                    "timeout": 10000
                }]
            }]);
            hooks.insert(gemini_event.into_owned(), hook_entry);
        }

        // Add framework-specific events that have no canonical equivalent
        for event_name in self.framework_specific_events() {
            // Skip if already added (shouldn't happen, but be safe)
            if hooks.contains_key(event_name) {
                continue;
            }

            let command = format!("{} ingest event {} --framework gemini", mi6_bin, event_name);

            let hook_entry = serde_json::json!([{
                "hooks": [{
                    "type": "command",
                    "command": command,
                    "timeout": 10000
                }]
            }]);
            hooks.insert(event_name.to_string(), hook_entry);
        }

        serde_json::json!({
            "tools": {
                "enableHooks": true
            },
            "hooks": hooks
        })
    }

    fn merge_config(
        &self,
        generated: serde_json::Value,
        existing: Option<serde_json::Value>,
    ) -> serde_json::Value {
        let mut settings = common::merge_json_hooks(generated, existing);

        // Gemini-specific: ensure tools.enableHooks is set
        if let Some(tools) = settings.get_mut("tools") {
            if let Some(tools_obj) = tools.as_object_mut() {
                tools_obj.insert("enableHooks".to_string(), serde_json::json!(true));
            }
        } else {
            settings["tools"] = serde_json::json!({ "enableHooks": true });
        }

        settings
    }

    fn parse_hook_input(
        &self,
        _event_type: &str,
        stdin_json: &serde_json::Value,
    ) -> ParsedHookInput {
        ParsedHookInputBuilder::new(stdin_json)
            .session_id("session_id")
            .tool_use_id("tool_use_id")
            .tool_name("tool_name")
            .cwd("cwd")
            .transcript_path("transcript_path")
            .subagent_type("tool_input.subagent_type")
            .model("llm_request.model")
            .session_source("source")
            .compact_trigger("trigger")
            .build()
        // Note: Gemini doesn't provide permission_mode, spawned_agent_id, agent_id,
        // agent_transcript_path, duration_ms, tokens, or cost via hooks
    }

    fn map_event_type(&self, framework_event: &str) -> EventType {
        match framework_event {
            // Gemini CLI uses different names for tool events
            "BeforeTool" => EventType::PreToolUse,
            "AfterTool" => EventType::PostToolUse,
            // BeforeAgent maps to UserPromptSubmit (after user submits prompt)
            "BeforeAgent" => EventType::UserPromptSubmit,
            // AfterAgent maps to Stop (both indicate agent finished processing)
            "AfterAgent" => EventType::Stop,
            // PreCompress maps to PreCompact
            "PreCompress" => EventType::PreCompact,
            // These are direct mappings
            "SessionStart" => EventType::SessionStart,
            "SessionEnd" => EventType::SessionEnd,
            "Notification" => EventType::Notification,
            // AfterModel also maps to Stop as a fallback for when AfterAgent doesn't fire.
            // Gemini CLI may not reliably fire AfterAgent, but AfterModel fires after each
            // model response. If tools are used, AfterTool (PostToolUse) sets status back
            // to busy, so the final AfterModel will correctly set status to idle.
            "AfterModel" => EventType::Stop,
            // Other model events become Custom
            "BeforeModel" | "BeforeToolSelection" => EventType::Custom(framework_event.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> {
        vec![
            "SessionStart",
            "SessionEnd",
            "BeforeAgent",
            "AfterAgent",
            "BeforeModel",
            "AfterModel",
            "BeforeToolSelection",
            "BeforeTool",
            "AfterTool",
            "PreCompress",
            "Notification",
        ]
    }

    fn framework_specific_events(&self) -> Vec<&'static str> {
        // These Gemini events don't have direct canonical mappings via canonical_to_gemini(),
        // so they must be added separately to ensure hooks are registered.
        // Note: AfterModel is included here even though it maps to Stop internally,
        // because we need to register a hook for it separately from AfterAgent.
        vec!["BeforeModel", "AfterModel", "BeforeToolSelection"]
    }

    fn detection_env_vars(&self) -> &[&'static str] {
        &["GEMINI_SESSION_ID", "GEMINI_PROJECT_DIR"]
    }

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

    fn otel_support(&self) -> super::OtelSupport {
        use super::OtelSupport;
        // Gemini supports OTEL via env vars in settings.json
        let Some(settings_path) = self.user_config_path() else {
            return OtelSupport::Disabled;
        };
        let Ok(contents) = std::fs::read_to_string(&settings_path) else {
            return OtelSupport::Disabled;
        };
        let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
            return OtelSupport::Disabled;
        };

        // Check for OTEL env vars in the "env" section
        if let Some(env) = json.get("env") {
            for key in GEMINI_OTEL_KEYS {
                if env.get(*key).is_some() {
                    return OtelSupport::Enabled;
                }
            }
        }
        OtelSupport::Disabled
    }

    fn remove_hooks(&self, existing: serde_json::Value) -> Option<serde_json::Value> {
        common::remove_json_hooks(existing, GEMINI_OTEL_KEYS)
    }
}

/// Map canonical event types to Gemini CLI event names
fn canonical_to_gemini(event: &EventType) -> Cow<'static, str> {
    match event {
        EventType::SessionStart => Cow::Borrowed("SessionStart"),
        EventType::SessionEnd => Cow::Borrowed("SessionEnd"),
        EventType::PreToolUse => Cow::Borrowed("BeforeTool"),
        EventType::PostToolUse => Cow::Borrowed("AfterTool"),
        EventType::UserPromptSubmit => Cow::Borrowed("BeforeAgent"),
        EventType::PreCompact => Cow::Borrowed("PreCompress"),
        EventType::Notification => Cow::Borrowed("Notification"),
        // Map Stop to AfterAgent - both indicate agent finished processing
        EventType::Stop => Cow::Borrowed("AfterAgent"),
        // Events that don't have a Gemini equivalent use their canonical name
        EventType::PermissionRequest => Cow::Borrowed("PermissionRequest"),
        EventType::SubagentStart => Cow::Borrowed("SubagentStart"),
        EventType::SubagentStop => Cow::Borrowed("SubagentStop"),
        EventType::ApiRequest => Cow::Borrowed("ApiRequest"),
        EventType::Custom(s) => Cow::Owned(s.clone()),
    }
}

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

    #[test]
    fn test_name() {
        let adapter = GeminiAdapter;
        assert_eq!(adapter.name(), "gemini");
        assert_eq!(adapter.display_name(), "Gemini CLI");
    }

    #[test]
    fn test_project_config_path() {
        let adapter = GeminiAdapter;
        assert_eq!(
            adapter.project_config_path(),
            PathBuf::from(".gemini/settings.json")
        );
    }

    #[test]
    fn test_map_event_type() {
        let adapter = GeminiAdapter;
        // Gemini-specific mappings
        assert_eq!(adapter.map_event_type("BeforeTool"), EventType::PreToolUse);
        assert_eq!(adapter.map_event_type("AfterTool"), EventType::PostToolUse);
        assert_eq!(
            adapter.map_event_type("BeforeAgent"),
            EventType::UserPromptSubmit
        );
        // AfterAgent maps to Stop (both indicate agent finished processing)
        assert_eq!(adapter.map_event_type("AfterAgent"), EventType::Stop);
        assert_eq!(adapter.map_event_type("PreCompress"), EventType::PreCompact);
        // Direct mappings
        assert_eq!(
            adapter.map_event_type("SessionStart"),
            EventType::SessionStart
        );
        assert_eq!(adapter.map_event_type("SessionEnd"), EventType::SessionEnd);
        assert_eq!(
            adapter.map_event_type("Notification"),
            EventType::Notification
        );
        // BeforeModel becomes Custom
        assert_eq!(
            adapter.map_event_type("BeforeModel"),
            EventType::Custom("BeforeModel".to_string())
        );
        // AfterModel maps to Stop (fallback for when AfterAgent doesn't fire)
        assert_eq!(adapter.map_event_type("AfterModel"), EventType::Stop);
    }

    #[test]
    fn test_parse_hook_input() {
        let adapter = GeminiAdapter;
        let input = serde_json::json!({
            "session_id": "gemini-session-123",
            "tool_use_id": "tool-456",
            "tool_name": "write_file",
            "cwd": "/projects/test",
            "transcript_path": "/tmp/transcript.jsonl",
            "tool_input": {
                "file_path": "/tmp/test.txt",
                "content": "hello"
            }
        });

        let parsed = adapter.parse_hook_input("BeforeTool", &input);

        assert_eq!(parsed.session_id, Some("gemini-session-123".to_string()));
        assert_eq!(parsed.tool_use_id, Some("tool-456".to_string()));
        assert_eq!(parsed.tool_name, Some("write_file".to_string()));
        assert_eq!(parsed.cwd, Some("/projects/test".to_string()));
        assert_eq!(
            parsed.transcript_path,
            Some("/tmp/transcript.jsonl".to_string())
        );
        // Verify that permission_mode and spawned_agent_id are not extracted
        assert_eq!(parsed.permission_mode, None);
        assert_eq!(parsed.spawned_agent_id, None);
    }

    #[test]
    fn test_parse_hook_input_with_subagent_type() {
        let adapter = GeminiAdapter;
        let input = serde_json::json!({
            "session_id": "gemini-123",
            "tool_input": {
                "subagent_type": "Explore"
            }
        });

        let parsed = adapter.parse_hook_input("BeforeTool", &input);

        assert_eq!(parsed.session_id, Some("gemini-123".to_string()));
        assert_eq!(parsed.subagent_type, Some("Explore".to_string()));
    }

    #[test]
    fn test_parse_hook_input_session_source() {
        let adapter = GeminiAdapter;
        // Gemini CLI uses "source" field for SessionStart events
        let input = serde_json::json!({
            "session_id": "gemini-session-456",
            "source": "startup",
            "cwd": "/projects/test"
        });

        let parsed = adapter.parse_hook_input("SessionStart", &input);

        assert_eq!(parsed.session_id, Some("gemini-session-456".to_string()));
        assert_eq!(parsed.session_source, Some("startup".to_string()));
        assert_eq!(parsed.cwd, Some("/projects/test".to_string()));

        // Test "resume" source
        let resume_input = serde_json::json!({
            "session_id": "gemini-session-789",
            "source": "resume"
        });
        let parsed = adapter.parse_hook_input("SessionStart", &resume_input);
        assert_eq!(parsed.session_source, Some("resume".to_string()));
    }

    #[test]
    fn test_parse_hook_input_compact_trigger() {
        let adapter = GeminiAdapter;
        // Gemini CLI uses "trigger" field for PreCompress events
        let input = serde_json::json!({
            "session_id": "gemini-session-101",
            "trigger": "auto"
        });

        let parsed = adapter.parse_hook_input("PreCompress", &input);

        assert_eq!(parsed.session_id, Some("gemini-session-101".to_string()));
        assert_eq!(parsed.compact_trigger, Some("auto".to_string()));

        // Test "manual" trigger
        let manual_input = serde_json::json!({
            "session_id": "gemini-session-102",
            "trigger": "manual"
        });
        let parsed = adapter.parse_hook_input("PreCompress", &manual_input);
        assert_eq!(parsed.compact_trigger, Some("manual".to_string()));
    }

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

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

        let hooks = config
            .get("hooks")
            .ok_or("missing hooks")?
            .as_object()
            .ok_or("hooks not an object")?;
        // Should use Gemini event names
        assert!(hooks.contains_key("SessionStart"));
        assert!(hooks.contains_key("BeforeTool")); // PreToolUse -> BeforeTool

        let before_tool = &hooks["BeforeTool"][0]["hooks"][0];
        let command = before_tool["command"].as_str().ok_or("missing command")?;
        assert!(command.contains("mi6 ingest event BeforeTool"));
        // Should include explicit --framework flag
        assert!(command.contains("--framework gemini"));
        // Type should be "command"
        assert_eq!(
            before_tool["type"].as_str().ok_or("missing type")?,
            "command"
        );
        // Timeout should be in milliseconds
        assert_eq!(
            before_tool["timeout"].as_i64().ok_or("missing timeout")?,
            10000
        );

        // Should also include framework-specific events
        assert!(
            hooks.contains_key("BeforeModel"),
            "Framework-specific BeforeModel should be included"
        );
        assert!(
            hooks.contains_key("AfterModel"),
            "AfterModel should be included (maps to Stop as fallback)"
        );
        assert!(
            hooks.contains_key("BeforeToolSelection"),
            "Framework-specific BeforeToolSelection should be included"
        );
        Ok(())
    }

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

        assert!(events.contains(&"BeforeModel"));
        // AfterModel is included to ensure a hook is registered, even though
        // it maps to Stop internally (as a fallback for when AfterAgent doesn't fire)
        assert!(events.contains(&"AfterModel"));
        assert!(events.contains(&"BeforeToolSelection"));
        assert_eq!(events.len(), 3);
    }

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

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

        let hooks = config
            .get("hooks")
            .ok_or("missing hooks")?
            .as_object()
            .ok_or("hooks not an object")?;
        let session_start = &hooks["SessionStart"][0]["hooks"][0];
        let command = session_start["command"].as_str().ok_or("missing command")?;

        assert!(command.contains("otel start"));
        assert!(command.contains("--port 4318"));
        assert!(command.contains("--framework gemini"));
        Ok(())
    }

    #[test]
    fn test_merge_config_new() {
        let adapter = GeminiAdapter;
        let generated = serde_json::json!({
            "hooks": {
                "BeforeTool": [{"matcher": "", "hooks": []}]
            }
        });

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

        assert!(merged.get("hooks").is_some());
        assert!(merged["hooks"].get("BeforeTool").is_some());
    }

    #[test]
    fn test_merge_config_existing() {
        let adapter = GeminiAdapter;
        let generated = serde_json::json!({
            "hooks": {
                "BeforeTool": [{"matcher": "", "hooks": [{"command": "mi6 ingest event BeforeTool"}]}]
            }
        });
        let existing = serde_json::json!({
            "theme": "dark",
            "hooks": {
                "SessionStart": [{"matcher": "", "hooks": [{"command": "other-tool"}]}]
            }
        });

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

        // Should preserve existing settings
        assert_eq!(merged["theme"], "dark");
        // Should merge hooks
        assert!(merged["hooks"].get("SessionStart").is_some());
        assert!(merged["hooks"].get("BeforeTool").is_some());
    }

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

        assert!(events.contains(&"SessionStart"));
        assert!(events.contains(&"BeforeTool"));
        assert!(events.contains(&"AfterTool"));
        assert!(events.contains(&"BeforeModel"));
        assert!(events.contains(&"AfterModel"));
    }

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

        assert!(vars.contains(&"GEMINI_SESSION_ID"));
        assert!(vars.contains(&"GEMINI_PROJECT_DIR"));
    }

    #[test]
    fn test_parse_hook_input_with_model() {
        let adapter = GeminiAdapter;
        // Gemini CLI provides model in llm_request.model for BeforeModel/AfterModel events
        let input = serde_json::json!({
            "session_id": "gemini-session-abc",
            "cwd": "/projects/test",
            "hook_event_name": "BeforeModel",
            "llm_request": {
                "model": "gemini-2.5-flash",
                "config": {
                    "temperature": 1
                },
                "messages": []
            }
        });

        let parsed = adapter.parse_hook_input("BeforeModel", &input);

        assert_eq!(parsed.session_id, Some("gemini-session-abc".to_string()));
        assert_eq!(parsed.model, Some("gemini-2.5-flash".to_string()));
        assert_eq!(parsed.cwd, Some("/projects/test".to_string()));
    }

    #[test]
    fn test_parse_hook_input_without_model() {
        let adapter = GeminiAdapter;
        // Events without llm_request should still work (model will be None)
        let input = serde_json::json!({
            "session_id": "gemini-session-xyz",
            "cwd": "/projects/test"
        });

        let parsed = adapter.parse_hook_input("SessionStart", &input);

        assert_eq!(parsed.session_id, Some("gemini-session-xyz".to_string()));
        assert_eq!(parsed.model, None);
    }
}
