use clap::Args;
use crossterm::{
    event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyModifiers, MouseEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::Rect,
    style::Style,
    widgets::StatefulWidget,
    Terminal,
};
use hefesto_widgets::{PopupSize, SpinPopup, SpinState, SpinVariant};

use crate::{keybinds, style};

use std::io::BufRead;
use std::process::Stdio;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

const TICK_MS: u64 = 100;
const MAX_OUTPUT_LINES: usize = 200;
const CMD_ICON: &str = " ";

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

#[derive(Args)]
pub struct SpinArgs {
    #[arg(short, long, default_value = "Procesando...")]
    pub title: String,

    #[arg(short = 'c', long)]
    pub cmd: Option<String>,

    #[arg(last = true)]
    pub command: Vec<String>,

    #[arg(short = 'l', long)]
    pub logs: bool,

    #[arg(short = 'v', long)]
    pub verbose: bool,

    /// Wait for Enter before closing after command completes
    #[arg(short = 'w', long)]
    pub wait: bool,

    /// Animation variant: dots, line, dots2, bounce, pulse, arrows, square, clock
    #[arg(short = 'a', long, default_value = "dots")]
    pub animation: String,

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

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

    #[arg(long)]
    pub guide: bool,
}

struct ResolvedCmd {
    display: String,
    program: String,
    args: Vec<String>,
}

fn resolve_cmd(cmd: &Option<String>, command: &[String]) -> Option<ResolvedCmd> {
    if let Some(c) = cmd {
        let parts: Vec<&str> = c.split_whitespace().collect();
        let first = parts.first()?;
        Some(ResolvedCmd {
            display: c.clone(),
            program: first.to_string(),
            args: parts[1..].iter().map(|s| s.to_string()).collect(),
        })
    } else if !command.is_empty() {
        Some(ResolvedCmd {
            display: command.join(" "),
            program: command[0].clone(),
            args: command[1..].to_vec(),
        })
    } else {
        None
}
}

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

fn is_on_border(popup: Rect, col: u16, row: u16) -> bool {
    if !contains(popup, col, row) {
        return false;
    }
    let inner = Rect {
        x: popup.x + 1,
        y: popup.y + 1,
        width: popup.width.saturating_sub(2),
        height: popup.height.saturating_sub(2),
    };
    !contains(inner, col, row)
}

fn is_drag_area(popup: Rect, col: u16, row: u16) -> bool {
    if !contains(popup, col, row) {
        return false;
    }
    if is_on_border(popup, col, row) {
        return true;
    }
    let header_top = popup.y + 1;
    let header_bottom = header_top + 2;
    row >= header_top && row < header_bottom
}

fn parse_variant(s: &str) -> SpinVariant {
    match s.to_lowercase().as_str() {
        "line" => SpinVariant::Line,
        "dots2" => SpinVariant::Dots2,
        "bounce" => SpinVariant::Bounce,
        "pulse" => SpinVariant::Pulse,
        "arrows" => SpinVariant::Arrows,
        "square" => SpinVariant::Square,
        "clock" => SpinVariant::Clock,
        _ => SpinVariant::Dots,
    }
}

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

    if resolve_cmd(&args.cmd, &args.command).is_none() {
        eprintln!("{} spin: se requiere un comando. Usa -c \"comando\" o -- comando.", crate::BIN_NAME);
        std::process::exit(1);
    }

    let mut tty: Box<dyn std::io::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!("{} spin: el terminal no es interactivo", crate::BIN_NAME);
        std::process::exit(1);
    }
    let mut terminal = Terminal::new(CrosstermBackend::new(tty)).unwrap();
    terminal.clear().unwrap();
    terminal.hide_cursor().unwrap();

    let mut spin_state = SpinState::default();

    let resolved = resolve_cmd(&args.cmd, &args.command);
    let cmd_display = resolved.as_ref().map_or(String::new(), |r| r.display.clone());
    let has_cmd = !cmd_display.is_empty();

    let output_lines: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));

    let (tx, rx) = mpsc::channel::<Option<(i32, Vec<u8>, Vec<u8>)>>();

    let cmd_thread = if has_cmd {
        let r = resolved.unwrap();
        let tx = tx.clone();
        let lines = output_lines.clone();
        let verbose = args.verbose;

        Some(thread::spawn(move || {
            if verbose {
                let mut child = match std::process::Command::new(&r.program)
                    .args(&r.args)
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .spawn()
                {
                    Ok(c) => c,
                    Err(e) => {
                        let _ = tx.send(None);
                        return Err(e);
                    }
                };

                let out_lines = lines.clone();
                let out_reader = if let Some(stdout) = child.stdout.take() {
                    Some(thread::spawn(move || {
                        for line in std::io::BufReader::new(stdout).lines() {
                            if let Ok(l) = line {
                                let mut buf = out_lines.lock().unwrap();
                                buf.push(l);
                                if buf.len() > MAX_OUTPUT_LINES {
                                    buf.remove(0);
                                }
                            }
                        }
                    }))
                } else {
                    None
                };

                let err_lines = lines.clone();
                let err_reader = if let Some(stderr) = child.stderr.take() {
                    Some(thread::spawn(move || {
                        for line in std::io::BufReader::new(stderr).lines() {
                            if let Ok(l) = line {
                                let mut buf = err_lines.lock().unwrap();
                                buf.push(l);
                                if buf.len() > MAX_OUTPUT_LINES {
                                    buf.remove(0);
                                }
                            }
                        }
                    }))
                } else {
                    None
                };

                let status = child.wait();
                if let Some(h) = out_reader { let _ = h.join(); }
                if let Some(h) = err_reader { let _ = h.join(); }

                let code = status.ok().and_then(|s| s.code()).unwrap_or(1);
                let _ = tx.send(Some((code, Vec::new(), Vec::new())));
                Ok(())
            } else {
                let output = std::process::Command::new(&r.program)
                    .args(&r.args)
                    .output();
                match output {
                    Ok(o) => {
                        let code = o.status.code().unwrap_or(1);
                        let _ = tx.send(Some((code, o.stdout, o.stderr)));
                        Ok(())
                    }
                    Err(e) => {
                        let _ = tx.send(None);
                        Err(e)
                    }
                }
            }
        }))
    } else {
        None
    };

    let mut should_exit = false;
    let mut command_finished = false;
    let mut waiting = false;
    let mut cmd_exit: Option<(i32, Vec<u8>, Vec<u8>)> = None;
    let mut drag_offset: Option<(u16, u16)> = None;
    let mut origin: Option<(u16, u16)> = None;
    let mut pending_g: bool = false;

    while !should_exit {
        let size = terminal.size().unwrap();
        let area = Rect::new(0, 0, size.width, size.height);

        let pw = if args.width > 0 { args.width } else { 44 };

        let output_snapshot: Vec<String> = {
            let locked = output_lines.lock().unwrap();
            let start = locked.len().saturating_sub(100);
            locked[start..].to_vec()
        };
        let mut output_strs: Vec<&str> = output_snapshot.iter().map(|s| s.as_str()).collect();

        if args.height > 0 {
            let cmd_rows: u16 = if has_cmd { 2 } else { 0 };
            let footer_rows: u16 = if spin_state.finished { 2 } else { 0 };
            let out_rows = args.height.saturating_sub(4).saturating_sub(cmd_rows).saturating_sub(footer_rows);
            if (output_strs.len() as u16) > out_rows {
                output_strs.truncate(out_rows as usize);
            }
            let pad = (out_rows as usize).saturating_sub(output_strs.len());
            let blanks: Vec<&str> = std::iter::repeat("").take(pad).collect();
            output_strs.extend(blanks);
        }

        let mut popup = SpinPopup::new()
            .title(&args.title)
            .variant(parse_variant(&args.animation))
            .spinner_style(Style::new().fg(style::ACCENT))
            .command_style(Style::new().fg(style::TEXT))
            .output_style(Style::new().fg(style::MUTED));

        if args.width > 0 {
            popup = popup.width(PopupSize::Fixed(pw));
        }

        if args.height > 0 {
            popup = popup.height(PopupSize::Fixed(args.height));
        }

        if let Some((ox, oy)) = origin {
            popup = popup.origin(ox, oy);
        }

        let cmd_display_icon = if has_cmd {
            Some(format!("{}{}", CMD_ICON, cmd_display))
        } else {
            None
        };

        if let Some(ref display) = cmd_display_icon {
            popup = popup.command(display);
        }

        if args.verbose {
            let max_rows = if args.height > 0 {
                args.height.saturating_sub(6)
            } else {
                10
            };
            popup = popup.max_output_rows(max_rows.max(3));
        }

        if !output_strs.is_empty() {
            popup = popup.output_lines(&output_strs);
        }

        let pr = popup.resolve_rect(area);

        terminal
            .draw(|frame| {
                StatefulWidget::render(popup, frame.area(), frame.buffer_mut(), &mut spin_state);
            })
            .unwrap();

        if let Some(ref handle) = cmd_thread {
            if !command_finished {
                if let Ok(Some(result)) = rx.try_recv() {
                    cmd_exit = Some(result);
                    command_finished = true;
                } else if rx.try_recv().is_ok() {
                    command_finished = true;
                }
            }
            if command_finished && handle.is_finished() {
                spin_state.finished = true;
                spin_state.exit_code = cmd_exit.as_ref().map(|(code, _, _)| *code);
                if args.wait {
                    waiting = true;
                } else {
                    should_exit = true;
                    continue;
                }
            }
        }

        if crossterm::event::poll(Duration::from_millis(TICK_MS)).unwrap_or(false) {
            match read().unwrap() {
                Event::Key(key) => {
                    if key.code == keybinds::EMERGENCY && key.modifiers == KeyModifiers::CONTROL {
                        should_exit = true;
                    } else if args.verbose {
                        match key.code {
                            keybinds::UP | keybinds::UP_ALT => {
                                spin_state.output_previous();
                                pending_g = false;
                            }
                            keybinds::DOWN | keybinds::DOWN_ALT => {
                                spin_state.output_next(output_strs.len());
                                pending_g = false;
                            }
                            keybinds::FIRST => {
                                if pending_g {
                                    spin_state.scroll_list.select(Some(0));
                                    pending_g = false;
                                } else {
                                    pending_g = true;
                                }
                            }
                            keybinds::LAST => {
                                spin_state.output_last(output_strs.len());
                                pending_g = false;
                            }
                            _ => pending_g = false,
                        }
                    }
                    if waiting {
                        match key.code {
                            keybinds::CONFIRM | keybinds::CANCEL | keybinds::CANCEL_ALT => {
                                should_exit = true;
                            }
                            _ => {}
                        }
                    } else {
                        match key.code {
                            keybinds::CANCEL | keybinds::CANCEL_ALT => {
                                if cmd_thread.is_none() {
                                    should_exit = true;
                                }
                            }
                            _ => {}
                        }
                    }
                }
                Event::Mouse(mouse) => {
                    let col = mouse.column;
                    let row = mouse.row;
                    if mouse.kind == MouseEventKind::Down(crossterm::event::MouseButton::Left) {
                        if is_drag_area(pr, col, row) {
                            let ox = col.saturating_sub(pr.x);
                            let oy = row.saturating_sub(pr.y);
                            drag_offset = Some((ox, oy));
                        }
                    }
                    else if let Some((dx, dy)) = drag_offset {
                        match mouse.kind {
                            MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
                                let max_w = area.width.saturating_sub(pr.width);
                                let max_h = area.height.saturating_sub(pr.height);
                                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;
                                origin = Some((nx, ny));
                            }
                            MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
                                drag_offset = None;
                            }
                            _ => {}
                        }
                    }
                }
                _ => {}
            }
        }

        spin_state.tick(10);
    }

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

    if let Some((code, stdout, stderr)) = cmd_exit {
        if args.logs {
            use std::io::Write;
            std::io::stdout().write_all(&stdout).unwrap();
            std::io::stderr().write_all(&stderr).unwrap();
        }
        std::process::exit(code);
    }
}
