//! Table rendering for the TUI
//!
//! Handles rendering of markdown tables with proper alignment,
//! borders, selection highlighting, and cell navigation.

use crate::parser::output::Alignment;
use crate::tui::theme::Theme;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};

use crate::tui::ui::format_inline_markdown;
use crate::tui::ui::util::{align_text, terminal_width, wrap_text};

/// Context for rendering a table row
pub struct TableRenderContext<'a> {
    pub theme: &'a Theme,
    pub row_num: usize,
    pub is_header: bool,
    pub in_table_mode: bool,
    pub is_table_selected: bool,
    pub selected_cell: Option<(usize, usize)>,
}

/// Minimum column width (including padding) to maintain readability
const MIN_COL_WIDTH: usize = 3;

/// Calculate column widths using content-weighted area approach
///
/// Instead of using max cell width, use average cell width weighted by content.
/// This gives fairer distribution when one column has a single outlier value.
fn calculate_column_widths(headers: &[String], rows: &[Vec<String>]) -> Vec<usize> {
    let col_count = headers.len();

    if rows.is_empty() {
        // No data rows, use header widths
        return headers.iter().map(|h| terminal_width(h).max(1)).collect();
    }

    let mut col_widths: Vec<usize> = vec![0; col_count];

    // Calculate average width per column from data rows (not headers)
    // This focuses on actual content, as forthrin suggested
    for (i, _header) in headers.iter().enumerate() {
        let mut total_width = 0usize;
        let mut cell_count = 0usize;
        let mut max_width = 0usize;

        for row in rows {
            if let Some(cell) = row.get(i) {
                let cell_width = terminal_width(cell);
                total_width += cell_width;
                cell_count += 1;
                max_width = max_width.max(cell_width);
            }
        }

        if let Some(avg_width) = total_width.checked_div(cell_count) {
            // Use weighted average: blend of average and max
            // 70% average, 30% max — balances fairness with readability
            col_widths[i] = (avg_width * 7 + max_width * 3) / 10;
        }

        // Ensure column is at least as wide as header
        col_widths[i] = col_widths[i].max(terminal_width(&headers[i])).max(1);
    }

    col_widths
}

/// Render a complete table with headers, alignments, and rows
///
/// # Arguments
/// * `headers` - Column headers
/// * `alignments` - Column alignments
/// * `rows` - Data rows
/// * `theme` - Color theme
/// * `is_selected` - Whether the table element is selected
/// * `in_table_mode` - Whether we're in table cell navigation mode
/// * `selected_cell` - Currently selected cell (row, col) if in table mode
/// * `available_width` - Optional maximum width to constrain table to
#[allow(clippy::too_many_arguments)]
pub fn render_table(
    headers: &[String],
    alignments: &[Alignment],
    rows: &[Vec<String>],
    theme: &Theme,
    is_selected: bool,
    in_table_mode: bool,
    selected_cell: Option<(usize, usize)>,
    available_width: Option<u16>,
) -> Vec<Line<'static>> {
    let mut lines = Vec::new();

    if headers.is_empty() {
        return lines;
    }

    let col_count = headers.len();

    // Calculate column widths using content-weighted approach
    let mut col_widths = calculate_column_widths(headers, rows);

    // Start with normal padding (1 space each side = 2 total)
    let mut padding = 2usize;

    // Add initial padding
    for width in &mut col_widths {
        *width += padding;
    }

    // Smart table collapsing: shrink columns proportionally if table is too wide
    if let Some(max_width) = available_width {
        let max_width = max_width as usize;
        let prefix_width = if in_table_mode || is_selected { 2 } else { 0 };
        let border_width = col_count + 1; // │ between and around columns

        // Try shrinking with progressively less padding
        loop {
            let total_width: usize = col_widths.iter().sum::<usize>() + border_width + prefix_width;

            if total_width <= max_width || max_width <= border_width + prefix_width {
                break;
            }

            // Available space for column content
            let available_for_cols = max_width.saturating_sub(border_width + prefix_width);
            let current_col_total: usize = col_widths.iter().sum();

            if current_col_total == 0 {
                break;
            }

            // Check if we can fit by reducing padding first (before shrinking content)
            if padding > 0 {
                let potential_savings = col_count * padding;
                if total_width - potential_savings <= max_width {
                    // Reducing padding is enough - recalculate with less padding
                    let needed_reduction = total_width - max_width;
                    // Ensure at least 1 reduction per iteration to avoid infinite loop
                    let padding_reduction = (needed_reduction / col_count).max(1).min(padding);
                    for width in &mut col_widths {
                        *width = width.saturating_sub(padding_reduction);
                    }
                    padding = padding.saturating_sub(padding_reduction);
                    continue;
                }
                // Remove all padding and try again
                for width in &mut col_widths {
                    *width = width.saturating_sub(padding);
                }
                padding = 0;
                continue;
            }

            // Padding exhausted, now shrink columns proportionally
            let shrink_ratio = available_for_cols as f64 / current_col_total as f64;
            for width in &mut col_widths {
                let new_width = ((*width as f64) * shrink_ratio) as usize;
                *width = new_width.max(MIN_COL_WIDTH);
            }

            // Iterative trim: MIN_COL_WIDTH clamping can push total back over budget.
            // Repeatedly reduce the widest column by 1 until we fit.
            let mut total_after: usize = col_widths.iter().sum();
            while total_after > available_for_cols {
                if let Some(max_idx) = col_widths
                    .iter()
                    .enumerate()
                    .filter(|(_, w)| **w > MIN_COL_WIDTH)
                    .max_by_key(|(_, w)| **w)
                    .map(|(i, _)| i)
                {
                    col_widths[max_idx] -= 1;
                    total_after -= 1;
                } else {
                    break; // All columns at minimum, can't shrink further
                }
            }
            break;
        }
    }

    // Top border (add selection indicator or spacing)
    let mut top_border_spans = vec![];

    if in_table_mode {
        // In table mode, add spacing to align with row arrows
        top_border_spans.push(Span::raw("  "));
    } else if is_selected {
        // Not in table nav mode: show arrow if table is selected as element
        top_border_spans.push(Span::styled(
            "→ ",
            Style::default()
                .fg(theme.selection_indicator_fg)
                .bg(theme.selection_indicator_bg)
                .add_modifier(Modifier::BOLD),
        ));
    }

    let mut top_border = String::from("┌");
    for (i, &width) in col_widths.iter().enumerate() {
        top_border.push_str(&"─".repeat(width));
        if i < col_widths.len() - 1 {
            top_border.push('┬');
        }
    }
    top_border.push('┐');
    top_border_spans.push(Span::styled(
        top_border,
        Style::default().fg(theme.table_border),
    ));
    lines.push(Line::from(top_border_spans));

    // Header row (row 0)
    let header_lines = render_table_row(
        headers,
        &col_widths,
        alignments,
        &TableRenderContext {
            theme,
            row_num: 0,
            is_header: true,
            in_table_mode,
            is_table_selected: is_selected,
            selected_cell,
        },
    );
    lines.extend(header_lines);

    // Header separator
    let mut separator_spans = vec![];
    if in_table_mode || is_selected {
        separator_spans.push(Span::raw("  "));
    }
    let mut separator = String::from("├");
    for (i, &width) in col_widths.iter().enumerate() {
        separator.push_str(&"─".repeat(width));
        if i < col_widths.len() - 1 {
            separator.push('┼');
        }
    }
    separator.push('┤');
    separator_spans.push(Span::styled(
        separator,
        Style::default().fg(theme.table_border),
    ));
    lines.push(Line::from(separator_spans));

    // Data rows
    for (row_idx, row) in rows.iter().enumerate() {
        let data_row = row_idx + 1; // +1 because row 0 is header
        let row_lines = render_table_row(
            row,
            &col_widths,
            alignments,
            &TableRenderContext {
                theme,
                row_num: data_row,
                is_header: false,
                in_table_mode,
                is_table_selected: is_selected,
                selected_cell,
            },
        );
        lines.extend(row_lines);
    }

    // Bottom border
    let mut bottom_border_spans = vec![];
    if in_table_mode || is_selected {
        bottom_border_spans.push(Span::raw("  "));
    }
    let mut bottom_border = String::from("└");
    for (i, &width) in col_widths.iter().enumerate() {
        bottom_border.push_str(&"─".repeat(width));
        if i < col_widths.len() - 1 {
            bottom_border.push('┴');
        }
    }
    bottom_border.push('┘');
    bottom_border_spans.push(Span::styled(
        bottom_border,
        Style::default().fg(theme.table_border),
    ));
    lines.push(Line::from(bottom_border_spans));

    lines
}

/// Render a single table row with proper alignment and styling
/// Supports multi-line cells via wrapping.
///
/// # Arguments
/// * `cells` - Cell contents for this row
/// * `col_widths` - Pre-calculated column widths
/// * `alignments` - Column alignments
/// * `ctx` - Rendering context with theme and selection state
pub fn render_table_row(
    cells: &[String],
    col_widths: &[usize],
    alignments: &[Alignment],
    ctx: &TableRenderContext,
) -> Vec<Line<'static>> {
    // 1. Wrap each cell into multiple lines
    let mut wrapped_cells: Vec<Vec<String>> = Vec::new();
    let mut max_lines = 1;

    for (i, cell) in cells.iter().enumerate() {
        let width = col_widths.get(i).copied().unwrap_or(10);
        // Available width for content is width - 2 (for padding)
        let content_width = width.saturating_sub(2);
        let wrapped = if content_width > 0 {
            wrap_text(cell, content_width)
        } else {
            vec![String::new()]
        };
        max_lines = max_lines.max(wrapped.len());
        wrapped_cells.push(wrapped);
    }

    // 2. Render each line of the row
    let mut row_lines = Vec::new();

    for line_idx in 0..max_lines {
        let mut spans = Vec::new();

        // Add arrow or space to keep table aligned
        if ctx.in_table_mode {
            let is_selected_row = ctx.selected_cell.map(|(r, _)| r) == Some(ctx.row_num);
            if is_selected_row && line_idx == 0 {
                spans.push(Span::styled(
                    "→ ",
                    Style::default()
                        .fg(ctx.theme.selection_indicator_fg)
                        .add_modifier(Modifier::BOLD),
                ));
            } else {
                spans.push(Span::raw("  "));
            }
        } else if ctx.is_table_selected {
            spans.push(Span::raw("  "));
        }

        spans.push(Span::styled(
            "│",
            Style::default().fg(ctx.theme.table_border),
        ));

        for (i, wrapped_cell) in wrapped_cells.iter().enumerate() {
            let width = col_widths.get(i).copied().unwrap_or(10);
            let alignment = alignments.get(i).unwrap_or(&Alignment::Left);

            // Get the text for this specific line of the cell, or empty string
            let line_text = wrapped_cell.get(line_idx).cloned().unwrap_or_default();

            // Determine if this specific cell is selected
            let is_selected = ctx.selected_cell == Some((ctx.row_num, i));

            let style = if is_selected {
                Style::default()
                    .fg(ctx.theme.link_selected_fg)
                    .bg(ctx.theme.link_selected_bg)
                    .add_modifier(Modifier::BOLD)
            } else if ctx.is_header {
                Style::default()
                    .fg(ctx.theme.heading_color(3))
                    .add_modifier(Modifier::BOLD)
            } else {
                ctx.theme.text_style()
            };

            if !is_selected && line_text.contains('`') {
                // Render inline code spans with theme styling
                let formatted = format_inline_markdown(&line_text, ctx.theme);
                let rendered_width: usize =
                    formatted.iter().map(|s| terminal_width(&s.content)).sum();
                let padding_total = width.saturating_sub(rendered_width);
                let (lead, trail) = match alignment {
                    Alignment::Right => (padding_total, 0),
                    Alignment::Center => {
                        let half = padding_total / 2;
                        (half, padding_total - half)
                    }
                    _ => (0, padding_total),
                };
                if lead > 0 {
                    spans.push(Span::styled(" ".repeat(lead), style));
                }
                for span in formatted {
                    // Plain text spans: apply row/header style; styled spans (code etc.) keep their style
                    let effective_style =
                        if span.style == Style::default() || span.style == ctx.theme.text_style() {
                            style
                        } else {
                            span.style
                        };
                    spans.push(Span::styled(span.content.into_owned(), effective_style));
                }
                if trail > 0 {
                    spans.push(Span::styled(" ".repeat(trail), style));
                }
            } else {
                let cell_text = align_text(&line_text, width, alignment);
                spans.push(Span::styled(cell_text, style));
            }
            spans.push(Span::styled(
                "│",
                Style::default().fg(ctx.theme.table_border),
            ));
        }

        row_lines.push(Line::from(spans));
    }

    row_lines
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::theme::ThemeName;

    fn test_theme() -> Theme {
        Theme::from_name(ThemeName::OceanDark)
    }

    mod render_table_tests {
        use super::*;

        #[test]
        fn test_empty_headers_returns_empty() {
            let theme = test_theme();
            let lines = render_table(&[], &[], &[], &theme, false, false, None, None);
            assert!(lines.is_empty());
        }

        #[test]
        fn test_single_column_table() {
            let theme = test_theme();
            let headers = vec!["Name".to_string()];
            let alignments = vec![Alignment::Left];
            let rows = vec![vec!["Alice".to_string()], vec!["Bob".to_string()]];

            let lines = render_table(
                &headers,
                &alignments,
                &rows,
                &theme,
                false,
                false,
                None,
                None,
            );

            // Should have: top border, header, separator, 2 data rows, bottom border
            // Wrapping might add lines if text is tight, current implementation yields 7
            assert!(lines.len() >= 6);
        }

        #[test]
        fn test_multi_column_table() {
            let theme = test_theme();
            let headers = vec!["Name".to_string(), "Age".to_string(), "City".to_string()];
            let alignments = vec![Alignment::Left, Alignment::Right, Alignment::Center];
            let rows = vec![
                vec!["Alice".to_string(), "30".to_string(), "NYC".to_string()],
                vec!["Bob".to_string(), "25".to_string(), "LA".to_string()],
            ];

            let lines = render_table(
                &headers,
                &alignments,
                &rows,
                &theme,
                false,
                false,
                None,
                None,
            );

            // Should have at least 6 lines
            assert!(lines.len() >= 6);
        }

        #[test]
        fn test_selected_table_adds_arrow() {
            let theme = test_theme();
            let headers = vec!["Col".to_string()];
            let rows = vec![vec!["Data".to_string()]];

            let _lines_unselected =
                render_table(&headers, &[], &rows, &theme, false, false, None, None);
            let lines_selected =
                render_table(&headers, &[], &rows, &theme, true, false, None, None);

            // Selected table should have arrow prefix on first line
            let first_selected = &lines_selected[0];

            // Selected version should have "→ " at the start
            assert!(first_selected.spans.iter().any(|s| s.content.contains("→")));
        }

        #[test]
        fn test_table_mode_shows_row_arrow() {
            let theme = test_theme();
            let headers = vec!["Col".to_string()];
            let rows = vec![vec!["Row1".to_string()], vec!["Row2".to_string()]];

            // Select cell at row 1, col 0
            let lines = render_table(&headers, &[], &rows, &theme, true, true, Some((1, 0)), None);

            // Find the row with the arrow
            assert!(
                lines
                    .iter()
                    .any(|l| l.spans.iter().any(|s| s.content.contains("→")))
            );
        }

        #[test]
        fn test_header_only_table() {
            let theme = test_theme();
            let headers = vec!["Header1".to_string(), "Header2".to_string()];
            let alignments = vec![Alignment::Left, Alignment::Right];
            let rows: Vec<Vec<String>> = vec![];

            let lines = render_table(
                &headers,
                &alignments,
                &rows,
                &theme,
                false,
                false,
                None,
                None,
            );

            // Should have: top border, header, separator, bottom border = 4 lines
            assert_eq!(lines.len(), 4);
        }

        #[test]
        fn test_halfwidth_katakana_table_widths_match_terminal_cells() {
            let theme = test_theme();
            let headers = vec!["Kana".to_string()];
            let rows = vec![vec!["ｶﾞ".to_string()], vec!["ﾊﾟ".to_string()]];

            let lines = render_table(
                &headers,
                &[Alignment::Left],
                &rows,
                &theme,
                false,
                false,
                None,
                Some(8),
            );

            for line in lines {
                let width: usize = line
                    .spans
                    .iter()
                    .map(|span| terminal_width(&span.content))
                    .sum();
                assert!(width <= 8, "line is {width} cells wide: {line:?}");
            }
        }

        #[test]
        fn test_table_width_constraint() {
            let theme = test_theme();
            let headers = vec![
                "Very Long Header Name".to_string(),
                "Another Long Header".to_string(),
            ];
            let alignments = vec![Alignment::Left, Alignment::Left];
            let rows = vec![vec![
                "Some content here".to_string(),
                "More content".to_string(),
            ]];

            // Without width constraint
            let lines_unconstrained = render_table(
                &headers,
                &alignments,
                &rows,
                &theme,
                false,
                false,
                None,
                None,
            );

            // With width constraint - table will wrap
            let lines_constrained = render_table(
                &headers,
                &alignments,
                &rows,
                &theme,
                false,
                false,
                None,
                Some(40),
            );

            // Constrained version should have MORE lines due to wrapping
            assert!(lines_constrained.len() >= lines_unconstrained.len());
        }

        #[test]
        fn test_seven_column_table_at_width_146_no_panic() {
            // Regression test for crash when MIN_COL_WIDTH clamping pushes
            // total column width over budget after proportional shrink
            let theme = test_theme();
            let headers = vec![
                "Protocol".to_string(),
                "Port(s)".to_string(),
                "Transport".to_string(),
                "Purpose".to_string(),
                "Encryption".to_string(),
                "Key Feature".to_string(),
                "Common Usage".to_string(),
            ];
            let alignments = vec![Alignment::Left; 7];
            let rows = vec![
                vec![
                    "HTTP".to_string(),
                    "80".to_string(),
                    "TCP".to_string(),
                    "Web".to_string(),
                    "No".to_string(),
                    "Stateless".to_string(),
                    "Websites".to_string(),
                ],
                vec![
                    "HTTPS".to_string(),
                    "443".to_string(),
                    "TCP".to_string(),
                    "Secure Web".to_string(),
                    "TLS/SSL".to_string(),
                    "Encrypted HTTP".to_string(),
                    "Secure websites".to_string(),
                ],
                vec![
                    "FTP".to_string(),
                    "20/21".to_string(),
                    "TCP".to_string(),
                    "File Transfer".to_string(),
                    "Optional".to_string(),
                    "Active/Passive".to_string(),
                    "File sharing".to_string(),
                ],
            ];

            // Test specific widths around the previously crashing point
            for width in [30, 50, 80, 100, 130, 140, 145, 146, 147, 150, 160, 180, 200] {
                let lines = render_table(
                    &headers,
                    &alignments,
                    &rows,
                    &theme,
                    false,
                    false,
                    None,
                    Some(width),
                );
                assert!(!lines.is_empty(), "Table should render at width {}", width);
            }
        }
    }

    mod render_table_row_tests {
        use super::*;

        #[test]
        fn test_basic_row() {
            let theme = test_theme();
            let cells = vec!["A".to_string(), "B".to_string()];
            let col_widths = vec![5, 5];
            let alignments = vec![Alignment::Left, Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 0,
                is_header: false,
                in_table_mode: false,
                is_table_selected: false,
                selected_cell: None,
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);
            let line = &row_lines[0];

            // Should have spans for: │, cell1, │, cell2, │
            assert!(line.spans.len() >= 5);
        }

        #[test]
        fn test_header_row_styling() {
            let theme = test_theme();
            let cells = vec!["Header".to_string()];
            let col_widths = vec![10];
            let alignments = vec![Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 0,
                is_header: true,
                in_table_mode: false,
                is_table_selected: false,
                selected_cell: None,
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);
            let line = &row_lines[0];

            // Header should have bold modifier
            let cell_span = line.spans.iter().find(|s| s.content.contains("Header"));
            assert!(cell_span.is_some());
            assert!(
                cell_span
                    .unwrap()
                    .style
                    .add_modifier
                    .contains(Modifier::BOLD)
            );
        }

        #[test]
        fn test_selected_cell_highlighting() {
            let theme = test_theme();
            let cells = vec!["A".to_string(), "B".to_string()];
            let col_widths = vec![5, 5];
            let alignments = vec![Alignment::Left, Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 1,
                is_header: false,
                in_table_mode: true,
                is_table_selected: true,
                selected_cell: Some((1, 1)), // Select cell B
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);
            let line = &row_lines[0];

            // The selected cell should have a background color
            let cell_b_span = line.spans.iter().find(|s| s.content.contains("B"));
            assert!(cell_b_span.is_some());
            // Check it has the highlight background
            assert!(cell_b_span.unwrap().style.bg.is_some());
        }

        #[test]
        fn test_row_with_arrow_when_selected() {
            let theme = test_theme();
            let cells = vec!["Data".to_string()];
            let col_widths = vec![8];
            let alignments = vec![Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 1,
                is_header: false,
                in_table_mode: true,
                is_table_selected: true,
                selected_cell: Some((1, 0)),
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);
            let line = &row_lines[0];

            // Should have arrow at start when row is selected in table mode
            assert!(line.spans[0].content.contains("→"));
        }

        #[test]
        fn test_row_wrapping() {
            let theme = test_theme();
            let cells = vec!["Very long text that should wrap".to_string()];
            let col_widths = vec![10]; // Small width will force wrapping
            let alignments = vec![Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 1,
                is_header: false,
                in_table_mode: false,
                is_table_selected: false,
                selected_cell: None,
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);

            // Should have multiple lines due to wrapping
            assert!(row_lines.len() > 1);
        }

        #[test]
        fn test_row_without_arrow_when_not_selected() {
            let theme = test_theme();
            let cells = vec!["Data".to_string()];
            let col_widths = vec![8];
            let alignments = vec![Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 2,
                is_header: false,
                in_table_mode: true,
                is_table_selected: true,
                selected_cell: Some((1, 0)), // Different row selected
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);
            let line = &row_lines[0];

            // Should have spaces, not arrow
            assert_eq!(line.spans[0].content, "  ");
        }

        #[test]
        fn test_header_inline_code_keeps_surrounding_header_style() {
            let theme = test_theme();
            let cells = vec!["Use `foo` now".to_string()];
            let col_widths = vec![20];
            let alignments = vec![Alignment::Left];

            let ctx = TableRenderContext {
                theme: &theme,
                row_num: 0,
                is_header: true,
                in_table_mode: false,
                is_table_selected: false,
                selected_cell: None,
            };

            let row_lines = render_table_row(&cells, &col_widths, &alignments, &ctx);
            let line = &row_lines[0];

            let trailing_text = line
                .spans
                .iter()
                .find(|span| span.content.as_ref() == " now")
                .expect("expected plain trailing text after inline code");

            assert!(
                trailing_text.style.add_modifier.contains(Modifier::BOLD),
                "plain text after inline code in a header cell should keep header styling"
            );
        }
    }
}
