use ratatui::Frame;
use ratatui::buffer::Buffer;
use ratatui::layout::{Margin, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph, Widget};

use crate::argv_serializer::{RenderedCommand, RenderedShellToken, RenderedShellTokenKind};
use crate::config::TuiConfig;
use crate::input::{HoverTarget, UiState};

use super::screen::ScreenView;
use super::styles;

struct PreviewWidget<'a> {
    config: &'a TuiConfig,
    command: Option<&'a RenderedCommand>,
    hovered: bool,
}

impl Widget for PreviewWidget<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        if area.width == 0 || area.height == 0 {
            return;
        }

        let surface = if self.hovered {
            styles::surface(self.config, styles::Surface::ControlActive)
        } else {
            styles::surface(self.config, styles::Surface::Result)
        };
        Widget::render(Block::default().style(surface), area, buf);

        let inner = area.inner(Margin {
            horizontal: 1,
            vertical: 0,
        });
        if inner.width == 0 {
            return;
        }

        let header_area = Rect::new(inner.x, area.y, inner.width, 1);
        Widget::render(
            Paragraph::new(preview_header_line(self.config, self.hovered, inner.width))
                .style(surface),
            header_area,
            buf,
        );

        if area.height < 2 {
            return;
        }

        let command_y = if area.height >= 4 {
            area.y.saturating_add(2)
        } else {
            area.y.saturating_add(1)
        };
        let command_inset = preview_command_inset(inner.width);
        let command_area = Rect::new(
            inner.x.saturating_add(command_inset),
            command_y,
            inner.width.saturating_sub(command_inset),
            1,
        );
        Widget::render(
            Paragraph::new(command_preview_line(
                self.config,
                self.command,
                self.hovered,
            ))
            .style(surface),
            command_area,
            buf,
        );
    }
}

pub(crate) fn render_preview(
    frame: &mut Frame<'_>,
    ui: &UiState,
    config: &TuiConfig,
    area: Rect,
    vm: &ScreenView<'_>,
) {
    let hovered = ui.hover == Some(HoverTarget::Preview);
    frame.render_widget(
        PreviewWidget {
            config,
            command: vm.rendered_command.as_ref(),
            hovered,
        },
        area,
    );
}

fn preview_header_line(config: &TuiConfig, hovered: bool, width: u16) -> Line<'static> {
    let title = "Command Preview";
    let hint = if hovered {
        "Click or Ctrl+Y copies"
    } else {
        "Click/Ctrl+Y copy"
    };
    let width = usize::from(width);
    let title_width = title.chars().count();
    let hint_width = hint.chars().count();
    let left_rule_width = if width > title_width.saturating_add(hint_width).saturating_add(8) {
        2
    } else {
        0
    };
    let separators = usize::from(left_rule_width > 0)
        .saturating_add(1)
        .saturating_add(1)
        .saturating_add(1);
    let middle_rule_width = width
        .saturating_sub(left_rule_width)
        .saturating_sub(title_width)
        .saturating_sub(hint_width)
        .saturating_sub(separators)
        .max(1);

    let mut spans = Vec::new();
    if left_rule_width > 0 {
        spans.push(Span::styled(
            "─".repeat(left_rule_width),
            preview_rule_style(config, hovered),
        ));
        spans.push(Span::raw(" "));
    }
    spans.push(Span::styled(title, preview_title_style(config, hovered)));
    spans.push(Span::raw(" "));
    spans.push(Span::styled(
        "─".repeat(middle_rule_width),
        preview_rule_style(config, hovered),
    ));
    spans.push(Span::raw(" "));
    spans.push(Span::styled(hint, preview_hint_style(config, hovered)));
    Line::from(spans)
}

fn preview_title_style(config: &TuiConfig, hovered: bool) -> Style {
    let style = Style::default().fg(config.theme.metadata);
    if hovered {
        style.fg(config.theme.metadata)
    } else {
        style
    }
}

fn preview_hint_style(config: &TuiConfig, hovered: bool) -> Style {
    if hovered {
        Style::default().fg(config.theme.metadata)
    } else {
        Style::default().fg(config.theme.dim)
    }
}

fn preview_rule_style(config: &TuiConfig, hovered: bool) -> Style {
    if hovered {
        Style::default().fg(config.theme.border)
    } else {
        Style::default().fg(config.theme.divider)
    }
}

fn preview_command_inset(width: u16) -> u16 {
    if width < 28 { 1 } else { 2 }
}

fn command_preview_line(
    config: &TuiConfig,
    command: Option<&RenderedCommand>,
    hovered: bool,
) -> Line<'static> {
    let prompt_style = if hovered {
        Style::default()
            .fg(config.theme.text)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default()
            .fg(config.theme.result_accent)
            .add_modifier(Modifier::BOLD)
    };
    let mut spans = vec![Span::styled("$ ", prompt_style)];
    let Some(command) = command else {
        spans.push(Span::styled(
            "serialization blocked",
            Style::default()
                .fg(config.theme.error)
                .add_modifier(Modifier::BOLD),
        ));
        return Line::from(spans);
    };

    spans.extend(rendered_token_spans(config, &command.tokens, hovered));
    Line::from(spans)
}

fn rendered_token_spans(
    config: &TuiConfig,
    tokens: &[RenderedShellToken],
    hovered: bool,
) -> Vec<Span<'static>> {
    let mut spans = Vec::with_capacity(tokens.len().saturating_mul(2));
    for (index, token) in tokens.iter().enumerate() {
        if index > 0 {
            spans.push(Span::raw(" "));
        }
        spans.push(Span::styled(
            token.text.clone(),
            preview_token_style(config, token.kind, hovered),
        ));
    }
    spans
}

fn preview_token_style(config: &TuiConfig, kind: RenderedShellTokenKind, hovered: bool) -> Style {
    let base_text = if hovered {
        config.theme.text
    } else {
        config.theme.focus
    };
    match kind {
        RenderedShellTokenKind::EntryPoint => Style::default()
            .fg(config.theme.result_accent)
            .add_modifier(Modifier::BOLD),
        RenderedShellTokenKind::SubcommandName
        | RenderedShellTokenKind::Value
        | RenderedShellTokenKind::DelimiterJoinedValue => {
            Style::default().fg(base_text).add_modifier(Modifier::BOLD)
        }
        RenderedShellTokenKind::OptionSpelling => Style::default()
            .fg(config.theme.accent)
            .add_modifier(Modifier::BOLD),
        RenderedShellTokenKind::RawBoundary => Style::default()
            .fg(config.theme.info)
            .add_modifier(Modifier::BOLD),
        RenderedShellTokenKind::Terminator => Style::default()
            .fg(config.theme.warning)
            .add_modifier(Modifier::BOLD),
        RenderedShellTokenKind::PreservedExternalToken => {
            Style::default().fg(config.theme.metadata)
        }
    }
}

#[cfg(test)]
mod tests {
    use ratatui::buffer::Buffer;
    use ratatui::layout::Rect;
    use ratatui::style::Modifier;

    use super::{PreviewWidget, command_preview_line, preview_command_inset, preview_header_line};
    use crate::argv_serializer::{RenderedCommand, RenderedShellToken, RenderedShellTokenKind};
    use crate::config::TuiConfig;
    use ratatui::widgets::Widget;

    fn rendered_command(tokens: &[(&str, RenderedShellTokenKind)]) -> RenderedCommand {
        let tokens = tokens
            .iter()
            .map(|(text, kind)| RenderedShellToken {
                text: (*text).to_string(),
                kind: *kind,
            })
            .collect::<Vec<_>>();
        let text = tokens
            .iter()
            .map(|token| token.text.as_str())
            .collect::<Vec<_>>()
            .join(" ");
        RenderedCommand { text, tokens }
    }

    fn row_text(buf: &Buffer, area: Rect, row: u16) -> String {
        (area.x..area.x + area.width)
            .map(|x| buf[(x, area.y + row)].symbol())
            .collect::<String>()
    }

    #[test]
    fn preview_highlights_binary_name_and_flags() {
        let config = TuiConfig::default();
        let command = rendered_command(&[
            ("clap-features", RenderedShellTokenKind::EntryPoint),
            ("serve", RenderedShellTokenKind::SubcommandName),
            ("--feature=gzip", RenderedShellTokenKind::OptionSpelling),
            ("-literal", RenderedShellTokenKind::Value),
        ]);
        let line = command_preview_line(&config, Some(&command), false);

        assert_eq!(line.spans[1].content.as_ref(), "clap-features");
        assert_eq!(line.spans[1].style.fg, Some(config.theme.result_accent));
        assert!(line.spans[1].style.add_modifier.contains(Modifier::BOLD));
        assert_eq!(line.spans[3].content.as_ref(), "serve");
        assert_eq!(line.spans[3].style.fg, Some(config.theme.focus));
        assert!(line.spans[3].style.add_modifier.contains(Modifier::BOLD));
        assert_eq!(line.spans[5].content.as_ref(), "--feature=gzip");
        assert_eq!(line.spans[5].style.fg, Some(config.theme.accent));
        assert!(line.spans[5].style.add_modifier.contains(Modifier::BOLD));
        assert_eq!(line.spans[7].content.as_ref(), "-literal");
        assert_eq!(line.spans[7].style.fg, Some(config.theme.focus));
        assert!(line.spans[7].style.add_modifier.contains(Modifier::BOLD));
    }

    #[test]
    fn preview_hover_uses_contrasting_text_on_command_prompt() {
        let config = TuiConfig::default();
        let command = rendered_command(&[
            ("tool", RenderedShellTokenKind::EntryPoint),
            ("--verbose", RenderedShellTokenKind::OptionSpelling),
        ]);
        let line = command_preview_line(&config, Some(&command), true);

        assert_eq!(line.spans[0].style.fg, Some(config.theme.text));
        assert_eq!(line.spans[1].style.bg, None);
    }

    #[test]
    fn preview_header_uses_single_label_and_secondary_copy_hint() {
        let config = TuiConfig::default();

        let line = preview_header_line(&config, false, 48);

        assert_eq!(line.spans[0].content.as_ref(), "──");
        assert_eq!(line.spans[0].style.fg, Some(config.theme.divider));
        assert_eq!(line.spans[2].content.as_ref(), "Command Preview");
        assert_eq!(line.spans[2].style.fg, Some(config.theme.metadata));
        assert!(!line.spans[2].style.add_modifier.contains(Modifier::BOLD));
        assert_eq!(line.spans[6].style.fg, Some(config.theme.dim));
        assert!(line.spans[6].content.as_ref().contains("copy"));
    }

    #[test]
    fn preview_command_prompt_and_content_are_emphasized_over_header_hint() {
        let config = TuiConfig::default();
        let command = rendered_command(&[
            ("tool", RenderedShellTokenKind::EntryPoint),
            ("value", RenderedShellTokenKind::Value),
        ]);

        let title = preview_header_line(&config, false, 48);
        let line = command_preview_line(&config, Some(&command), false);

        assert_eq!(line.spans[0].style.fg, Some(config.theme.result_accent));
        assert!(line.spans[0].style.add_modifier.contains(Modifier::BOLD));
        assert_eq!(line.spans[1].style.fg, Some(config.theme.result_accent));
        assert_eq!(line.spans[3].style.fg, Some(config.theme.focus));
        assert!(line.spans[3].style.add_modifier.contains(Modifier::BOLD));
        assert_eq!(title.spans[2].style.fg, Some(config.theme.metadata));
    }

    #[test]
    fn preview_hover_keeps_copy_hint_secondary() {
        let config = TuiConfig::default();

        let line = preview_header_line(&config, true, 48);

        assert_eq!(line.spans[2].style.fg, Some(config.theme.metadata));
        assert_eq!(line.spans[6].style.fg, Some(config.theme.metadata));
    }

    #[test]
    fn preview_styles_boundaries_and_preserved_external_tokens() {
        let config = TuiConfig::default();
        let command = rendered_command(&[
            ("tool", RenderedShellTokenKind::EntryPoint),
            ("--", RenderedShellTokenKind::RawBoundary),
            (";", RenderedShellTokenKind::Terminator),
            (
                "'external arg'",
                RenderedShellTokenKind::PreservedExternalToken,
            ),
        ]);
        let line = command_preview_line(&config, Some(&command), false);

        assert_eq!(line.spans[3].content.as_ref(), "--");
        assert_eq!(line.spans[3].style.fg, Some(config.theme.info));
        assert_eq!(line.spans[5].content.as_ref(), ";");
        assert_eq!(line.spans[5].style.fg, Some(config.theme.warning));
        assert_eq!(line.spans[7].content.as_ref(), "'external arg'");
        assert_eq!(line.spans[7].style.fg, Some(config.theme.metadata));
    }

    #[test]
    fn roomy_dock_renders_header_padding_command_and_bottom_padding_rows() {
        let config = TuiConfig::default();
        let command = rendered_command(&[
            ("tool", RenderedShellTokenKind::EntryPoint),
            ("serve", RenderedShellTokenKind::SubcommandName),
        ]);
        let area = Rect::new(0, 0, 48, 4);
        let mut buf = Buffer::empty(area);

        Widget::render(
            PreviewWidget {
                config: &config,
                command: Some(&command),
                hovered: false,
            },
            area,
            &mut buf,
        );

        let header = row_text(&buf, area, 0);
        let spacer = row_text(&buf, area, 1);
        let command_row = row_text(&buf, area, 2);
        let bottom = row_text(&buf, area, 3);

        assert!(header.contains("Command Preview"));
        assert!(header.contains("Click/Ctrl+Y copy"));
        assert!(header.find("Click/Ctrl+Y copy") > header.find("Command Preview"));
        assert!(spacer.trim().is_empty());
        assert_eq!(preview_command_inset(area.width.saturating_sub(2)), 2);
        assert!(command_row.starts_with("   $ tool serve"));
        assert!(bottom.trim().is_empty());
    }

    #[test]
    fn compact_dock_renders_header_and_command_rows() {
        let config = TuiConfig::default();
        let command = rendered_command(&[
            ("tool", RenderedShellTokenKind::EntryPoint),
            ("serve", RenderedShellTokenKind::SubcommandName),
        ]);
        let area = Rect::new(0, 0, 40, 2);
        let mut buf = Buffer::empty(area);

        Widget::render(
            PreviewWidget {
                config: &config,
                command: Some(&command),
                hovered: false,
            },
            area,
            &mut buf,
        );

        let header = row_text(&buf, area, 0);
        let command_row = row_text(&buf, area, 1);

        assert!(header.contains("Command Preview"));
        assert!(header.contains("─"));
        assert!(header.contains("Click/Ctrl+Y copy"));
        assert_eq!(preview_command_inset(area.width.saturating_sub(2)), 2);
        assert!(command_row.starts_with("   $ tool serve"));
    }
}
