use std::collections::HashMap;

use serde::Deserialize;

use crate::model::session::SessionStats;

/// Parsed agent response including content text and usage statistics.
pub struct ParsedResponse {
    /// Final response text from the agent.
    pub content: String,
    /// Token usage statistics associated with `content`.
    pub stats: SessionStats,
}

/// Claude CLI JSON response shape.
#[derive(Deserialize)]
struct ClaudeResponse {
    result: Option<String>,
    usage: Option<ClaudeUsage>,
}

/// Token usage from a Claude CLI response.
#[derive(Deserialize)]
struct ClaudeUsage {
    input_tokens: Option<i64>,
    output_tokens: Option<i64>,
}

/// Gemini CLI JSON response shape.
#[derive(Deserialize)]
struct GeminiResponse {
    response: Option<String>,
    stats: Option<GeminiStats>,
}

/// Gemini CLI stream event shape.
#[derive(Deserialize)]
struct GeminiStreamEvent {
    #[serde(rename = "type")]
    event_type: Option<String>,
    stats: Option<GeminiStreamResultStats>,
}

/// Token usage from a Gemini stream `result` event.
#[derive(Deserialize)]
struct GeminiStreamResultStats {
    input_tokens: Option<i64>,
    output_tokens: Option<i64>,
}

/// Top-level `stats` object from Gemini CLI JSON output.
#[derive(Deserialize)]
struct GeminiStats {
    models: Option<HashMap<String, GeminiModelStats>>,
}

/// Per-model statistics from Gemini CLI.
#[derive(Deserialize)]
struct GeminiModelStats {
    tokens: Option<GeminiTokens>,
}

/// Token counts from a single Gemini model.
#[derive(Deserialize)]
struct GeminiTokens {
    /// Output/completion tokens generated by the model.
    candidates: Option<i64>,
    /// Uncached prompt tokens (`max(0, prompt - cached)`).
    input: Option<i64>,
}

/// Single NDJSON event emitted by Codex CLI (`--json`).
#[derive(Deserialize)]
struct CodexEvent {
    #[serde(rename = "type")]
    event_type: Option<String>,
    item: Option<CodexItem>,
    usage: Option<CodexUsage>,
}

/// Token usage from a Codex `turn.completed` event.
#[derive(Deserialize)]
struct CodexUsage {
    input_tokens: Option<i64>,
    output_tokens: Option<i64>,
}

/// Nested item payload from a Codex stream event.
#[derive(Deserialize)]
struct CodexItem {
    #[serde(rename = "type")]
    item_type: Option<String>,
    text: Option<String>,
    delta: Option<String>,
}

/// Parses Claude output and falls back to raw output when parsing fails.
pub(super) fn parse_claude_response_with_fallback(stdout: &str, stderr: &str) -> ParsedResponse {
    parse_claude_response(stdout).unwrap_or_else(|| ParsedResponse {
        content: fallback_response(stdout, stderr),
        stats: SessionStats::default(),
    })
}

/// Parses Gemini output and falls back to raw output when parsing fails.
pub(super) fn parse_gemini_response_with_fallback(stdout: &str, stderr: &str) -> ParsedResponse {
    parse_gemini_response(stdout).unwrap_or_else(|| ParsedResponse {
        content: fallback_response(stdout, stderr),
        stats: SessionStats::default(),
    })
}

/// Parses Codex output and falls back to raw output when parsing fails.
pub(super) fn parse_codex_response_with_fallback(stdout: &str, stderr: &str) -> ParsedResponse {
    parse_codex_response(stdout).unwrap_or_else(|| ParsedResponse {
        content: fallback_response(stdout, stderr),
        stats: SessionStats::default(),
    })
}

/// Parses Antigravity output as plain print-mode stdout.
pub(super) fn parse_antigravity_response_with_fallback(
    stdout: &str,
    stderr: &str,
) -> ParsedResponse {
    ParsedResponse {
        content: fallback_response(stdout, stderr),
        stats: SessionStats::default(),
    }
}

/// Parses one Antigravity stream line.
///
/// Antigravity print mode currently writes final response text to stdout
/// without a documented structured stream format, so Agentty suppresses
/// partial stdout rendering and parses the complete output after exit.
pub(super) fn parse_antigravity_stream_output_line(_stdout_line: &str) -> Option<(String, bool)> {
    None
}

/// Parses one full Claude turn payload from stream-json stdout.
///
/// Prefers the final non-empty `result` field when present. When Claude emits
/// an empty or missing `result` field (observed in some stream-json runs), this
/// falls back to the latest assistant stream message content so callers still
/// receive the assistant reply instead of raw NDJSON.
fn parse_claude_response(stdout: &str) -> Option<ParsedResponse> {
    let trimmed_stdout = stdout.trim();
    if trimmed_stdout.is_empty() {
        return None;
    }

    let mut latest_result_stats: Option<SessionStats> = None;
    let mut latest_non_empty_result: Option<ParsedResponse> = None;
    let mut latest_stream_message_content: Option<String> = None;

    record_claude_complete_payload(
        trimmed_stdout,
        &mut latest_result_stats,
        &mut latest_non_empty_result,
    );

    for line in stdout.lines() {
        let trimmed_line = line.trim();
        let Some(stream_event) = record_claude_line_payload(
            trimmed_line,
            &mut latest_result_stats,
            &mut latest_non_empty_result,
        ) else {
            continue;
        };

        if let Some(stats) = extract_claude_usage_stats(&stream_event) {
            latest_result_stats = Some(stats);
        }

        if let Some(content) = extract_claude_stream_assistant_content(&stream_event) {
            latest_stream_message_content = Some(content);
        }
    }

    if let Some(parsed_response) = latest_non_empty_result {
        return Some(parsed_response);
    }

    let content = latest_stream_message_content?;
    if content.trim().is_empty() {
        return None;
    }

    let stats = latest_result_stats.unwrap_or_default();

    Some(ParsedResponse { content, stats })
}

/// Records complete Claude payload formats from one stdout segment.
fn record_claude_complete_payload(
    payload: &str,
    latest_result_stats: &mut Option<SessionStats>,
    latest_non_empty_result: &mut Option<ParsedResponse>,
) {
    if let Some(parsed_response) = parse_claude_response_payload(payload) {
        record_claude_parsed_response(
            parsed_response,
            latest_result_stats,
            latest_non_empty_result,
        );
    }

    if let Some(parsed_response) = parse_claude_response_array_payload(payload) {
        record_claude_parsed_response(
            parsed_response,
            latest_result_stats,
            latest_non_empty_result,
        );
    }
}

/// Records a Claude line payload and returns stream JSON that still needs
/// parsing.
fn record_claude_line_payload(
    trimmed_line: &str,
    latest_result_stats: &mut Option<SessionStats>,
    latest_non_empty_result: &mut Option<ParsedResponse>,
) -> Option<serde_json::Value> {
    if trimmed_line.is_empty() {
        return None;
    }

    if let Some(parsed_response) = parse_claude_response_payload(trimmed_line) {
        record_claude_parsed_response(
            parsed_response,
            latest_result_stats,
            latest_non_empty_result,
        );

        return None;
    }

    if let Some(parsed_response) = parse_claude_response_array_payload(trimmed_line) {
        record_claude_parsed_response(
            parsed_response,
            latest_result_stats,
            latest_non_empty_result,
        );

        return None;
    }

    serde_json::from_str::<serde_json::Value>(trimmed_line).ok()
}

/// Updates latest Claude result state from one parsed complete response.
fn record_claude_parsed_response(
    parsed_response: ParsedResponse,
    latest_result_stats: &mut Option<SessionStats>,
    latest_non_empty_result: &mut Option<ParsedResponse>,
) {
    latest_result_stats.replace(parsed_response.stats.clone());
    if !parsed_response.content.trim().is_empty() {
        latest_non_empty_result.replace(parsed_response);
    }
}

/// Parses one Claude stream line into display text and content/progress type.
pub(super) fn parse_claude_stream_output_line(stdout_line: &str) -> Option<(String, bool)> {
    let trimmed_line = stdout_line.trim();
    if trimmed_line.is_empty() {
        return None;
    }

    let stream_event = serde_json::from_str::<serde_json::Value>(trimmed_line).ok()?;

    if let Some(content) = extract_claude_stream_assistant_content(&stream_event) {
        return Some((content, true));
    }

    let progress_message = compact_progress_message_from_json(&stream_event)?;

    Some((progress_message, false))
}

/// Parses Gemini JSON/stream output into final response text and usage.
fn parse_gemini_response(stdout: &str) -> Option<ParsedResponse> {
    let trimmed_stdout = stdout.trim();
    if let Some(parsed_response) = parse_gemini_response_payload(trimmed_stdout) {
        return Some(parsed_response);
    }

    let mut latest_legacy_response: Option<ParsedResponse> = None;
    let mut stream_response = String::new();
    let mut stream_stats: Option<SessionStats> = None;

    for line in stdout.lines() {
        let trimmed_line = line.trim();
        if trimmed_line.is_empty() {
            continue;
        }

        if let Some(parsed_response) = parse_gemini_response_payload(trimmed_line) {
            latest_legacy_response = Some(parsed_response);

            continue;
        }

        if let Some(stream_chunk) = extract_gemini_stream_response(trimmed_line) {
            stream_response.push_str(&stream_chunk);

            continue;
        }

        if let Some(parsed_stream_stats) = extract_gemini_stream_stats(trimmed_line) {
            stream_stats = Some(parsed_stream_stats);
        }
    }

    if let Some(parsed_response) = latest_legacy_response {
        return Some(parsed_response);
    }

    if stream_response.is_empty() && stream_stats.is_none() {
        return None;
    }

    let stats = stream_stats.unwrap_or_default();

    Some(ParsedResponse {
        content: stream_response,
        stats,
    })
}

/// Parses one Gemini stream line into display text and content/progress type.
pub(super) fn parse_gemini_stream_output_line(stdout_line: &str) -> Option<(String, bool)> {
    let trimmed_line = stdout_line.trim();
    if trimmed_line.is_empty() {
        return None;
    }

    let stream_event = serde_json::from_str::<serde_json::Value>(trimmed_line).ok()?;

    if let Some(content) = extract_gemini_stream_response_from_json(&stream_event) {
        return Some((content, true));
    }

    let progress_message = compact_progress_message_from_json(&stream_event)?;

    Some((progress_message, false))
}

/// Parses Codex NDJSON output into the final assistant message and usage.
///
/// Synthetic completion-status `agent_message` events are ignored so trailing
/// status lines do not replace the assistant-authored reply. When both
/// `agent_message` and `reasoning`/`thought` items are present, this parser
/// always prefers `agent_message` content for the final response so internal
/// reasoning payloads (including raw JSON snippets) do not overwrite
/// user-visible assistant text.
fn parse_codex_response(stdout: &str) -> Option<ParsedResponse> {
    let mut last_agent_message: Option<String> = None;
    let mut last_reasoning_message: Option<String> = None;
    let mut total_input_tokens: u64 = 0;
    let mut total_output_tokens: u64 = 0;

    for line in stdout.lines() {
        let trimmed_line = line.trim();
        if trimmed_line.is_empty() {
            continue;
        }

        let Ok(event) = serde_json::from_str::<CodexEvent>(trimmed_line) else {
            continue;
        };

        record_codex_turn_usage(&event, &mut total_input_tokens, &mut total_output_tokens);

        if event.event_type.as_deref() != Some("item.completed") {
            continue;
        }

        let Some(item) = event.item else {
            continue;
        };

        record_codex_completed_item(item, &mut last_agent_message, &mut last_reasoning_message);
    }

    let stats = SessionStats {
        added_lines: 0,
        deleted_lines: 0,
        input_tokens: total_input_tokens,
        output_tokens: total_output_tokens,
    };

    last_agent_message
        .or(last_reasoning_message)
        .map(|content| ParsedResponse { content, stats })
}

/// Adds token usage from one Codex turn completion event.
fn record_codex_turn_usage(
    event: &CodexEvent,
    total_input_tokens: &mut u64,
    total_output_tokens: &mut u64,
) {
    if event.event_type.as_deref() != Some("turn.completed") {
        return;
    }

    let Some(usage) = event.usage.as_ref() else {
        return;
    };

    *total_input_tokens += usage.input_tokens.unwrap_or(0).cast_unsigned();
    *total_output_tokens += usage.output_tokens.unwrap_or(0).cast_unsigned();
}

/// Records visible content from one completed Codex item event.
fn record_codex_completed_item(
    item: CodexItem,
    last_agent_message: &mut Option<String>,
    last_reasoning_message: &mut Option<String>,
) {
    let Some(text) = item.text.or(item.delta) else {
        return;
    };
    if is_codex_completion_status_message(&text) {
        return;
    }

    let item_type = item.item_type.as_deref().unwrap_or_default();
    if item_type == "agent_message" || item_type == "agentmessage" {
        last_agent_message.replace(text);

        return;
    }

    if item_type == "reasoning" || item_type == "thought" {
        last_reasoning_message.replace(text);
    }
}

/// Parses one Codex stream line into display text and content/progress type.
pub(super) fn parse_codex_stream_output_line(stdout_line: &str) -> Option<(String, bool)> {
    let trimmed_line = stdout_line.trim();
    if trimmed_line.is_empty() {
        return None;
    }

    let event = serde_json::from_str::<CodexEvent>(trimmed_line).ok()?;
    let event_type = event.event_type.as_deref();
    if event_type == Some("item.started") {
        let item = event.item.as_ref()?;
        let item_type = item.item_type.as_deref()?;
        let progress_message = compact_codex_progress_message(item_type)?;

        return Some((progress_message, false));
    }

    if event_type == Some("item.updated") {
        let item = event.item.as_ref()?;
        let item_type = item.item_type.as_deref()?;
        if item_type == "agent_message"
            || item_type == "agentmessage"
            || item_type == "reasoning"
            || item_type == "thought"
        {
            let delta = item.delta.as_ref().or(item.text.as_ref())?.clone();
            let is_response_content = item_type == "agent_message" || item_type == "agentmessage";

            return Some((delta, is_response_content));
        }
    }

    if event_type != Some("item.completed") {
        return None;
    }

    let item = event.item?;
    let item_type = item.item_type.as_deref()?;
    if !(item_type == "agent_message"
        || item_type == "agentmessage"
        || item_type == "reasoning"
        || item_type == "thought")
    {
        return None;
    }

    let text = item.text?;
    if is_codex_completion_status_message(&text) {
        return None;
    }

    let is_response_content = item_type == "agent_message" || item_type == "agentmessage";

    Some((text, is_response_content))
}

fn parse_claude_response_payload(stdout: &str) -> Option<ParsedResponse> {
    let response = serde_json::from_str::<ClaudeResponse>(stdout).ok()?;
    let content = response.result?;
    let stats = SessionStats {
        added_lines: 0,
        deleted_lines: 0,
        input_tokens: response
            .usage
            .as_ref()
            .and_then(|usage| usage.input_tokens)
            .unwrap_or(0)
            .cast_unsigned(),
        output_tokens: response
            .usage
            .as_ref()
            .and_then(|usage| usage.output_tokens)
            .unwrap_or(0)
            .cast_unsigned(),
    };

    Some(ParsedResponse { content, stats })
}

/// Parses one legacy Gemini JSON response payload.
fn parse_gemini_response_payload(stdout: &str) -> Option<ParsedResponse> {
    let response = serde_json::from_str::<GeminiResponse>(stdout).ok()?;
    let content = response.response?;
    let stats = extract_gemini_stats(response.stats.as_ref());

    Some(ParsedResponse { content, stats })
}

/// Extracts aggregate Gemini token usage from the first reported model entry.
fn extract_gemini_stats(stats: Option<&GeminiStats>) -> SessionStats {
    let Some(tokens) = stats
        .and_then(|stats| stats.models.as_ref())
        .and_then(|models| models.values().next())
        .and_then(|model| model.tokens.as_ref())
    else {
        return SessionStats::default();
    };

    SessionStats {
        added_lines: 0,
        deleted_lines: 0,
        input_tokens: tokens.input.unwrap_or(0).cast_unsigned(),
        output_tokens: tokens.candidates.unwrap_or(0).cast_unsigned(),
    }
}

/// Extracts assistant text from one Gemini stream-json line.
fn extract_gemini_stream_response(stdout_line: &str) -> Option<String> {
    let stream_event = serde_json::from_str::<serde_json::Value>(stdout_line).ok()?;

    extract_gemini_stream_response_from_json(&stream_event)
}

/// Extracts assistant text from one parsed Gemini stream event.
fn extract_gemini_stream_response_from_json(stream_event: &serde_json::Value) -> Option<String> {
    if let Some(response) = stream_event
        .get("response")
        .and_then(serde_json::Value::as_str)
        && !response.is_empty()
    {
        return Some(response.to_string());
    }

    let event_type = stream_event
        .get("type")
        .and_then(serde_json::Value::as_str)
        .unwrap_or_default();
    if !matches!(event_type, "content" | "delta" | "message" | "response") {
        return None;
    }

    stream_event
        .get("text")
        .or_else(|| stream_event.get("delta"))
        .or_else(|| stream_event.get("content"))
        .and_then(serde_json::Value::as_str)
        .filter(|text| !text.is_empty())
        .map(ToString::to_string)
}

/// Extracts usage stats from one Gemini stream-json result line.
fn extract_gemini_stream_stats(stdout_line: &str) -> Option<SessionStats> {
    let stream_event = serde_json::from_str::<GeminiStreamEvent>(stdout_line).ok()?;
    if stream_event.event_type.as_deref() != Some("result") {
        return None;
    }

    let stats = stream_event.stats?;

    Some(SessionStats {
        added_lines: 0,
        deleted_lines: 0,
        input_tokens: stats.input_tokens.unwrap_or(0).cast_unsigned(),
        output_tokens: stats.output_tokens.unwrap_or(0).cast_unsigned(),
    })
}

/// Parses one Claude JSON-array payload and extracts the best assistant
/// result text and usage stats.
///
/// Newer CLI variants can emit one JSON array containing init/assistant/result
/// events rather than one flat object. This parser keeps the latest non-empty
/// `result` string when present, otherwise falls back to assistant message
/// text.
fn parse_claude_response_array_payload(stdout: &str) -> Option<ParsedResponse> {
    let events = serde_json::from_str::<serde_json::Value>(stdout).ok()?;
    let events = events.as_array()?;
    let mut latest_result_content: Option<String> = None;
    let mut latest_assistant_content: Option<String> = None;
    let mut latest_stats: Option<SessionStats> = None;

    for event in events {
        if let Some(stats) = extract_claude_usage_stats(event) {
            latest_stats = Some(stats);
        }

        if let Some(result_content) = event.get("result").and_then(serde_json::Value::as_str)
            && !result_content.trim().is_empty()
        {
            latest_result_content = Some(result_content.to_string());
        }

        if let Some(assistant_content) = extract_claude_stream_assistant_content(event) {
            latest_assistant_content = Some(assistant_content);
        }
    }

    let content = latest_result_content.or(latest_assistant_content)?;
    let stats = latest_stats.unwrap_or_default();

    Some(ParsedResponse { content, stats })
}

/// Extracts Claude usage statistics from one JSON stream event.
fn extract_claude_usage_stats(stream_event: &serde_json::Value) -> Option<SessionStats> {
    let usage = stream_event.get("usage")?;

    Some(SessionStats {
        added_lines: 0,
        deleted_lines: 0,
        input_tokens: usage
            .get("input_tokens")
            .and_then(serde_json::Value::as_i64)
            .unwrap_or(0)
            .cast_unsigned(),
        output_tokens: usage
            .get("output_tokens")
            .and_then(serde_json::Value::as_i64)
            .unwrap_or(0)
            .cast_unsigned(),
    })
}

/// Extracts assistant text chunks from one Claude `stream-json` line.
///
/// Supports both legacy `{"type":"assistant","message":...}` events and newer
/// `{"type":"message",...}` assistant events. Text blocks are joined into one
/// displayable chunk.
fn extract_claude_stream_assistant_content(stream_event: &serde_json::Value) -> Option<String> {
    let message = extract_claude_stream_assistant_message(stream_event)?;
    if message.get("role").and_then(serde_json::Value::as_str) != Some("assistant") {
        return None;
    }

    if let Some(content) = message.get("content").and_then(serde_json::Value::as_str) {
        let trimmed_content = content.trim();
        if !trimmed_content.is_empty() {
            return Some(trimmed_content.to_string());
        }
    }

    let content_blocks = message
        .get("content")
        .and_then(serde_json::Value::as_array)?;
    let text = content_blocks
        .iter()
        .filter_map(extract_claude_stream_content_block_text)
        .collect::<Vec<_>>()
        .join("");
    if text.is_empty() {
        return None;
    }

    Some(text)
}

/// Resolves the assistant message payload from one Claude stream event.
///
/// Claude has emitted both nested assistant events (`type=assistant`) and
/// direct message events (`type=message`) across releases.
fn extract_claude_stream_assistant_message(
    stream_event: &serde_json::Value,
) -> Option<&serde_json::Value> {
    let event_type = stream_event
        .get("type")
        .and_then(serde_json::Value::as_str)?;
    match event_type {
        "assistant" => stream_event.get("message"),
        "message" => Some(stream_event),
        _ => None,
    }
}

/// Returns text from one Claude assistant content block when present.
///
/// Supports both direct text blocks and nested `delta.text` payloads used by
/// partial streaming updates.
fn extract_claude_stream_content_block_text(content_block: &serde_json::Value) -> Option<&str> {
    let block_type = content_block
        .get("type")
        .and_then(serde_json::Value::as_str)?;
    if block_type != "text" && block_type != "text_delta" {
        return None;
    }

    if let Some(text) = content_block
        .get("text")
        .and_then(serde_json::Value::as_str)
    {
        return Some(text);
    }

    content_block
        .get("delta")
        .and_then(|delta| delta.get("text"))
        .and_then(serde_json::Value::as_str)
}

fn compact_progress_message_from_json(stream_event: &serde_json::Value) -> Option<String> {
    let assistant_content_type = stream_event
        .get("message")
        .and_then(|message| message.get("content"))
        .and_then(serde_json::Value::as_array)
        .and_then(|content| {
            content.iter().find_map(|content_block| {
                content_block
                    .get("type")
                    .and_then(serde_json::Value::as_str)
            })
        });
    if let Some(progress_message) =
        assistant_content_type.and_then(compact_progress_message_from_stream_label)
    {
        return Some(progress_message);
    }

    let item_type = stream_event
        .get("item")
        .and_then(|item| item.get("type"))
        .and_then(serde_json::Value::as_str);
    if let Some(progress_message) = item_type.and_then(compact_progress_message_from_stream_label) {
        return Some(progress_message);
    }

    let tool_name = stream_event
        .get("tool_name")
        .and_then(serde_json::Value::as_str);
    if let Some(progress_message) = tool_name.and_then(compact_progress_message_from_stream_label) {
        return Some(progress_message);
    }

    let name = stream_event.get("name").and_then(serde_json::Value::as_str);
    if let Some(progress_message) = name.and_then(compact_progress_message_from_stream_label) {
        return Some(progress_message);
    }

    let tool_name = stream_event
        .get("tool")
        .and_then(|tool| tool.get("name"))
        .and_then(serde_json::Value::as_str);
    if let Some(progress_message) = tool_name.and_then(compact_progress_message_from_stream_label) {
        return Some(progress_message);
    }

    let event = stream_event
        .get("event")
        .and_then(serde_json::Value::as_str);
    if let Some(progress_message) = event.and_then(compact_progress_message_from_stream_label) {
        return Some(progress_message);
    }

    let subtype = stream_event
        .get("subtype")
        .and_then(serde_json::Value::as_str);
    if let Some(progress_message) = subtype.and_then(compact_progress_message_from_stream_label) {
        return Some(progress_message);
    }

    let event_type = stream_event.get("type").and_then(serde_json::Value::as_str);
    if let Some(progress_message) = event_type.and_then(compact_progress_message_from_stream_label)
    {
        return Some(progress_message);
    }

    None
}

/// Maps one provider stream label to a compact user-facing progress message.
///
/// This intentionally recognizes broad families of labels because provider
/// stream payloads evolve over time and may rename internal phases while still
/// describing the same user-visible activity.
fn compact_progress_message_from_stream_label(label: &str) -> Option<String> {
    let normalized_label = label.to_ascii_lowercase().replace('-', "_");
    if normalized_label.contains("search") {
        return Some("Searching the web".to_string());
    }

    if normalized_label.contains("compact") || normalized_label.contains("compress") {
        return Some("Compacting context".to_string());
    }

    if normalized_label.contains("reasoning")
        || normalized_label.contains("thinking")
        || normalized_label.contains("thought")
    {
        return Some("Thinking".to_string());
    }

    if normalized_label.contains("command")
        || normalized_label.contains("bash")
        || normalized_label.contains("terminal")
        || normalized_label.contains("shell")
        || normalized_label.contains("tool_use")
        || normalized_label.contains("tool_call")
        || normalized_label.contains("toolcall")
        || normalized_label.contains("execute")
    {
        return Some("Running a command".to_string());
    }

    None
}

/// Maps a Codex item type to a compact progress message for the UI.
///
/// Returns `None` for `agent_message` since it carries response content, not
/// progress.
pub fn compact_codex_progress_message(item_type: &str) -> Option<String> {
    match item_type {
        "agent_message" => None,
        "command_execution" => Some("Running a command".to_string()),
        "context_compaction" => Some("Compacting context".to_string()),
        "reasoning" | "thought" => Some("Thinking".to_string()),
        "web_search" => Some("Searching the web".to_string()),
        other => Some(format!("Working: {}", other.replace('_', " "))),
    }
}

/// Returns whether a Codex `agent_message` is a synthetic completion status.
///
/// These short status lines are emitted for internal tool/reasoning steps and
/// should not be rendered as chat content.
pub fn is_codex_completion_status_message(message: &str) -> bool {
    let normalized_message = message.trim().trim_end_matches('.').to_ascii_lowercase();

    matches!(
        normalized_message.as_str(),
        "command completed"
            | "command execution completed"
            | "thinking completed"
            | "reasoning completed"
            | "search completed"
            | "web search completed"
    )
}

fn fallback_response(stdout: &str, stderr: &str) -> String {
    let trimmed_stdout = stdout.trim();
    if trimmed_stdout.is_empty() {
        return stderr.trim().to_string();
    }

    trimmed_stdout.to_string()
}

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

    #[test]
    fn test_claude_parse_response_reads_result_payload() {
        // Arrange
        let stdout =
            r#"{"result":"Planned response","usage":{"input_tokens":11,"output_tokens":7}}"#;

        // Act
        let parsed = parse_claude_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Planned response");
        assert_eq!(parsed.stats.input_tokens, 11);
        assert_eq!(parsed.stats.output_tokens, 7);
    }

    #[test]
    /// Ensures Antigravity print-mode responses are parsed from stdout as
    /// complete final content.
    fn test_antigravity_parse_response_reads_plain_stdout() {
        // Arrange
        let stdout = r#"{"answer":"Antigravity ok","questions":[],"summary":null}"#;
        let stderr = "diagnostics";

        // Act
        let parsed = parse_antigravity_response_with_fallback(stdout, stderr);

        // Assert
        assert_eq!(parsed.content, stdout);
        assert_eq!(parsed.stats, SessionStats::default());
    }

    #[test]
    /// Ensures Antigravity stream parsing waits for the final print-mode
    /// output instead of treating stdout lines as incremental transcript.
    fn test_antigravity_stream_output_line_is_not_incremental() {
        // Arrange
        let stdout_line = "partial output";

        // Act
        let parsed_line = parse_antigravity_stream_output_line(stdout_line);

        // Assert
        assert_eq!(parsed_line, None);
    }

    #[test]
    fn test_claude_parse_response_uses_stream_message_when_result_is_empty() {
        // Arrange
        let stdout = concat!(
            r#"{"type":"message","role":"assistant","content":"Recovered response payload"}"#,
            "\n",
            r#"{"type":"result","subtype":"success","result":"","usage":{"input_tokens":11,"output_tokens":7}}"#
        );

        // Act
        let parsed = parse_claude_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Recovered response payload");
        assert_eq!(parsed.stats.input_tokens, 11);
        assert_eq!(parsed.stats.output_tokens, 7);
    }

    #[test]
    fn test_claude_parse_response_reads_usage_when_result_is_missing() {
        // Arrange
        let stdout = concat!(
            r#"{"type":"message","role":"assistant","content":"Recovered response payload"}"#,
            "\n",
            r#"{"type":"result","subtype":"success","usage":{"input_tokens":13,"output_tokens":5}}"#
        );

        // Act
        let parsed = parse_claude_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Recovered response payload");
        assert_eq!(parsed.stats.input_tokens, 13);
        assert_eq!(parsed.stats.output_tokens, 5);
    }

    #[test]
    fn test_claude_parse_response_reads_result_from_json_array_payload() {
        // Arrange
        let stdout = r#"[{"type":"system","subtype":"init"},{"type":"result","subtype":"success","result":"{\"answer\":\"Recovered payload\",\"questions\":[],\"summary\":null}","usage":{"input_tokens":5,"output_tokens":3}}]"#;

        // Act
        let parsed = parse_claude_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(
            parsed.content,
            r#"{"answer":"Recovered payload","questions":[],"summary":null}"#
        );
        assert_eq!(parsed.stats.input_tokens, 5);
        assert_eq!(parsed.stats.output_tokens, 3);
    }

    #[test]
    fn test_parse_claude_stream_output_line_reads_assistant_message_content_blocks() {
        // Arrange
        let stdout_line = concat!(
            r#"{"type":"assistant","message":{"role":"assistant","content":["#,
            r#"{"type":"text","text":"Partial "},{"type":"text","text":"answer"}]}}"#
        );

        // Act
        let parsed_line = parse_claude_stream_output_line(stdout_line);

        // Assert
        assert_eq!(parsed_line, Some(("Partial answer".to_string(), true)));
    }

    #[test]
    fn test_parse_claude_stream_output_line_reads_message_event_content() {
        // Arrange
        let stdout_line = r#"{"type":"message","role":"assistant","content":"Partial answer"}"#;

        // Act
        let parsed_line = parse_claude_stream_output_line(stdout_line);

        // Assert
        assert_eq!(parsed_line, Some(("Partial answer".to_string(), true)));
    }

    #[test]
    fn test_parse_claude_stream_output_line_ignores_result_events_for_delta_streaming() {
        // Arrange
        let stdout_line = r#"{"type":"result","subtype":"success","result":"Final answer"}"#;

        // Act
        let parsed_line = parse_claude_stream_output_line(stdout_line);

        // Assert
        assert_eq!(parsed_line, None);
    }

    #[test]
    fn test_parse_claude_stream_output_line_maps_tool_use_blocks_to_progress() {
        // Arrange
        let stdout_line = concat!(
            r#"{"type":"assistant","message":{"role":"assistant","content":["#,
            r#"{"type":"tool_use","name":"Bash"}]}}"#
        );

        // Act
        let parsed_line = parse_claude_stream_output_line(stdout_line);

        // Assert
        assert_eq!(parsed_line, Some(("Running a command".to_string(), false)));
    }

    #[test]
    fn test_compact_progress_message_from_stream_label_maps_compaction_labels() {
        // Arrange
        let compaction_label = "context_compaction";
        let compression_label = "context-compression";

        // Act
        let compaction_progress = compact_progress_message_from_stream_label(compaction_label);
        let compression_progress = compact_progress_message_from_stream_label(compression_label);

        // Assert
        assert_eq!(compaction_progress, Some("Compacting context".to_string()));
        assert_eq!(compression_progress, Some("Compacting context".to_string()));
    }

    /// Ensures final NDJSON parsing keeps the real assistant reply when
    /// completion-status messages trail the stream.
    #[test]
    fn test_parse_response_codex_ignores_trailing_completion_status_message() {
        // Arrange
        let stdout = concat!(
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Planned final answer"}}"#,
            "\n",
            r#"{"type":"turn.completed","usage":{"input_tokens":21,"output_tokens":8}}"#,
            "\n",
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Command completed"}}"#,
        );

        // Act
        let parsed = parse_codex_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Planned final answer");
        assert_eq!(parsed.stats.input_tokens, 21);
        assert_eq!(parsed.stats.output_tokens, 8);
    }

    #[test]
    /// Ensures trailing reasoning payloads do not overwrite final
    /// `agent_message` output.
    fn test_parse_response_codex_prefers_agent_message_over_trailing_reasoning_payload() {
        // Arrange
        let stdout = concat!(
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Focused review summary"}}"#,
            "\n",
            r#"{"type":"item.completed","item":{"type":"reasoning","text":"{\"title\":\"Inject clock dependency to enforce time boundaries\"}"}}"#,
            "\n",
            r#"{"type":"turn.completed","usage":{"input_tokens":13,"output_tokens":9}}"#,
        );

        // Act
        let parsed = parse_codex_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Focused review summary");
        assert_eq!(parsed.stats.input_tokens, 13);
        assert_eq!(parsed.stats.output_tokens, 9);
    }

    #[test]
    /// Ensures parsing still returns reasoning text when no
    /// `agent_message` item exists.
    fn test_parse_response_codex_falls_back_to_reasoning_without_agent_message() {
        // Arrange
        let stdout = concat!(
            r#"{"type":"item.completed","item":{"type":"reasoning","text":"Fallback reasoning output"}}"#,
            "\n",
            r#"{"type":"turn.completed","usage":{"input_tokens":5,"output_tokens":3}}"#,
        );

        // Act
        let parsed = parse_codex_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Fallback reasoning output");
        assert_eq!(parsed.stats.input_tokens, 5);
        assert_eq!(parsed.stats.output_tokens, 3);
    }

    #[test]
    fn test_parse_response_codex_ignores_completion_status_as_final_message() {
        // Arrange
        let stdout = concat!(
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Final answer"}}"#,
            "\n",
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Command completed"}}"#,
            "\n",
            r#"{"type":"turn.completed","usage":{"input_tokens":13,"output_tokens":9}}"#,
        );

        // Act
        let parsed = parse_codex_response_with_fallback(stdout, "");

        // Assert
        assert_eq!(parsed.content, "Final answer");
        assert_eq!(parsed.stats.input_tokens, 13);
        assert_eq!(parsed.stats.output_tokens, 9);
    }

    #[test]
    fn test_parse_stream_output_line_codex_ignores_completion_status_messages() {
        // Arrange
        let completion_status_lines = [
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Command completed"}}"#,
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Thinking completed"}}"#,
        ];

        // Act & Assert
        for completion_status_line in completion_status_lines {
            let parsed_line = parse_codex_stream_output_line(completion_status_line);

            assert_eq!(parsed_line, None);
        }
    }

    #[test]
    fn test_parse_stream_output_line_codex_keeps_assistant_message_content() {
        // Arrange
        let assistant_line =
            r#"{"type":"item.completed","item":{"type":"agent_message","text":"Final answer"}}"#;

        // Act
        let parsed_line = parse_codex_stream_output_line(assistant_line);

        // Assert
        assert_eq!(parsed_line, Some(("Final answer".to_string(), true)));
    }

    #[test]
    fn test_parse_stream_output_line_codex_marks_reasoning_as_loader_text() {
        // Arrange
        let reasoning_line =
            r#"{"type":"item.updated","item":{"type":"reasoning","delta":"Inspecting files"}}"#;

        // Act
        let parsed_line = parse_codex_stream_output_line(reasoning_line);

        // Assert
        assert_eq!(parsed_line, Some(("Inspecting files".to_string(), false)));
    }

    #[test]
    fn compact_codex_progress_message_returns_compacting_for_context_compaction() {
        // Arrange

        // Act
        let progress = compact_codex_progress_message("context_compaction");

        // Assert
        assert_eq!(progress, Some("Compacting context".to_string()));
    }
}
