//! Claude Code CLI integration for embedding AI chat in applications.
//!
//! Provides a background worker thread that spawns the `claude` CLI as a subprocess
//! and streams its JSON output back to the application. This lets games and editors
//! embed an AI assistant that can send queries, receive streamed responses, and
//! manage sessions.
//!
//! Requires the `claude` CLI to be installed and available on `PATH`.
//! Native only (not available on WASM).
//!
//! # Usage
//!
//! ```ignore
//! let (command_sender, command_receiver, event_sender, event_receiver) =
//!     nightshade::claude::create_cli_channels();
//!
//! nightshade::claude::spawn_cli_worker(
//!     command_receiver,
//!     event_sender,
//!     ClaudeConfig {
//!         system_prompt: Some("You are a scene designer.".to_string()),
//!         mcp_config: McpConfig::Auto,
//!         ..Default::default()
//!     },
//! );
//!
//! // Send a query
//! command_sender.send(CliCommand::StartQuery {
//!     prompt: "Create a forest scene".to_string(),
//!     session_id: None,
//!     model: None,
//! }).ok();
//!
//! // Poll for events each frame
//! while let Ok(event) = event_receiver.try_recv() {
//!     match event {
//!         CliEvent::TextDelta { text } => { /* append to chat */ }
//!         CliEvent::Complete { .. } => { /* query finished */ }
//!         _ => {}
//!     }
//! }
//! ```
//!
//! When both `claude` and `mcp` features are enabled and [`McpConfig::Auto`] is used
//! (the default), the worker automatically passes `--mcp-config` pointing at the
//! engine's MCP server, so Claude Code can call engine tools without manual setup.

use std::io::BufRead;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex};

/// How the Claude CLI worker connects to an MCP server.
pub enum McpConfig {
    /// Automatically connect to the engine's MCP server at `http://127.0.0.1:3333/mcp`.
    /// Only effective when the `mcp` feature is also enabled; otherwise behaves like `None`.
    Auto,
    /// Use a custom MCP config JSON string (passed via `--mcp-config`).
    Custom(String),
    /// Do not configure any MCP connection.
    None,
}

impl Default for McpConfig {
    fn default() -> Self {
        Self::Auto
    }
}

/// Configuration for the Claude CLI worker.
#[derive(Default)]
pub struct ClaudeConfig {
    /// System prompt appended via `--append-system-prompt`.
    pub system_prompt: Option<String>,
    /// Restrict which tools Claude can use (`--allowedTools`).
    pub allowed_tools: Option<Vec<String>>,
    /// Block specific tools (`--disallowedTools`).
    pub disallowed_tools: Option<Vec<String>>,
    /// MCP server connection configuration.
    pub mcp_config: McpConfig,
    /// Additional CLI arguments passed directly to the `claude` command.
    pub custom_args: Vec<String>,
    /// Environment variables set on the spawned `claude` process.
    pub env: Vec<(String, String)>,
}

/// Commands sent to the Claude CLI worker thread.
pub enum CliCommand {
    /// Start a new query with the given prompt. Optionally resume a session or override the model.
    StartQuery {
        prompt: String,
        session_id: Option<String>,
        model: Option<String>,
    },
    /// Cancel the currently running query.
    Cancel,
}

/// Events received from the Claude CLI worker thread.
pub enum CliEvent {
    /// A new Claude session was created.
    SessionStarted { session_id: String },
    /// Incremental text output from Claude.
    TextDelta { text: String },
    /// Incremental thinking/reasoning output from Claude.
    ThinkingDelta { text: String },
    /// Claude began calling a tool.
    ToolUseStarted { tool_name: String, tool_id: String },
    /// Streaming tool input JSON.
    ToolUseInputDelta {
        tool_id: String,
        partial_json: String,
    },
    /// Tool call completed.
    ToolUseFinished { tool_id: String },
    /// Claude finished a turn (may continue with more turns).
    TurnComplete { session_id: String },
    /// Full query completed with cost and turn count.
    Complete {
        session_id: String,
        total_cost_usd: Option<f64>,
        num_turns: u32,
    },
    /// An error occurred.
    Error { message: String },
}

/// Creates the command and event channel pairs for communicating with the CLI worker.
///
/// Returns `(command_sender, command_receiver, event_sender, event_receiver)`.
/// Pass the receiver/sender halves to [`spawn_cli_worker`] and keep the other halves
/// in your application for sending commands and receiving events.
pub fn create_cli_channels() -> (
    Sender<CliCommand>,
    Receiver<CliCommand>,
    Sender<CliEvent>,
    Receiver<CliEvent>,
) {
    let (command_sender, command_receiver) = std::sync::mpsc::channel();
    let (event_sender, event_receiver) = std::sync::mpsc::channel();
    (
        command_sender,
        command_receiver,
        event_sender,
        event_receiver,
    )
}

#[cfg(feature = "mcp")]
fn auto_mcp_config() -> String {
    serde_json::json!({
        "mcpServers": {
            "nightshade": {
                "type": "http",
                "url": format!("http://{}:{}/mcp", crate::mcp::MCP_DEFAULT_HOST, crate::mcp::MCP_DEFAULT_PORT)
            }
        }
    })
    .to_string()
}

fn resolve_mcp_config(config: &McpConfig) -> Option<String> {
    match config {
        McpConfig::Auto => {
            #[cfg(feature = "mcp")]
            {
                Some(auto_mcp_config())
            }
            #[cfg(not(feature = "mcp"))]
            {
                Option::None
            }
        }
        McpConfig::Custom(json) => Some(json.clone()),
        McpConfig::None => Option::None,
    }
}

fn is_auto_mcp(config: &McpConfig) -> bool {
    #[cfg(feature = "mcp")]
    {
        matches!(config, McpConfig::Auto)
    }
    #[cfg(not(feature = "mcp"))]
    {
        let _ = config;
        false
    }
}

/// Spawns a background thread that listens for [`CliCommand`]s and streams
/// [`CliEvent`]s back. The thread runs until the command channel is dropped.
pub fn spawn_cli_worker(
    command_receiver: Receiver<CliCommand>,
    event_sender: Sender<CliEvent>,
    config: ClaudeConfig,
) {
    std::thread::spawn(move || {
        let mut current_child: Option<Child> = None;
        let shared_session_id: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));

        loop {
            match command_receiver.recv() {
                Ok(CliCommand::StartQuery {
                    prompt,
                    session_id,
                    model,
                }) => {
                    if let Some(mut child) = current_child.take() {
                        let _ = child.kill();
                        let _ = child.wait();
                    }

                    let mut args = vec![
                        "-p".to_string(),
                        prompt,
                        "--output-format".to_string(),
                        "stream-json".to_string(),
                        "--verbose".to_string(),
                        "--include-partial-messages".to_string(),
                    ];

                    if let Some(ref system_prompt) = config.system_prompt {
                        args.push("--append-system-prompt".to_string());
                        args.push(system_prompt.clone());
                    }

                    if let Some(ref allowed) = config.allowed_tools {
                        for tool in allowed {
                            args.push("--allowedTools".to_string());
                            args.push(tool.clone());
                        }
                    }

                    if let Some(ref disallowed) = config.disallowed_tools {
                        for tool in disallowed {
                            args.push("--disallowedTools".to_string());
                            args.push(tool.clone());
                        }
                    }

                    if let Some(mcp_json) = resolve_mcp_config(&config.mcp_config) {
                        args.push("--mcp-config".to_string());
                        args.push(mcp_json);
                    }

                    if is_auto_mcp(&config.mcp_config) {
                        if config.allowed_tools.is_none() {
                            args.push("--allowedTools".to_string());
                            args.push("mcp__nightshade__*".to_string());
                        }
                        if config.disallowed_tools.is_none() {
                            args.push("--disallowedTools".to_string());
                            args.push("Bash,Edit,Write,NotebookEdit,Task".to_string());
                        }
                    }

                    for arg in &config.custom_args {
                        args.push(arg.clone());
                    }

                    if let Some(session) = session_id {
                        args.push("--resume".to_string());
                        args.push(session);
                    }

                    if let Some(model_name) = model {
                        args.push("--model".to_string());
                        args.push(model_name);
                    }

                    let mut command = Command::new("claude");
                    command
                        .args(&args)
                        .stdout(Stdio::piped())
                        .stderr(Stdio::piped())
                        .env_remove("CLAUDECODE");

                    for (key, value) in &config.env {
                        command.env(key, value);
                    }

                    #[cfg(target_os = "windows")]
                    {
                        use std::os::windows::process::CommandExt;
                        command.creation_flags(0x08000000);
                    }

                    match command.spawn() {
                        Ok(mut child) => {
                            let stdout = child.stdout.take().expect("stdout was piped");
                            let stderr = child.stderr.take().expect("stderr was piped");
                            current_child = Some(child);

                            std::thread::spawn(move || {
                                let reader = std::io::BufReader::new(stderr);
                                for _ in reader.lines() {}
                            });

                            let event_sender_clone = event_sender.clone();
                            let session_id_writer = shared_session_id.clone();
                            *shared_session_id.lock().unwrap() = String::new();

                            std::thread::spawn(move || {
                                let reader = std::io::BufReader::new(stdout);
                                let mut session_id = String::new();
                                let mut current_tool_id = String::new();

                                for line_result in reader.lines() {
                                    let line = match line_result {
                                        Ok(line) => line,
                                        Err(_) => break,
                                    };

                                    if line.trim().is_empty() {
                                        continue;
                                    }

                                    let json_value: serde_json::Value =
                                        match serde_json::from_str(&line) {
                                            Ok(value) => value,
                                            Err(_) => continue,
                                        };

                                    let events = parse_stream_json_line(
                                        &json_value,
                                        &mut session_id,
                                        &mut current_tool_id,
                                    );
                                    for event in &events {
                                        if let CliEvent::SessionStarted { session_id: sid } = event
                                        {
                                            *session_id_writer.lock().unwrap() = sid.clone();
                                        }
                                    }
                                    for event in events {
                                        if event_sender_clone.send(event).is_err() {
                                            return;
                                        }
                                    }
                                }
                            });
                        }
                        Err(error) => {
                            let _ = event_sender.send(CliEvent::Error {
                                message: format!("Failed to spawn claude CLI: {error}"),
                            });
                        }
                    }
                }
                Ok(CliCommand::Cancel) => {
                    if let Some(mut child) = current_child.take() {
                        let _ = child.kill();
                        let _ = child.wait();
                    }
                    let session_id = shared_session_id.lock().unwrap().clone();
                    let _ = event_sender.send(CliEvent::TurnComplete { session_id });
                }
                Err(_) => {
                    if let Some(mut child) = current_child.take() {
                        let _ = child.kill();
                        let _ = child.wait();
                    }
                    break;
                }
            }
        }
    });
}

/// Parses a single line of Claude CLI stream-json output into [`CliEvent`]s.
pub fn parse_stream_json_line(
    value: &serde_json::Value,
    session_id: &mut String,
    current_tool_id: &mut String,
) -> Vec<CliEvent> {
    let mut events = Vec::new();

    let message_type = value
        .get("type")
        .and_then(|value| value.as_str())
        .unwrap_or("");

    match message_type {
        "system" => {
            if let Some(sid) = value.get("session_id").and_then(|value| value.as_str()) {
                *session_id = sid.to_string();
                events.push(CliEvent::SessionStarted {
                    session_id: sid.to_string(),
                });
            }
        }

        "stream_event" => {
            if let Some(event) = value.get("event") {
                let event_type = event
                    .get("type")
                    .and_then(|value| value.as_str())
                    .unwrap_or("");

                match event_type {
                    "content_block_start" => {
                        if let Some(content_block) = event.get("content_block") {
                            let block_type = content_block
                                .get("type")
                                .and_then(|value| value.as_str())
                                .unwrap_or("");
                            if block_type == "tool_use" {
                                let tool_name = content_block
                                    .get("name")
                                    .and_then(|value| value.as_str())
                                    .unwrap_or("unknown")
                                    .to_string();
                                let tool_id = content_block
                                    .get("id")
                                    .and_then(|value| value.as_str())
                                    .unwrap_or("")
                                    .to_string();
                                *current_tool_id = tool_id.clone();
                                events.push(CliEvent::ToolUseStarted { tool_name, tool_id });
                            }
                        }
                    }

                    "content_block_delta" => {
                        if let Some(delta) = event.get("delta") {
                            let delta_type = delta
                                .get("type")
                                .and_then(|value| value.as_str())
                                .unwrap_or("");

                            match delta_type {
                                "text_delta" => {
                                    if let Some(text) =
                                        delta.get("text").and_then(|value| value.as_str())
                                    {
                                        events.push(CliEvent::TextDelta {
                                            text: text.to_string(),
                                        });
                                    }
                                }
                                "input_json_delta" => {
                                    if let Some(partial) =
                                        delta.get("partial_json").and_then(|value| value.as_str())
                                    {
                                        events.push(CliEvent::ToolUseInputDelta {
                                            tool_id: current_tool_id.clone(),
                                            partial_json: partial.to_string(),
                                        });
                                    }
                                }
                                "thinking_delta" => {
                                    if let Some(text) =
                                        delta.get("thinking").and_then(|value| value.as_str())
                                    {
                                        events.push(CliEvent::ThinkingDelta {
                                            text: text.to_string(),
                                        });
                                    }
                                }
                                _ => {}
                            }
                        }
                    }

                    "content_block_stop" => {
                        if !current_tool_id.is_empty() {
                            events.push(CliEvent::ToolUseFinished {
                                tool_id: current_tool_id.clone(),
                            });
                            current_tool_id.clear();
                        }
                    }

                    "message_stop" => {
                        events.push(CliEvent::TurnComplete {
                            session_id: session_id.clone(),
                        });
                    }

                    _ => {}
                }
            }
        }

        "result" => {
            let total_cost = value.get("total_cost_usd").and_then(|value| value.as_f64());
            let num_turns = value
                .get("num_turns")
                .and_then(|value| value.as_u64())
                .unwrap_or(0) as u32;
            events.push(CliEvent::Complete {
                session_id: session_id.clone(),
                total_cost_usd: total_cost,
                num_turns,
            });
        }

        _ => {}
    }

    events
}
