use std::collections::HashSet;
use std::io::{IsTerminal, Read, Write};

use clap::Args;
use crossterm::{
    cursor::Show,
    event::{
        read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
        MouseButton, MouseEventKind,
    },
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use hefesto_widgets::{Tree, TreeNode, TreeState};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Alignment, Rect},
    style::Style,
    widgets::{Block, Borders, StatefulWidget, Widget},
    Terminal,
};

use crate::{keybinds, style};

const MENU_GUIDE: &str = include_str!("../MENU_GUIDE.md");

#[derive(Args)]
pub struct MenuArgs {
    /// Path to JSON file (reads from stdin if not provided)
    #[arg()]
    pub file: Option<String>,

    /// Input format: auto (detect from extension), json, plano
    #[arg(short = 'f', long, default_value = "auto")]
    pub format: String,

    /// Title displayed in the menu header
    #[arg(short, long, default_value = "Menú")]
    pub title: String,

    /// Popup width (0 = auto)
    #[arg(short = 'W', long, default_value = "0")]
    pub width: u16,

    /// Popup height (0 = auto)
    #[arg(short = 'H', long, default_value = "0")]
    pub height: u16,

    /// Show detailed guide for this command
    #[arg(long)]
    pub guide: bool,
}

// ── JSON deserialization ─────────────────────────────────

#[derive(serde::Deserialize)]
struct TreeNodeInput {
    id: usize,
    text: String,
    #[serde(default)]
    children: Vec<TreeNodeInput>,
}

impl From<TreeNodeInput> for TreeNode<'static> {
    fn from(input: TreeNodeInput) -> Self {
        TreeNode {
            id: input.id,
            text: ratatui::text::Line::from(input.text),
            children: input.children.into_iter().map(Into::into).collect(),
        }
    }
}

/// Check for duplicate ids across the tree. Returns the first duplicate found.
fn find_duplicate_id(nodes: &[TreeNodeInput]) -> Option<usize> {
    let mut seen = HashSet::new();
    fn walk(nodes: &[TreeNodeInput], seen: &mut HashSet<usize>) -> Option<usize> {
        for node in nodes {
            if !seen.insert(node.id) {
                return Some(node.id);
            }
            if let Some(dup) = walk(&node.children, seen) {
                return Some(dup);
            }
        }
        None
    }
    walk(nodes, &mut seen)
}

// ── Plano parser ───────────────────────────────────────────

/// Parse a flat list of `/`-separated paths into a tree.
/// Each line is a path from root to leaf, e.g. `Archivo/Nuevo/Documento`.
fn parse_plano(input: &str) -> Vec<TreeNodeInput> {
    let mut roots: Vec<TreeNodeInput> = Vec::new();
    let mut next_id: usize = 1;

    for line in input.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        let parts: Vec<&str> = line.split('/').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
        if parts.is_empty() {
            continue;
        }
        insert_path(&mut roots, &parts, &mut next_id);
    }

    roots
}

/// Insert a path (split by `/`) into the tree, creating nodes as needed.
fn insert_path(nodes: &mut Vec<TreeNodeInput>, parts: &[&str], next_id: &mut usize) {
    let name = parts[0];
    let idx = if let Some(pos) = nodes.iter().position(|n| n.text == name) {
        pos
    } else {
        let id = *next_id;
        *next_id += 1;
        nodes.push(TreeNodeInput {
            id,
            text: name.to_string(),
            children: Vec::new(),
        });
        nodes.len() - 1
    };

    if parts.len() > 1 {
        insert_path(&mut nodes[idx].children, &parts[1..], next_id);
    }
}

/// Detect format from file extension when `--format auto`.
fn detect_format(file: &Option<String>) -> &str {
    match file {
        Some(path) if path.ends_with(".json") => "json",
        _ => "plano",
    }
}

// ── Tree helpers ─────────────────────────────────────────

/// Find the node id at the given visible flat index (0-based).
fn node_id_at_visible(
    nodes: &[TreeNode],
    expanded: &HashSet<usize>,
    cursor: usize,
) -> Option<usize> {
    fn walk(
        nodes: &[TreeNode],
        expanded: &HashSet<usize>,
        cursor: usize,
        count: &mut usize,
    ) -> Option<usize> {
        for node in nodes {
            if *count == cursor {
                return Some(node.id);
            }
            *count += 1;
            if !node.children.is_empty() && expanded.contains(&node.id) {
                if let Some(found) = walk(&node.children, expanded, cursor, count) {
                    return Some(found);
                }
            }
        }
        None
    }
    walk(nodes, expanded, cursor, &mut 0)
}

/// Total number of visible items given the current expand state.
fn total_visible(nodes: &[TreeNode], expanded: &HashSet<usize>) -> usize {
    fn walk(nodes: &[TreeNode], expanded: &HashSet<usize>, count: &mut usize) {
        for node in nodes {
            *count += 1;
            if !node.children.is_empty() && expanded.contains(&node.id) {
                walk(&node.children, expanded, count);
            }
        }
    }
    let mut count = 0;
    walk(nodes, expanded, &mut count);
    count
}

/// Whether the node with the given id has children.
fn has_children(nodes: &[TreeNode], id: usize) -> bool {
    fn walk(nodes: &[TreeNode], id: usize) -> Option<bool> {
        for node in nodes {
            if node.id == id {
                return Some(!node.children.is_empty());
            }
            if let Some(found) = walk(&node.children, id) {
                return Some(found);
            }
        }
        None
    }
    walk(nodes, id).unwrap_or(false)
}


fn node_path(nodes: &[TreeNode], id: usize) -> Option<String> {
    let mut segments = Vec::new();
    fn walk<'a>(nodes: &'a [TreeNode<'a>], id: usize, segments: &mut Vec<String>) -> bool {
        for node in nodes {
            segments.push(node.text.to_string());
            if node.id == id {
                return true;
            }
            if !node.children.is_empty() && walk(&node.children, id, segments) {
                return true;
            }
            segments.pop();
        }
        false
    }
    if walk(nodes, id, &mut segments) {
        Some(segments.join("/"))
    } else {
        None
    }
}

// ── Popup helpers ────────────────────────────────────────

const POPUP_MIN_W: u16 = 40;
const POPUP_MIN_H: u16 = 8;

fn centered_rect(area: Rect, w: u16, h: u16) -> Rect {
    let w = w.min(area.width.saturating_sub(2));
    let h = h.min(area.height.saturating_sub(2));
    let x = (area.width.saturating_sub(w)) / 2;
    let y = (area.height.saturating_sub(h)) / 2;
    Rect::new(x, y, w, h)
}

fn contains(rect: Rect, col: u16, row: u16) -> bool {
    col >= rect.x
        && col < rect.x.saturating_add(rect.width)
        && row >= rect.y
        && row < rect.y.saturating_add(rect.height)
}

fn is_drag_area(popup: Rect, col: u16, row: u16) -> bool {
    let inner = Rect {
        x: popup.x + 1,
        y: popup.y + 1,
        width: popup.width.saturating_sub(2),
        height: popup.height.saturating_sub(2),
    };
    col >= popup.x
        && col < popup.x.saturating_add(popup.width)
        && row >= popup.y
        && row < popup.y.saturating_add(popup.height)
        && !contains(inner, col, row)
}

// ── Run ──────────────────────────────────────────────────
/// RAII guard: restores the terminal (raw mode, alternate screen, cursor)
/// on any exit path — normal return, early `break`, or explicit `cleanup()`.
struct TerminalGuard {
    cleaned: bool,
}

impl TerminalGuard {
    fn new() -> Self {
        Self { cleaned: false }
    }

    fn cleanup(&mut self) {
        if self.cleaned {
            return;
        }
        self.cleaned = true;

        // Re-open the terminal writer to match the setup logic.
        let mut writer: Box<dyn Write> =
            match std::fs::OpenOptions::new().write(true).open("/dev/tty") {
                Ok(f) => Box::new(f),
                Err(_) => Box::new(std::io::stdout()),
            };

        let _ = disable_raw_mode();
        let _ = execute!(writer, LeaveAlternateScreen, DisableMouseCapture, Show);
    }
}
impl Drop for TerminalGuard {
    fn drop(&mut self) {
        self.cleanup();
    }
}

pub fn run(args: MenuArgs) {
    if args.guide {
        println!("{}", MENU_GUIDE);
        return;
    }

    // ── Detect format ────────────────────────────────────

    let format = if args.format == "auto" {
        detect_format(&args.file)
    } else {
        &args.format
    };

    match format {
        "json" | "plano" => {}
        _ => {
            eprintln!("{} menu: formato '{}' no soportado (json, plano)", crate::BIN_NAME, format);
            std::process::exit(1);
        }
    }

    // ── Read input ───────────────────────────────────────

    let input = if let Some(path) = &args.file {
        match std::fs::read_to_string(path) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("{} menu: error al leer '{}': {}", crate::BIN_NAME, path, e);
                std::process::exit(1);
            }
        }
    } else {
        // If stdin is a terminal (no pipe), error immediately instead of hanging
        if std::io::stdin().is_terminal() {
            eprintln!("{} menu: se requiere un archivo como argumento o datos por pipe a stdin", crate::BIN_NAME);
            std::process::exit(1);
        }
        let mut buf = String::new();
        if std::io::stdin().read_to_string(&mut buf).is_err() || buf.trim().is_empty() {
            eprintln!("{} menu: no se recibieron datos (provee un archivo como argumento o pipe a stdin)", crate::BIN_NAME);
            std::process::exit(1);
        }
        buf
    };

    // ── Parse ─────────────────────────────────────────────

    let input_nodes: Vec<TreeNodeInput> = match format {
        "json" => match serde_json::from_str(&input) {
            Ok(n) => n,
            Err(e) => {
                eprintln!("{} menu: error al parsear JSON: {}", crate::BIN_NAME, e);
                std::process::exit(1);
            }
        },
        "plano" => parse_plano(&input),
        _ => unreachable!(),
    };

    // Validate unique ids
    if let Some(dup) = find_duplicate_id(&input_nodes) {
        eprintln!("{} menu: id duplicado '{}' en el árbol", crate::BIN_NAME, dup);
        std::process::exit(1);
    }

    let nodes: Vec<TreeNode<'static>> = input_nodes.into_iter().map(Into::into).collect();

    if nodes.is_empty() {
        eprintln!("{} menu: árbol vacío", crate::BIN_NAME);
        std::process::exit(1);
    }

    // ── Terminal setup ───────────────────────────────────

    let mut tty: Box<dyn Write> =
        match std::fs::OpenOptions::new().write(true).open("/dev/tty") {
            Ok(f) => Box::new(f),
            Err(_) => Box::new(std::io::stdout()),
        };
    if enable_raw_mode().is_err()
        || execute!(tty, EnterAlternateScreen, EnableMouseCapture).is_err()
    {
        eprintln!("{} menu: el terminal no es interactivo", crate::BIN_NAME);
        std::process::exit(1);
    }
    let mut terminal = match Terminal::new(CrosstermBackend::new(tty)) {
        Ok(t) => t,
        Err(e) => {
            eprintln!("{} menu: error al inicializar terminal: {}", crate::BIN_NAME, e);
            std::process::exit(1);
        }
    };
    let mut guard = TerminalGuard::new();
    if let Err(e) = terminal.clear() {
        eprintln!("{} menu: error al limpiar pantalla: {}", crate::BIN_NAME, e);
        guard.cleanup();
        std::process::exit(1);
    }
    if let Err(e) = terminal.hide_cursor() {
        eprintln!("{} menu: error al ocultar cursor: {}", crate::BIN_NAME, e);
        guard.cleanup();
        std::process::exit(1);
    }

    // ── Tree state ───────────────────────────────────────

    let mut tree_state = TreeState::default();
    tree_state.scroll_state.follow = false;
    tree_state.scroll_state.select(Some(0));

    let title = args.title;
    let mut result: Option<String> = None;
    let mut pending_g = false;

    // ── Popup state ──────────────────────────────────────

    let mut drag_offset: Option<(u16, u16)> = None;
    let mut popup_origin: Option<(u16, u16)> = None;

    // ── Event loop ───────────────────────────────────────

    while result.is_none() {
        let size = match terminal.size() {
            Ok(s) => s,
            Err(e) => {
                eprintln!("{} menu: error al leer tamaño del terminal: {}", crate::BIN_NAME, e);
                break;
            }
        };
        let area = Rect::new(0, 0, size.width, size.height);

        // Popup dimensions
        let pw = if args.width > 0 {
            args.width
        } else {
            POPUP_MIN_W.max(area.width.saturating_div(3).min(60))
        };
        let ph = if args.height > 0 {
            args.height
        } else {
            let estimated = total_visible(&nodes, &tree_state.expanded) as u16 + 4;
            POPUP_MIN_H.max(estimated.min(area.height.saturating_sub(4)).min(25))
        };

        let (mut ox, mut oy) = popup_origin.unwrap_or_else(|| {
            let c = centered_rect(area, pw, ph);
            (c.x, c.y)
        });
        ox = ox.min(area.width.saturating_sub(pw));
        oy = oy.min(area.height.saturating_sub(ph));

        let popup_rect = Rect::new(ox, oy, pw.min(area.width), ph.min(area.height));
        let inner = Rect {
            x: popup_rect.x + 1,
            y: popup_rect.y + 1,
            width: popup_rect.width.saturating_sub(2),
            height: popup_rect.height.saturating_sub(2),
        };

        if let Err(e) = terminal
            .draw(|frame| {
                let block = Block::default()
                    .title(title.as_str())
                    .title_alignment(Alignment::Center)
                    .borders(Borders::ALL)
                    .border_style(Style::default().fg(style::ACCENT));
                block.render(popup_rect, frame.buffer_mut());
                StatefulWidget::render(
                    Tree::new(nodes.clone()),
                    inner,
                    frame.buffer_mut(),
                    &mut tree_state,
                );
            }) {
            eprintln!("{} menu: error al dibujar: {}", crate::BIN_NAME, e);
            break;
        }

        let event = match read() {
            Ok(e) => e,
            Err(e) => {
                eprintln!("{} menu: error al leer entrada: {}", crate::BIN_NAME, e);
                break;
            }
        };
        match event {
            Event::Key(key) => {
                if key.code == keybinds::EMERGENCY && key.modifiers == KeyModifiers::CONTROL {
                    result = Some(String::new());
                    break;
                }

                let total = total_visible(&nodes, &tree_state.expanded);

                match key.code {
                    keybinds::UP | keybinds::UP_ALT | keybinds::BACK_TAB => {
                        tree_state.scroll_state.previous();
                    }
                    keybinds::DOWN | keybinds::DOWN_ALT | keybinds::TAB => {
                        tree_state.scroll_state.next(total);
                    }
                    keybinds::CONFIRM => {
                        let cursor = tree_state.scroll_state.list_state.selected().unwrap_or(0);
                        if let Some(id) = node_id_at_visible(&nodes, &tree_state.expanded, cursor)
                        {
                            if has_children(&nodes, id) {
                                tree_state.toggle(id);
                                let new_total =
                                    total_visible(&nodes, &tree_state.expanded);
                                if new_total > 0 {
                                    let sel = tree_state
                                        .scroll_state
                                        .list_state
                                        .selected()
                                        .unwrap_or(0);
                                    if sel >= new_total {
                                        tree_state.scroll_state.select(Some(new_total - 1));
                                    }
                                }
                            } else {
                                result = node_path(&nodes, id);
                            }
                        }
                    }
                    keybinds::TOGGLE_MULTI | KeyCode::Right => {
                        let cursor = tree_state.scroll_state.list_state.selected().unwrap_or(0);
                        if let Some(id) = node_id_at_visible(&nodes, &tree_state.expanded, cursor)
                        {
                            if has_children(&nodes, id)
                                && !tree_state.expanded.contains(&id)
                            {
                                tree_state.expanded.insert(id);
                            }
                        }
                    }
                    KeyCode::Left => {
                        let cursor = tree_state.scroll_state.list_state.selected().unwrap_or(0);
                        if let Some(id) = node_id_at_visible(&nodes, &tree_state.expanded, cursor)
                        {
                            if tree_state.expanded.contains(&id) {
                                tree_state.expanded.remove(&id);
                                let new_total =
                                    total_visible(&nodes, &tree_state.expanded);
                                if new_total > 0 {
                                    let sel = tree_state
                                        .scroll_state
                                        .list_state
                                        .selected()
                                        .unwrap_or(0);
                                    if sel >= new_total {
                                        tree_state.scroll_state.select(Some(new_total - 1));
                                    }
                                }
                            }
                        }
                    }
                    keybinds::CANCEL => {
                        result = Some(String::new());
                    }
                    keybinds::FIRST => {
                        if pending_g {
                            tree_state.scroll_state.select(Some(0));
                            pending_g = false;
                        } else {
                            pending_g = true;
                        }
                    }
                    keybinds::LAST => {
                        let total = total_visible(&nodes, &tree_state.expanded);
                        if total > 0 {
                            tree_state.scroll_state.select(Some(total - 1));
                        }
                    }
                    _ => pending_g = false,
                }
            }
            Event::Mouse(mouse) => {
                let col = mouse.column;
                let row = mouse.row;

                match mouse.kind {
                    MouseEventKind::Down(MouseButton::Left) => {
                        if is_drag_area(popup_rect, col, row) {
                            let ox = col.saturating_sub(popup_rect.x);
                            let oy = row.saturating_sub(popup_rect.y);
                            drag_offset = Some((ox, oy));
                        } else if contains(inner, col, row) {
                            let item_idx = (row - inner.y) as usize;
                            let total = total_visible(&nodes, &tree_state.expanded);
                            if item_idx < total {
                                tree_state.scroll_state.select(Some(item_idx));
                            }
                        }
                    }
                    MouseEventKind::Drag(MouseButton::Left) => {
                        if let Some((dx, dy)) = drag_offset {
                            let max_w = area.width.saturating_sub(pw);
                            let max_h = area.height.saturating_sub(ph);
                            let nx = (col as i16 - dx as i16).clamp(0, max_w as i16) as u16;
                            let ny = (row as i16 - dy as i16).clamp(0, max_h as i16) as u16;
                            popup_origin = Some((nx, ny));
                        }
                    }
                    MouseEventKind::Up(MouseButton::Left) => {
                        drag_offset = None;
                    }
                    _ => {}
                }
            }
            _ => {}
        }
    }

    // ── Cleanup ──────────────────────────────────────────
    // Guard restores the terminal on drop (or via explicit cleanup()).

    if result.is_none() {
        eprintln!("{} menu: el terminal dejó de responder", crate::BIN_NAME);
        guard.cleanup();
        std::process::exit(1);
    }

    let output = result.unwrap();
    guard.cleanup();

    if output.is_empty() {
        std::process::exit(1);
    }

    println!("{}", output);
}
