#![allow(dead_code)] // Phase 2: consumed by Issue List + MR List screens

//! Generic entity table widget for list screens.
//!
//! `EntityTable<R>` renders rows with sortable, responsive columns.
//! Columns hide gracefully when the terminal is too narrow, using
//! priority-based visibility.

use ftui::core::geometry::Rect;
use ftui::render::cell::{Cell, PackedRgba};
use ftui::render::drawing::Draw;
use ftui::render::frame::Frame;

// ---------------------------------------------------------------------------
// Column definition
// ---------------------------------------------------------------------------

/// Describes a single table column.
#[derive(Debug, Clone)]
pub struct ColumnDef {
    /// Display name shown in the header.
    pub name: &'static str,
    /// Minimum width in characters. Column is hidden if it can't meet this.
    pub min_width: u16,
    /// Flex weight for distributing extra space.
    pub flex_weight: u16,
    /// Visibility priority (0 = always shown, higher = hidden first).
    pub priority: u8,
    /// Text alignment within the column.
    pub align: Align,
}

/// Text alignment within a column.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Align {
    #[default]
    Left,
    Right,
}

// ---------------------------------------------------------------------------
// TableRow trait
// ---------------------------------------------------------------------------

/// Trait for types that can be rendered as a table row.
pub trait TableRow {
    /// Return the cell text for each column, in column order.
    fn cells(&self, col_count: usize) -> Vec<String>;
}

// ---------------------------------------------------------------------------
// EntityTable state
// ---------------------------------------------------------------------------

/// Rendering state for the entity table.
#[derive(Debug, Clone)]
pub struct EntityTableState {
    /// Index of the selected row (0-based, within the full data set).
    pub selected: usize,
    /// Scroll offset (first visible row index).
    pub scroll_offset: usize,
    /// Index of the column used for sorting.
    pub sort_column: usize,
    /// Sort direction.
    pub sort_ascending: bool,
}

impl Default for EntityTableState {
    fn default() -> Self {
        Self {
            selected: 0,
            scroll_offset: 0,
            sort_column: 0,
            sort_ascending: true,
        }
    }
}

impl EntityTableState {
    /// Move selection down by 1.
    pub fn select_next(&mut self, total_rows: usize) {
        if total_rows == 0 {
            return;
        }
        self.selected = (self.selected + 1).min(total_rows - 1);
    }

    /// Move selection up by 1.
    pub fn select_prev(&mut self) {
        self.selected = self.selected.saturating_sub(1);
    }

    /// Page down (move by `page_size` rows).
    pub fn page_down(&mut self, total_rows: usize, page_size: usize) {
        if total_rows == 0 {
            return;
        }
        self.selected = (self.selected + page_size).min(total_rows - 1);
    }

    /// Page up.
    pub fn page_up(&mut self, page_size: usize) {
        self.selected = self.selected.saturating_sub(page_size);
    }

    /// Jump to top.
    pub fn select_first(&mut self) {
        self.selected = 0;
    }

    /// Jump to bottom.
    pub fn select_last(&mut self, total_rows: usize) {
        if total_rows > 0 {
            self.selected = total_rows - 1;
        }
    }

    /// Cycle sort column forward (wraps around).
    pub fn cycle_sort(&mut self, col_count: usize) {
        if col_count == 0 {
            return;
        }
        self.sort_column = (self.sort_column + 1) % col_count;
    }

    /// Toggle sort direction on current column.
    pub fn toggle_sort_direction(&mut self) {
        self.sort_ascending = !self.sort_ascending;
    }

    /// Ensure scroll offset keeps selection visible.
    fn adjust_scroll(&mut self, visible_rows: usize) {
        if visible_rows == 0 {
            return;
        }
        if self.selected < self.scroll_offset {
            self.scroll_offset = self.selected;
        }
        if self.selected >= self.scroll_offset + visible_rows {
            self.scroll_offset = self.selected - visible_rows + 1;
        }
    }
}

// ---------------------------------------------------------------------------
// Colors
// ---------------------------------------------------------------------------

/// Colors for the entity table. Will be replaced by Theme injection.
pub struct TableColors {
    pub header_fg: PackedRgba,
    pub header_bg: PackedRgba,
    pub row_fg: PackedRgba,
    pub row_alt_bg: PackedRgba,
    pub selected_fg: PackedRgba,
    pub selected_bg: PackedRgba,
    pub sort_indicator: PackedRgba,
    pub border: PackedRgba,
}

// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------

/// Compute which columns are visible given the available width.
///
/// Returns indices of visible columns sorted by original order,
/// along with their allocated widths.
pub fn visible_columns(columns: &[ColumnDef], available_width: u16) -> Vec<(usize, u16)> {
    // Sort by priority (lowest = most important).
    let mut indexed: Vec<(usize, &ColumnDef)> = columns.iter().enumerate().collect();
    indexed.sort_by_key(|(_, col)| col.priority);

    let mut result: Vec<(usize, u16)> = Vec::new();
    let mut used_width: u16 = 0;
    let gap = 1u16; // 1-char gap between columns.

    for (idx, col) in &indexed {
        let needed = col.min_width + if result.is_empty() { 0 } else { gap };
        if used_width + needed <= available_width {
            result.push((*idx, col.min_width));
            used_width += needed;
        }
    }

    // Distribute remaining space by flex weight.
    let remaining = available_width.saturating_sub(used_width);
    if remaining > 0 {
        let total_weight: u16 = result
            .iter()
            .map(|(idx, _)| columns[*idx].flex_weight)
            .sum();

        if total_weight > 0 {
            for (idx, width) in &mut result {
                let weight = columns[*idx].flex_weight;
                let extra =
                    (u32::from(remaining) * u32::from(weight) / u32::from(total_weight)) as u16;
                *width += extra;
            }
        }
    }

    // Sort by original column order for rendering.
    result.sort_by_key(|(idx, _)| *idx);
    result
}

/// Render the entity table header row.
pub fn render_header(
    frame: &mut Frame<'_>,
    columns: &[ColumnDef],
    visible: &[(usize, u16)],
    state: &EntityTableState,
    y: u16,
    area_x: u16,
    colors: &TableColors,
) {
    let header_cell = Cell {
        fg: colors.header_fg,
        bg: colors.header_bg,
        ..Cell::default()
    };
    let sort_cell = Cell {
        fg: colors.sort_indicator,
        bg: colors.header_bg,
        ..Cell::default()
    };

    // Fill header background.
    let total_width: u16 = visible.iter().map(|(_, w)| w + 1).sum();
    let header_rect = Rect::new(area_x, y, total_width, 1);
    frame.draw_rect_filled(
        header_rect,
        Cell {
            bg: colors.header_bg,
            ..Cell::default()
        },
    );

    let mut x = area_x;
    for (col_idx, col_width) in visible {
        let col = &columns[*col_idx];
        let col_max = x.saturating_add(*col_width);

        let after_name = frame.print_text_clipped(x, y, col.name, header_cell, col_max);

        // Sort indicator.
        if *col_idx == state.sort_column {
            let arrow = if state.sort_ascending { " ^" } else { " v" };
            frame.print_text_clipped(after_name, y, arrow, sort_cell, col_max);
        }

        x = col_max.saturating_add(1); // gap
    }
}

/// Style context for rendering a single row.
pub struct RowContext<'a> {
    pub columns: &'a [ColumnDef],
    pub visible: &'a [(usize, u16)],
    pub is_selected: bool,
    pub is_alt: bool,
    pub colors: &'a TableColors,
}

/// Render a data row.
pub fn render_row<R: TableRow>(
    frame: &mut Frame<'_>,
    row: &R,
    y: u16,
    area_x: u16,
    ctx: &RowContext<'_>,
) {
    let (fg, bg) = if ctx.is_selected {
        (ctx.colors.selected_fg, ctx.colors.selected_bg)
    } else if ctx.is_alt {
        (ctx.colors.row_fg, ctx.colors.row_alt_bg)
    } else {
        (ctx.colors.row_fg, Cell::default().bg)
    };

    let cell_style = Cell {
        fg,
        bg,
        ..Cell::default()
    };

    // Fill row background if selected or alt.
    if ctx.is_selected || ctx.is_alt {
        let total_width: u16 = ctx.visible.iter().map(|(_, w)| w + 1).sum();
        frame.draw_rect_filled(
            Rect::new(area_x, y, total_width, 1),
            Cell {
                bg,
                ..Cell::default()
            },
        );
    }

    let cells = row.cells(ctx.columns.len());
    let mut x = area_x;

    for (col_idx, col_width) in ctx.visible {
        let col_max = x.saturating_add(*col_width);
        let text = cells.get(*col_idx).map(String::as_str).unwrap_or("");

        match ctx.columns[*col_idx].align {
            Align::Left => {
                frame.print_text_clipped(x, y, text, cell_style, col_max);
            }
            Align::Right => {
                let text_len = text.len() as u16;
                let start = if text_len < *col_width {
                    x + col_width - text_len
                } else {
                    x
                };
                frame.print_text_clipped(start, y, text, cell_style, col_max);
            }
        }

        x = col_max.saturating_add(1); // gap
    }
}

/// Render a complete entity table: header + scrollable rows.
pub fn render_entity_table<R: TableRow>(
    frame: &mut Frame<'_>,
    rows: &[R],
    columns: &[ColumnDef],
    state: &mut EntityTableState,
    area: Rect,
    colors: &TableColors,
) {
    if area.height < 2 || area.width < 5 {
        return;
    }

    let visible = visible_columns(columns, area.width);
    if visible.is_empty() {
        return;
    }

    // Header row.
    render_header(frame, columns, &visible, state, area.y, area.x, colors);

    // Separator.
    let sep_y = area.y.saturating_add(1);
    let sep_cell = Cell {
        fg: colors.border,
        ..Cell::default()
    };
    let rule = "─".repeat(area.width as usize);
    frame.print_text_clipped(
        area.x,
        sep_y,
        &rule,
        sep_cell,
        area.x.saturating_add(area.width),
    );

    // Data rows.
    let data_start_y = area.y.saturating_add(2);
    let visible_rows = area.height.saturating_sub(2) as usize; // minus header + separator

    state.adjust_scroll(visible_rows);

    let start = state.scroll_offset;
    let end = (start + visible_rows).min(rows.len());

    for (i, row) in rows[start..end].iter().enumerate() {
        let row_y = data_start_y.saturating_add(i as u16);
        let absolute_idx = start + i;
        let ctx = RowContext {
            columns,
            visible: &visible,
            is_selected: absolute_idx == state.selected,
            is_alt: absolute_idx % 2 == 1,
            colors,
        };

        render_row(frame, row, row_y, area.x, &ctx);
    }

    // Scroll indicator if more rows below.
    if end < rows.len() {
        let indicator_y = data_start_y.saturating_add(visible_rows as u16);
        if indicator_y < area.y.saturating_add(area.height) {
            let muted = Cell {
                fg: colors.border,
                ..Cell::default()
            };
            let remaining = rows.len() - end;
            frame.print_text_clipped(
                area.x,
                indicator_y,
                &format!("... {remaining} more"),
                muted,
                area.x.saturating_add(area.width),
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use ftui::render::grapheme_pool::GraphemePool;

    macro_rules! with_frame {
        ($width:expr, $height:expr, |$frame:ident| $body:block) => {{
            let mut pool = GraphemePool::new();
            let mut $frame = Frame::new($width, $height, &mut pool);
            $body
        }};
    }

    fn test_columns() -> Vec<ColumnDef> {
        vec![
            ColumnDef {
                name: "IID",
                min_width: 5,
                flex_weight: 0,
                priority: 0,
                align: Align::Right,
            },
            ColumnDef {
                name: "Title",
                min_width: 10,
                flex_weight: 3,
                priority: 0,
                align: Align::Left,
            },
            ColumnDef {
                name: "State",
                min_width: 8,
                flex_weight: 1,
                priority: 1,
                align: Align::Left,
            },
            ColumnDef {
                name: "Author",
                min_width: 8,
                flex_weight: 1,
                priority: 2,
                align: Align::Left,
            },
            ColumnDef {
                name: "Updated",
                min_width: 10,
                flex_weight: 0,
                priority: 3,
                align: Align::Right,
            },
        ]
    }

    struct TestRow {
        cells: Vec<String>,
    }

    impl TableRow for TestRow {
        fn cells(&self, _col_count: usize) -> Vec<String> {
            self.cells.clone()
        }
    }

    fn test_colors() -> TableColors {
        TableColors {
            header_fg: PackedRgba::rgb(0xFF, 0xFF, 0xFF),
            header_bg: PackedRgba::rgb(0x30, 0x30, 0x30),
            row_fg: PackedRgba::rgb(0xCE, 0xCD, 0xC3),
            row_alt_bg: PackedRgba::rgb(0x28, 0x28, 0x24),
            selected_fg: PackedRgba::rgb(0xFF, 0xFF, 0xFF),
            selected_bg: PackedRgba::rgb(0xDA, 0x70, 0x2C),
            sort_indicator: PackedRgba::rgb(0xDA, 0x70, 0x2C),
            border: PackedRgba::rgb(0x87, 0x87, 0x80),
        }
    }

    #[test]
    fn test_visible_columns_all_fit() {
        let cols = test_columns();
        let vis = visible_columns(&cols, 100);
        assert_eq!(vis.len(), 5, "All 5 columns should fit at 100 cols");
    }

    #[test]
    fn test_visible_columns_hides_low_priority() {
        let cols = test_columns();
        // min widths: 5 + 10 + 8 + 8 + 10 + 4 gaps = 45.
        // At 25 cols, only priority 0 columns (IID + Title) should fit.
        let vis = visible_columns(&cols, 25);
        let visible_indices: Vec<usize> = vis.iter().map(|(idx, _)| *idx).collect();
        assert!(visible_indices.contains(&0), "IID should always be visible");
        assert!(
            visible_indices.contains(&1),
            "Title should always be visible"
        );
        assert!(
            !visible_indices.contains(&4),
            "Updated (priority 3) should be hidden"
        );
    }

    #[test]
    fn test_column_hiding_at_60_cols() {
        let cols = test_columns();
        let vis = visible_columns(&cols, 60);
        // min widths for priority 0,1,2: 5+10+8+8 + 3 gaps = 34.
        // Priority 3 (Updated, min 10 + gap) = 45 total, should still fit.
        assert!(vis.len() >= 3, "At least 3 columns at 60 cols");
    }

    #[test]
    fn test_state_select_next_prev() {
        let mut state = EntityTableState::default();
        state.select_next(5);
        assert_eq!(state.selected, 1);
        state.select_next(5);
        assert_eq!(state.selected, 2);
        state.select_prev();
        assert_eq!(state.selected, 1);
    }

    #[test]
    fn test_state_select_bounds() {
        let mut state = EntityTableState::default();
        state.select_prev(); // at 0, can't go below
        assert_eq!(state.selected, 0);

        state.select_next(3);
        state.select_next(3);
        state.select_next(3); // at 2, can't go above last
        assert_eq!(state.selected, 2);
    }

    #[test]
    fn test_state_page_up_down() {
        let mut state = EntityTableState::default();
        state.page_down(20, 5);
        assert_eq!(state.selected, 5);
        state.page_up(3);
        assert_eq!(state.selected, 2);
    }

    #[test]
    fn test_state_first_last() {
        let mut state = EntityTableState {
            selected: 5,
            ..Default::default()
        };
        state.select_first();
        assert_eq!(state.selected, 0);
        state.select_last(10);
        assert_eq!(state.selected, 9);
    }

    #[test]
    fn test_state_cycle_sort() {
        let mut state = EntityTableState::default();
        assert_eq!(state.sort_column, 0);
        state.cycle_sort(5);
        assert_eq!(state.sort_column, 1);
        state.sort_column = 4;
        state.cycle_sort(5); // wraps to 0
        assert_eq!(state.sort_column, 0);
    }

    #[test]
    fn test_state_toggle_sort_direction() {
        let mut state = EntityTableState::default();
        assert!(state.sort_ascending);
        state.toggle_sort_direction();
        assert!(!state.sort_ascending);
    }

    #[test]
    fn test_state_adjust_scroll() {
        let mut state = EntityTableState {
            selected: 15,
            scroll_offset: 0,
            ..Default::default()
        };
        state.adjust_scroll(10);
        assert_eq!(state.scroll_offset, 6); // selected=15 should be at bottom of 10-row window
    }

    #[test]
    fn test_render_entity_table_no_panic() {
        with_frame!(80, 20, |frame| {
            let cols = test_columns();
            let rows = vec![
                TestRow {
                    cells: vec![
                        "#42".into(),
                        "Fix auth bug".into(),
                        "opened".into(),
                        "taylor".into(),
                        "2h ago".into(),
                    ],
                },
                TestRow {
                    cells: vec![
                        "#43".into(),
                        "Add tests".into(),
                        "merged".into(),
                        "alice".into(),
                        "1d ago".into(),
                    ],
                },
            ];
            let mut state = EntityTableState::default();
            let colors = test_colors();

            render_entity_table(
                &mut frame,
                &rows,
                &cols,
                &mut state,
                Rect::new(0, 0, 80, 20),
                &colors,
            );
        });
    }

    #[test]
    fn test_render_entity_table_tiny_noop() {
        with_frame!(3, 1, |frame| {
            let cols = test_columns();
            let rows: Vec<TestRow> = vec![];
            let mut state = EntityTableState::default();
            let colors = test_colors();

            render_entity_table(
                &mut frame,
                &rows,
                &cols,
                &mut state,
                Rect::new(0, 0, 3, 1),
                &colors,
            );
        });
    }

    #[test]
    fn test_render_entity_table_empty_rows() {
        with_frame!(80, 10, |frame| {
            let cols = test_columns();
            let rows: Vec<TestRow> = vec![];
            let mut state = EntityTableState::default();
            let colors = test_colors();

            render_entity_table(
                &mut frame,
                &rows,
                &cols,
                &mut state,
                Rect::new(0, 0, 80, 10),
                &colors,
            );
        });
    }

    #[test]
    fn test_state_select_next_empty() {
        let mut state = EntityTableState::default();
        state.select_next(0); // no rows
        assert_eq!(state.selected, 0);
    }
}
