//! Tmux integration utilities.
//!
//! This module provides functionality for running skim within tmux panes,
//! allowing skim to be used as a tmux popup or split pane.

use std::{
    borrow::Cow,
    env,
    fmt::Write as FmtWrite,
    io::{BufRead as _, BufReader, BufWriter, IsTerminal as _, Write as _},
    process::{Command, Stdio},
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    thread,
};

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use nix::sys::stat::Mode;
use nix::unistd::mkfifo;
use rand::{RngExt as _, distr::Alphanumeric};
use which::which;

use crate::{
    Rank, SkimItem, SkimOptions, SkimOutput,
    item::{MatchedItem, RankBuilder},
    tui::{Event, event::Action},
};

#[derive(Debug, PartialEq, Eq)]
enum TmuxWindowDir {
    Center,
    Top,
    Bottom,
    Left,
    Right,
}

impl From<&str> for TmuxWindowDir {
    fn from(value: &str) -> Self {
        use TmuxWindowDir::{Bottom, Center, Left, Right, Top};
        match value {
            "top" => Top,
            "bottom" => Bottom,
            "left" => Left,
            "right" => Right,
            _ => Center, // includes "center" and all unknown values
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
struct TmuxOptions<'a> {
    width: &'a str,
    height: &'a str,
    x: &'a str,
    y: &'a str,
}

struct SkimTmuxOutput {
    line: String,
}

impl SkimItem for SkimTmuxOutput {
    fn text(&self) -> Cow<'_, str> {
        Cow::from(&self.line)
    }
}

impl<'a> From<&'a String> for TmuxOptions<'a> {
    fn from(value: &'a String) -> Self {
        let (raw_dir, size) = value.split_once(',').unwrap_or((value, "50%"));
        let dir = TmuxWindowDir::from(raw_dir);
        let (height, width) = if let Some((lhs, rhs)) = size.split_once(',') {
            match dir {
                TmuxWindowDir::Center | TmuxWindowDir::Left | TmuxWindowDir::Right => (rhs, lhs),
                TmuxWindowDir::Top | TmuxWindowDir::Bottom => (lhs, rhs),
            }
        } else {
            match dir {
                TmuxWindowDir::Left | TmuxWindowDir::Right => ("100%", size),
                TmuxWindowDir::Top | TmuxWindowDir::Bottom => (size, "100%"),
                TmuxWindowDir::Center => (size, size),
            }
        };

        let (x, y) = match dir {
            TmuxWindowDir::Center => ("C", "C"),
            TmuxWindowDir::Top => ("C", "0%"),
            TmuxWindowDir::Bottom => ("C", "100%"),
            TmuxWindowDir::Left => ("0%", "C"),
            TmuxWindowDir::Right => ("100%", "C"),
        };

        Self { width, height, x, y }
    }
}

/// Run skim in a tmux popup
///
/// This will extract the tmux options, then build a new sk command
/// without them and send it to tmux in a popup.
///
/// # Panics
///
/// Panics if the temporary directory for IPC cannot be created.
#[allow(clippy::too_many_lines)]
pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
    // Create temp dir for downstream output
    let temp_dir_name = format!(
        "sk-tmux-{}",
        &rand::rng()
            .sample_iter(&Alphanumeric)
            .take(8)
            .map(char::from)
            .collect::<String>(),
    );
    let temp_dir = std::env::temp_dir().join(&temp_dir_name);
    std::fs::create_dir(&temp_dir)
        .unwrap_or_else(|e| panic!("Failed to create temp dir {}: {}", temp_dir.display(), e));

    debug!("Created temp dir {}", temp_dir.display());
    let tmp_stdout = temp_dir.join("stdout");
    let tmp_stdin = temp_dir.join("stdin");

    let has_piped_input = !std::io::stdin().is_terminal();
    let mut stdin_reader = BufReader::new(std::io::stdin());
    let line_ending = if opts.read0 { b'\0' } else { b'\n' };

    let stop_reading = Arc::new(AtomicBool::new(false));
    let _stdin_handle = if has_piped_input {
        debug!("Reading stdin and piping to fifo");

        // Create a named pipe (FIFO)
        // This allows the nested skim to continuously read as data arrives
        let stdin_path_str = tmp_stdin
            .to_str()
            .unwrap_or_else(|| panic!("Failed to convert stdin path to string"));
        mkfifo(stdin_path_str, Mode::S_IRUSR | Mode::S_IWUSR)
            .unwrap_or_else(|e| panic!("Failed to create fifo {}: {}", tmp_stdin.display(), e));

        let tmp_stdin_clone = tmp_stdin.clone();
        let stop_flag = Arc::clone(&stop_reading);
        Some(thread::spawn(move || {
            debug!("Opening fifo for writing (may block until reader starts)");
            let stdin_f = std::fs::File::create(tmp_stdin_clone.clone())
                .unwrap_or_else(|e| panic!("Failed to open fifo {}: {}", tmp_stdin_clone.display(), e));
            debug!("Fifo opened for writing");
            let mut stdin_writer = BufWriter::new(stdin_f);
            loop {
                // Check if we should stop reading
                if stop_flag.load(Ordering::Relaxed) {
                    debug!("Stop signal received, exiting stdin reader thread");
                    break;
                }

                let mut buf = vec![];
                match stdin_reader.read_until(line_ending, &mut buf) {
                    Ok(0) => break,
                    Ok(n) => {
                        debug!("Read {n} bytes from stdin");
                        stdin_writer.write_all(&buf).unwrap();
                    }
                    Err(e) => panic!("Failed to read from stdin: {e}"),
                }
            }
            // Ensure all buffered data is written to the file
            let _ = stdin_writer.flush();
        }))
    } else {
        None
    };

    // Build args to send to downstream sk invocation
    let mut tmux_shell_cmd = String::new();
    let mut prev_is_tmux_flag = false;
    let mut prev_is_output_format_flag = false;
    // We keep argv[0] to use in the popup's command
    for arg in std::env::args() {
        debug!("Got arg {arg}");
        if prev_is_tmux_flag {
            prev_is_tmux_flag = false;
            if !arg.starts_with('-') {
                continue;
            }
        } else if prev_is_output_format_flag {
            prev_is_output_format_flag = false;
            continue;
        }
        if arg == "--tmux" {
            debug!("Found tmux arg, skipping this and the next");
            prev_is_tmux_flag = true;
            continue;
        } else if arg.starts_with("--tmux") {
            debug!("Found equal tmux arg, skipping");
            continue;
        } else if arg == "--output-format" {
            debug!("Found output format arg, skipping this and the next");
            prev_is_output_format_flag = true;
            continue;
        } else if arg.starts_with("--output-format") {
            debug!("Found equal output format arg, skipping");
            continue;
        }
        push_quoted_arg(&mut tmux_shell_cmd, &arg);
    }
    // Always add all --print-xxx flags to the child sk command so that the output
    // is fully structured and can be parsed unconditionally below, regardless of
    // which flags the user originally passed.
    for flag in &[
        "--print-query",
        "--print-cmd",
        "--print-header",
        "--print-current",
        "--print-score",
    ] {
        let _ = write!(tmux_shell_cmd, " {flag}");
    }
    tmux_shell_cmd = tmux_shell_cmd.replace("--output-format", "");

    if has_piped_input {
        let _ = write!(tmux_shell_cmd, " <{}", tmp_stdin.display());
    }
    let _ = write!(tmux_shell_cmd, " >{}", tmp_stdout.display());

    debug!("build cmd {}", &tmux_shell_cmd);

    // Run downstream sk in tmux
    let raw_tmux_opts = &opts.tmux.clone().unwrap();
    let tmux_opts = TmuxOptions::from(raw_tmux_opts);
    let mut tmux_cmd = Command::new(which("tmux").unwrap_or_else(|e| panic!("Failed to find tmux in path: {e}")));

    tmux_cmd
        .arg("display-popup")
        .arg("-E")
        .args(["-d", std::env::current_dir().unwrap().to_str().unwrap()])
        .args(["-h", tmux_opts.height])
        .args(["-w", tmux_opts.width])
        .args(["-x", tmux_opts.x])
        .args(["-y", tmux_opts.y]);

    for (name, value) in std::env::vars() {
        if name.starts_with("SKIM") || name == "PATH" || name.starts_with("RUST") {
            let value = sanitize_value(value);
            debug!("adding {name} = {value} to the command's env");
            tmux_cmd.args(["-e", &format!("{name}={value}")]);
        }
    }

    tmux_cmd.args(["sh", "-c", &tmux_shell_cmd]);

    debug!("tmux command: {tmux_cmd:?}");

    let status = tmux_cmd
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .stdin(Stdio::null())
        .status()
        .unwrap_or_else(|e| panic!("Tmux invocation failed with {e}"));

    // Signal the stdin thread to stop and wait for it to exit
    stop_reading.store(true, Ordering::Relaxed);

    let output_ending = if opts.print0 { "\0" } else { "\n" };
    let mut stdout_bytes = std::fs::read_to_string(tmp_stdout).unwrap_or_default();
    stdout_bytes.pop();
    let mut stdout = stdout_bytes.split(output_ending);
    let _ = std::fs::remove_dir_all(temp_dir);

    // The child sk process always runs with --print-query, --print-cmd, --print-header,
    // and --print-score, so we always read those lines unconditionally.
    let query_str = if status.success() {
        stdout.next().unwrap_or_default()
    } else {
        ""
    };

    let command_str = if status.success() {
        stdout.next().unwrap_or_default()
    } else {
        ""
    };

    let header = if status.success() {
        stdout.next().unwrap_or_default()
    } else {
        ""
    }
    .to_string();

    let current: Option<MatchedItem> = if status.success() {
        let line = stdout.next().unwrap_or_default();
        if line.is_empty() {
            None
        } else {
            Some(MatchedItem::new(
                Arc::new(SkimTmuxOutput { line: line.to_string() }),
                Rank::default(),
                None,
                &RankBuilder::default(),
            ))
        }
    } else {
        None
    };

    let mut output_lines: Vec<MatchedItem> = vec![];
    while let Some(line) = stdout.next() {
        debug!("Adding output line: {line}");
        // --print-score is always enabled in the child, so every item is followed by its score.
        let score: i32 = stdout.next().unwrap_or_default().parse().unwrap_or_default();
        let rank = Rank {
            score,
            ..Default::default()
        };
        let item = MatchedItem::new(
            Arc::new(SkimTmuxOutput { line: line.to_string() }),
            rank,
            None,
            &RankBuilder::default(),
        );
        output_lines.push(item);
    }

    let is_abort = !status.success();
    let final_event = if is_abort {
        Event::Action(Action::Abort)
    } else {
        Event::Action(Action::Accept(None))
    };

    let skim_output = SkimOutput {
        final_event,
        is_abort,
        final_key: KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
        // Note: In tmux mode, the actual final key is not available since skim runs in a separate
        // tmux popup process. Only the output text is captured. Use --expect with --bind to capture
        // specific accept keys in the output if needed.
        query: query_str.to_string(),
        cmd: command_str.to_string(),
        selected_items: output_lines,
        current,
        header,
    };
    Some(skim_output)
}

fn push_quoted_arg(args_str: &mut String, arg: &str) {
    use shell_quote::{Bash, Fish, Quote as _, Sh, Zsh};
    let shell_path = env::var("SHELL").unwrap_or(String::from("/bin/sh"));
    let shell = shell_path.rsplit_once('/').unwrap_or(("", "sh")).1;
    let quoted_arg: Vec<u8> = match shell {
        "zsh" => Zsh::quote(arg),
        "bash" => Bash::quote(arg),
        "fish" => Fish::quote(arg),
        _ => Sh::quote(arg),
    };
    let _ = write!(
        args_str,
        " {}",
        String::from_utf8(quoted_arg).expect("Failed to parse quoted arg as utf8, this should not happen")
    );
}

fn sanitize_value(value: String) -> String {
    if !value.ends_with(';') {
        return value;
    }

    let mut value = value.clone();
    value.replace_range(value.len() - 1.., "\\;");
    value
}

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

    fn check(input: &str, height: &str, width: &str, x: &str, y: &str) {
        assert_eq!(
            TmuxOptions::from(&String::from(input)),
            TmuxOptions { width, height, x, y }
        );
    }

    #[test]
    fn tmux_options_default() {
        check("", "50%", "50%", "C", "C");
    }
    #[test]
    fn tmux_options_center() {
        let (x, y) = ("C", "C");
        check("center", "50%", "50%", x, y);
        check("center,10", "10", "10", x, y);
        check("center,10,20", "20", "10", x, y);
        check("center,10%,20", "20", "10%", x, y);
        check("center,10%,20%", "20%", "10%", x, y);
    }
    #[test]
    fn tmux_options_top() {
        let (x, y) = ("C", "0%");
        check("top", "50%", "100%", x, y);
        check("top,10", "10", "100%", x, y);
        check("top,10,20", "10", "20", x, y);
        check("top,10%,20", "10%", "20", x, y);
        check("top,10%,20%", "10%", "20%", x, y);
    }
    #[test]
    fn tmux_options_bottom() {
        let (x, y) = ("C", "100%");
        check("bottom", "50%", "100%", x, y);
        check("bottom,10", "10", "100%", x, y);
        check("bottom,10,20", "10", "20", x, y);
        check("bottom,10%,20", "10%", "20", x, y);
        check("bottom,10%,20%", "10%", "20%", x, y);
    }
    #[test]
    fn tmux_options_left() {
        let (x, y) = ("0%", "C");
        check("left", "100%", "50%", x, y);
        check("left,10", "100%", "10", x, y);
        check("left,10,20", "20", "10", x, y);
        check("left,10%,20", "20", "10%", x, y);
        check("left,10%,20%", "20%", "10%", x, y);
    }
    #[test]
    fn tmux_options_right() {
        let (x, y) = ("100%", "C");
        check("right", "100%", "50%", x, y);
        check("right,10", "100%", "10", x, y);
        check("right,10,20", "20", "10", x, y);
        check("right,10%,20", "20", "10%", x, y);
        check("right,10%,20%", "20%", "10%", x, y);
    }

    #[test]
    fn test_sanitize_value() {
        assert_eq!(sanitize_value("some-value".to_string()), "some-value".to_string());
        assert_eq!(sanitize_value("some-value;".to_string()), "some-value\\;".to_string());
        assert_eq!(sanitize_value("some-value;;".to_string()), "some-value;\\;".to_string());
        assert_eq!(
            sanitize_value("some-value;;;".to_string()),
            "some-value;;\\;".to_string()
        );
        assert_eq!(sanitize_value("some-value;x".to_string()), "some-value;x".to_string());
        assert_eq!(
            sanitize_value("some-value;x;".to_string()),
            "some-value;x\\;".to_string()
        );
    }
}
