use super::*;
use crate::conversation::message::MessageContent;

impl AgentLoop {
    /// Forward a TurnEvent to the TUI as an AgentEvent.
    /// Also writes to the datalog for persistent turn logging.
    pub(crate) fn forward_turn_event(&mut self, event: TurnEvent) {
        match event {
            TurnEvent::TextDelta(text) => {
                self.discipline_state.model_produced_text = true;
                let _ = self.event_tx.send(AgentEvent::TextDelta(text));
            }
            TurnEvent::ReasoningDelta(text) => {
                let _ = self.event_tx.send(AgentEvent::ReasoningDelta(text));
            }
            TurnEvent::ToolCallStreaming { name, hint } => {
                let _ = self
                    .event_tx
                    .send(AgentEvent::ToolCallStreaming { name, hint });
            }
            TurnEvent::ToolBatchStarted { batch_id, calls } => {
                let _ = self
                    .event_tx
                    .send(AgentEvent::ToolBatchStarted { batch_id, calls });
            }
            TurnEvent::ToolBatchCompleted {
                batch_id,
                ok,
                total,
                elapsed_ms,
            } => {
                let _ = self.event_tx.send(AgentEvent::ToolBatchCompleted {
                    batch_id,
                    ok,
                    total,
                    elapsed_ms,
                });
            }
            TurnEvent::ToolOutputChunk { call_id, chunk } => {
                let _ = self
                    .event_tx
                    .send(AgentEvent::ToolOutputChunk { call_id, chunk });
            }
            TurnEvent::ToolCallStarted {
                ref id,
                ref name,
                ref arguments,
            } => {
                // Dedupe across retries — see the matching guard in
                // `agent/mod.rs` inline forward path. Same id arriving
                // twice means the previous attempt's stream got cut off
                // (429 / timeout) and was retried; we've already painted
                // a row for it.
                if !self.emitted_tool_ids.insert(id.clone()) {
                    return;
                }
                self.datalog.log_tool_call(name, arguments);

                self.current_tool_name = name.clone();
                self.phase = AgentPhase::CallingTool(name.clone());
                let _ = self
                    .event_tx
                    .send(AgentEvent::PhaseChange(self.phase.clone()));

                // Track bash command for failure categorization
                if name == "bash" {
                    if let Ok(args) = serde_json::from_str::<serde_json::Value>(arguments) {
                        self.discipline_state.last_bash_cmd = args
                            .get("command")
                            .and_then(|v| v.as_str())
                            .unwrap_or("")
                            .to_string();
                    }
                }

                // Track files for Working Set
                if matches!(
                    name.as_str(),
                    "read_file" | "edit_file" | "create_file" | "search_replace" | "glob" | "grep"
                ) {
                    if let Ok(args) = serde_json::from_str::<serde_json::Value>(arguments) {
                        let fp = args
                            .get("file_path")
                            .and_then(|v| v.as_str())
                            .or_else(|| args.get("path").and_then(|v| v.as_str()));
                        if let Some(fp) = fp {
                            let short = std::path::Path::new(fp)
                                .file_name()
                                .map(|n| n.to_string_lossy().to_string())
                                .unwrap_or_else(|| fp.to_string());
                            self.session_files
                                .insert(short, std::path::PathBuf::from(fp));
                        }
                    }
                }

                let _ = self.event_tx.send(AgentEvent::ToolCallStarted {
                    id: id.clone(),
                    name: name.clone(),
                    arguments: arguments.clone(),
                });
            }
            TurnEvent::ToolCallResult {
                call_id,
                name,
                output,
                success,
                duration,
            } => {
                // Track files for discipline
                if let Some(pos) = output.find("Edited ") {
                    // Extract full path from "Edited /path/to/file ..." or "Edited /path/to/file\n..."
                    let rest = &output[pos + 7..];
                    let full_path_end = rest
                        .find(|c: char| c == ' ' || c == '\n' || c == '(')
                        .unwrap_or(rest.len());
                    let full_path_str = rest[..full_path_end].trim();
                    if !full_path_str.is_empty() {
                        self.active_file = Some(PathBuf::from(full_path_str));
                    }
                    if !full_path_str.is_empty() {
                        let file = full_path_str.to_string();
                        if !self.files_edited_this_turn.contains(&file) {
                            self.files_edited_this_turn.push(file);
                        }
                    }
                }
                if let Some(pos) = output.find("Wrote ").or_else(|| output.find("Overwrote ")).or_else(|| output.find("Created new file ")) {
                    let keyword_len = if output[pos..].starts_with("Overwrote ") {
                        10
                    } else if output[pos..].starts_with("Created new file ") {
                        17
                    } else {
                        6
                    };
                    let rest = &output[pos + keyword_len..];
                    let full_path_end = rest
                        .find(|c: char| c == ' ' || c == '\n' || c == '(')
                        .unwrap_or(rest.len());
                    let full_path_str = rest[..full_path_end].trim();
                    if !full_path_str.is_empty() {
                        self.active_file = Some(PathBuf::from(full_path_str));
                    }
                    if !full_path_str.is_empty() {
                        let file = full_path_str.to_string();
                        if !self.files_edited_this_turn.contains(&file) {
                            self.files_edited_this_turn.push(file);
                        }
                    }
                }
                if success {
                    track_tool_modified_files(
                        &name,
                        &self.discipline_state.last_bash_cmd,
                        &output,
                        &mut self.files_edited_this_turn,
                    );
                }
                if matches!(
                    name.as_str(),
                    "read_file" | "list_directory" | "glob" | "grep"
                ) {
                    self.discipline_state.consecutive_reads += 1;
                } else if matches!(name.as_str(), "edit_file" | "create_file") {
                    self.discipline_state.consecutive_reads = 0;
                }

                // Track scouting commands for datalog metrics (no injection).
                if name == "bash" {
                    let cmd = self.discipline_state.last_bash_cmd.to_lowercase();
                    if cmd.contains("curl")
                        || cmd.contains("lsof")
                        || cmd.contains("ps aux")
                        || cmd.contains("tail")
                    {
                        self.discipline_state.scouting_count += 1;
                    }
                } else if matches!(name.as_str(), "read_file" | "edit_file" | "create_file") {
                    self.discipline_state.scouting_count = 0;
                }

                self.datalog.log_tool_result(&output, success);
                let _ = self.event_tx.send(AgentEvent::ToolCallResult {
                    call_id,
                    name,
                    output,
                    success,
                    duration,
                });
            }
            TurnEvent::TokenUsage {
                prompt_tokens,
                completion_tokens,
                total_tokens: _,
                cached_tokens,
            } => {
                if cached_tokens > 0 {
                    self.datalog.log_cache_hit(prompt_tokens, cached_tokens);
                }
                let _ = self
                    .event_tx
                    .send(AgentEvent::TokenUsage(crate::stream::TokenUsage {
                        prompt_tokens,
                        completion_tokens,
                        cached_tokens,
                    }));
            }
            TurnEvent::ContextStats {
                system_tokens,
                sent_tokens,
                dropped_tokens,
                working_set_tokens,
                total_messages,
            } => {
                self.datalog.log_context_stats(
                    system_tokens,
                    sent_tokens,
                    dropped_tokens,
                    working_set_tokens,
                    total_messages,
                );
                // Narrow stats — rich breakdown comes from handle_send_message.
                let _ = self.event_tx.send(AgentEvent::ContextStats {
                    system_tokens,
                    sent_tokens,
                    dropped_tokens,
                    working_set_tokens,
                    total_messages,
                    tool_defs_tokens: 0,
                    cold_zone_tokens: 0,
                    ctx_window: 0,
                    ctx_name: String::new(),
                    system_prompt: String::new(),
                });
            }
            TurnEvent::Error(e) => {
                let _ = self.event_tx.send(AgentEvent::Error {
                    error: e,
                    messages: self.conversation.messages.clone(),
                });
            }
            TurnEvent::Warning(w) => {
                self.datalog.log_warning(&w);
                let _ = self.event_tx.send(AgentEvent::Warning(w));
            }
            TurnEvent::WorkingDirChanged(new_dir) => {
                // The tool itself (change_dir / bash cd) already mutated
                // the shared `ctx.working_dir`. Just surface the new path
                // so the TUI footer can redraw. Deliberately NOT doing the
                // heavier work that `services.rs::change_dir` performs for
                // `/cd` (clearing the conversation, reloading the code
                // graph, spawning a new indexer) — those are destructive
                // mid-turn; the LLM expects its context to survive a cd.
                let _ = self.event_tx.send(AgentEvent::WorkingDirChanged(new_dir));
            }
            TurnEvent::ApprovalRequested { .. } => {
                // ApprovalRequested is handled inline in the `select!`
                // loop inside `run_turn_loop`, not through this dispatch
                // method.  The event carries conversation.messages for
                // mid-turn persistence; the approval flow itself is
                // managed by the `approval_req_rx` channel.
            }
        }
    }

    /// Post-process tool results added by TurnRunner: CLI-specific
    /// semantic enrichment (pre-read files mentioned in failed-bash
    /// errors). The generic byte-level truncation (head/tail caps,
    /// 300-line universal cap, 32K char cap, per-turn budget) moved
    /// into `TurnRunner::run_with_filter` so daemon + any other caller
    /// gets it for free — previously each caller had to remember.
    pub(crate) fn post_process_tool_results(&mut self, tool_count: usize) {
        // Error file pre-injection: when a bash command fails, extract file paths
        // from the output and inject their content. This saves the model from
        // manually reading files mentioned in error messages (e.g., rustc errors
        // like "src/api.rs:42:5: error").
        // Language-agnostic: just find paths that exist on disk.
        if self.current_tool_name == "bash" {
            let wd = self
                .turn_runner
                .context
                .working_dir
                .try_read()
                .map(|g| g.clone())
                .unwrap_or_default();

            // Only for failed bash results
            let len = self.conversation.messages.len();
            let start = len.saturating_sub(tool_count);
            let mut files_to_inject: Vec<(String, String)> = Vec::new();

            for i in start..len {
                if let MessageContent::ToolResult(ref r) = self.conversation.messages[i].content {
                    if !r.success {
                        // Extract file paths from error output
                        let paths = extract_file_paths(&r.output, &wd);
                        for path in paths.into_iter().take(3) {
                            // Skip files already read this turn
                            let short = path
                                .strip_prefix(&wd)
                                .map(|p| p.to_string_lossy().to_string())
                                .unwrap_or_else(|_| path.to_string_lossy().to_string());
                            if self
                                .files_read_this_turn
                                .iter()
                                .any(|f| f.contains(&short) || short.contains(f))
                            {
                                continue;
                            }
                            if let Ok(content) = std::fs::read_to_string(&path) {
                                let lines: Vec<&str> = content.lines().collect();
                                let preview = if lines.len() > 50 {
                                    format!(
                                        "{}\n[... {} more lines]",
                                        lines[..50].join("\n"),
                                        lines.len() - 50
                                    )
                                } else {
                                    content.clone()
                                };
                                files_to_inject.push((short, preview));
                            }
                        }
                    }
                }
            }

            if !files_to_inject.is_empty() {
                let injection = files_to_inject
                    .iter()
                    .map(|(path, content)| format!("[Auto-read from error: {}]\n{}", path, content))
                    .collect::<Vec<_>>()
                    .join("\n\n");
                self.conversation.add_user_message(&injection);
            }
        }
    }
}

/// Extract file paths from error output. Language-agnostic: finds tokens
/// that look like file paths (contain `/` or `\`, have a code extension)
/// and checks if they exist on disk.
fn extract_file_paths(output: &str, working_dir: &std::path::Path) -> Vec<std::path::PathBuf> {
    let mut paths: Vec<std::path::PathBuf> = Vec::new();
    let mut seen = std::collections::HashSet::new();

    let code_extensions = [
        "rs", "py", "js", "ts", "tsx", "jsx", "java", "go", "c", "cpp", "h", "vue", "svelte",
        "html", "css", "scss", "toml", "yaml", "yml", "json",
    ];

    for line in output.lines() {
        // Split on common delimiters to find path-like tokens
        for token in line.split(&[' ', ':', '"', '\'', '(', ')', ',', '='][..]) {
            let token = token.trim();
            if token.len() < 4 || token.len() > 300 {
                continue;
            }
            if !token.contains('/') && !token.contains('\\') {
                continue;
            }

            // Strip trailing :line:col (e.g., "src/main.rs:42:5" → "src/main.rs")
            let path_str = token.split(':').next().unwrap_or(token);

            // Check extension
            let has_code_ext = code_extensions
                .iter()
                .any(|ext| path_str.ends_with(&format!(".{}", ext)));
            if !has_code_ext {
                continue;
            }

            // Try as absolute path first, then relative to working dir
            let path = std::path::Path::new(path_str);
            let full_path = if path.is_absolute() {
                path.to_path_buf()
            } else {
                working_dir.join(path_str)
            };

            if full_path.is_file() && !seen.contains(&full_path) {
                seen.insert(full_path.clone());
                paths.push(full_path);
            }
        }
    }
    paths
}