use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::prelude::*;
use ratatui::symbols::Marker;
use ratatui::widgets::canvas::{Canvas, Context, Line as CanvasLine, Rectangle};
use ratatui::widgets::*;

use crate::app_theme::AppThemeColors;
use crate::draw::state::{DrawData, DrawElement, Shape, Stroke};
use crate::pinstar::data::{CanvasData, CanvasNode};

const PREVIEW_COLS: u16 = 78;

const PREVIEW_ROWS: u16 = 38;

pub fn render_canvas_snapshot(
    data: &CanvasData,
    theme: &AppThemeColors,
) -> Vec<Vec<(char, Style)>> {
    let width = PREVIEW_COLS;
    let height = PREVIEW_ROWS;

    if data.nodes.is_empty() || width == 0 || height == 0 {
        return empty_grid(width, height);
    }

    let (bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y) = canvas_bounds(data);
    let padding = 50.0;
    let min_x = bounds_min_x - padding;
    let min_y = bounds_min_y - padding;
    let max_x = bounds_max_x + padding;
    let max_y = bounds_max_y + padding;

    let content_w = (max_x - min_x).max(1.0);
    let content_h = (max_y - min_y).max(1.0);

    let zoom_x = (width as f64 - 4.0) / content_w;
    let zoom_y = (height as f64 - 4.0) / content_h;
    let zoom = zoom_x.min(zoom_y).clamp(0.01, 10.0);

    let center_x = (min_x + max_x) / 2.0;
    let center_y = (min_y + max_y) / 2.0;

    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).unwrap();

    let _ = terminal.draw(|frame| {
        let area = frame.area();
        let preview_bg = theme.preview_bg();
        fill_buf_bg(frame.buffer_mut(), area, preview_bg);
        let buf = frame.buffer_mut();

        for edge in &data.edges {
            let from = data.nodes.iter().find(|n| n.id() == edge.from_node);
            let to = data.nodes.iter().find(|n| n.id() == edge.to_node);
            if let (Some(f), Some(t)) = (from, to) {
                let (fx, fy) = f.pos();
                let (fw, fh) = f.size();
                let (tx, ty) = t.pos();
                let (tw, th) = t.size();

                let ax = fx + fw / 2.0;
                let ay = fy + fh / 2.0;
                let bx = tx + tw / 2.0;
                let by = ty + th / 2.0;

                let sfx = ((ax - center_x) * zoom) + (area.x as f64 + area.width as f64 / 2.0);
                let sfy = ((ay - center_y) * zoom) + (area.y as f64 + area.height as f64 / 2.0);
                let stx = ((bx - center_x) * zoom) + (area.x as f64 + area.width as f64 / 2.0);
                let sty = ((by - center_y) * zoom) + (area.y as f64 + area.height as f64 / 2.0);

                draw_braille_line(buf, sfx, sfy, stx, sty, theme.muted);
            }
        }

        for node in &data.nodes {
            let (nx, ny) = node.pos();
            let (nw, nh) = node.size();

            let sx = ((nx - center_x) * zoom) + (area.x as f64 + area.width as f64 / 2.0);
            let sy = ((ny - center_y) * zoom) + (area.y as f64 + area.height as f64 / 2.0);
            let sw = (nw * zoom).max(4.0);
            let sh = (nh * zoom).max(2.0);

            if sx + sw < area.left() as f64
                || sx > area.right() as f64
                || sy + sh < area.top() as f64
                || sy > area.bottom() as f64
            {
                continue;
            }

            let left = sx.max(area.left() as f64) as u16;
            let top = sy.max(area.top() as f64) as u16;
            let right = (sx + sw).min(area.right() as f64) as u16;
            let bottom = (sy + sh).min(area.bottom() as f64) as u16;
            if right <= left || bottom <= top {
                continue;
            }

            let node_rect = Rect::new(left, top, right - left, bottom - top);

            let color_str = match node {
                CanvasNode::Text(n) => n.color.as_deref(),
                CanvasNode::File(n) => n.color.as_deref(),
                CanvasNode::Link(n) => n.color.as_deref(),
                CanvasNode::Group(_) => None,
            };
            let node_color = canvas_color_to_style(color_str, theme);

            let title = match node {
                CanvasNode::File(n) => std::path::Path::new(&n.file)
                    .file_name()
                    .and_then(|s| s.to_str())
                    .unwrap_or(&n.file)
                    .to_string(),
                CanvasNode::Link(n) => n.url.clone(),
                _ => node.id().to_string(),
            };

            let inner_text = node.text();

            let max_text_len = (node_rect.width.saturating_sub(2) as usize).min(
                node_rect.height.saturating_sub(2) as usize
                    * node_rect.width.saturating_sub(2) as usize,
            );
            let display_text = if inner_text.len() > max_text_len && max_text_len > 10 {
                format!("{}…", &inner_text[..max_text_len.saturating_sub(1)])
            } else {
                inner_text.to_string()
            };

            let block = Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(node_color))
                .title(title)
                .style(theme.bg_style());

            let text = Paragraph::new(display_text)
                .block(block)
                .style(Style::default().fg(theme.fg))
                .wrap(Wrap { trim: false });

            frame.render_widget(Clear, node_rect);
            frame.render_widget(text, node_rect);
        }
    });

    extract_grid(terminal, width, height)
}

pub fn render_draw_snapshot(data: &DrawData, theme: &AppThemeColors) -> Vec<Vec<(char, Style)>> {
    render_draw_snapshot_with_size(data, theme, PREVIEW_COLS, PREVIEW_ROWS)
}

pub fn render_draw_snapshot_with_size(
    data: &DrawData,
    theme: &AppThemeColors,
    width: u16,
    height: u16,
) -> Vec<Vec<(char, Style)>> {
    if width == 0 || height == 0 {
        return empty_grid(width, height);
    }

    let (min_x, min_y, max_x, max_y) = draw_bounds(data);
    let padding = 20.0;
    let x_bounds = [min_x - padding, max_x + padding];
    let y_bounds = [min_y - padding, max_y + padding];

    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).unwrap();

    let _ = terminal.draw(|frame| {
        let area = frame.area();
        let bg_opt = theme.preview_bg();
        fill_buf_bg(frame.buffer_mut(), area, bg_opt);
        let bg_color = bg_opt.unwrap_or(Color::Reset);
        let canvas = Canvas::default()
            .block(Block::default().style(Style::default().bg(bg_color)))
            .background_color(bg_color)
            .marker(Marker::Braille)
            .x_bounds(x_bounds)
            .y_bounds(y_bounds)
            .paint(|ctx| {
                for element in &data.elements {
                    match element {
                        DrawElement::Stroke(stroke) => {
                            draw_stroke_lines(ctx, stroke);
                        }
                        DrawElement::Shape(shape) => {
                            draw_shape_on_canvas(ctx, shape);
                        }
                        DrawElement::Text(text) => {
                            let color = Color::Rgb(text.color.0, text.color.1, text.color.2);
                            ctx.print(
                                text.x,
                                text.y,
                                ratatui::text::Line::from(text.content.clone())
                                    .style(Style::default().fg(color)),
                            );
                        }
                    }
                }
            });
        frame.render_widget(canvas, frame.area());
    });

    extract_grid(terminal, width, height)
}

pub struct RenderedSnapshot<'a> {
    grid: &'a [Vec<(char, Style)>],
    scroll_offset: u16,
    block: Option<Block<'a>>,
}

impl<'a> RenderedSnapshot<'a> {
    pub fn new(grid: &'a [Vec<(char, Style)>]) -> Self {
        Self {
            grid,
            scroll_offset: 0,
            block: None,
        }
    }

    #[must_use]
    pub fn scroll_offset(mut self, offset: u16) -> Self {
        self.scroll_offset = offset;
        self
    }

    #[must_use]
    pub fn block(mut self, block: Block<'a>) -> Self {
        self.block = Some(block);
        self
    }
}

impl Widget for RenderedSnapshot<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let area = if let Some(block) = self.block {
            let inner = block.inner(area);
            block.render(area, buf);
            inner
        } else {
            area
        };

        let rows = self.grid.len();

        let scroll = self.scroll_offset as usize;
        for (row_idx, buf_y) in (area.top()..area.bottom()).enumerate() {
            let src_row = scroll + row_idx;
            if src_row >= rows {
                break;
            }
            let row = &self.grid[src_row];
            for (col_idx, buf_x) in (area.left()..area.right()).enumerate() {
                if col_idx >= row.len() {
                    break;
                }
                let (ch, style) = row[col_idx];
                let safe_ch = if ch.is_control() { ' ' } else { ch };
                if let Some(cell) = buf.cell_mut((buf_x, buf_y)) {
                    cell.set_char(safe_ch).set_style(style);
                }
            }
        }
    }
}

#[allow(deprecated)]
fn fill_buf_bg(buf: &mut Buffer, area: Rect, bg: Option<Color>) {
    let Some(bg) = bg else { return };
    for y in area.top()..area.bottom() {
        for x in area.left()..area.right() {
            if let Some(cell) = buf.cell_mut((x, y))
                && !cell.skip
            {
                cell.set_style(Style::default().bg(bg));
            }
        }
    }
}

fn empty_grid(width: u16, height: u16) -> Vec<Vec<(char, Style)>> {
    let empty_style = Style::default();
    (0..height)
        .map(|_| vec![(' ', empty_style); width as usize])
        .collect()
}

fn extract_grid(
    terminal: Terminal<TestBackend>,
    width: u16,
    height: u16,
) -> Vec<Vec<(char, Style)>> {
    let buffer = terminal.backend().buffer();
    let mut grid = Vec::with_capacity(height as usize);
    for y in 0..height {
        let mut row = Vec::with_capacity(width as usize);
        for x in 0..width {
            if let Some(cell) = buffer.cell((x, y)) {
                let ch = cell.symbol().chars().next().unwrap_or(' ');
                row.push((ch, cell.style()));
            } else {
                row.push((' ', Style::default()));
            }
        }
        grid.push(row);
    }
    grid
}

fn canvas_bounds(data: &CanvasData) -> (f64, f64, f64, f64) {
    let mut min_x = f64::MAX;
    let mut min_y = f64::MAX;
    let mut max_x = f64::MIN;
    let mut max_y = f64::MIN;
    for node in &data.nodes {
        let (x, y) = node.pos();
        let (w, h) = node.size();
        min_x = min_x.min(x);
        min_y = min_y.min(y);
        max_x = max_x.max(x + w);
        max_y = max_y.max(y + h);
    }
    if min_x == f64::MAX {
        (0.0, 0.0, 100.0, 100.0)
    } else {
        (min_x, min_y, max_x, max_y)
    }
}

fn draw_bounds(data: &DrawData) -> (f64, f64, f64, f64) {
    let mut min_x = f64::MAX;
    let mut min_y = f64::MAX;
    let mut max_x = f64::MIN;
    let mut max_y = f64::MIN;
    for el in &data.elements {
        match el {
            DrawElement::Stroke(s) => {
                for &(px, py) in &s.points {
                    min_x = min_x.min(px);
                    min_y = min_y.min(py);
                    max_x = max_x.max(px);
                    max_y = max_y.max(py);
                }
            }
            DrawElement::Shape(shape) => match shape {
                Shape::Rect {
                    x,
                    y,
                    width,
                    height,
                    ..
                } => {
                    min_x = min_x.min(*x);
                    min_y = min_y.min(*y);
                    max_x = max_x.max(x + width);
                    max_y = max_y.max(y + height);
                }
                Shape::Ellipse {
                    x,
                    y,
                    width,
                    height,
                    ..
                } => {
                    min_x = min_x.min(*x);
                    min_y = min_y.min(*y);
                    max_x = max_x.max(x + width);
                    max_y = max_y.max(y + height);
                }
                Shape::Diamond {
                    x,
                    y,
                    width,
                    height,
                    ..
                } => {
                    min_x = min_x.min(*x);
                    min_y = min_y.min(*y);
                    max_x = max_x.max(x + width);
                    max_y = max_y.max(y + height);
                }
                Shape::Line { x1, y1, x2, y2, .. } => {
                    min_x = min_x.min(x1.min(*x2));
                    min_y = min_y.min(y1.min(*y2));
                    max_x = max_x.max(x1.max(*x2));
                    max_y = max_y.max(y1.max(*y2));
                }
                Shape::Arrow { x1, y1, x2, y2, .. } => {
                    min_x = min_x.min(x1.min(*x2));
                    min_y = min_y.min(y1.min(*y2));
                    max_x = max_x.max(x1.max(*x2));
                    max_y = max_y.max(y1.max(*y2));
                }
            },
            DrawElement::Text(t) => {
                min_x = min_x.min(t.x);
                min_y = min_y.min(t.y);
                max_x = max_x.max(t.x + t.content.len() as f64 * 8.0);
                max_y = max_y.max(t.y + 12.0);
            }
        }
    }
    if min_x == f64::MAX {
        (0.0, 0.0, 100.0, 100.0)
    } else {
        (min_x, min_y, max_x, max_y)
    }
}

fn canvas_color_to_style(color: Option<&str>, theme: &AppThemeColors) -> Color {
    match color {
        Some(s) if s.starts_with('#') && s.len() == 7 => {
            let r = u8::from_str_radix(&s[1..3], 16).unwrap_or(0);
            let g = u8::from_str_radix(&s[3..5], 16).unwrap_or(0);
            let b = u8::from_str_radix(&s[5..7], 16).unwrap_or(0);
            Color::Rgb(r, g, b)
        }
        Some("1") | Some("red") => Color::Rgb(255, 82, 82),
        Some("2") | Some("orange") => Color::Rgb(255, 152, 0),
        Some("3") | Some("yellow") => Color::Rgb(255, 235, 59),
        Some("4") | Some("green") => Color::Rgb(76, 175, 80),
        Some("5") | Some("cyan") => Color::Rgb(0, 188, 212),
        Some("6") | Some("purple") => Color::Rgb(156, 39, 176),
        _ => theme.accent,
    }
}

fn draw_braille_line(buf: &mut Buffer, mut x1: f64, mut y1: f64, x2: f64, y2: f64, color: Color) {
    let dx = x2 - x1;
    let dy = y2 - y1;
    let dist = (dx * dx + dy * dy).sqrt();
    let steps = (dist * 2.0) as usize;
    if steps == 0 {
        return;
    }
    let sx = dx / steps as f64;
    let sy = dy / steps as f64;

    for _ in 0..=steps {
        let cx = x1 as u16;
        let cy = y1 as u16;
        let dot_x = ((x1 - cx as f64) * 2.0) as u16;
        let dot_y = ((y1 - cy as f64) * 4.0) as u16;

        if let Some(cell) = buf.cell_mut((cx, cy)) {
            let mut braile_code = match cell.symbol().chars().next() {
                Some(c) if ('\u{2800}'..='\u{28FF}').contains(&c) => c as u32 - 0x2800,
                _ => 0,
            };
            let dot_bit = match (dot_x, dot_y) {
                (0, 0) => 0x01,
                (0, 1) => 0x02,
                (0, 2) => 0x04,
                (1, 0) => 0x08,
                (1, 1) => 0x10,
                (1, 2) => 0x20,
                (0, 3) => 0x40,
                (1, 3) => 0x80,
                _ => 0,
            };
            braile_code |= dot_bit;
            if let Some(c) = char::from_u32(0x2800 + braile_code) {
                cell.set_char(c).set_fg(color);
            }
        }

        x1 += sx;
        y1 += sy;
    }
}

fn draw_stroke_lines(ctx: &mut Context, stroke: &Stroke) {
    let color = Color::Rgb(stroke.color.0, stroke.color.1, stroke.color.2);
    for window in stroke.points.windows(2) {
        if let [p1, p2] = window {
            ctx.draw(&CanvasLine {
                x1: p1.0,
                y1: p1.1,
                x2: p2.0,
                y2: p2.1,
                color,
            });
        }
    }
}

fn draw_shape_on_canvas(ctx: &mut Context, shape: &Shape) {
    match shape {
        Shape::Rect {
            x,
            y,
            width,
            height,
            color,
        } => {
            ctx.draw(&Rectangle {
                x: *x,
                y: *y,
                width: *width,
                height: *height,
                color: Color::Rgb(color.0, color.1, color.2),
            });
        }
        Shape::Ellipse {
            x,
            y,
            width,
            height,
            color,
        } => {
            let color = Color::Rgb(color.0, color.1, color.2);
            let rx = width / 2.0;
            let ry = height / 2.0;
            let cx_center = x + rx;
            let cy_center = y + ry;
            let segments = 32;
            for i in 0..segments {
                let angle1 = (i as f64 / segments as f64) * 2.0 * std::f64::consts::PI;
                let angle2 = ((i + 1) as f64 / segments as f64) * 2.0 * std::f64::consts::PI;
                ctx.draw(&CanvasLine {
                    x1: cx_center + rx * angle1.cos(),
                    y1: cy_center + ry * angle1.sin(),
                    x2: cx_center + rx * angle2.cos(),
                    y2: cy_center + ry * angle2.sin(),
                    color,
                });
            }
        }
        Shape::Diamond {
            x,
            y,
            width,
            height,
            color,
        } => {
            let color = Color::Rgb(color.0, color.1, color.2);
            let p1 = (x + width / 2.0, *y);
            let p2 = (x + width, y + height / 2.0);
            let p3 = (x + width / 2.0, y + height);
            let p4 = (*x, y + height / 2.0);
            for (start, end) in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)] {
                ctx.draw(&CanvasLine {
                    x1: start.0,
                    y1: start.1,
                    x2: end.0,
                    y2: end.1,
                    color,
                });
            }
        }
        Shape::Line {
            x1,
            y1,
            x2,
            y2,
            color,
        } => {
            ctx.draw(&CanvasLine {
                x1: *x1,
                y1: *y1,
                x2: *x2,
                y2: *y2,
                color: Color::Rgb(color.0, color.1, color.2),
            });
        }
        Shape::Arrow {
            x1,
            y1,
            x2,
            y2,
            color,
        } => {
            let color = Color::Rgb(color.0, color.1, color.2);
            ctx.draw(&CanvasLine {
                x1: *x1,
                y1: *y1,
                x2: *x2,
                y2: *y2,
                color,
            });
            let angle = (y2 - y1).atan2(x2 - x1);
            let head_len = 5.0;
            let head_angle = std::f64::consts::PI / 6.0;
            ctx.draw(&CanvasLine {
                x1: *x2,
                y1: *y2,
                x2: x2 - head_len * (angle - head_angle).cos(),
                y2: y2 - head_len * (angle - head_angle).sin(),
                color,
            });
            ctx.draw(&CanvasLine {
                x1: *x2,
                y1: *y2,
                x2: x2 - head_len * (angle + head_angle).cos(),
                y2: y2 - head_len * (angle + head_angle).sin(),
                color,
            });
        }
    }
}

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

    /// Regression guard: a grid cell containing a control char must not
    /// reach `Cell::set_char` as-is (ratatui debug-assert!-panics on
    /// control chars in `cell_width.rs`).
    #[test]
    fn rendered_snapshot_replaces_control_char() {
        let grid = vec![vec![('\n', Style::default())]];
        let snapshot = RenderedSnapshot::new(&grid);

        let backend = TestBackend::new(1, 1);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal
            .draw(|frame| {
                frame.render_widget(snapshot, frame.area());
            })
            .unwrap();

        let buffer = terminal.backend().buffer();
        let cell = buffer.cell((0, 0)).unwrap();
        assert_eq!(cell.symbol(), " ", "control char replaced by space");
    }
}
