//! Global status bar widget.
//!
//! Views populate a [`StatusBarBuilder`] with left-side items; the
//! [`StatusBarRenderer`] then draws the full status bar — left items from the
//! view on the left, task-status on the right — into a one-row [`Rect`].

use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::Paragraph,
};

use crate::commands::CommandId;
use crate::task_registry::{TaskRegistry, TaskStatus};

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// A single item on the left side of the status bar.
#[derive(Debug, Clone)]
pub enum StatusItem {
    /// Non-interactive text label.
    Label(String),
    /// Clickable item that dispatches a command when selected.
    Menu { label: String, command: CommandId },
    /// Clickable action button.
    Action { label: String, command: CommandId },
    /// File path rendered with `format_path_spans_with_min_tail`: filename
    /// first, then elided directory context.  The `suffix` (e.g. `" [+]"`) is
    /// appended after the path spans.  The renderer allocates all remaining
    /// left-chunk space to this item so the path grows with the terminal width.
    FilePath { path: std::path::PathBuf, suffix: String },
}

/// Collects left-side status bar items from a view.
#[derive(Debug, Default)]
pub struct StatusBarBuilder {
    items: Vec<StatusItem>,
}

impl StatusBarBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    /// Append a non-interactive text label.
    pub fn label(&mut self, text: impl Into<String>) -> &mut Self {
        self.items.push(StatusItem::Label(text.into()));
        self
    }

    /// Append a clickable menu item (shows a dropdown indicator `▼`).
    pub fn menu(&mut self, label: impl Into<String>, command: CommandId) -> &mut Self {
        self.items.push(StatusItem::Menu { label: label.into(), command });
        self
    }

    /// Append a clickable action button.
    pub fn action(&mut self, label: impl Into<String>, command: CommandId) -> &mut Self {
        self.items.push(StatusItem::Action { label: label.into(), command });
        self
    }

    /// Append a file-path item rendered with filename-first elision.
    ///
    /// The renderer gives this item all remaining left-side space, so the
    /// displayed path grows as the terminal widens.  `dirty` appends `" [+]"`.
    pub fn file_path(&mut self, path: std::path::PathBuf, dirty: bool) -> &mut Self {
        let suffix = if dirty { " [+]".to_owned() } else { String::new() };
        self.items.push(StatusItem::FilePath { path, suffix });
        self
    }

    /// Consume the builder and return the collected items.
    pub fn build(self) -> Vec<StatusItem> {
        self.items
    }
}

// ---------------------------------------------------------------------------
// Click state
// ---------------------------------------------------------------------------

/// Records the clickable regions produced by the last render call.
///
/// Stored on `AppState` so that `handle_mouse` can dispatch the right command
/// or cancel-task operation without re-running layout logic.
#[derive(Debug, Default, Clone)]
pub struct StatusBarClickState {
    /// `(rect, command_id)` for each interactive status item.
    pub item_clicks: Vec<(Rect, CommandId)>,
    /// `(rect, task_id)` for each rendered cancel button.
    pub cancel_clicks: Vec<(Rect, crate::task_registry::TaskId)>,
    /// `(rect, task_id)` for each rendered task label (click → open log view).
    pub task_label_clicks: Vec<(Rect, crate::task_registry::TaskId)>,
}

impl StatusBarClickState {
    /// Returns the `CommandId` to dispatch if `(col, row)` hit a menu/action item.
    pub fn hit_command(&self, col: u16, row: u16) -> Option<&CommandId> {
        for (rect, cmd) in &self.item_clicks {
            if col >= rect.x && col < rect.x + rect.width && row >= rect.y && row < rect.y + rect.height {
                return Some(cmd);
            }
        }
        None
    }

    /// Returns the `TaskId` to cancel if `(col, row)` hit a cancel button.
    pub fn hit_cancel(&self, col: u16, row: u16) -> Option<crate::task_registry::TaskId> {
        for (rect, id) in &self.cancel_clicks {
            if col >= rect.x && col < rect.x + rect.width && row >= rect.y && row < rect.y + rect.height {
                return Some(*id);
            }
        }
        None
    }

    /// Returns the `TaskId` whose log view to open if `(col, row)` hit a task label.
    pub fn hit_task_label(&self, col: u16, row: u16) -> Option<crate::task_registry::TaskId> {
        for (rect, id) in &self.task_label_clicks {
            if col >= rect.x && col < rect.x + rect.width && row >= rect.y && row < rect.y + rect.height {
                return Some(*id);
            }
        }
        None
    }
}

// ---------------------------------------------------------------------------
// Renderer
// ---------------------------------------------------------------------------

/// Renders the global status bar.
pub struct StatusBarRenderer;

impl StatusBarRenderer {
    /// Draw the status bar into `area` (expected to be 1 row tall).
    ///
    /// Returns a [`StatusBarClickState`] recording the clickable regions so
    /// the caller can wire up mouse dispatch.
    pub fn render(
        frame: &mut Frame,
        area: Rect,
        items: Vec<StatusItem>,
        task_registry: &TaskRegistry,
        theme: &crate::theme::Theme,
    ) -> StatusBarClickState {
        if area.height == 0 {
            return StatusBarClickState::default();
        }

        let bg = theme.status_bar_bg();
        let fg = theme.status_bar_fg();
        let base_style = Style::default().fg(fg).bg(bg);
        let mut click_state = StatusBarClickState::default();

        // --- Build right side first (needed to know the width for layout) ---

        struct RightEntry {
            task_id: crate::task_registry::TaskId,
            sep:     String, // "  "
            ind:     String, // "● " / "✓ " etc.
            label:   String,
            cancel:  Option<String>, // " ⊘" for running tasks only
            style:   Style,
        }

        let mut entries: Vec<RightEntry> = Vec::new();
        let mut shown_queues: std::collections::HashSet<crate::task_registry::TaskQueueId> =
            std::collections::HashSet::new();

        // Running tasks first; mark their queues as shown.
        for (queue_id, &task_id) in task_registry.running_tasks() {
            if let Some(task) = task_registry.get(task_id) {
                let raw = format!("{}: {}", queue_id.0, task.command);
                let (indicator, style) = task_status_style(task.status.clone(), theme);
                entries.push(RightEntry {
                    task_id,
                    sep:    "  ".into(),
                    ind:    format!("{} ", indicator),
                    label:  truncate(&raw, 25),
                    cancel: Some(" ⊘".into()),
                    style,
                });
                shown_queues.insert(queue_id.clone());
            }
        }

        // Recently finished tasks: at most one per queue, skipping running queues.
        for task_id in task_registry.recently_finished_tasks() {
            if let Some(task) = task_registry.get(task_id) {
                if shown_queues.contains(&task.key.queue) {
                    continue;
                }
                let raw = format!("{}: {}", task.key.queue.0, task.command);
                let (indicator, style) = task_status_style(task.status.clone(), theme);
                entries.push(RightEntry {
                    task_id,
                    sep:    "  ".into(),
                    ind:    format!("{} ", indicator),
                    label:  truncate(&raw, 25),
                    cancel: None,
                    style,
                });
                shown_queues.insert(task.key.queue.clone());
            }
        }

        // Exact column width of all right-side content.
        let right_total_w: u16 = entries
            .iter()
            .map(|e| {
                uw(e.sep.as_str())
                    + uw(e.ind.as_str())
                    + uw(e.label.as_str())
                    + e.cancel.as_deref().map(uw).unwrap_or(0)
            })
            .sum::<usize>() as u16;

        // Only allocate right space when content exists and leaves room for the
        // left side (minimum 4 columns).
        const MIN_LEFT_W: u16 = 4;
        let right_w = if right_total_w > 0
            && right_total_w <= area.width.saturating_sub(MIN_LEFT_W)
        {
            right_total_w
        } else {
            0
        };

        // Split the row with Layout: left takes all remaining space, right gets
        // exactly the measured content width (or 0).  The layout system computes
        // the right chunk's x position — no manual formula needed.
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Min(0), Constraint::Length(right_w)])
            .split(area);
        let left_chunk = chunks[0];
        let right_chunk = chunks[1];

        // Pre-measure: compute the column budget available for a FilePath item.
        //
        // The left chunk holds:
        //   1 col padding + all item widths + 2-col separators between items
        //
        // FilePath items contribute only their suffix to `non_fp_w`; the path
        // spans themselves take whatever is left over.
        let path_spans_avail: usize = {
            let n = items.len() as u16;
            let non_fp_w: u16 = items
                .iter()
                .map(|i| match i {
                    StatusItem::Label(t) => uw(&truncate(t, 30)) as u16,
                    StatusItem::Menu { label, .. } => {
                        uw(&format!("{} ▼", truncate(label, 28))) as u16
                    }
                    StatusItem::Action { label, .. } => uw(&truncate(label, 20)) as u16,
                    // Only the suffix consumes fixed width; path spans are flexible.
                    StatusItem::FilePath { suffix, .. } => uw(suffix.as_str()) as u16,
                })
                .sum();
            let seps_w = 2u16 * n.saturating_sub(1);
            (left_chunk.width as isize - 1 - non_fp_w as isize - seps_w as isize).max(0) as usize
        };

        // --- Build left spans ---
        let mut left_spans: Vec<Span> = Vec::new();
        // One column of left padding; x_cursor tracks click-region positions.
        let mut x_cursor = left_chunk.x + 1;

        for item in &items {
            if !left_spans.is_empty() {
                left_spans.push(Span::styled("  ", base_style));
                x_cursor += 2;
            }

            match item {
                StatusItem::Label(text) => {
                    let text = truncate(text, 30);
                    x_cursor += uw(text.as_str()) as u16;
                    left_spans.push(Span::styled(text, base_style));
                }
                StatusItem::Menu { label, command } => {
                    let text = truncate(label, 28);
                    let full = format!("{} ▼", text);
                    let w = uw(full.as_str()) as u16;
                    // Extend the hit region by one cell so the arrow and its
                    // surrounding space are reliably clickable.
                    click_state.item_clicks.push((
                        Rect { x: x_cursor, y: area.y, width: w + 1, height: 1 },
                        command.clone(),
                    ));
                    x_cursor += w + 1;
                    left_spans.push(Span::styled(
                        full,
                        base_style.add_modifier(Modifier::UNDERLINED),
                    ));
                }
                StatusItem::Action { label, command } => {
                    let text = truncate(label, 20);
                    let w = uw(text.as_str()) as u16;
                    click_state.item_clicks.push((
                        Rect { x: x_cursor, y: area.y, width: w + 1, height: 1 },
                        command.clone(),
                    ));
                    x_cursor += w + 1;
                    left_spans.push(Span::styled(
                        text,
                        base_style.add_modifier(Modifier::BOLD),
                    ));
                }
                StatusItem::FilePath { path, suffix } => {
                    // Build a StyleConfig that inherits the status bar background.
                    let path_cfg = crate::path_format::StyleConfig {
                        filename: base_style.add_modifier(Modifier::BOLD),
                        path:     base_style,
                        dim:      base_style.add_modifier(Modifier::DIM),
                        separator: base_style.add_modifier(Modifier::DIM),
                    };
                    let path_spans = crate::path_format::format_path_spans_with_min_tail(
                        path.as_path(),
                        path_spans_avail,
                        path_cfg,
                        1,
                    );
                    // Advance cursor by the actual rendered width of the path spans.
                    let path_w: u16 = path_spans
                        .iter()
                        .map(|s| uw(s.content.as_ref()) as u16)
                        .sum();
                    x_cursor += path_w;
                    left_spans.extend(path_spans);
                    if !suffix.is_empty() {
                        x_cursor += uw(suffix.as_str()) as u16;
                        left_spans.push(Span::styled(suffix.clone(), base_style));
                    }
                }
            }
        }

        // --- Build right spans ---
        // `rx` is seeded from the layout-computed chunk origin rather than a
        // manual right_x_start formula, so click regions always match rendering.
        let mut right_spans: Vec<Span> = Vec::new();
        if right_chunk.width > 0 {
            let mut rx = right_chunk.x;
            for e in &entries {
                right_spans.push(Span::styled(e.sep.clone(), base_style));
                rx += uw(e.sep.as_str()) as u16;

                right_spans.push(Span::styled(e.ind.clone(), e.style.bg(bg)));
                rx += uw(e.ind.as_str()) as u16;

                let label_w = uw(e.label.as_str()) as u16;
                click_state.task_label_clicks.push((
                    Rect { x: rx, y: area.y, width: label_w, height: 1 },
                    e.task_id,
                ));
                right_spans.push(Span::styled(
                    e.label.clone(),
                    e.style.bg(bg).add_modifier(Modifier::UNDERLINED),
                ));
                rx += label_w;

                if let Some(ref cancel_str) = e.cancel {
                    let cancel_w = uw(cancel_str.as_str()) as u16;
                    click_state.cancel_clicks.push((
                        Rect { x: rx, y: area.y, width: cancel_w, height: 1 },
                        e.task_id,
                    ));
                    right_spans.push(Span::styled(cancel_str.clone(), e.style.bg(bg)));
                    rx += cancel_w;
                }
            }
        }

        // --- Render ---
        // Fill the entire row with the background colour first.
        frame.render_widget(
            Paragraph::new(Line::from(vec![Span::styled(
                " ".repeat(area.width as usize),
                base_style,
            )])),
            area,
        );

        // Left side (1-col inset padding).
        if !left_spans.is_empty() {
            let left_area = Rect {
                x: left_chunk.x + 1,
                width: left_chunk.width.saturating_sub(1),
                ..left_chunk
            };
            frame.render_widget(Paragraph::new(Line::from(left_spans)), left_area);
        }

        // Right side — ratatui clips content to right_chunk automatically.
        if !right_spans.is_empty() {
            frame.render_widget(Paragraph::new(Line::from(right_spans)), right_chunk);
        }

        click_state
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Shorthand for `unicode_width::UnicodeWidthStr::width`.
fn uw(s: &str) -> usize {
    unicode_width::UnicodeWidthStr::width(s)
}

fn truncate(s: &str, max_chars: usize) -> String {
    let chars: Vec<char> = s.chars().collect();
    if chars.len() <= max_chars {
        s.to_owned()
    } else {
        chars[..max_chars.saturating_sub(1)].iter().collect::<String>() + "…"
    }
}

fn task_status_style(status: TaskStatus, theme: &crate::theme::Theme) -> (&'static str, Style) {
    match status {
        TaskStatus::Running  => ("●", Style::default().fg(theme.fg())),
        TaskStatus::Success  => ("✓", Style::default().fg(theme.level_success())),
        TaskStatus::Warning  => ("⚠", Style::default().fg(theme.level_warn())),
        TaskStatus::Error    => ("✗", Style::default().fg(theme.level_error())),
        TaskStatus::Cancelled => ("—", Style::default().fg(theme.fg_dim())),
        TaskStatus::Pending  => ("○", Style::default().fg(theme.fg_dim())),
    }
}
