use std::{cell::RefCell, sync::LazyLock};

use ratatui::{
    Frame,
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span, Text},
    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs, Wrap},
};
use syntect::{
    easy::HighlightLines,
    highlighting::{FontStyle, ThemeSet},
    parsing::SyntaxSet,
    util::LinesWithEndings,
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

use crate::{
    app::{ActivityKind, App, ItemDecoration, ScreenKind, Tab, WorkItemState},
    editor::Editor,
};

const ACCENT: Color = Color::Cyan;
static SYNTAXES: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEMES: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
thread_local! {
    static HIGHLIGHT_CACHE: RefCell<Option<(String, String, Text<'static>)>> = const { RefCell::new(None) };
}

pub fn draw(frame: &mut Frame<'_>, app: &mut App) {
    let area = frame.area();
    let rows = Layout::vertical([
        Constraint::Length(3),
        Constraint::Min(4),
        Constraint::Length(1),
    ])
    .split(area);

    draw_tabs(frame, app, rows[0]);
    draw_content(frame, app, rows[1]);
    draw_footer(frame, app, rows[2]);

    if let Some(editor) = &app.editor {
        draw_editor(frame, editor);
    } else if let Some((message, _)) = &app.confirm {
        draw_confirm(frame, message);
    }
}

fn draw_tabs(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
    let titles = Tab::ALL.map(|tab| Line::from(tab.title()));
    let selected = Tab::ALL.iter().position(|tab| *tab == app.tab).unwrap_or(0);
    let tabs = Tabs::new(titles)
        .select(selected)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title(format!(" Gotcha · {} ", app.server_name)),
        )
        .highlight_style(Style::default().fg(ACCENT).add_modifier(Modifier::BOLD))
        .divider("│");
    frame.render_widget(tabs, area);

    let inner = area.inner(ratatui::layout::Margin {
        horizontal: 1,
        vertical: 1,
    });
    app.tab_areas = tab_areas(inner);
}

fn draw_content(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
    let direction = content_direction(area.width);
    let panes = Layout::default()
        .direction(direction)
        .constraints(if direction == Direction::Horizontal {
            [Constraint::Percentage(42), Constraint::Percentage(58)]
        } else {
            [Constraint::Percentage(55), Constraint::Percentage(45)]
        })
        .split(area);
    app.list_area = panes[0];
    app.detail_area = panes[1];
    draw_list(frame, app, panes[0]);
    draw_detail(frame, app, panes[1]);
}

fn tab_areas(area: Rect) -> Vec<Rect> {
    let mut x = area.x;
    Tab::ALL
        .iter()
        .filter_map(|tab| {
            let remaining = area.right().saturating_sub(x);
            if remaining == 0 {
                return None;
            }
            let width = (tab.title().len() as u16 + 2).min(remaining);
            let tab_area = Rect::new(x, area.y, width, area.height.min(1));
            x = x.saturating_add(width).saturating_add(1);
            Some(tab_area)
        })
        .collect()
}

fn content_direction(width: u16) -> Direction {
    if width >= 90 {
        Direction::Horizontal
    } else {
        Direction::Vertical
    }
}

fn draw_list(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
    let items = if app.screen.items.is_empty() {
        vec![ListItem::new(Line::styled(
            "No items",
            Style::default().fg(Color::DarkGray),
        ))]
    } else {
        app.screen
            .items
            .iter()
            .map(|item| {
                ListItem::new(vec![
                    item_title(
                        &item.title,
                        item.graph_lane,
                        item.decoration,
                        area.width.saturating_sub(4) as usize,
                    ),
                    item_meta(
                        &item.meta,
                        item.decoration,
                        area.width.saturating_sub(4) as usize,
                    ),
                ])
            })
            .collect()
    };
    let list = List::new(items)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title(format!(" {} ", app.screen.title)),
        )
        .highlight_symbol(Line::from(Span::styled(
            "▸ ",
            Style::default().fg(ACCENT).add_modifier(Modifier::BOLD),
        )))
        .highlight_style(Style::default().add_modifier(Modifier::BOLD));
    let mut state = ListState::default()
        .with_selected((!app.screen.items.is_empty()).then_some(app.screen.selected));
    frame.render_stateful_widget(list, area, &mut state);
    app.list_offset = state.offset();
}

fn item_title(
    title: &str,
    graph_lane: Option<usize>,
    decoration: ItemDecoration,
    max_width: usize,
) -> Line<'static> {
    let mut spans = graph_lane.map_or_else(Vec::new, |node_lane| {
        let mut spans = (0..node_lane)
            .map(|lane| Span::styled("│ ", Style::default().fg(lane_color(lane))))
            .collect::<Vec<_>>();
        spans.push(Span::styled(
            "● ",
            Style::default().fg(lane_color(node_lane)),
        ));
        spans
    });
    let icon = match decoration {
        ItemDecoration::Activity(kind) => Some(activity_icon(kind)),
        ItemDecoration::State(state) | ItemDecoration::Milestone { state, .. } => {
            Some(state_icon(state))
        }
        ItemDecoration::None => None,
    };
    if let Some((icon, color)) = icon {
        spans.push(Span::styled(icon, Style::default().fg(color)));
    }
    let prefix_width = spans.iter().map(Span::width).sum::<usize>();
    spans.push(Span::styled(
        truncate(title, max_width.saturating_sub(prefix_width)),
        Style::default().add_modifier(Modifier::BOLD),
    ));
    Line::from(spans)
}

fn item_meta(meta: &str, decoration: ItemDecoration, max_width: usize) -> Line<'static> {
    let ItemDecoration::Milestone { closed, total, .. } = decoration else {
        return Line::from(Span::styled(
            truncate(meta, max_width),
            Style::default().fg(Color::DarkGray),
        ));
    };
    let bar_width = 10.min(max_width.saturating_sub(1));
    let closed = closed.max(0) as usize;
    let total = total.max(0) as usize;
    let filled = closed
        .saturating_mul(bar_width)
        .saturating_add(total / 2)
        .checked_div(total)
        .unwrap_or_default()
        .min(bar_width);
    let mut spans = vec![
        Span::styled("█".repeat(filled), Style::default().fg(Color::LightGreen)),
        Span::styled(
            "░".repeat(bar_width - filled),
            Style::default().fg(if total == 0 {
                Color::DarkGray
            } else {
                Color::LightYellow
            }),
        ),
    ];
    if bar_width < max_width {
        spans.push(Span::raw(" "));
        spans.push(Span::styled(
            truncate(meta, max_width - bar_width - 1),
            Style::default().fg(Color::DarkGray),
        ));
    }
    Line::from(spans)
}

fn truncate(value: &str, width: usize) -> String {
    if UnicodeWidthStr::width(value) <= width {
        return value.to_string();
    }
    if width == 0 {
        return String::new();
    }
    let mut result = String::new();
    let mut used = 0;
    for character in value.chars() {
        let character_width = UnicodeWidthChar::width(character).unwrap_or_default();
        if used + character_width >= width {
            break;
        }
        result.push(character);
        used += character_width;
    }
    result.push('…');
    result
}

fn activity_icon(kind: ActivityKind) -> (&'static str, Color) {
    match kind {
        ActivityKind::Repository => ("▣ ", Color::Yellow),
        ActivityKind::Issue => ("◉ ", Color::LightGreen),
        ActivityKind::PullRequest => ("⇄ ", Color::LightMagenta),
        ActivityKind::Branch => ("⑂ ", Color::LightCyan),
        ActivityKind::Tag => ("◆ ", Color::LightYellow),
        ActivityKind::Push => ("↑ ", Color::LightBlue),
        ActivityKind::Release => ("★ ", Color::LightRed),
    }
}

fn state_icon(state: WorkItemState) -> (&'static str, Color) {
    match state {
        WorkItemState::Open => ("● ", Color::LightGreen),
        WorkItemState::Closed => ("✓ ", Color::LightMagenta),
        WorkItemState::Unknown => ("? ", Color::DarkGray),
    }
}

fn lane_color(lane: usize) -> Color {
    const COLORS: [Color; 6] = [
        Color::LightRed,
        Color::LightGreen,
        Color::LightBlue,
        Color::LightMagenta,
        Color::LightCyan,
        Color::Yellow,
    ];
    COLORS[lane % COLORS.len()]
}

fn draw_detail(frame: &mut Frame<'_>, app: &App, area: Rect) {
    let mut detail = render_screen_detail(&app.screen.kind, &app.screen.detail);
    if let Some(item) = app.screen.selected_item()
        && !item.detail.is_empty()
    {
        if !detail.lines.is_empty() {
            detail.lines.extend([
                Line::default(),
                Line::styled("────", Style::default().fg(Color::DarkGray)),
                Line::default(),
            ]);
        }
        detail
            .lines
            .extend(render_item_detail(&app.screen.kind, &item.detail).lines);
    }
    if detail.lines.is_empty() {
        detail = Text::from("Select an item to see details.");
    }
    let paragraph = Paragraph::new(detail)
        .block(Block::default().borders(Borders::ALL).title(" Preview "))
        .wrap(Wrap { trim: false })
        .scroll((app.screen.detail_scroll, 0));
    frame.render_widget(paragraph, area);
}

fn render_screen_detail<'a>(kind: &ScreenKind, detail: &'a str) -> Text<'a> {
    match kind {
        ScreenKind::File(_, path) => highlight(detail, path),
        ScreenKind::Text(_, _) if detail.starts_with("diff --git ") => {
            highlight(detail, "change.diff")
        }
        ScreenKind::Text(title, _) => highlight(detail, title),
        ScreenKind::Issue(_, _) | ScreenKind::Pull(_, _) | ScreenKind::Milestone(_, _) => {
            render_markdown(detail)
        }
        _ => Text::from(detail),
    }
}

fn render_item_detail<'a>(kind: &ScreenKind, detail: &'a str) -> Text<'a> {
    if detail.starts_with("diff --git ") {
        highlight(detail, "change.diff")
    } else if matches!(
        kind,
        ScreenKind::Home
            | ScreenKind::Issue(_, _)
            | ScreenKind::Pull(_, _)
            | ScreenKind::Milestone(_, _)
    ) {
        render_markdown(detail)
    } else {
        Text::from(detail)
    }
}

fn render_markdown(markdown: &str) -> Text<'_> {
    tui_markdown::from_str(markdown)
}

fn highlight(source: &str, path: &str) -> Text<'static> {
    HIGHLIGHT_CACHE.with_borrow_mut(|cache| {
        if let Some((cached_path, cached_source, text)) = cache.as_ref()
            && cached_path == path
            && cached_source == source
        {
            return text.clone();
        }
        let text = highlight_uncached(source, path);
        *cache = Some((path.to_owned(), source.to_owned(), text.clone()));
        text
    })
}

fn highlight_uncached(source: &str, path: &str) -> Text<'static> {
    let syntax = SYNTAXES
        .find_syntax_for_file(path)
        .ok()
        .flatten()
        .or_else(|| SYNTAXES.find_syntax_by_first_line(source))
        .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
    let theme = &THEMES.themes["base16-ocean.dark"];
    let mut highlighter = HighlightLines::new(syntax, theme);
    let lines = LinesWithEndings::from(source)
        .map(|line| match highlighter.highlight_line(line, &SYNTAXES) {
            Ok(ranges) => {
                let last = ranges.len().saturating_sub(1);
                Line::from(
                    ranges
                        .into_iter()
                        .enumerate()
                        .map(|(index, (style, text))| {
                            let text = if index == last {
                                text.trim_end_matches(['\r', '\n'])
                            } else {
                                text
                            };
                            let mut terminal_style = Style::default().fg(Color::Rgb(
                                style.foreground.r,
                                style.foreground.g,
                                style.foreground.b,
                            ));
                            if style.font_style.contains(FontStyle::BOLD) {
                                terminal_style = terminal_style.add_modifier(Modifier::BOLD);
                            }
                            if style.font_style.contains(FontStyle::ITALIC) {
                                terminal_style = terminal_style.add_modifier(Modifier::ITALIC);
                            }
                            if style.font_style.contains(FontStyle::UNDERLINE) {
                                terminal_style = terminal_style.add_modifier(Modifier::UNDERLINED);
                            }
                            Span::styled(text.to_owned(), terminal_style)
                        })
                        .collect::<Vec<_>>(),
                )
            }
            Err(_) => Line::from(line.trim_end_matches(['\r', '\n']).to_owned()),
        })
        .collect::<Vec<_>>();
    Text::from(lines)
}

fn draw_footer(frame: &mut Frame<'_>, app: &App, area: Rect) {
    let status = if let Some(status) = app.visible_status() {
        format!(
            "{} · Page {} · j/k move · Enter open · Backspace back · n/p pages · q quit",
            status, app.screen.page
        )
    } else {
        format!(
            "Page {} · j/k move · Enter open · Backspace back · n/p pages · a/e/c/x/d actions · / filters · * favorite · r reload · q quit",
            app.screen.page
        )
    };
    frame.render_widget(
        Paragraph::new(status)
            .style(Style::default().fg(Color::DarkGray))
            .alignment(Alignment::Center),
        area,
    );
}

fn draw_editor(frame: &mut Frame<'_>, editor: &Editor) {
    let area = centered(frame.area(), 86, 86);
    frame.render_widget(Clear, area);
    frame.render_widget(
        Block::default()
            .borders(Borders::ALL)
            .title(format!(
                " {} · Tab fields · Ctrl-S save · Esc cancel ",
                editor.title
            ))
            .border_style(Style::default().fg(ACCENT)),
        area,
    );
    let inner = area.inner(ratatui::layout::Margin {
        horizontal: 2,
        vertical: 1,
    });
    let heights = editor
        .fields
        .iter()
        .map(|field| Constraint::Length(if field.multiline { 5 } else { 3 }));
    let fields = Layout::vertical(heights).split(inner);
    for (index, (field, field_area)) in editor.fields.iter().zip(fields.iter()).enumerate() {
        let value = if field.secret {
            "•".repeat(field.value.chars().count())
        } else {
            field.value.clone()
        };
        let block = Block::default()
            .borders(Borders::ALL)
            .title(format!(" {} ", field.label))
            .border_style(if index == editor.focus {
                Style::default().fg(ACCENT)
            } else {
                Style::default()
            });
        frame.render_widget(
            Paragraph::new(value)
                .block(block)
                .wrap(Wrap { trim: false }),
            *field_area,
        );
    }
    if let Some(field_area) = fields.get(editor.focus) {
        let prefix: String = editor.fields[editor.focus]
            .value
            .chars()
            .take(editor.cursor)
            .collect();
        let lines: Vec<_> = prefix.split('\n').collect();
        let x = field_area.x
            + 1
            + lines
                .last()
                .map_or(0, |line| line.chars().count())
                .min(field_area.width.saturating_sub(3) as usize) as u16;
        let y = field_area.y
            + 1
            + (lines.len().saturating_sub(1)).min(field_area.height.saturating_sub(3) as usize)
                as u16;
        frame.set_cursor_position((x, y));
    }
}

fn draw_confirm(frame: &mut Frame<'_>, message: &str) {
    let area = centered(frame.area(), 64, 24);
    frame.render_widget(Clear, area);
    frame.render_widget(
        Paragraph::new(format!(
            "{message}\n\nPress y or Enter to confirm; n or Esc to cancel."
        ))
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title(" Confirm destructive action ")
                .border_style(Style::default().fg(Color::Red)),
        )
        .alignment(Alignment::Center)
        .wrap(Wrap { trim: true }),
        area,
    );
}

fn centered(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
    let vertical = Layout::vertical([
        Constraint::Percentage((100 - height_percent) / 2),
        Constraint::Percentage(height_percent),
        Constraint::Percentage((100 - height_percent) / 2),
    ])
    .split(area);
    Layout::horizontal([
        Constraint::Percentage((100 - width_percent) / 2),
        Constraint::Percentage(width_percent),
        Constraint::Percentage((100 - width_percent) / 2),
    ])
    .split(vertical[1])[1]
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::{Terminal, backend::TestBackend, widgets::ListItem};

    #[test]
    fn responsive_popup_stays_inside_small_terminals() {
        let area = centered(Rect::new(0, 0, 40, 20), 86, 86);
        assert!(area.width <= 40);
        assert!(area.height <= 20);
        assert!(area.width > 0);
        assert!(area.height > 0);
        assert_eq!(content_direction(80), Direction::Vertical);
        assert_eq!(content_direction(120), Direction::Horizontal);
    }

    #[test]
    fn tab_hit_areas_follow_the_rendered_titles() {
        let areas = tab_areas(Rect::new(1, 1, 100, 1));
        assert_eq!(areas[0], Rect::new(1, 1, 8, 1));
        assert_eq!(areas[1], Rect::new(10, 1, 10, 1));
        assert_eq!(areas[4], Rect::new(39, 1, 14, 1));
        assert!(!areas[0].contains((9, 1).into()));
        assert!(!areas[4].contains((80, 1).into()));
    }

    #[test]
    fn commit_graph_lanes_use_stable_distinct_colors() {
        let title = item_title("Merge feature", Some(2), ItemDecoration::None, 30);
        assert_eq!(title.to_string(), "│ │ ● Merge feature");
        assert_eq!(title.spans[0].style.fg, Some(lane_color(0)));
        assert_eq!(title.spans[1].style.fg, Some(lane_color(1)));
        assert_eq!(title.spans[2].style.fg, Some(lane_color(2)));
        assert_ne!(lane_color(0), lane_color(1));
        assert_eq!(lane_color(6), lane_color(0));

        let backend = TestBackend::new(30, 1);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal
            .draw(|frame| {
                let list = List::new([ListItem::new(title)])
                    .highlight_symbol(Line::from(Span::styled("▸ ", Style::default().fg(ACCENT))))
                    .highlight_style(Style::default().add_modifier(Modifier::BOLD));
                let mut state = ListState::default().with_selected(Some(0));
                frame.render_stateful_widget(list, frame.area(), &mut state);
            })
            .unwrap();
        let buffer = terminal.backend().buffer();
        assert_eq!(buffer[(0, 0)].fg, ACCENT);
        assert_eq!(buffer[(2, 0)].fg, lane_color(0));
        assert_eq!(buffer[(4, 0)].fg, lane_color(1));
        assert_eq!(buffer[(6, 0)].fg, lane_color(2));
    }

    #[test]
    fn home_activity_icons_are_colored_and_titles_fit_the_list() {
        let issue = item_title(
            "Notification settings opening issue · hugo/Gotcha",
            None,
            ItemDecoration::Activity(ActivityKind::Issue),
            24,
        );
        assert!(issue.to_string().ends_with('…'));
        assert!(issue.width() <= 24);
        assert_eq!(issue.spans[0].content, "◉ ");
        assert_eq!(issue.spans[0].style.fg, Some(Color::LightGreen));

        let kinds = [
            ActivityKind::Repository,
            ActivityKind::Issue,
            ActivityKind::PullRequest,
            ActivityKind::Branch,
            ActivityKind::Tag,
            ActivityKind::Push,
            ActivityKind::Release,
        ];
        let colors = kinds.map(activity_icon).map(|(_, color)| color);
        for (index, color) in colors.iter().enumerate() {
            assert!(!colors[..index].contains(color));
        }
    }

    #[test]
    fn work_item_states_and_milestone_progress_have_native_terminal_treatments() {
        let open = item_title(
            "Open issue",
            None,
            ItemDecoration::State(WorkItemState::Open),
            20,
        );
        let closed = item_title(
            "Closed issue",
            None,
            ItemDecoration::State(WorkItemState::Closed),
            20,
        );
        assert_eq!(open.spans[0].content, "● ");
        assert_eq!(open.spans[0].style.fg, Some(Color::LightGreen));
        assert_eq!(closed.spans[0].content, "✓ ");
        assert_eq!(closed.spans[0].style.fg, Some(Color::LightMagenta));

        let progress = item_meta(
            "open · 2 of 5 closed · no due date",
            ItemDecoration::Milestone {
                state: WorkItemState::Open,
                closed: 2,
                total: 5,
            },
            40,
        );
        assert_eq!(progress.spans[0].content, "████");
        assert_eq!(progress.spans[1].content, "░░░░░░");
        assert_eq!(progress.spans[0].style.fg, Some(Color::LightGreen));
        assert_eq!(progress.spans[1].style.fg, Some(Color::LightYellow));
        assert!(progress.width() <= 40);
    }

    #[test]
    fn previews_render_markdown_source_and_diffs_with_styles() {
        let repository = gotcha_gitea::RepositoryId {
            owner: "owner".into(),
            repository: "project".into(),
        };
        let markdown = render_item_detail(
            &ScreenKind::Home,
            "# Title\n\nSome *emphasis* and **strong text**.",
        );
        assert!(
            markdown.lines[0]
                .style
                .add_modifier
                .contains(Modifier::BOLD)
        );
        assert!(
            markdown
                .lines
                .iter()
                .flat_map(|line| &line.spans)
                .any(|span| { span.style.add_modifier.contains(Modifier::ITALIC) })
        );

        let source = render_screen_detail(
            &ScreenKind::File(repository, "main.rs".into()),
            "fn main() { println!(\"hello\"); }\n",
        );
        let source_colors = source
            .lines
            .iter()
            .flat_map(|line| &line.spans)
            .filter_map(|span| span.style.fg)
            .collect::<Vec<_>>();
        assert!(
            source_colors
                .iter()
                .any(|color| Some(color) != source_colors.first())
        );

        let diff_source = "diff --git a/main.rs b/main.rs\n@@ -1 +1 @@\n-old\n+new\n";
        let diff = render_screen_detail(
            &ScreenKind::Text("main.rs".into(), diff_source.into()),
            diff_source,
        );
        assert_ne!(
            diff.lines[2].spans[0].style.fg,
            diff.lines[3].spans[0].style.fg
        );
    }
}
