use std::collections::{HashMap, HashSet};

use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::{Color, Modifier, Style},
    widgets::Widget,
};

use unicode_width::UnicodeWidthStr;

use super::theme::Theme;
use crate::graph::{GraphLayout, StageHeader};
use crate::model::{CiSystem, Pipeline};

/// Width of a job box (characters)
pub const JOB_BOX_WIDTH: u16 = 28;
/// Height of a job box
pub const JOB_BOX_HEIGHT: u16 = 3;
/// Horizontal gap between columns
pub const STAGE_GAP: u16 = 4;
/// Vertical gap between jobs
pub const JOB_GAP: u16 = 1;
/// Width of a stage column (box + gap)
pub const COL_WIDTH: u16 = JOB_BOX_WIDTH + STAGE_GAP;

/// Bitflags for wire directions passing through a cell.
#[derive(Clone, Copy, Default)]
struct WireCell {
    /// Directions of wires passing through this cell
    dirs: u8,
    /// Color of the wire (last written wins for the base, but we pick the best)
    color: Color,
    /// Whether this cell has an arrowhead
    arrow: bool,
}

const DIR_LEFT: u8 = 1;
const DIR_RIGHT: u8 = 2;
const DIR_UP: u8 = 4;
const DIR_DOWN: u8 = 8;

impl WireCell {
    fn char(&self) -> &'static str {
        match self.dirs {
            // Single direction (shouldn't happen much, but handle gracefully)
            d if d == DIR_LEFT || d == DIR_RIGHT => "─",
            d if d == DIR_UP || d == DIR_DOWN => "│",
            // Straight lines
            d if d == DIR_LEFT | DIR_RIGHT => "─",
            d if d == DIR_UP | DIR_DOWN => "│",
            // Corners
            d if d == DIR_RIGHT | DIR_DOWN => "┌",
            d if d == DIR_LEFT | DIR_DOWN => "┐",
            d if d == DIR_RIGHT | DIR_UP => "└",
            d if d == DIR_LEFT | DIR_UP => "┘",
            // T-junctions
            d if d == DIR_LEFT | DIR_RIGHT | DIR_DOWN => "┬",
            d if d == DIR_LEFT | DIR_RIGHT | DIR_UP => "┴",
            d if d == DIR_UP | DIR_DOWN | DIR_RIGHT => "├",
            d if d == DIR_UP | DIR_DOWN | DIR_LEFT => "┤",
            // Cross
            d if d == DIR_LEFT | DIR_RIGHT | DIR_UP | DIR_DOWN => "┼",
            // Fallback
            _ => "·",
        }
    }
}

pub struct DagView<'a> {
    pub layout: &'a GraphLayout,
    pub selected_job: Option<&'a str>,
    pub scroll_x: u16,
    pub scroll_y: u16,
    pub theme: &'a Theme,
    pub pipeline: &'a Pipeline,
    #[allow(dead_code)] // Available for CI-system-specific rendering
    pub ci_system: CiSystem,
    pub run_results: &'a HashMap<String, i32>,
}

impl<'a> Widget for DagView<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        // Draw stage headers
        for header in &self.layout.stage_headers {
            draw_stage_header(buf, area, header, self.scroll_x, self.theme);
        }

        // Build wire grid from all edges, then render once to handle crossings.
        // Plot dimmed (unrelated) edges first, then highlighted (selected) edges on top
        // so highlighted colors win at shared cells.
        let mut wire_grid: HashMap<(u16, u16), WireCell> = HashMap::new();

        // Partition edges into dimmed and highlighted.
        // Collect the set of neighbor jobs for box highlighting.
        let selected = self.selected_job;
        let is_highlighted = |edge: &crate::graph::Edge| -> bool {
            selected.is_some_and(|sel| edge.from == sel || edge.to == sel)
        };

        let mut neighbor_jobs: HashSet<&str> = HashSet::new();
        if let Some(sel) = selected {
            for edge in self.layout.edges.iter() {
                if edge.from == sel {
                    neighbor_jobs.insert(&edge.to);
                } else if edge.to == sel {
                    neighbor_jobs.insert(&edge.from);
                }
            }
        }

        // Pass 1: dimmed edges (not connected to selected job)
        for edge in self.layout.edges.iter() {
            if is_highlighted(edge) {
                continue;
            }
            if let (Some(from_node), Some(to_node)) = (
                self.layout.get_node(&edge.from),
                self.layout.get_node(&edge.to),
            ) {
                let (from_x, from_y, to_x, to_y) = edge_coords(from_node, to_node);
                plot_edge(
                    &mut wire_grid,
                    from_x,
                    from_y,
                    to_x,
                    to_y,
                    self.theme.edge_dim,
                );
            }
        }

        // Pass 2: highlighted edges — all use Cyan so they stand out
        for edge in self.layout.edges.iter() {
            if !is_highlighted(edge) {
                continue;
            }
            if let (Some(from_node), Some(to_node)) = (
                self.layout.get_node(&edge.from),
                self.layout.get_node(&edge.to),
            ) {
                let (from_x, from_y, to_x, to_y) = edge_coords(from_node, to_node);
                plot_edge(
                    &mut wire_grid,
                    from_x,
                    from_y,
                    to_x,
                    to_y,
                    self.theme.accent,
                );
            }
        }

        // Render wire grid
        let to_screen = |x: u16, y: u16| -> Option<(u16, u16)> {
            let sx = area.x + x.checked_sub(self.scroll_x)?;
            let sy = area.y + y.checked_sub(self.scroll_y)?;
            if sx >= area.x && sx < area.x + area.width && sy >= area.y && sy < area.y + area.height
            {
                Some((sx, sy))
            } else {
                None
            }
        };

        for (&(x, y), cell) in &wire_grid {
            if let Some((sx, sy)) = to_screen(x, y) {
                let ch = if cell.arrow { "►" } else { cell.char() };
                buf.set_string(sx, sy, ch, Style::default().fg(cell.color));
            }
        }

        // Draw job boxes (on top of edges)
        for node in &self.layout.nodes {
            let box_x = (node.col as u16) * COL_WIDTH;
            let box_y = 2 + (node.row as u16) * (JOB_BOX_HEIGHT + JOB_GAP);

            if box_x + JOB_BOX_WIDTH <= self.scroll_x {
                continue;
            }
            if box_y + JOB_BOX_HEIGHT <= self.scroll_y {
                continue;
            }

            let screen_x = area.x + box_x.saturating_sub(self.scroll_x);
            let screen_y = area.y + box_y.saturating_sub(self.scroll_y);

            if screen_x + JOB_BOX_WIDTH > area.x + area.width {
                continue;
            }
            if screen_y + JOB_BOX_HEIGHT > area.y + area.height {
                continue;
            }

            let is_selected = self.selected_job == Some(node.name.as_str());
            let is_neighbor = neighbor_jobs.contains(node.name.as_str());

            let th = self.theme;
            let (border_style, name_style) = if is_selected {
                (
                    Style::default().fg(th.accent).add_modifier(Modifier::BOLD),
                    Style::default()
                        .fg(th.accent_text)
                        .bg(th.accent)
                        .add_modifier(Modifier::BOLD),
                )
            } else if is_neighbor {
                (Style::default().fg(th.accent), Style::default().fg(th.text))
            } else if node.is_manual {
                (
                    Style::default().fg(th.warning),
                    Style::default().fg(th.warning),
                )
            } else if node.is_never {
                (
                    Style::default().fg(th.text_muted),
                    Style::default().fg(th.text_muted),
                )
            } else if node.is_unresolved {
                (
                    Style::default().fg(th.warning),
                    Style::default().fg(th.warning),
                )
            } else {
                (
                    Style::default().fg(th.box_border),
                    Style::default().fg(th.text),
                )
            };

            let is_trigger = self
                .pipeline
                .jobs
                .get(&node.name)
                .is_some_and(|j| j.trigger_file.is_some() || !j.steps.is_empty());
            let is_parent_link = node.name.starts_with("« ");

            let w = JOB_BOX_WIDTH as usize;
            let run_result = self.run_results.get(&node.name);
            let allow_failure = node.allow_failure;
            let suffix = if let Some(&exit_code) = run_result {
                if exit_code == 0 {
                    " ✓"
                } else if allow_failure {
                    " ⚠"
                } else {
                    " ✗"
                }
            } else if node.is_manual {
                " ⏸"
            } else if is_trigger {
                " »"
            } else {
                ""
            };
            let suffix_display_len = suffix.width();
            let display_name = truncate(&node.name, w - 4 - suffix_display_len);
            let label = format!("{}{}", display_name, suffix);

            let suffix_style = match run_result {
                Some(&0) => Some(Style::default().fg(th.success).add_modifier(Modifier::BOLD)),
                Some(_) if allow_failure => {
                    Some(Style::default().fg(th.warning).add_modifier(Modifier::BOLD))
                }
                Some(_) => Some(Style::default().fg(th.error).add_modifier(Modifier::BOLD)),
                None => None,
            };

            if (is_trigger || is_parent_link) && (node.is_never || node.is_unresolved) {
                // Interrupted double-line border for disabled trigger jobs
                let dash = "┄".repeat(w - 2);
                let top = format!("╔{}╗", dash);
                buf.set_string(screen_x, screen_y, &top, border_style);
                let padded_label = format!("┆ {:<width$}┆", label, width = w - 3);
                buf.set_string(screen_x, screen_y + 1, &padded_label, border_style);
                buf.set_string(screen_x + 2, screen_y + 1, &label, name_style);
                let bottom = format!("╚{}╝", dash);
                buf.set_string(screen_x, screen_y + 2, &bottom, border_style);
            } else if is_trigger || is_parent_link {
                // Double-line border for trigger and parent-link jobs
                let top = format!("╔{:═<width$}╗", "", width = w - 2);
                buf.set_string(screen_x, screen_y, &top, border_style);
                let padded_label = format!("║ {:<width$}║", label, width = w - 3);
                buf.set_string(screen_x, screen_y + 1, &padded_label, border_style);
                buf.set_string(screen_x + 2, screen_y + 1, &label, name_style);
                let bottom = format!("╚{:═<width$}╝", "", width = w - 2);
                buf.set_string(screen_x, screen_y + 2, &bottom, border_style);
            } else if node.is_never || node.is_unresolved {
                // Dashed border for jobs disabled by rules or with unresolved extends
                let dash = "┄".repeat(w - 2);
                let top = format!("┌{}┐", dash);
                buf.set_string(screen_x, screen_y, &top, border_style);
                let padded_label = format!("┆ {:<width$}┆", label, width = w - 3);
                buf.set_string(screen_x, screen_y + 1, &padded_label, border_style);
                buf.set_string(screen_x + 2, screen_y + 1, &label, name_style);
                let bottom = format!("└{}┘", dash);
                buf.set_string(screen_x, screen_y + 2, &bottom, border_style);
            } else {
                let top = format!("┌{:─<width$}┐", "", width = w - 2);
                buf.set_string(screen_x, screen_y, &top, border_style);
                let padded_label = format!("│ {:<width$}│", label, width = w - 3);
                buf.set_string(screen_x, screen_y + 1, &padded_label, border_style);
                buf.set_string(screen_x + 2, screen_y + 1, &label, name_style);
                let bottom = format!("└{:─<width$}┘", "", width = w - 2);
                buf.set_string(screen_x, screen_y + 2, &bottom, border_style);
            }

            // Overlay the suffix with its own color (for run result markers)
            if let Some(style) = suffix_style {
                let name_char_len = display_name.width();
                let suffix_x = screen_x + 2 + name_char_len as u16;
                if suffix_x + (suffix_display_len as u16) < screen_x + JOB_BOX_WIDTH {
                    buf.set_string(suffix_x, screen_y + 1, suffix, style);
                }
            }
        }
    }
}

/// Compute the pixel coordinates for an edge between two nodes.
fn edge_coords(
    from_node: &crate::graph::JobNode,
    to_node: &crate::graph::JobNode,
) -> (u16, u16, u16, u16) {
    let from_x = (from_node.col as u16) * COL_WIDTH + JOB_BOX_WIDTH;
    let from_y = 2 + (from_node.row as u16) * (JOB_BOX_HEIGHT + JOB_GAP) + JOB_BOX_HEIGHT / 2;
    let to_x = (to_node.col as u16) * COL_WIDTH;
    let to_y = 2 + (to_node.row as u16) * (JOB_BOX_HEIGHT + JOB_GAP) + JOB_BOX_HEIGHT / 2;
    (from_x, from_y, to_x, to_y)
}

/// Plot an edge into the wire grid. Routes using an S-shaped path through
/// the gap between columns to avoid overlapping with boxes.
fn plot_edge(
    grid: &mut HashMap<(u16, u16), WireCell>,
    from_x: u16,
    from_y: u16,
    to_x: u16,
    to_y: u16,
    color: Color,
) {
    // Pick a vertical routing column in the gap between source and target boxes.
    // Offset within the gap based on vertical distance to spread parallel wires.
    // Skip backward or zero-width edges (same column)
    if from_x >= to_x {
        return;
    }

    let gap_start = from_x; // right edge of source box
    let gap_end = to_x; // left edge of target box
    let gap_width = gap_end - gap_start;

    if from_y == to_y {
        // Straight horizontal line
        for x in from_x..to_x {
            add_wire(grid, x, from_y, DIR_LEFT | DIR_RIGHT, color);
        }
        set_arrow(grid, to_x, to_y, color);
        return;
    }

    // S-shaped routing: horizontal → vertical → horizontal
    // Place the vertical segment 1 cell from the source box (in the gap)
    let vert_x = if gap_width >= 3 {
        gap_start + 1
    } else {
        gap_start + gap_width / 2
    };

    // First horizontal segment (from source to vertical column)
    for x in from_x..vert_x {
        add_wire(grid, x, from_y, DIR_LEFT | DIR_RIGHT, color);
    }

    // Vertical segment
    let (y_min, y_max) = if from_y < to_y {
        (from_y, to_y)
    } else {
        (to_y, from_y)
    };

    // Top corner
    if from_y < to_y {
        add_wire(grid, vert_x, from_y, DIR_LEFT | DIR_DOWN, color);
    } else {
        add_wire(grid, vert_x, from_y, DIR_LEFT | DIR_UP, color);
    }

    // Vertical line
    for y in (y_min + 1)..y_max {
        add_wire(grid, vert_x, y, DIR_UP | DIR_DOWN, color);
    }

    // Bottom corner
    if from_y < to_y {
        add_wire(grid, vert_x, to_y, DIR_UP | DIR_RIGHT, color);
    } else {
        add_wire(grid, vert_x, to_y, DIR_DOWN | DIR_RIGHT, color);
    }

    // Second horizontal segment (from vertical column to target)
    for x in (vert_x + 1)..to_x {
        add_wire(grid, x, to_y, DIR_LEFT | DIR_RIGHT, color);
    }

    set_arrow(grid, to_x, to_y, color);
}

/// Add wire directions to a cell, merging with existing wires.
fn add_wire(grid: &mut HashMap<(u16, u16), WireCell>, x: u16, y: u16, dirs: u8, color: Color) {
    let cell = grid.entry((x, y)).or_default();
    cell.dirs |= dirs;
    cell.color = color;
}

/// Set the arrowhead at the target cell.
fn set_arrow(grid: &mut HashMap<(u16, u16), WireCell>, x: u16, y: u16, color: Color) {
    let cell = grid.entry((x, y)).or_default();
    cell.arrow = true;
    cell.color = color;
}

fn draw_stage_header(
    buf: &mut Buffer,
    area: Rect,
    header: &StageHeader,
    scroll_x: u16,
    theme: &Theme,
) {
    let x = (header.start_col as u16) * COL_WIDTH;
    if x + COL_WIDTH <= scroll_x {
        return;
    }

    let screen_x = area.x + x.saturating_sub(scroll_x);
    let screen_y = area.y;

    if screen_y >= area.y + area.height {
        return;
    }

    let total_width = (header.num_cols as u16) * COL_WIDTH - STAGE_GAP;
    let available = (area.x + area.width).saturating_sub(screen_x) as usize;
    let leader_width = (total_width as usize).min(available);

    if leader_width < 4 {
        return;
    }

    let display_name = truncate(&header.name, leader_width - 3);
    let label = format!("─ {} ", display_name);
    let padded = format!("{:─<width$}", label, width = leader_width);
    buf.set_string(
        screen_x,
        screen_y,
        &padded,
        Style::default().fg(theme.success),
    );
}

fn truncate(s: &str, max_width: usize) -> String {
    if max_width == 0 {
        return String::new();
    }
    if s.width() <= max_width {
        return s.to_string();
    }
    // Accumulate chars until we'd exceed max_width - 1 (reserving space for '…')
    let mut width = 0;
    let mut truncated = String::new();
    for ch in s.chars() {
        let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
        if width + cw > max_width - 1 {
            break;
        }
        width += cw;
        truncated.push(ch);
    }
    format!("{}…", truncated)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_truncate_ascii_short() {
        assert_eq!(truncate("hello", 10), "hello");
    }

    #[test]
    fn test_truncate_ascii_exact() {
        assert_eq!(truncate("hello", 5), "hello");
    }

    #[test]
    fn test_truncate_ascii_long() {
        assert_eq!(truncate("hello world", 8), "hello w…");
    }

    #[test]
    fn test_truncate_empty() {
        assert_eq!(truncate("hello", 0), "");
    }

    #[test]
    fn test_truncate_cjk() {
        // CJK characters are 2 columns wide
        // "测试任务" = 4 chars, 8 columns
        assert_eq!(truncate("测试任务", 10), "测试任务");
        assert_eq!(truncate("测试任务", 8), "测试任务");
        // 7 columns: 3 CJK (6 cols) + ellipsis (1 col) = 7
        assert_eq!(truncate("测试任务", 7), "测试任…");
        // 6 columns: 2 CJK (4 cols) + ellipsis (1 col) = 5 (can't fit 3rd char)
        assert_eq!(truncate("测试任务", 6), "测试…");
    }

    #[test]
    fn test_truncate_mixed_ascii_cjk() {
        // "ab测试cd" = 2 + 4 + 2 = 8 columns
        assert_eq!(truncate("ab测试cd", 8), "ab测试cd");
        assert_eq!(truncate("ab测试cd", 7), "ab测试…");
    }
}
