use std::collections::HashMap;
use std::io::Read;

use anyhow::{Context, Result};
use fyi_core::{
    Config, EventBuilder, EventType, FrameworkAdapter, Storage, default_adapter,
    detect_all_frameworks, detect_framework, get_adapter,
};

use crate::process::{ClaudeProcessInfo, find_claude_process, get_parent_pid};

/// Run the log command. This must be fast (<10ms) and never block the calling framework.
pub fn run_log<S: Storage>(
    storage: &S,
    event_type_arg: Option<String>,
    json_payload: Option<String>,
    framework_name: Option<String>,
) -> Result<()> {
    // Determine the framework adapter:
    // 1. Use explicit --framework flag if provided
    // 2. Otherwise, try to auto-detect from environment variables
    // 3. Fall back to default (claude)
    let adapter: &dyn FrameworkAdapter = if let Some(ref name) = framework_name {
        get_adapter(name).ok_or_else(|| anyhow::anyhow!("unknown framework: {}", name))?
    } else {
        // Warn if multiple frameworks detected (to stderr, doesn't affect hook)
        let detected = detect_all_frameworks();
        if detected.len() > 1 {
            let names: Vec<_> = detected.iter().map(|a| a.name()).collect();
            eprintln!(
                "fyi: warning: multiple frameworks detected ({}), using {}",
                names.join(", "),
                detected[0].name()
            );
        }
        detect_framework().unwrap_or_else(default_adapter)
    };

    // Handle argument parsing:
    // - Claude/Gemini: `fyi log SessionStart` with JSON on stdin
    // - Codex: `fyi log --framework codex '{"type":"agent-turn-complete",...}'`
    //
    // Codex passes JSON as the only positional arg, which clap captures as event_type_arg.
    // We detect this by checking if event_type_arg looks like JSON (starts with '{').
    let (actual_event_type, json_str) = if let Some(ref arg) = event_type_arg {
        if arg.trim().starts_with('{') {
            // First positional arg is JSON (Codex format) - extract event type from it
            (None, arg.clone())
        } else {
            // First positional arg is event type (Claude/Gemini format)
            let json = if let Some(payload) = json_payload {
                payload
            } else {
                let mut stdin_data = String::new();
                std::io::stdin()
                    .read_to_string(&mut stdin_data)
                    .context("failed to read stdin")?;
                stdin_data
            };
            (Some(arg.clone()), json)
        }
    } else {
        // No positional args - read JSON from stdin
        let mut stdin_data = String::new();
        std::io::stdin()
            .read_to_string(&mut stdin_data)
            .context("failed to read stdin")?;
        (None, stdin_data)
    };

    // Parse the hook JSON
    let hook_data: serde_json::Value = if json_str.trim().is_empty() {
        serde_json::json!({})
    } else {
        serde_json::from_str(&json_str).context("failed to parse hook JSON")?
    };

    // Determine event type:
    // 1. Use explicit CLI argument if provided
    // 2. Otherwise extract from JSON payload's "type" field (Codex CLI format)
    // 3. Fall back to "Unknown" if neither available
    let event_type_str = if let Some(ref et) = actual_event_type {
        et.clone()
    } else {
        // Extract from JSON "type" field (Codex CLI format)
        hook_data
            .get("type")
            .and_then(|v| v.as_str())
            .map_or_else(|| "Unknown".to_string(), String::from)
    };

    // Map the event type using the adapter (handles framework-specific event names)
    let event_type: EventType = adapter.map_event_type(&event_type_str);

    // Use adapter to parse hook input into normalized fields
    let parsed = adapter.parse_hook_input(&event_type_str, &hook_data);

    // Extract session_id (required field, default to "unknown")
    let session_id = parsed.session_id.unwrap_or_else(|| "unknown".to_string());

    // For SessionStart, find calling process once and reuse (avoids duplicate ps calls)
    let process_info = if event_type == EventType::SessionStart {
        find_claude_process()
    } else {
        None
    };

    // Capture PID - use cached process info for SessionStart, parent PID otherwise
    let pid = process_info
        .as_ref()
        .map(|info| info.pid)
        .or_else(get_parent_pid);

    // Build payload: merge hook JSON with environment variables and process info
    let payload = build_payload(hook_data, &event_type, process_info.as_ref())?;

    // Load config and get machine_id
    let config = Config::load().unwrap_or_default();
    let machine_id = config.machine_id();

    // Create and insert event
    let event = EventBuilder::new(machine_id, event_type, session_id)
        .framework(adapter.name())
        .tool_use_id_opt(parsed.tool_use_id)
        .spawned_agent_id_opt(parsed.spawned_agent_id)
        .tool_name_opt(parsed.tool_name)
        .subagent_type_opt(parsed.subagent_type)
        .permission_mode_opt(parsed.permission_mode)
        .transcript_path_opt(parsed.transcript_path)
        .pid_opt(pid)
        .cwd_opt(parsed.cwd)
        .payload(payload)
        .build();

    storage.insert(&event).context("failed to insert event")?;

    // Opportunistic GC (~1.2% of calls: 3/256)
    if rand::random::<u8>() < 3 {
        let _ = storage.gc(config.retention_duration());
    }

    // Success: no output (silent operation)
    Ok(())
}

/// Build the payload JSON by merging hook data with environment variables
fn build_payload(
    mut hook_data: serde_json::Value,
    event_type: &EventType,
    process_info: Option<&ClaudeProcessInfo>,
) -> Result<String> {
    // Environment variables to capture
    let env_vars = [
        ("CLAUDE_PROJECT_DIR", "project_dir"),
        ("CLAUDE_FILE_PATHS", "file_paths"),
        ("CLAUDE_TOOL_INPUT", "tool_input_env"),
        ("CLAUDE_TOOL_OUTPUT", "tool_output_env"),
        ("CLAUDE_NOTIFICATION", "notification_env"),
        ("CLAUDE_CODE_REMOTE", "remote"),
        ("CLAUDE_ENV_FILE", "env_file"),
    ];

    // Ensure we have an object to work with
    if !hook_data.is_object() {
        hook_data = serde_json::json!({ "_raw": hook_data });
    }

    let Some(obj) = hook_data.as_object_mut() else {
        // We just ensured it's an object above, so this is unreachable
        return Ok(serde_json::to_string(&hook_data)?);
    };

    // Add environment variables to the payload
    let mut env_data: HashMap<String, String> = HashMap::new();
    for (env_var, key) in env_vars {
        if let Ok(value) = std::env::var(env_var) {
            env_data.insert(key.to_string(), value);
        }
    }

    if !env_data.is_empty() {
        obj.insert("_env".to_string(), serde_json::to_value(env_data)?);
    }

    // For SessionStart, add process info to payload (using cached info)
    if *event_type == EventType::SessionStart
        && let Some(info) = process_info
    {
        obj.insert(
            "_claude_process".to_string(),
            serde_json::json!({
                "pid": info.pid,
                "comm": info.comm
            }),
        );
    }

    Ok(serde_json::to_string(&hook_data)?)
}
