use std::{
    collections::{HashMap, HashSet},
    fs,
    io::stdout,
    path::{Path, PathBuf},
    time::{Duration, Instant},
};

use anyhow::{Context, Result};
use crossterm::{
    event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
    backend::CrosstermBackend,
    prelude::*,
    widgets::canvas::{Canvas, Line},
    widgets::{Block, Borders, Clear, Paragraph, Wrap, block::Title},
};
use serde::{Deserialize, Serialize};

type NodeId = u64;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MindMap {
    name: String,
    nodes: Vec<Node>,
    root: NodeId,
    next_id: NodeId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Node {
    id: NodeId,
    title: String,
    note: String,
    #[serde(default)]
    color: NodeColor,
    position: Position,
    parent: Option<NodeId>,
    children: Vec<NodeId>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct Position {
    x: f64,
    y: f64,
}

#[derive(Debug, Clone)]
struct App {
    map: MindMap,
    selected: NodeId,
    file_path: Option<PathBuf>,
    autosave_path: PathBuf,
    mode: Mode,
    message: Option<Toast>,
    dirty: bool,
    show_help: bool,
    undo_stack: Vec<AppState>,
    redo_stack: Vec<AppState>,
    last_autosave: Option<Instant>,
    autosave_interval: Duration,
    moving_snapshot: bool,
}

#[derive(Debug, Clone)]
struct AppState {
    map: MindMap,
    selected: NodeId,
}

#[derive(Debug, Clone)]
enum Mode {
    Normal,
    Moving,
    Renaming {
        buffer: String,
    },
    Prompt {
        kind: PromptKind,
        buffer: String,
    },
    NoteEditor {
        buffer: String,
    },
    Search {
        query: String,
        results: Vec<NodeId>,
        index: usize,
    },
}

#[derive(Debug, Clone, Copy)]
enum PromptKind {
    Open,
    SaveAs,
    AutosavePath,
    AutosaveInterval,
}

#[derive(Debug, Clone, Copy)]
enum NavDir {
    Left,
    Right,
    Up,
    Down,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
enum NodeColor {
    Default,
    Red,
    Green,
    Blue,
    Yellow,
    Magenta,
    Cyan,
}

impl NodeColor {
    fn to_color(self) -> Color {
        match self {
            NodeColor::Default => Color::White,
            NodeColor::Red => Color::Red,
            NodeColor::Green => Color::Green,
            NodeColor::Blue => Color::Blue,
            NodeColor::Yellow => Color::Yellow,
            NodeColor::Magenta => Color::Magenta,
            NodeColor::Cyan => Color::Cyan,
        }
    }

    fn cycle_next(self) -> Self {
        match self {
            NodeColor::Default => NodeColor::Red,
            NodeColor::Red => NodeColor::Green,
            NodeColor::Green => NodeColor::Blue,
            NodeColor::Blue => NodeColor::Yellow,
            NodeColor::Yellow => NodeColor::Magenta,
            NodeColor::Magenta => NodeColor::Cyan,
            NodeColor::Cyan => NodeColor::Default,
        }
    }

    fn label(self) -> &'static str {
        match self {
            NodeColor::Default => "default",
            NodeColor::Red => "red",
            NodeColor::Green => "green",
            NodeColor::Blue => "blue",
            NodeColor::Yellow => "yellow",
            NodeColor::Magenta => "magenta",
            NodeColor::Cyan => "cyan",
        }
    }
}

impl Default for NodeColor {
    fn default() -> Self {
        NodeColor::Default
    }
}

#[derive(Debug, Clone)]
struct Toast {
    text: String,
    expires_at: Option<Instant>,
}

fn main() -> Result<()> {
    let initial_path = std::env::args().nth(1).map(PathBuf::from);
    let initial_map = match initial_path.as_ref() {
        Some(path) if path.exists() => load_map(path).unwrap_or_else(|_| MindMap::new("Mind Map")),
        Some(path) => {
            let mut map = MindMap::new(path.to_string_lossy().as_ref());
            map.name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("Mind Map")
                .to_string();
            map
        }
        None => MindMap::new("Mind Map"),
    };

    let mut app = App::new(initial_map, initial_path);

    enable_raw_mode()?;
    let mut stdout = stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let res = run_app(&mut terminal, &mut app);

    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;

    res
}

fn run_app(
    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
    app: &mut App,
) -> Result<()> {
    let tick_rate = Duration::from_millis(50);
    loop {
        app.prune_message();
        terminal.draw(|f| draw(f, app))?;

        if event::poll(tick_rate)? {
            match event::read()? {
                Event::Key(key) if key.kind == KeyEventKind::Press => {
                    if handle_key(app, key)? {
                        return Ok(());
                    }
                }
                Event::Resize(_, _) => {}
                _ => {}
            }
        }
        app.maybe_autosave();
    }
}

fn handle_key(app: &mut App, key: KeyEvent) -> Result<bool> {
    let mode = app.mode.clone();
    match mode {
        Mode::Normal => handle_normal_mode(app, key),
        Mode::Moving => handle_moving_mode(app, key),
        Mode::Renaming { .. } => handle_renaming_mode(app, key),
        Mode::Prompt { .. } => handle_prompt_mode(app, key),
        Mode::NoteEditor { .. } => handle_note_mode(app, key),
        Mode::Search { .. } => handle_search_mode(app, key),
    }
}

fn handle_normal_mode(app: &mut App, key: KeyEvent) -> Result<bool> {
    match key.code {
        KeyCode::Char('q') => return Ok(true),
        KeyCode::Char('?') => {
            app.show_help = !app.show_help;
        }
        KeyCode::Char('/') => {
            app.mode = Mode::Search {
                query: String::new(),
                results: Vec::new(),
                index: 0,
            };
            app.update_search_results();
        }
        KeyCode::Char('A') => {
            app.mode = Mode::Prompt {
                kind: PromptKind::AutosavePath,
                buffer: app.autosave_path.to_string_lossy().to_string(),
            };
        }
        KeyCode::Char('I') => {
            app.mode = Mode::Prompt {
                kind: PromptKind::AutosaveInterval,
                buffer: app.autosave_interval.as_secs().to_string(),
            };
        }
        KeyCode::Char('s') => {
            if let Some(path) = app.file_path.clone() {
                save_current_map(app, path)?;
            } else {
                app.mode = Mode::Prompt {
                    kind: PromptKind::SaveAs,
                    buffer: String::new(),
                };
            }
        }
        KeyCode::Char('S') => {
            app.mode = Mode::Prompt {
                kind: PromptKind::SaveAs,
                buffer: String::new(),
            };
        }
        KeyCode::Char('o') => {
            app.mode = Mode::Prompt {
                kind: PromptKind::Open,
                buffer: String::new(),
            };
        }
        KeyCode::Char('n') => {
            app.reset_to_new();
        }
        KeyCode::Char('a') => {
            app.push_undo();
            if let Some(new_id) = app.map.add_child(
                app.selected,
                "New node".to_string(),
                offset_for_new_child(&app.map, app.selected),
            ) {
                app.selected = new_id;
                app.dirty = true;
            }
        }
        KeyCode::Char('[') => {
            app.jump_to_parent();
        }
        KeyCode::Char(']') => {
            app.jump_to_first_child();
        }
        KeyCode::Char('d') => {
            app.push_undo();
            if let Some(next) = app.map.delete_subtree(app.selected) {
                app.selected = next;
                app.dirty = true;
            } else {
                app.set_message("Cannot delete the root node");
            }
        }
        KeyCode::Char('c') => {
            app.push_undo();
            if let Some(new_id) = app.map.clone_subtree(app.selected) {
                app.selected = new_id;
                app.dirty = true;
            }
        }
        KeyCode::Char('m') => {
            app.mode = Mode::Moving;
            app.moving_snapshot = false;
        }
        KeyCode::Char('t') => {
            if let Some(node) = app.map.node(app.selected) {
                app.mode = Mode::Renaming {
                    buffer: node.title.clone(),
                };
            }
        }
        KeyCode::Char('k') => {
            app.push_undo();
            if let Some(node) = app.map.node_mut(app.selected) {
                let new_color = node.color.cycle_next();
                node.color = new_color;
                app.dirty = true;
                let label = new_color.label().to_string();
                app.set_message(format!("Color set to {label}"));
            }
        }
        KeyCode::Enter => {
            if let Some(node) = app.map.node(app.selected) {
                app.mode = Mode::NoteEditor {
                    buffer: node.note.clone(),
                };
            }
        }
        KeyCode::Left => app.map_select_direction(NavDir::Left),
        KeyCode::Right => app.map_select_direction(NavDir::Right),
        KeyCode::Up => app.map_select_direction(NavDir::Up),
        KeyCode::Down => app.map_select_direction(NavDir::Down),
        KeyCode::Tab => {
            if let Some(next) = app.map.next_node(app.selected) {
                app.selected = next;
            }
        }
        KeyCode::BackTab => {
            if let Some(prev) = app.map.prev_node(app.selected) {
                app.selected = prev;
            }
        }
        KeyCode::Char('u') | KeyCode::Char('z')
            if key.modifiers.contains(KeyModifiers::CONTROL) =>
        {
            app.undo();
        }
        KeyCode::Char('u') => {
            app.undo();
        }
        KeyCode::Char('r') | KeyCode::Char('y')
            if key.modifiers.contains(KeyModifiers::CONTROL) =>
        {
            app.redo();
        }
        KeyCode::Char('r') => {
            app.redo();
        }
        _ => {}
    }
    Ok(false)
}

fn handle_moving_mode(app: &mut App, key: KeyEvent) -> Result<bool> {
    let step = if key.modifiers.contains(KeyModifiers::SHIFT) {
        2.5
    } else {
        1.0
    };

    match key.code {
        KeyCode::Esc | KeyCode::Char('m') => {
            app.mode = Mode::Normal;
            app.moving_snapshot = false;
        }
        KeyCode::Left => {
            if !app.moving_snapshot {
                app.push_undo();
                app.moving_snapshot = true;
            }
            app.map.move_node(app.selected, -step, 0.0);
            app.dirty = true;
        }
        KeyCode::Right => {
            if !app.moving_snapshot {
                app.push_undo();
                app.moving_snapshot = true;
            }
            app.map.move_node(app.selected, step, 0.0);
            app.dirty = true;
        }
        KeyCode::Up => {
            if !app.moving_snapshot {
                app.push_undo();
                app.moving_snapshot = true;
            }
            app.map.move_node(app.selected, 0.0, step);
            app.dirty = true;
        }
        KeyCode::Down => {
            if !app.moving_snapshot {
                app.push_undo();
                app.moving_snapshot = true;
            }
            app.map.move_node(app.selected, 0.0, -step);
            app.dirty = true;
        }
        _ => {}
    }

    Ok(false)
}

fn handle_renaming_mode(app: &mut App, key: KeyEvent) -> Result<bool> {
    let mut commit: Option<String> = None;
    let mut exit_mode = false;
    if let Mode::Renaming { buffer } = &mut app.mode {
        match key.code {
            KeyCode::Esc => {
                exit_mode = true;
            }
            KeyCode::Enter => {
                commit = Some(buffer.trim().to_string());
                exit_mode = true;
            }
            KeyCode::Backspace => {
                buffer.pop();
            }
            KeyCode::Char(c) => {
                buffer.push(c);
            }
            _ => {}
        }
    }
    if let Some(new_title) = commit {
        app.push_undo();
        if let Some(node) = app.map.node_mut(app.selected) {
            node.title = new_title;
            app.dirty = true;
        }
    }
    if exit_mode {
        app.mode = Mode::Normal;
    }
    Ok(false)
}

fn handle_prompt_mode(app: &mut App, key: KeyEvent) -> Result<bool> {
    if let Mode::Prompt { kind, buffer } = &mut app.mode {
        match key.code {
            KeyCode::Esc => {
                app.mode = Mode::Normal;
            }
            KeyCode::Enter => {
                match kind {
                    PromptKind::Open => {
                        let path = PathBuf::from(buffer.trim());
                        match load_map(&path) {
                            Ok(map) => {
                                app.map = map;
                                app.selected = app.map.root;
                                app.file_path = Some(path.clone());
                                app.autosave_path = path;
                                app.set_message("Opened map");
                                app.dirty = false;
                                app.undo_stack.clear();
                                app.redo_stack.clear();
                                app.last_autosave = None;
                            }
                            Err(err) => {
                                app.set_message(format!("Failed to open: {err}"));
                            }
                        }
                    }
                    PromptKind::SaveAs => {
                        let path = PathBuf::from(buffer.trim());
                        if path.as_os_str().is_empty() {
                            app.set_message("Path cannot be empty");
                            return Ok(false);
                        }
                        save_current_map(app, path.clone())?;
                        app.autosave_path = path;
                    }
                    PromptKind::AutosavePath => {
                        let path = PathBuf::from(buffer.trim());
                        if path.as_os_str().is_empty() {
                            app.set_message("Autosave path cannot be empty");
                            return Ok(false);
                        }
                        app.set_autosave_path(path);
                    }
                    PromptKind::AutosaveInterval => {
                        let text = buffer.trim();
                        let secs: u64 = text.parse().unwrap_or(0);
                        app.set_autosave_interval(secs);
                    }
                }
                app.mode = Mode::Normal;
            }
            KeyCode::Backspace => {
                buffer.pop();
            }
            KeyCode::Char(c) => {
                buffer.push(c);
            }
            _ => {}
        }
    }
    Ok(false)
}

fn handle_note_mode(app: &mut App, key: KeyEvent) -> Result<bool> {
    let mut commit_note: Option<String> = None;
    let mut exit_mode = false;
    if let Mode::NoteEditor { buffer } = &mut app.mode {
        match key.code {
            KeyCode::Esc => {
                commit_note = Some(buffer.clone());
                exit_mode = true;
            }
            KeyCode::Backspace => {
                buffer.pop();
            }
            KeyCode::Enter => {
                buffer.push('\n');
            }
            KeyCode::Char(c) => {
                if !key.modifiers.contains(KeyModifiers::CONTROL) {
                    buffer.push(c);
                }
            }
            _ => {}
        }
    }
    if let Some(note) = commit_note {
        app.push_undo();
        if let Some(node) = app.map.node_mut(app.selected) {
            node.note = note;
            app.dirty = true;
        }
    }
    if exit_mode {
        app.mode = Mode::Normal;
    }
    Ok(false)
}

fn handle_search_mode(app: &mut App, key: KeyEvent) -> Result<bool> {
    if let Mode::Search {
        query,
        results,
        index,
    } = &mut app.mode
    {
        match key.code {
            KeyCode::Esc => {
                app.mode = Mode::Normal;
            }
            KeyCode::Enter => {
                if let Some(id) = results.get(*index) {
                    app.selected = *id;
                }
                app.mode = Mode::Normal;
            }
            KeyCode::Up => {
                if !results.is_empty() {
                    if *index == 0 {
                        *index = results.len() - 1;
                    } else {
                        *index -= 1;
                    }
                }
            }
            KeyCode::Down => {
                if !results.is_empty() {
                    *index = (*index + 1) % results.len();
                }
            }
            KeyCode::Backspace => {
                query.pop();
                app.update_search_results();
            }
            KeyCode::Char(c) => {
                if !key.modifiers.contains(KeyModifiers::CONTROL) {
                    query.push(c);
                    app.update_search_results();
                }
            }
            _ => {}
        }
    }
    Ok(false)
}

fn save_current_map(app: &mut App, path: PathBuf) -> Result<()> {
    save_map(&path, &app.map)?;
    app.file_path = Some(path.clone());
    app.autosave_path = path;
    app.dirty = false;
    app.set_message("Map saved");
    app.last_autosave = Some(Instant::now());
    Ok(())
}

fn draw(f: &mut Frame, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Min(12),
            Constraint::Length(3),
        ])
        .split(f.size());

    render_header(f, chunks[0], app);

    let body_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(65), Constraint::Percentage(35)])
        .split(chunks[1]);

    let side_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
        .split(body_chunks[1]);

    render_map(f, body_chunks[0], app);
    render_details(f, side_chunks[0], app);
    render_shortcuts(f, side_chunks[1], app);

    render_footer(f, chunks[2], app);

    match &app.mode {
        Mode::Renaming { buffer } => render_input_overlay(f, "Rename node", buffer),
        Mode::Prompt { kind, buffer } => {
            let label = match kind {
                PromptKind::Open => "Open map path",
                PromptKind::SaveAs => "Save map as",
                PromptKind::AutosavePath => "Autosave path",
                PromptKind::AutosaveInterval => "Autosave interval (seconds, 0 disables)",
            };
            render_input_overlay(f, label, buffer);
        }
        Mode::NoteEditor { buffer } => render_note_overlay(f, buffer),
        Mode::Search {
            query,
            results,
            index,
        } => {
            render_search_overlay(f, query, *index, results.len());
        }
        Mode::Moving | Mode::Normal => {}
    }

    if app.show_help {
        render_help(f);
    }
}

fn render_header(f: &mut Frame, area: Rect, app: &App) {
    let file_label = app
        .file_path
        .as_ref()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_else(|| "unsaved".to_string());
    let mode = match app.mode {
        Mode::Normal => "normal",
        Mode::Moving => "move node",
        Mode::Renaming { .. } => "rename",
        Mode::Prompt { .. } => "prompt",
        Mode::NoteEditor { .. } => "notes",
        Mode::Search { .. } => "search",
    };

    let title_line = format!(
        "ponder | {} | {}{}",
        app.map.name,
        file_label,
        if app.dirty { " *" } else { "" }
    );

    let info = format!(
        "selected: {} | mode: {} | nodes: {} | undo: {} | redo: {}",
        app.map
            .node(app.selected)
            .map(|n| n.title.as_str())
            .unwrap_or(""),
        mode,
        app.map.nodes.len(),
        app.undo_stack.len(),
        app.redo_stack.len()
    );

    let autosave_info = if app.autosave_interval.is_zero() {
        format!(
            "autosave: off | path {} | last {}",
            app.autosave_path.to_string_lossy(),
            app.format_last_autosave()
        )
    } else {
        format!(
            "autosave: every {}s to {} | last {}",
            app.autosave_interval.as_secs(),
            app.autosave_path.to_string_lossy(),
            app.format_last_autosave()
        )
    };

    let header = Paragraph::new(format!("{title_line}\n{info}\n{autosave_info}"))
        .block(Block::default().borders(Borders::BOTTOM).title("Status"))
        .wrap(Wrap { trim: true });
    f.render_widget(header, area);
}

fn render_map(f: &mut Frame, area: Rect, app: &App) {
    let (min_x, max_x, min_y, max_y) = app.map.bounds();
    let mut search_hits = HashSet::new();
    let mut focused_hit: Option<NodeId> = None;
    if let Mode::Search { results, index, .. } = &app.mode {
        for id in results {
            search_hits.insert(*id);
        }
        if !results.is_empty() && *index < results.len() {
            focused_hit = Some(results[*index]);
        }
    }
    let canvas = Canvas::default()
        .block(
            Block::default()
                .title(Title::from("Mind map").alignment(Alignment::Center))
                .borders(Borders::ALL),
        )
        .x_bounds([min_x - 2.0, max_x + 2.0])
        .y_bounds([min_y - 2.0, max_y + 2.0])
        .paint(|ctx| {
            for node in &app.map.nodes {
                for child_id in &node.children {
                    if let Some(child) = app.map.node(*child_id) {
                        ctx.draw(&Line {
                            x1: node.position.x,
                            y1: node.position.y,
                            x2: child.position.x,
                            y2: child.position.y,
                            color: Color::DarkGray,
                        });
                    }
                }
            }

            for node in &app.map.nodes {
                let is_selected = node.id == app.selected;
                let base_color = node.color.to_color();
                let mut style = Style::default().fg(base_color);
                if Some(node.id) == focused_hit {
                    style = style
                        .fg(Color::Yellow)
                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED);
                } else if is_selected {
                    style = style
                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
                        .bg(Color::DarkGray);
                } else if search_hits.contains(&node.id) {
                    style = style.fg(Color::LightYellow);
                }
                ctx.print(
                    node.position.x,
                    node.position.y,
                    Span::styled(node.title.clone(), style),
                );
            }
        });

    f.render_widget(canvas, area);
}

fn render_details(f: &mut Frame, area: Rect, app: &App) {
    let node = app.map.node(app.selected);
    let (title, note, color_label) = node
        .map(|n| (n.title.clone(), n.note.clone(), n.color.label().to_string()))
        .unwrap_or_else(|| ("".to_string(), "".to_string(), "default".to_string()));
    let content = format!(
        "{}\ncolor: {}\n\n{}",
        title,
        color_label,
        if note.is_empty() {
            "No notes yet."
        } else {
            note.as_str()
        }
    );
    let block = Block::default()
        .title(Title::from("Selected").alignment(Alignment::Center))
        .borders(Borders::ALL);
    let paragraph = Paragraph::new(content)
        .block(block)
        .wrap(Wrap { trim: false });
    f.render_widget(paragraph, area);
}

fn render_shortcuts(f: &mut Frame, area: Rect, _app: &App) {
    let lines = [
        "q quit | o open | s save | S save as | n new",
        "A autosave path | I autosave interval",
        "a add | c clone | d delete | m move | t rename | k color",
        "[ parent | ] child | arrows move/select | Tab/Shift-Tab cycle",
        "Enter notes | / search | u undo | r redo | ? toggle help",
    ];
    let block = Block::default()
        .title(Title::from("Shortcuts").alignment(Alignment::Center))
        .borders(Borders::ALL);
    let paragraph = Paragraph::new(lines.join("\n"))
        .block(block)
        .wrap(Wrap { trim: false });
    f.render_widget(paragraph, area);
}

fn render_footer(f: &mut Frame, area: Rect, app: &App) {
    let msg = app.message_text();
    let block = Block::default().borders(Borders::TOP).title("Messages");
    let paragraph = Paragraph::new(msg).block(block).wrap(Wrap { trim: true });
    f.render_widget(paragraph, area);
}

fn render_input_overlay(f: &mut Frame, label: &str, buffer: &str) {
    let area = centered_rect(60, 20, f.size());
    let block = Block::default()
        .title(Title::from(label).alignment(Alignment::Center))
        .borders(Borders::ALL);
    let paragraph = Paragraph::new(buffer.to_string() + "_").block(block);
    f.render_widget(Clear, area);
    f.render_widget(paragraph, area);
}

fn render_search_overlay(f: &mut Frame, query: &str, index: usize, count: usize) {
    let area = centered_rect(60, 20, f.size());
    let title = format!(
        "Search ({}/{})",
        if count == 0 { 0 } else { index + 1 },
        count
    );
    let block = Block::default()
        .title(Title::from(title).alignment(Alignment::Center))
        .borders(Borders::ALL);
    let hint = "\nUp/Down to cycle, Enter to jump, Esc to close";
    let paragraph = Paragraph::new(query.to_string() + "_" + hint).block(block);
    f.render_widget(Clear, area);
    f.render_widget(paragraph, area);
}

fn render_note_overlay(f: &mut Frame, buffer: &str) {
    let area = centered_rect(90, 80, f.size());
    let block = Block::default()
        .title(Title::from("Notes (Esc to close)").alignment(Alignment::Center))
        .borders(Borders::ALL);
    let paragraph = Paragraph::new(buffer.to_string())
        .block(block)
        .wrap(Wrap { trim: false });
    f.render_widget(Clear, area);
    f.render_widget(paragraph, area);
}

fn render_help(f: &mut Frame) {
    let area = centered_rect(70, 60, f.size());
    let lines = [
        "Movement: arrows; parent [: child ]",
        "Select next/prev: Tab / Shift-Tab",
        "Add child: a | Clone: c | Delete: d",
        "Move node: m then arrows (hold Shift for bigger steps)",
        "Rename: t | Notes: Enter (multi-line, Esc to close) | Color: k",
        "Search: / (type, Enter to jump, Up/Down cycle)",
        "Undo: u or Ctrl+Z | Redo: r or Ctrl+Y",
        "Autosave path: A | Autosave interval: I (seconds, 0 = off)",
        "Open: o | Save: s | Save As: S | New map: n",
        "Toggle help: ? | Quit: q",
    ];
    let block = Block::default()
        .title(Title::from("Help").alignment(Alignment::Center))
        .borders(Borders::ALL);
    let paragraph = Paragraph::new(lines.join("\n"))
        .block(block)
        .wrap(Wrap { trim: false });
    f.render_widget(Clear, area);
    f.render_widget(paragraph, area);
}

fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
    let popup_layout = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage((100 - percent_y) / 2),
                Constraint::Percentage(percent_y),
                Constraint::Percentage((100 - percent_y) / 2),
            ]
            .as_ref(),
        )
        .split(r);

    Layout::default()
        .direction(Direction::Horizontal)
        .constraints(
            [
                Constraint::Percentage((100 - percent_x) / 2),
                Constraint::Percentage(percent_x),
                Constraint::Percentage((100 - percent_x) / 2),
            ]
            .as_ref(),
        )
        .split(popup_layout[1])[1]
}

impl App {
    fn new(map: MindMap, file_path: Option<PathBuf>) -> Self {
        let selected = map.root;
        let autosave_path = file_path
            .clone()
            .unwrap_or_else(|| PathBuf::from("ponder_autosave.json"));
        Self {
            map,
            selected,
            file_path,
            autosave_path,
            mode: Mode::Normal,
            message: None,
            dirty: false,
            show_help: false,
            undo_stack: Vec::new(),
            redo_stack: Vec::new(),
            last_autosave: None,
            autosave_interval: Duration::from_secs(10),
            moving_snapshot: false,
        }
    }

    fn set_message(&mut self, text: impl Into<String>) {
        self.set_message_with_timeout(text, Some(Duration::from_secs(4)));
    }

    fn set_message_with_timeout(&mut self, text: impl Into<String>, timeout: Option<Duration>) {
        let expires_at = timeout.map(|d| Instant::now() + d);
        self.message = Some(Toast {
            text: text.into(),
            expires_at,
        });
    }

    fn prune_message(&mut self) {
        if let Some(toast) = &self.message {
            if let Some(expire) = toast.expires_at {
                if Instant::now() >= expire {
                    self.message = None;
                }
            }
        }
    }

    fn message_text(&self) -> String {
        self.message
            .as_ref()
            .map(|m| m.text.clone())
            .unwrap_or_else(|| "Ready. Press ? for help.".to_string())
    }

    fn reset_to_new(&mut self) {
        self.map = MindMap::new("Mind Map");
        self.selected = self.map.root;
        self.file_path = None;
        self.autosave_path = PathBuf::from("ponder_autosave.json");
        self.dirty = false;
        self.set_message("Started a new map");
        self.undo_stack.clear();
        self.redo_stack.clear();
        self.moving_snapshot = false;
        self.last_autosave = None;
    }

    fn map_select_direction(&mut self, dir: NavDir) {
        if let Some(next) = self.map.nearest_in_direction(self.selected, dir) {
            self.selected = next;
        }
    }

    fn jump_to_parent(&mut self) {
        if let Some(parent) = self.map.node(self.selected).and_then(|n| n.parent) {
            self.selected = parent;
            self.set_message("Jumped to parent");
        } else {
            self.set_message("No parent");
        }
    }

    fn jump_to_first_child(&mut self) {
        if let Some(child) = self
            .map
            .node(self.selected)
            .and_then(|n| n.children.first())
            .copied()
        {
            self.selected = child;
            self.set_message("Jumped to child");
        } else {
            self.set_message("No child");
        }
    }

    fn push_undo(&mut self) {
        self.push_current_to_undo();
        self.redo_stack.clear();
    }

    fn push_current_to_undo(&mut self) {
        const HISTORY_LIMIT: usize = 64;
        self.undo_stack.push(AppState {
            map: self.map.clone(),
            selected: self.selected,
        });
        if self.undo_stack.len() > HISTORY_LIMIT {
            self.undo_stack.remove(0);
        }
    }

    fn undo(&mut self) {
        if let Some(state) = self.undo_stack.pop() {
            let redo_state = AppState {
                map: self.map.clone(),
                selected: self.selected,
            };
            self.redo_stack.push(redo_state);
            self.map = state.map;
            self.selected = state.selected;
            self.dirty = true;
            self.set_message("Undo");
        } else {
            self.set_message("Nothing to undo");
        }
    }

    fn redo(&mut self) {
        if let Some(state) = self.redo_stack.pop() {
            self.push_current_to_undo();
            self.map = state.map;
            self.selected = state.selected;
            self.dirty = true;
            self.set_message("Redo");
        } else {
            self.set_message("Nothing to redo");
        }
    }

    fn maybe_autosave(&mut self) {
        if !self.dirty {
            return;
        }
        if let Some(last) = self.last_autosave {
            if last.elapsed() < self.autosave_interval {
                return;
            }
        }
        if self.autosave_interval.is_zero() {
            return;
        }
        let path = self.autosave_path.clone();
        if save_map(&path, &self.map).is_ok() {
            self.last_autosave = Some(Instant::now());
            self.set_message(format!("Autosaved {}", path.to_string_lossy()));
        }
    }

    fn set_autosave_path(&mut self, path: PathBuf) {
        self.autosave_path = path.clone();
        self.set_message(format!("Autosave path set to {}", path.to_string_lossy()));
    }

    fn set_autosave_interval(&mut self, secs: u64) {
        self.autosave_interval = Duration::from_secs(secs);
        if secs == 0 {
            self.set_message("Autosave disabled");
        } else {
            self.set_message(format!("Autosave every {}s", secs));
        }
    }

    fn format_last_autosave(&self) -> String {
        if self.autosave_interval.is_zero() {
            return "disabled".to_string();
        }
        if let Some(last) = self.last_autosave {
            let secs = last.elapsed().as_secs();
            if secs == 0 {
                "just now".to_string()
            } else {
                format!("{secs}s ago")
            }
        } else {
            "never".to_string()
        }
    }

    fn update_search_results(&mut self) {
        if let Mode::Search {
            query,
            results,
            index,
        } = &mut self.mode
        {
            let q = query.to_lowercase();
            results.clear();
            if q.is_empty() {
                return;
            }
            for node in &self.map.nodes {
                let title = node.title.to_lowercase();
                let note = node.note.to_lowercase();
                if title.contains(&q) || note.contains(&q) {
                    results.push(node.id);
                }
            }
            if results.is_empty() {
                *index = 0;
            } else if *index >= results.len() {
                *index = 0;
            }
        }
    }
}

impl MindMap {
    fn new(name: &str) -> Self {
        let root_id = 1;
        Self {
            name: name.to_string(),
            nodes: vec![Node {
                id: root_id,
                title: "Central Idea".to_string(),
                note: String::new(),
                color: NodeColor::Default,
                position: Position { x: 0.0, y: 0.0 },
                parent: None,
                children: Vec::new(),
            }],
            root: root_id,
            next_id: root_id + 1,
        }
    }

    fn node(&self, id: NodeId) -> Option<&Node> {
        self.nodes.iter().find(|n| n.id == id)
    }

    fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
        self.nodes.iter_mut().find(|n| n.id == id)
    }

    fn node_index(&self, id: NodeId) -> Option<usize> {
        self.nodes.iter().position(|n| n.id == id)
    }

    fn next_node(&self, current: NodeId) -> Option<NodeId> {
        let idx = self.node_index(current)?;
        let next_idx = (idx + 1) % self.nodes.len();
        self.nodes.get(next_idx).map(|n| n.id)
    }

    fn prev_node(&self, current: NodeId) -> Option<NodeId> {
        let idx = self.node_index(current)?;
        let prev_idx = if idx == 0 {
            self.nodes.len().saturating_sub(1)
        } else {
            idx - 1
        };
        self.nodes.get(prev_idx).map(|n| n.id)
    }

    fn add_child(&mut self, parent: NodeId, title: String, offset: Position) -> Option<NodeId> {
        let parent_pos = self.node(parent)?.position;
        let id = self.next_id;
        self.next_id += 1;
        let node = Node {
            id,
            title,
            note: String::new(),
            color: NodeColor::Default,
            position: Position {
                x: parent_pos.x + offset.x,
                y: parent_pos.y + offset.y,
            },
            parent: Some(parent),
            children: Vec::new(),
        };
        self.nodes.push(node);
        if let Some(p) = self.node_mut(parent) {
            p.children.push(id);
        }
        Some(id)
    }

    fn delete_subtree(&mut self, id: NodeId) -> Option<NodeId> {
        if id == self.root {
            return None;
        }
        let parent_id = self.node(id)?.parent?;
        let mut to_remove = Vec::new();
        self.collect_subtree(id, &mut to_remove);
        let remove_set: HashSet<NodeId> = to_remove.iter().copied().collect();
        self.nodes.retain(|n| !remove_set.contains(&n.id));
        if let Some(parent) = self.node_mut(parent_id) {
            parent.children.retain(|c| *c != id);
        }
        Some(parent_id)
    }

    fn collect_subtree(&self, id: NodeId, out: &mut Vec<NodeId>) {
        out.push(id);
        if let Some(node) = self.node(id) {
            for child in &node.children {
                self.collect_subtree(*child, out);
            }
        }
    }

    fn clone_subtree(&mut self, id: NodeId) -> Option<NodeId> {
        let parent = self.node(id).and_then(|n| n.parent);
        let target_parent = parent.unwrap_or(self.root);
        let mut mapping = HashMap::new();
        self.duplicate_recursive(
            id,
            Some(target_parent),
            &mut mapping,
            Position { x: 2.0, y: 0.5 },
        )?;
        mapping.get(&id).copied()
    }

    fn duplicate_recursive(
        &mut self,
        id: NodeId,
        parent: Option<NodeId>,
        mapping: &mut HashMap<NodeId, NodeId>,
        offset: Position,
    ) -> Option<()> {
        let original = self.node(id)?.clone();
        let new_id = self.next_id;
        self.next_id += 1;

        let mut new_node = original.clone();
        new_node.id = new_id;
        new_node.parent = parent;
        new_node.children.clear();
        new_node.position.x += offset.x;
        new_node.position.y += offset.y;

        self.nodes.push(new_node);
        if let Some(p) = parent {
            if let Some(parent_node) = self.node_mut(p) {
                parent_node.children.push(new_id);
            }
        }
        mapping.insert(id, new_id);

        for child in original.children {
            self.duplicate_recursive(child, Some(new_id), mapping, offset)?;
        }
        Some(())
    }

    fn move_node(&mut self, id: NodeId, dx: f64, dy: f64) {
        if let Some(node) = self.node_mut(id) {
            node.position.x += dx;
            node.position.y += dy;
        }
    }

    fn bounds(&self) -> (f64, f64, f64, f64) {
        let mut min_x: f64 = 0.0;
        let mut max_x: f64 = 0.0;
        let mut min_y: f64 = 0.0;
        let mut max_y: f64 = 0.0;
        for node in &self.nodes {
            min_x = min_x.min(node.position.x);
            max_x = max_x.max(node.position.x);
            min_y = min_y.min(node.position.y);
            max_y = max_y.max(node.position.y);
        }
        if min_x == max_x {
            min_x -= 5.0;
            max_x += 5.0;
        }
        if min_y == max_y {
            min_y -= 5.0;
            max_y += 5.0;
        }
        (min_x, max_x, min_y, max_y)
    }

    fn nearest_in_direction(&self, from: NodeId, dir: NavDir) -> Option<NodeId> {
        let current = self.node(from)?;
        let (vx, vy) = match dir {
            NavDir::Left => (-1.0, 0.0),
            NavDir::Right => (1.0, 0.0),
            NavDir::Up => (0.0, 1.0),
            NavDir::Down => (0.0, -1.0),
        };
        let mut best: Option<(NodeId, f64)> = None;

        for node in &self.nodes {
            if node.id == from {
                continue;
            }
            let dx = node.position.x - current.position.x;
            let dy = node.position.y - current.position.y;
            let dot = dx * vx + dy * vy;
            if dot <= 0.0 {
                continue;
            }
            let dist2 = dx * dx + dy * dy;
            let perp_penalty = (dx * vy - dy * vx).abs();
            let score = dist2 + perp_penalty * 2.0;
            if best.map(|(_, s)| score < s).unwrap_or(true) {
                best = Some((node.id, score));
            }
        }
        best.map(|(id, _)| id)
    }
}

fn offset_for_new_child(map: &MindMap, parent: NodeId) -> Position {
    let count = map.node(parent).map(|n| n.children.len()).unwrap_or(0);
    Position {
        x: 3.0,
        y: (count as f64) * 1.5 - 1.5,
    }
}

fn load_map(path: &Path) -> Result<MindMap> {
    let data = fs::read_to_string(path)
        .with_context(|| format!("Unable to read {}", path.to_string_lossy()))?;
    let mut map: MindMap =
        serde_json::from_str(&data).with_context(|| "File is not a valid mindmap")?;
    if map.nodes.is_empty() {
        map = MindMap::new("Recovered");
    }
    Ok(map)
}

fn save_map(path: &Path, map: &MindMap) -> Result<()> {
    let serialized = serde_json::to_string_pretty(map)?;
    fs::write(path, serialized)
        .with_context(|| format!("Unable to write {}", path.to_string_lossy()))
}
