use serde_json::Value;
use tracing::debug;

use super::ChatSessionHandler;
use super::emitter::TurnEmitter;
use crate::tools::{is_file_write_tool, tool_names as tn};

impl ChatSessionHandler {
    /// Emits the appropriate frontend approval event for the given tool call.
    ///
    /// | Tool kind        | Event emitted                                         |
    /// |------------------|-------------------------------------------------------|
    /// | file-write tools | `PendingWrite` with before/after diff (IO concurrent) |
    /// | `execute_cmd`    | `PendingWrite` with command preview                   |
    /// | `restart`        | `PendingWrite` with restart description               |
    /// | everything else  | `ApprovalRequired`                                    |
    ///
    /// Called from both `llm_loop` and `resume_pending_tools` to avoid duplication.
    pub(super) async fn emit_approval_event(
        &self,
        em:           &TurnEmitter<'_>,
        request_id:   i64,
        tool_call_id: i64,
        tool_name:    &str,
        arguments:    &Value,
    ) {
        if is_file_write_tool(tool_name) {
            let path = arguments["path"].as_str().unwrap_or("").to_string();
            // Read current file and compute new content concurrently — both are disk I/O.
            let (old_content, new_content) = tokio::join!(
                self.read_current_content(&path),
                self.compute_new_content(tool_name, arguments),
            );
            if let Some(new_content) = new_content {
                em.pending_write(request_id, tool_call_id, path, old_content, new_content).await;
            } else {
                // File doesn't exist yet or diff can't be computed — fall back to generic.
                debug!(tool = tool_name, "emit_approval_event: no diff available, using ApprovalRequired");
                em.approval_required(request_id, tool_call_id, tool_name.to_string(), arguments.clone()).await;
            }
        } else if tool_name == tn::EXECUTE_CMD {
            let cmd = arguments["command"].as_str().unwrap_or("");
            em.pending_write(request_id, tool_call_id, "$ execute_cmd".to_string(), None, format!("$ {cmd}")).await;
        } else if tool_name == tn::RESTART {
            em.pending_write(
                request_id, tool_call_id,
                "$ restart".to_string(),
                None,
                "Riavvia il processo (exit -1 → supervisor ricompila e rilancia)".to_string(),
            ).await;
        } else {
            em.approval_required(request_id, tool_call_id, tool_name.to_string(), arguments.clone()).await;
        }
    }

    /// Reads the current content of a file from disk (for diff generation in PendingWrite events).
    pub(super) async fn read_current_content(&self, path: &str) -> Option<String> {
        let abs = crate::tools::fs::resolve(path).ok()?;
        tokio::fs::read_to_string(&abs).await.ok()
    }

    /// Computes what a file would look like after the tool runs, without writing it.
    /// Returns `None` if the result cannot be determined (e.g. edit_file on a missing file).
    pub(super) async fn compute_new_content(&self, name: &str, args: &Value) -> Option<String> {
        match name {
            "write_file" => args["content"].as_str().map(|s| s.to_string()),
            "edit_file" => {
                let path     = args["path"].as_str()?;
                let old_text = args["old"].as_str()?;
                let new_text = args["new"].as_str()?;
                let current  = self.read_current_content(path).await?;
                if current.contains(old_text) {
                    Some(current.replacen(old_text, new_text, 1))
                } else {
                    None
                }
            }
            "insert_at_line" => {
                let path      = args["path"].as_str()?;
                let line_num  = args["line"].as_u64()? as usize;
                let new_text  = args["content"].as_str()?;
                let placement = args["placement"].as_str().unwrap_or("after");
                if line_num == 0 { return None; }
                let current = self.read_current_content(path).await?;
                let mut lines: Vec<&str> = current.split('\n').collect();
                let idx        = (line_num - 1).min(lines.len().saturating_sub(1));
                let insert_idx = if placement == "before" { idx } else { idx + 1 };
                let new_lines: Vec<&str> = new_text.split('\n').collect();
                for (i, l) in new_lines.iter().enumerate() {
                    lines.insert(insert_idx + i, l);
                }
                Some(lines.join("\n"))
            }
            "replace_lines" => {
                let path      = args["path"].as_str()?;
                let from_line = args["from_line"].as_u64()? as usize;
                let to_line   = args["to_line"].as_u64()? as usize;
                let new_text  = args["new"].as_str()?;
                if from_line == 0 || to_line < from_line { return None; }
                let current = self.read_current_content(path).await?;
                let mut lines: Vec<&str> = current.lines().collect();
                let total = lines.len();
                if from_line > total { return None; }
                let to_clamped = to_line.min(total);
                let new_lines: Vec<&str> = new_text.lines().collect();
                lines.splice((from_line - 1)..to_clamped, new_lines);
                let has_trailing = current.ends_with('\n');
                let mut result = lines.join("\n");
                if has_trailing { result.push('\n'); }
                Some(result)
            }
            _ => None,
        }
    }
}
