//! Transcript export, block copying, viewer/modal, and input-log dump dispatchers.

use super::ctx::with_active_agent;
use super::session::lifecycle::skip_picker_and_create_session;
use crate::app::actions::Effect;
use crate::app::agent::AgentId;
use crate::app::app_view::{ActiveView, AppView};
use crate::scrollback::block::{BlockContent, RenderBlock};
use crate::scrollback::blocks::ToolCallBlock;
use agent_client_protocol as acp;

/// Copy the selected block's content to the system clipboard.
///
/// Respects the block's raw/pretty mode for markdown content.
/// Shows a toast notification on theExtensionsTab
pub(super) fn dispatch_copy_block_content(app: &mut AppView) {
    with_active_agent(app, |agent| {
        let Some(idx) = agent.scrollback.selected() else {
            return;
        };
        if agent.scrollback.entry_content_hidden_by_group(idx) {
            return;
        }
        let Some(entry) = agent.scrollback.entry(idx) else {
            return;
        };

        // BgTask blocks: copy stdout from central store
        let text = if let RenderBlock::BgTask(block) = &entry.block {
            let stdout = agent
                .session
                .bg_tasks
                .get(&block.task_id)
                .map(|t| t.stdout.clone())
                .unwrap_or_default();
            if stdout.is_empty() {
                None
            } else {
                Some(stdout)
            }
        } else {
            entry.block.copy_text(entry.raw)
        };

        if let Some(text) = text
            && !text.is_empty()
        {
            agent.copy_to_clipboard(&text);
        }
    });
}

/// Copy the Nth most recent assistant message to the clipboard.
pub(super) fn dispatch_copy_assistant_message(app: &mut AppView, n: usize) {
    with_active_agent(app, |agent| {
        // Collect agent messages in reverse order (most recent first).
        let mut agent_messages: Vec<String> = Vec::new();
        for i in (0..agent.scrollback.len()).rev() {
            if let Some(entry) = agent.scrollback.entry(i)
                && let RenderBlock::AgentMessage(msg) = &entry.block
            {
                agent_messages.push(msg.copy_text(false));
            }
        }

        if agent_messages.is_empty() {
            agent
                .scrollback
                .push_block(RenderBlock::system("No assistant messages to copy"));
            return;
        }

        if n > agent_messages.len() {
            agent.scrollback.push_block(RenderBlock::system(format!(
                "Only {} assistant {} available to copy",
                agent_messages.len(),
                if agent_messages.len() == 1 {
                    "message"
                } else {
                    "messages"
                }
            )));
            return;
        }

        let text = &agent_messages[n - 1];
        if text.is_empty() {
            agent
                .scrollback
                .push_block(RenderBlock::system("Assistant message is empty"));
            return;
        }

        let stats = crate::clipboard::clipboard_stats_suffix(text);
        agent
            .scrollback
            .push_block(RenderBlock::system(format!("Copied to clipboard{stats}")));
        agent.copy_to_clipboard(text);
    });
}

/// Dispatch for the `/export` command.
/// Collects the active (sub)agent's scrollback, renders a clean Markdown transcript,
/// and either writes it to the (expanded) file or copies it to the clipboard using the
/// full route (native + tmux + OSC 52) with appropriate feedback.
pub(super) fn dispatch_export_conversation(
    app: &mut AppView,
    file_path: Option<std::path::PathBuf>,
) {
    with_active_agent(app, |agent| {
        let blocks: Vec<_> = (0..agent.scrollback.len())
            .filter_map(|i| agent.scrollback.entry(i).map(|e| &e.block))
            .collect();

        let md = crate::scrollback::export::render_blocks_to_markdown(blocks);

        if md.is_empty() {
            agent
                .scrollback
                .push_block(RenderBlock::system("No conversation content to export"));
            return;
        }

        if let Some(p) = file_path {
            // All fs logic (tilde, mkdir, write) lives here (single owner, thin command layer).
            let expanded =
                std::path::PathBuf::from(shellexpand::tilde(&p.to_string_lossy()).as_ref());
            if let Some(parent) = expanded.parent()
                && let Err(e) = std::fs::create_dir_all(parent)
            {
                agent.scrollback.push_block(RenderBlock::system(format!(
                    "Failed to create directory: {e}"
                )));
                return;
            }
            match std::fs::write(&expanded, &md) {
                Ok(()) => {
                    agent.scrollback.push_block(RenderBlock::system(format!(
                        "Conversation exported to {}",
                        expanded.display()
                    )));
                }
                Err(e) => {
                    // Do not blindly re-emit a user-supplied path in the error message
                    // (it may contain secrets or PII); the generic failure is sufficient.
                    agent
                        .scrollback
                        .push_block(RenderBlock::system(format!("Failed to write file: {}", e)));
                }
            }
        } else {
            // Clipboard path: stats block (like assistant copy) + route-aware toast
            // (like block content copy / selection). Good UX for a potentially large transcript.
            let stats = crate::clipboard::clipboard_stats_suffix(&md);
            agent.scrollback.push_block(RenderBlock::system(format!(
                "Conversation copied to clipboard{stats}"
            )));
            agent.copy_to_clipboard(&md);
        }
    });
}

/// Open the full transcript in `$PAGER`.
///
/// **Minimal mode** renders a full-fidelity ANSI transcript — every block
/// fully expanded (reasoning in full, tool output uncapped, diff colors kept)
/// — a full layout + syntax-highlight + ANSI-serialization pass over the whole
/// session. Rendering that inline froze the event loop for seconds on long
/// sessions ("laggy /transcript"), and the block model is `!Send` (syntect's
/// resumable highlighter state lives inside markdown blocks), so it can't be
/// shipped to a worker either. Instead this only ARMS the request; the minimal
/// render loop builds the transcript **incrementally, a time-budgeted slice
/// per frame** (`full_view::pump_transcript`, the same time-sliced amortization
/// pattern other TUIs use for heavy transcript work), then arms `pending_pager_path`
/// for the event loop's suspend-into-`$PAGER`.
///
/// **Other modes** keep the compact markdown export (string concatenation, no
/// layout or highlighting — cheap enough to stay synchronous).
pub(crate) fn dispatch_open_transcript_pager(app: &mut AppView) {
    if app.screen_mode.is_minimal() {
        crate::minimal_api::request_minimal_transcript(app);
        return;
    }

    let mut md = None;
    with_active_agent(app, |agent| {
        let blocks: Vec<_> = (0..agent.scrollback.len())
            .filter_map(|i| agent.scrollback.entry(i).map(|e| &e.block))
            .collect();
        let rendered = crate::scrollback::export::render_blocks_to_markdown(blocks);
        if !rendered.is_empty() {
            md = Some(rendered);
        }
    });

    let Some(content) = md else {
        with_active_agent(app, |agent| {
            agent.scrollback.push_block(RenderBlock::system(
                "No conversation transcript to view yet",
            ));
        });
        return;
    };

    let path = std::env::temp_dir().join(format!("kigi-transcript-{}.md", uuid::Uuid::new_v4()));
    match std::fs::write(&path, content) {
        Ok(()) => {
            app.pending_pager_path = Some(path);
            app.pending_pager_ansi = false;
        }
        Err(e) => {
            with_active_agent(app, |agent| {
                agent.scrollback.push_block(RenderBlock::system(format!(
                    "Failed to write transcript: {e}"
                )));
            });
        }
    }
}

/// Open the fullscreen block viewer for the selected entry.
/// Falls back to the image viewer only for entries without a normal block viewer.
pub(super) fn dispatch_open_block_viewer(app: &mut AppView) {
    use crate::views::block_viewer::BlockViewerPane;

    with_active_agent(app, |agent| {
        let Some(idx) = agent.scrollback.selected() else {
            return;
        };
        let Some(entry) = agent.scrollback.entry(idx) else {
            return;
        };

        // Block has images/media but terminal can't render pixels — toast and bail.
        let has_media =
            !entry.block.image_references().is_empty() || entry.block.inline_media().is_some();
        if has_media && !crate::terminal::image::detect_graphics_protocol().supports_images() {
            agent.guard_image_support();
            return;
        }

        if !entry.block.has_normal_fullscreen_viewer() {
            // Video: Enter starts inline playback (no modal).
            if let Some(first_ref) = entry.block.video_references().first() {
                let path = first_ref.path.clone();
                agent.start_inline_video_playback(&path);
                return;
            }
            // Image: Enter opens the file in the OS-native viewer.
            if let Some(first_ref) = entry.block.image_references().first() {
                let path = first_ref.path.clone();
                agent.open_media_natively(&path);
            }
            return;
        }

        // Try to create a normal viewer for the selected block type.
        let viewer = match &entry.block {
            RenderBlock::Thinking(_) | RenderBlock::AgentMessage(_) => {
                BlockViewerPane::for_markdown(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::Execute(_)) => {
                BlockViewerPane::for_execute(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::Edit(_)) => {
                BlockViewerPane::for_edit(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::Read(_)) => {
                BlockViewerPane::for_read(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::Search(_)) => {
                BlockViewerPane::for_grep(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::ListDir(_)) => {
                BlockViewerPane::for_list_dir(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::WebFetch(_)) => {
                BlockViewerPane::for_web_fetch(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::WebSearch(_)) => {
                BlockViewerPane::for_web_search(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::IntegrationSearch(_)) => {
                BlockViewerPane::for_integration_search(entry.id, entry)
            }
            RenderBlock::ToolCall(ToolCallBlock::UseTool(_)) => {
                BlockViewerPane::for_use_tool(entry.id, entry)
            }
            RenderBlock::BgTask(block) => {
                let stdout = agent
                    .session
                    .bg_tasks
                    .get(&block.task_id)
                    .map(|t| t.stdout.as_str())
                    .unwrap_or("");
                let is_running = agent
                    .session
                    .bg_tasks
                    .get(&block.task_id)
                    .is_some_and(|t| t.status == crate::app::agent::BgTaskStatus::Running);
                Some(BlockViewerPane::for_bg_task(
                    entry.id,
                    &block.task_id,
                    stdout,
                    is_running,
                ))
            }
            _ => None,
        };

        if viewer.is_some() {
            agent.block_viewer = viewer;
            return;
        }

        // Video: Enter starts inline playback.
        if let Some(first_ref) = entry.block.video_references().first() {
            let path = first_ref.path.clone();
            agent.start_inline_video_playback(&path);
            return;
        }
        // Image: Enter opens the file in the OS-native viewer.
        if let Some(first_ref) = entry.block.image_references().first() {
            let path = first_ref.path.clone();
            agent.open_media_natively(&path);
        }
    });
}

/// Fetch-set that populates every Extensions-modal tab. Shared by the manual
/// open path, the post-CTA-install auth handoff, and the deferred-fetch
/// session-ready handlers so they can't drift and leave a tab stuck on its
/// initial `Loading` state.
pub(super) fn extensions_modal_tab_fetches(
    agent_id: AgentId,
    session_id: acp::SessionId,
) -> Vec<Effect> {
    vec![
        Effect::FetchHooksList {
            agent_id,
            session_id: session_id.clone(),
        },
        Effect::FetchPluginsList {
            agent_id,
            session_id: session_id.clone(),
        },
        Effect::FetchMcpsList {
            agent_id,
            session_id: session_id.clone(),
            cache: true,
        },
        Effect::FetchSkillsList {
            agent_id,
            session_id,
        },
    ]
}

/// Open the hooks/plugins modal on the active agent view and fetch list data.
pub(super) fn dispatch_open_extensions_modal(
    app: &mut AppView,
    tab: crate::views::extensions_modal::ExtensionsTab,
) -> Vec<Effect> {
    use crate::views::extensions_modal::ExtensionsModalState;

    let ActiveView::Agent(id) = app.active_view else {
        return vec![];
    };
    let Some(agent) = app.agents.get_mut(&id) else {
        return vec![];
    };

    // Mutual exclusivity: close agents modal when opening extensions.
    agent.agents_modal = None;
    let modal = ExtensionsModalState::new(tab);
    agent.extensions_modal = Some(modal);

    let Some(session_id) = agent.session.session_id.clone() else {
        // Tabs default to Loading; the fetch fires on SessionCreated. With a
        // picker-deferred session nothing else would create one, so do it now.
        agent.pending_extensions_fetch = true;
        return skip_picker_and_create_session(app, id);
    };
    agent.pending_extensions_fetch = false;
    extensions_modal_tab_fetches(id, session_id)
}

/// Open the agents modal, showing all agent definitions.
pub(super) fn dispatch_open_config_agents_modal(
    app: &mut AppView,
    initial_tab: Option<crate::views::agents_modal::AgentsTab>,
) -> Vec<Effect> {
    use crate::views::agents_modal::{AgentsModalState, load_agent_toggle};

    let ActiveView::Agent(id) = app.active_view else {
        return vec![];
    };
    let bundle = app.bundle_state.clone();
    let Some(agent) = app.agents.get_mut(&id) else {
        return vec![];
    };

    // Mutual exclusivity with extensions_modal
    agent.extensions_modal = None;

    let cwd = agent.session.cwd.clone();
    let toggle = load_agent_toggle();
    let model_agent_type = agent
        .session
        .models
        .current
        .as_ref()
        .and_then(|id| agent.session.models.available.get(id))
        .and_then(model_agent_type_from_info);
    let session_id = agent.session.session_id.clone();
    let active_agent = agent.session_agent_name.clone();
    let mut modal = AgentsModalState::new(
        &cwd,
        &toggle,
        &bundle,
        model_agent_type.as_deref(),
        active_agent,
    );
    if let Some(tab) = initial_tab {
        modal.active_tab = tab;
    }
    agent.agents_modal = Some(modal);
    if let Some(session_id) = session_id {
        return vec![Effect::FetchSessionAgentName {
            agent_id: id,
            session_id,
        }];
    }
    vec![]
}

/// `agentType` / `agent_type` from a catalog `ModelInfo` meta blob.
fn model_agent_type_from_info(info: &agent_client_protocol::ModelInfo) -> Option<String> {
    let meta = info.meta.as_ref()?;
    ["agentType", "agent_type"]
        .into_iter()
        .find_map(|key| meta.get(key))
        .and_then(|v| v.as_str())
        .filter(|s| !s.is_empty())
        .map(str::to_owned)
}

/// Copy the selected block's metadata (e.g., command) to clipboard.
pub(super) fn dispatch_copy_block_meta(app: &mut AppView) {
    with_active_agent(app, |agent| {
        let Some(idx) = agent.scrollback.selected() else {
            return;
        };
        if agent.scrollback.entry_content_hidden_by_group(idx) {
            return;
        }
        let Some(entry) = agent.scrollback.entry(idx) else {
            return;
        };
        if let Some(text) = entry.block.copy_meta()
            && !text.is_empty()
        {
            agent.copy_to_clipboard(&text);
        }
    });
}

/// Dump the input flight recorder to a JSON file for debugging.
/// See `input_log.rs` module docs for lifecycle/removal instructions.
pub(super) fn dispatch_dump_input_log(app: &mut AppView) -> Vec<Effect> {
    let ActiveView::Agent(id) = app.active_view else {
        return vec![];
    };
    let Some(agent) = app.agents.get_mut(&id) else {
        return vec![];
    };

    if agent.input_log.entry_count() == 0 {
        agent.show_toast("No input events recorded yet.");
        return vec![];
    }

    let time_span_ms = agent.input_log.time_span_ms();
    let entries = agent.input_log.snapshot_entries();
    let entry_count = entries.len();
    let terminal = crate::terminal::terminal_context().diagnostics_snapshot();
    let session_id = agent.session.session_id.as_ref().map(|s| s.0.to_string());
    let pager_version = crate::client_identity::PAGER_CLIENT_VERSION;

    let now = chrono::Utc::now();
    let dump = crate::input_log::InputDump {
        dumped_at: now.to_rfc3339(),
        session_id: session_id.clone(),
        pager_version,
        terminal,
        active_pane: format!("{:?}", agent.active_pane),
        textarea_cursor: agent.prompt.cursor(),
        textarea_text_len: agent.prompt.text().len(),
        textarea_has_selection: agent.prompt.textarea.selection_range().is_some(),
        entry_count,
        time_span_ms,
        entries,
    };

    let json = match serde_json::to_string_pretty(&dump) {
        Ok(j) => j,
        Err(e) => {
            agent.show_toast(&format!("Failed to serialize input log: {e}"));
            return vec![];
        }
    };

    let kigi_home = kigi_tools::util::kigi_home::kigi_home();
    let logs_dir = kigi_home.join("logs");
    let _ = std::fs::create_dir_all(&logs_dir);
    let ts = now.format("%Y%m%d-%H%M%S");
    let path = logs_dir.join(format!("input-debug-{ts}.json"));

    match std::fs::write(&path, json) {
        Ok(()) => {
            let display_path = path.display();
            agent.show_toast(&format!(
                "Input log ({entry_count} events) → {display_path}"
            ));
            crate::unified_log::info(
                &format!("input debug dump: {entry_count} events, {time_span_ms}ms span"),
                session_id.as_deref(),
                None,
            );
        }
        Err(e) => {
            agent.show_toast(&format!("Failed to write input log: {e}"));
        }
    }
    vec![]
}

// TaskResult handlers.

pub(super) fn handle_hooks_list_loaded(
    app: &mut AppView,
    agent_id: AgentId,
    result: Result<kigi_hooks_plugins_types::HooksListResponse, String>,
) -> Vec<Effect> {
    use crate::views::extensions_modal::TabDataState;
    if let Some(agent) = app.agents.get_mut(&agent_id)
        && let Some(ref mut modal) = agent.extensions_modal
    {
        modal.hooks_data = match result {
            Ok(response) => {
                // Default all groups to collapsed.
                let mut seen = std::collections::HashSet::new();
                for hook in &response.hooks {
                    seen.insert(hook.source_dir.clone());
                }
                modal.hooks_collapsed_groups = seen;
                TabDataState::Loaded(response)
            }
            Err(e) => TabDataState::Error(e),
        };
    }
    vec![]
}

pub(super) fn handle_plugins_list_loaded(
    app: &mut AppView,
    agent_id: AgentId,
    result: Result<kigi_hooks_plugins_types::PluginsListResponse, String>,
) -> Vec<Effect> {
    use crate::views::extensions_modal::TabDataState;
    if let Some(agent) = app.agents.get_mut(&agent_id)
        && let Some(ref mut modal) = agent.extensions_modal
    {
        modal.plugins_data = match result {
            Ok(response) => {
                modal.seed_plugin_groups_once(&response.plugins);
                TabDataState::Loaded(response)
            }
            Err(e) => TabDataState::Error(e),
        };
        // Clear pending_action so the UI unblocks as soon as the
        // plugins list arrives.
        modal.pending_action = None;
        modal.pending_entry_index = None;
    }
    vec![]
}

pub(super) fn handle_mcp_toggle_done(
    app: &mut AppView,
    agent_id: AgentId,
    result: Result<(), String>,
) -> Vec<Effect> {
    let Some(agent) = app.agents.get_mut(&agent_id) else {
        return vec![];
    };
    if let Some(ref mut modal) = agent.extensions_modal
        && let Err(e) = result
    {
        modal.pending_action = None;
        modal.pending_entry_index = None;
        modal.modal_message = Some(crate::views::extensions_modal::ModalMessage::Error(e));
        return vec![];
    }
    let Some(session_id) = agent.session.session_id.clone() else {
        return vec![];
    };
    vec![Effect::FetchMcpsList {
        agent_id,
        session_id,
        cache: false,
    }]
}

pub(super) fn handle_skills_toggle_done(
    app: &mut AppView,
    agent_id: AgentId,
    result: Result<Vec<kigi_tools::implementations::skills::types::SkillInfo>, String>,
) -> Vec<Effect> {
    use crate::views::extensions_modal::TabDataState;
    if let Some(agent) = app.agents.get_mut(&agent_id)
        && let Some(ref mut modal) = agent.extensions_modal
    {
        modal.pending_action = None;
        modal.pending_entry_index = None;
        match result {
            Ok(skills) => {
                let len = skills.len();
                modal.skills_data = TabDataState::Loaded(skills);
                if len > 0 && modal.picker_state.selected >= len {
                    modal.picker_state.selected = len.saturating_sub(1);
                }
            }
            Err(e) => {
                modal.modal_message = Some(crate::views::extensions_modal::ModalMessage::Error(e));
            }
        }
    }
    // The toggle effect already called kigi/skills/refresh-baseline
    // which triggers the session to reload skills and push an
    // AvailableCommandsUpdate notification with the updated list.
    vec![]
}
