//! Minimal-mode plan-approval host (design PR10).
//!
//! The full TUI renders plan approval as a fullscreen line-viewer plus a live
//! feedback prompt. Minimal takes a simpler route: the **whole plan is committed
//! into native scrollback** as a normal conversation block (see
//! [`maybe_commit_plan`]), so it reads and scrolls exactly like the rest of the
//! transcript. The prompt-anchored live region then holds only the decision
//! controls — approve / revise / keep planning — plus the feedback input when
//! revising. Nothing of the plan body is drawn under the prompt.
//!
//! Input routing is unchanged: while `line_viewer.is_some()` the agent's input
//! handler already routes keys to `handle_line_viewer_key` (Preview focus:
//! `a` approve / `s`/`Tab` revise / `q` keep planning) and `handle_plan_feedback_key`
//! (Prompt focus: type feedback, `Enter` send, `Esc` back). Minimal keeps the
//! line viewer open (so those keys fire) but renders this compact controls strip
//! in place of the never-drawn fullscreen viewer.

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;

use kigi_tui::app::agent_view::AgentView;
use kigi_tui::app::app_view::{ActiveView, AppView};
use kigi_tui::minimal_api;
use kigi_tui::scrollback::block::RenderBlock;
use kigi_tui::theme::Theme;
use kigi_tui::views::plan_approval_view::PlanApprovalFocus;
use kigi_tui::views::prompt_widget::PromptStyle;

/// The active plan-approval focus, defaulting to `Preview`.
fn focus(agent: &AgentView) -> PlanApprovalFocus {
    minimal_api::plan_approval_view(agent)
        .map(|p| p.focus)
        .unwrap_or(PlanApprovalFocus::Preview)
}

/// Scrollback notice when exit_plan_mode parks with no plan body.
///
/// Kept short and plain (no markdown chrome) so native scrollback reads cleanly
/// under minimal mode's chromeless commit path.
const EMPTY_PLAN_SCROLLBACK: &str = "\
No plan written yet.

Approve to leave plan mode and start implementing, request changes to send the \
agent back to planning, or quit to abandon.";

/// Controls-strip header for the parked plan-approval surface.
fn plan_header(has_plan: bool) -> &'static str {
    if has_plan {
        "Plan ready for review"
    } else {
        "No plan written yet"
    }
}

/// Body committed into native scrollback for a parked plan approval.
fn plan_scrollback_body(plan_content: Option<&str>) -> String {
    plan_content
        .filter(|s| !s.trim().is_empty())
        .map(str::to_owned)
        .unwrap_or_else(|| EMPTY_PLAN_SCROLLBACK.to_owned())
}

/// Commit the active plan into native scrollback, once per plan (and once per
/// revision).
///
/// Minimal has no separate plan pane: the terminal's scrollback *is* the
/// history, so the plan is pushed as an ordinary finalized agent-message block
/// and printed into native scrollback by the normal commit pass — leaving only
/// the decision controls under the prompt. De-duplicated by the plan's
/// `tool_call_id`; a revised plan arrives as a fresh ExitPlanMode with a new id
/// and is committed as its own block. Empty / whitespace-only plans still commit
/// a short notice so the user sees *why* approval is parked (otherwise only the
/// controls strip appears and the session looks stuck).
///
/// NOTE (draw-path state mutation + replay durability): this pushes into
/// `ScrollbackState` from the render path — a deliberate exception, since the
/// plan block must enter the normal commit pipeline. The pushed block is
/// client-render state, not a server event: a resumed session will not replay
/// it, so post-reload `/transcript` shows the plan only through whatever the
/// agent itself messaged. Accepted for v1 (the live session — the mode's whole
/// surface — is consistent).
///
/// Call once per frame from [`crate::draw`], before the commit pass.
pub fn maybe_commit_plan(app: &mut AppView) {
    let ActiveView::Agent(id) = &app.active_view else {
        return;
    };
    let id = *id;

    // Extract the plan (owned) under a short immutable borrow so the mutable
    // scrollback push and the `minimal_state` read/write below don't overlap it.
    let plan = app.agents.get(&id).and_then(|agent| {
        minimal_api::plan_approval_view(agent).map(|pav| {
            let content = plan_scrollback_body(pav.plan_content.as_deref());
            (pav.tool_call_id.clone(), content)
        })
    });
    let Some((tool_call_id, content)) = plan else {
        return;
    };

    if minimal_api::minimal_committed_plan_id(app) == Some(tool_call_id.as_str()) {
        return; // already emitted this plan
    }

    // Mark the plan as emitted only when the block was actually pushed: the
    // agent borrow can't fail here (the plan was just extracted from it), but
    // if it ever did, stamping the id anyway would treat the plan as committed
    // while nothing ever reaches native scrollback.
    if let Some(agent) = app.agents.get_mut(&id) {
        agent
            .scrollback
            .push_block(RenderBlock::agent_message(content));
        minimal_api::set_minimal_committed_plan_id(app, Some(tool_call_id));
    }
}

/// Desired controls-strip height: header + controls + optional feedback input.
pub fn height(agent: &AgentView) -> u16 {
    let input = if focus(agent) == PlanApprovalFocus::Prompt {
        1
    } else {
        0
    };
    // header (1) + controls (1) + input (0/1)
    2u16.saturating_add(input)
}

/// Render the compact plan-approval controls strip into `area`. The plan itself
/// lives in native scrollback ([`maybe_commit_plan`]); this only draws the
/// header, the decision hint, and — when revising — the feedback input. Returns
/// the text cursor when the feedback input is focused, else `None`.
pub fn render(
    buf: &mut Buffer,
    area: Rect,
    agent: &mut AgentView,
    theme: &Theme,
) -> Option<(u16, u16)> {
    if area.height == 0 || area.width < 4 {
        return None;
    }
    let foc = focus(agent);
    let input_h: u16 = if foc == PlanApprovalFocus::Prompt {
        1
    } else {
        0
    };

    // header (1) · controls (1) · input (0/1)
    let controls_y = (area.y + area.height).saturating_sub(1 + input_h);

    // ── header ──
    let has_plan = minimal_api::plan_approval_view(agent)
        .map(|p| p.has_plan)
        .unwrap_or(false);
    let header_style = Style::default()
        .fg(theme.accent_user)
        .bg(Color::Reset)
        .add_modifier(Modifier::BOLD);
    buf.set_style(
        Rect { height: 1, ..area },
        Style::default().bg(Color::Reset),
    );
    buf.set_span(
        area.x,
        area.y,
        &Span::styled(plan_header(has_plan), header_style),
        area.width,
    );

    // ── controls hint ──
    let has_content = minimal_api::plan_approval_view(agent)
        .map(|p| !p.comments.is_empty())
        .unwrap_or(false)
        || !agent.prompt.text().trim().is_empty();
    // Tab reopens the preview (including the empty-plan placeholder).
    let hint = match foc {
        PlanApprovalFocus::Prompt if has_content => {
            "enter request changes \u{00b7} tab plan \u{00b7} esc back"
        }
        PlanApprovalFocus::Prompt => "enter approve \u{00b7} tab plan \u{00b7} esc back",
        PlanApprovalFocus::Commenting => "enter save comment \u{00b7} esc cancel",
        PlanApprovalFocus::Preview => "a approve \u{00b7} s revise \u{00b7} q keep planning",
    };
    let hint_style = theme.dim().bg(Color::Reset);
    let controls_rect = Rect {
        x: area.x,
        y: controls_y,
        width: area.width,
        height: 1,
    };
    buf.set_style(controls_rect, hint_style);
    buf.set_span(
        area.x,
        controls_y,
        &Span::styled(hint, hint_style),
        area.width,
    );

    // ── feedback input (revise mode) ──
    if input_h > 0 {
        let row = Rect {
            x: area.x,
            y: (area.y + area.height).saturating_sub(1),
            width: area.width,
            height: 1,
        };
        let style = input_style(theme);
        buf.set_style(row, Style::default().bg(theme.bg_visual));
        return agent.prompt.draw(buf, row, None, &style, None).cursor_pos;
    }
    None
}

/// Chromeless prompt style for the feedback editor (the modal supplies framing).
fn input_style(theme: &Theme) -> PromptStyle {
    PromptStyle {
        focused: true,
        show_prefix: false,
        vpad_top: 0,
        compact: false,
        chrome: false,
        chrome_pad_left: 0,
        chrome_pad_right: 0,
        bg_override: Some(theme.bg_visual),
        accent_color_override: None,
        border_color_override: None,
        prefix_override: None,
        placeholder_override: None,
        show_accent_line: false,
        show_borders: false,
        title: None,
        image_preview: true,
    }
}

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

    #[test]
    fn empty_plan_header_is_explicit() {
        assert_eq!(plan_header(true), "Plan ready for review");
        assert_eq!(plan_header(false), "No plan written yet");
    }

    #[test]
    fn empty_plan_scrollback_uses_notice_not_silence() {
        let body = plan_scrollback_body(None);
        assert!(body.contains("No plan written yet"));
        assert!(body.contains("Approve"));

        let whitespace = plan_scrollback_body(Some("  \n\t  "));
        assert_eq!(whitespace, body, "whitespace-only counts as empty");

        let real = plan_scrollback_body(Some("# Plan\n- do it"));
        assert_eq!(real, "# Plan\n- do it");
    }
}
