use std::process::{Command, Stdio};

use anyhow::{Context, Result, anyhow, bail};

#[derive(Debug, Clone)]
pub struct PaneInfo {
    pub id: String,
    pub pid: i32,
    pub cwd: String,
    pub command: String,
    pub session: String,
    pub window: String,
    pub active: bool,
    pub zoomed: bool,
    pub tag: Option<String>,
    pub home: Option<String>,
    pub state: Option<String>,
}

pub fn has_session(name: &str) -> bool {
    Command::new("tmux")
        .args(["has-session", "-t", name])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

pub fn list_panes() -> Result<Vec<PaneInfo>> {
    let output = tmux(&[
        "list-panes",
        "-a",
        "-F",
        "#{pane_id}\t#{pane_pid}\t#{pane_current_path}\t#{pane_current_command}\t#{session_name}\t#{window_id}\t#{pane_active}\t#{window_zoomed_flag}\t#{@tpane_tag}\t#{@tpane_home}\t#{@tpane_state}",
    ])?;

    output
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(parse_pane_info)
        .collect()
}

fn parse_pane_info(line: &str) -> Result<PaneInfo> {
    let mut parts = line.splitn(11, '\t');
    let id = parts
        .next()
        .ok_or_else(|| anyhow!("missing pane id in tmux output"))?
        .to_string();
    let pid = parts
        .next()
        .ok_or_else(|| anyhow!("missing pane pid in tmux output"))?
        .parse::<i32>()
        .with_context(|| format!("invalid pane pid in tmux output: {line}"))?;
    let cwd = parts.next().unwrap_or_default().to_string();
    let command = parts.next().unwrap_or_default().to_string();
    let session = parts.next().unwrap_or_default().to_string();
    let window = parts.next().unwrap_or_default().to_string();
    let active = parts.next() == Some("1");
    let zoomed = parts.next() == Some("1");
    let tag = nonempty(parts.next().unwrap_or_default());
    let home = nonempty(parts.next().unwrap_or_default());
    let state = nonempty(parts.next().unwrap_or_default());
    Ok(PaneInfo {
        id,
        pid,
        cwd,
        command,
        session,
        window,
        active,
        zoomed,
        tag,
        home,
        state,
    })
}

fn nonempty(value: &str) -> Option<String> {
    (!value.is_empty()).then(|| value.to_string())
}

pub fn bind_key(mode: &str, key: &str, command: &str, popup: bool, raw: bool) -> Result<()> {
    tmux_owned(bind_key_args(mode, key, command, popup, raw)).map(|_| ())
}

pub fn unbind_key(mode: &str, key: &str) -> Result<()> {
    tmux_owned(unbind_key_args(mode, key)).map(|_| ())
}

pub struct NewWindowOptions {
    pub name: Option<String>,
    pub cwd: Option<String>,
    pub command: Option<String>,
}

pub fn new_window(opts: NewWindowOptions) -> Result<String> {
    tmux_owned(new_window_args(&opts))
}

fn new_window_args(opts: &NewWindowOptions) -> Vec<String> {
    let mut args = vec![
        "new-window".to_string(),
        "-P".to_string(),
        "-F".to_string(),
        "#{window_id}".to_string(),
    ];
    if let Some(name) = &opts.name {
        args.push("-n".to_string());
        args.push(name.clone());
    }
    if let Some(cwd) = &opts.cwd {
        args.push("-c".to_string());
        args.push(cwd.clone());
    }
    if let Some(command) = &opts.command {
        args.push(command.clone());
    }
    args
}

pub fn select_window(target: &str) -> Result<()> {
    tmux_owned(select_window_args(target)).map(|_| ())
}

fn select_window_args(target: &str) -> Vec<String> {
    vec![
        "select-window".to_string(),
        "-t".to_string(),
        target.to_string(),
    ]
}

pub fn send_keys(target: &str, keys: &str, enter: bool) -> Result<()> {
    tmux_owned(send_keys_args(target, keys, enter)).map(|_| ())
}

fn send_keys_args(target: &str, keys: &str, enter: bool) -> Vec<String> {
    let mut args = vec![
        "send-keys".to_string(),
        "-t".to_string(),
        target.to_string(),
        keys.to_string(),
    ];
    if enter {
        args.push("Enter".to_string());
    }
    args
}

fn bind_key_args(mode: &str, key: &str, command: &str, popup: bool, raw: bool) -> Vec<String> {
    let mut args = vec!["bind-key".to_string()];
    match mode {
        "prefix" | "normal" | "n" => {}
        "root" => args.push("-n".to_string()),
        table => {
            args.push("-T".to_string());
            args.push(table.to_string());
        }
    }
    args.push(key.to_string());
    if popup {
        args.extend([
            "display-popup".to_string(),
            "-E".to_string(),
            "-w".to_string(),
            "80%".to_string(),
            "-h".to_string(),
            "80%".to_string(),
            command.to_string(),
        ]);
    } else if raw {
        args.push(command.to_string());
    } else {
        args.extend([
            "run-shell".to_string(),
            "-b".to_string(),
            command.to_string(),
        ]);
    }
    args
}

fn unbind_key_args(mode: &str, key: &str) -> Vec<String> {
    let mut args = vec!["unbind-key".to_string()];
    match mode {
        "prefix" | "normal" | "n" => {}
        "root" => args.push("-n".to_string()),
        table => {
            args.push("-T".to_string());
            args.push(table.to_string());
        }
    }
    args.push(key.to_string());
    args
}

pub fn set_global_var(name: &str, value: &str) -> Result<()> {
    tmux(&["set-option", "-g", name, value]).map(|_| ())
}

pub fn append_global_var(name: &str, value: &str) -> Result<()> {
    tmux(&["set-option", "-ga", name, value]).map(|_| ())
}

pub fn get_global_var(name: &str) -> Result<String> {
    tmux(&["show-option", "-gqv", name])
}

pub fn unset_global_var(name: &str) -> Result<()> {
    tmux(&["set-option", "-gu", name]).map(|_| ())
}

pub fn set_status(slot: &str, value: &str) -> Result<()> {
    tmux_owned(set_status_args(slot, value)).map(|_| ())
}

fn set_status_args(slot: &str, value: &str) -> Vec<String> {
    vec![
        "set-option".to_string(),
        "-g".to_string(),
        format!("status-{slot}"),
        value.to_string(),
    ]
}

pub fn set_status_position(position: &str) -> Result<()> {
    tmux_owned(set_status_position_args(position)).map(|_| ())
}

fn set_status_position_args(position: &str) -> Vec<String> {
    vec![
        "set-option".to_string(),
        "-g".to_string(),
        "status-position".to_string(),
        position.to_string(),
    ]
}

pub fn set_status_interval(interval: u64) -> Result<()> {
    tmux_owned(set_status_interval_args(interval)).map(|_| ())
}

pub fn refresh_status() -> Result<()> {
    tmux(&["refresh-client", "-S"]).map(|_| ())
}

fn set_status_interval_args(interval: u64) -> Vec<String> {
    vec![
        "set-option".to_string(),
        "-g".to_string(),
        "status-interval".to_string(),
        interval.to_string(),
    ]
}

pub fn set_pane_var(pane_id: &str, name: &str, value: &str) -> Result<()> {
    tmux(&["set-option", "-p", "-t", pane_id, name, value]).map(|_| ())
}

pub fn unset_pane_var(pane_id: &str, name: &str) -> Result<()> {
    tmux(&["set-option", "-u", "-p", "-t", pane_id, name]).map(|_| ())
}

pub fn get_pane_var(pane_id: &str, name: &str) -> Result<Option<String>> {
    let value = tmux(&[
        "display-message",
        "-p",
        "-t",
        pane_id,
        &format!("#{{{name}}}"),
    ])?;
    if value.is_empty() {
        Ok(None)
    } else {
        Ok(Some(value))
    }
}

pub fn select_pane(pane_id: &str) -> Result<()> {
    tmux(&["select-window", "-t", pane_id])?;
    tmux(&["select-pane", "-t", pane_id]).map(|_| ())
}

pub fn kill_pane(pane_id: &str) -> Result<()> {
    tmux_owned(kill_pane_args(pane_id)).map(|_| ())
}

fn kill_pane_args(pane_id: &str) -> Vec<String> {
    vec![
        "kill-pane".to_string(),
        "-t".to_string(),
        pane_id.to_string(),
    ]
}

pub fn set_pane_title(pane_id: &str, title: &str) -> Result<()> {
    tmux_owned(pane_title_args(pane_id, title)).map(|_| ())
}

fn pane_title_args(pane_id: &str, title: &str) -> Vec<String> {
    vec![
        "select-pane".to_string(),
        "-t".to_string(),
        pane_id.to_string(),
        "-T".to_string(),
        title.to_string(),
    ]
}

pub struct SplitOptions {
    pub direction: SplitDirection,
    pub before: bool,
    pub size: Option<String>,
    pub cwd: Option<String>,
    pub command: Option<String>,
    pub detached: bool,
    pub full: bool,
}

pub enum SplitDirection {
    Horizontal,
    Vertical,
}

pub fn split(target: &str, opts: SplitOptions) -> Result<String> {
    unzoom(target)?;
    tmux_owned(split_args(target, &opts))
}

fn split_args(target: &str, opts: &SplitOptions) -> Vec<String> {
    let mut args = vec![
        "split-window".to_string(),
        "-P".to_string(),
        "-F".to_string(),
        "#{pane_id}".to_string(),
        "-t".to_string(),
        target.to_string(),
    ];
    match opts.direction {
        SplitDirection::Horizontal => args.push("-h".to_string()),
        SplitDirection::Vertical => args.push("-v".to_string()),
    }
    if opts.before {
        args.push("-b".to_string());
    }
    if opts.detached {
        args.push("-d".to_string());
    }
    if opts.full {
        args.push("-f".to_string());
    }
    if let Some(size) = &opts.size {
        args.push("-l".to_string());
        args.push(size.clone());
    }
    if let Some(cwd) = &opts.cwd {
        args.push("-c".to_string());
        args.push(cwd.clone());
    }
    if let Some(command) = &opts.command {
        args.push(command.clone());
    }
    args
}

pub struct JoinOptions {
    pub horizontal: bool,
    pub size: Option<String>,
    pub full: bool,
}

pub fn join(src_pane: &str, target: &str, opts: JoinOptions) -> Result<()> {
    tmux_owned(join_args(src_pane, target, &opts)).map(|_| ())
}

fn join_args(src_pane: &str, target: &str, opts: &JoinOptions) -> Vec<String> {
    let mut args = vec![
        "join-pane".to_string(),
        "-s".to_string(),
        src_pane.to_string(),
        "-t".to_string(),
        target.to_string(),
    ];
    args.push(if opts.horizontal { "-h" } else { "-v" }.to_string());
    if opts.full {
        args.push("-f".to_string());
    }
    if let Some(size) = &opts.size {
        args.push("-l".to_string());
        args.push(size.clone());
    }
    args
}

pub fn break_pane(pane: &str, dst_session: &str, name: &str) -> Result<()> {
    tmux_owned(break_pane_args(pane, dst_session, name)).map(|_| ())
}

pub struct StashOptions {
    pub pane: String,
    pub window: String,
    pub cwd: String,
    pub name: String,
}

pub fn stash(opts: StashOptions) -> Result<()> {
    unzoom(&opts.window)?;
    let session = hidden_session(&opts.window);
    let created = !has_session(&session);
    if created {
        tmux_owned(new_hidden_session_args(&session, &opts.cwd))?;
    }
    tmux_owned(stash_break_args(&opts.pane, &session, &opts.name))?;
    if created {
        let _ = tmux_owned(kill_hidden_scratch_args(&session));
    }
    Ok(())
}

pub struct UnstashOptions {
    pub pane: String,
    pub target: String,
    pub horizontal: bool,
    pub size: Option<String>,
    pub full: bool,
}

pub fn unstash(opts: UnstashOptions) -> Result<()> {
    unzoom(&opts.target)?;
    join(
        &opts.pane,
        &opts.target,
        JoinOptions {
            horizontal: opts.horizontal,
            size: opts.size,
            full: opts.full,
        },
    )
}

pub fn cleanup_stash(window: &str) -> Result<()> {
    for session in [hidden_session(window), legacy_hidden_session(window)] {
        if has_session(&session) {
            kill_session(&session)?;
        }
    }
    Ok(())
}

pub fn kill_session(session: &str) -> Result<()> {
    tmux(&["kill-session", "-t", session]).map(|_| ())
}

pub fn hidden_session(window: &str) -> String {
    format!("__tpane-hidden-{window}")
}

fn legacy_hidden_session(window: &str) -> String {
    format!("__pi-hidden-{window}")
}

fn break_pane_args(pane: &str, dst_session: &str, name: &str) -> Vec<String> {
    vec![
        "break-pane".to_string(),
        "-d".to_string(),
        "-s".to_string(),
        pane.to_string(),
        "-t".to_string(),
        dst_session.to_string(),
        "-n".to_string(),
        name.to_string(),
    ]
}

fn new_hidden_session_args(session: &str, cwd: &str) -> Vec<String> {
    vec![
        "new-session".to_string(),
        "-d".to_string(),
        "-s".to_string(),
        session.to_string(),
        "-n".to_string(),
        "scratch".to_string(),
        "-c".to_string(),
        cwd.to_string(),
    ]
}

fn stash_break_args(pane: &str, session: &str, name: &str) -> Vec<String> {
    vec![
        "break-pane".to_string(),
        "-d".to_string(),
        "-s".to_string(),
        pane.to_string(),
        "-t".to_string(),
        format!("{session}:"),
        "-n".to_string(),
        name.to_string(),
    ]
}

fn kill_hidden_scratch_args(session: &str) -> Vec<String> {
    vec![
        "kill-window".to_string(),
        "-t".to_string(),
        format!("{session}:scratch"),
    ]
}

pub fn zoom(pane: &str) -> Result<()> {
    tmux(&["resize-pane", "-Z", "-t", pane]).map(|_| ())
}

pub fn unzoom(target: &str) -> Result<bool> {
    if !is_zoomed(target)? {
        return Ok(false);
    }
    let active = active_pane(target)?;
    zoom(&active)?;
    Ok(true)
}

pub fn is_zoomed(target: &str) -> Result<bool> {
    Ok(tmux(&[
        "display-message",
        "-p",
        "-t",
        target,
        "#{window_zoomed_flag}",
    ])? == "1")
}

pub fn current_pane() -> Result<String> {
    tmux(&["display-message", "-p", "#{pane_id}"])
}

pub fn current_window() -> Result<String> {
    tmux(&["display-message", "-p", "#{window_id}"])
}

pub fn active_pane(target: &str) -> Result<String> {
    tmux(&["display-message", "-p", "-t", target, "#{pane_id}"])
}

pub fn window_id(target: &str) -> Result<String> {
    tmux(&["display-message", "-p", "-t", target, "#{window_id}"])
}

pub fn display_message(target: &str, message: &str) -> Result<()> {
    tmux(&["display-message", "-t", target, message]).map(|_| ())
}

pub fn display_global_message(message: &str) -> Result<()> {
    tmux(&["display-message", message]).map(|_| ())
}

pub fn capture(pane: &str) -> Result<String> {
    tmux(&["capture-pane", "-p", "-t", pane])
}

pub fn server_alive() -> bool {
    Command::new("tmux")
        .args(["display-message", "-p", "ok"])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

fn tmux_owned(args: Vec<String>) -> Result<String> {
    let refs = args.iter().map(String::as_str).collect::<Vec<_>>();
    tmux(&refs)
}

fn tmux(args: &[&str]) -> Result<String> {
    let output = Command::new("tmux")
        .args(args)
        .output()
        .with_context(|| format!("failed to run tmux {}", args.join(" ")))?;

    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout)
            .trim_end()
            .to_string())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("tmux {} failed: {}", args.join(" "), stderr.trim())
    }
}

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

    #[test]
    fn parse_pane_info_reads_window_id_tag_and_home() {
        let pane =
            parse_pane_info("%1\t42\t/tmp/work\tzsh\tmain\t@7\t1\t0\tagent\t@7\tblocked").unwrap();
        assert_eq!(pane.id, "%1");
        assert_eq!(pane.pid, 42);
        assert_eq!(pane.command, "zsh");
        assert_eq!(pane.window, "@7");
        assert!(pane.active);
        assert!(!pane.zoomed);
        assert_eq!(pane.tag.as_deref(), Some("agent"));
        assert_eq!(pane.home.as_deref(), Some("@7"));
        assert_eq!(pane.state.as_deref(), Some("blocked"));
    }

    #[test]
    fn parse_pane_info_treats_empty_tag_and_home_as_none() {
        let pane = parse_pane_info("%1\t42\t/tmp/work\tzsh\tmain\t@7\t0\t1\t\t\t").unwrap();
        assert!(!pane.active);
        assert!(pane.zoomed);
        assert_eq!(pane.tag, None);
        assert_eq!(pane.home, None);
        assert_eq!(pane.state, None);
    }

    #[test]
    fn bind_key_args_are_built_without_running_tmux() {
        assert_eq!(
            bind_key_args("prefix", "A", "tpane pi expand", false, false),
            vec!["bind-key", "A", "run-shell", "-b", "tpane pi expand"]
        );
        assert_eq!(
            bind_key_args("root", "M-a", "tpane pi", false, false),
            vec!["bind-key", "-n", "M-a", "run-shell", "-b", "tpane pi"]
        );
        assert_eq!(
            bind_key_args("copy-mode-vi", "v", "tpane copy", false, false),
            vec![
                "bind-key",
                "-T",
                "copy-mode-vi",
                "v",
                "run-shell",
                "-b",
                "tpane copy"
            ]
        );
        assert_eq!(
            bind_key_args("prefix", "Space", "tpane control", true, false),
            vec![
                "bind-key",
                "Space",
                "display-popup",
                "-E",
                "-w",
                "80%",
                "-h",
                "80%",
                "tpane control"
            ]
        );
        assert_eq!(
            bind_key_args("prefix", "h", "select-pane -L", false, true),
            vec!["bind-key", "h", "select-pane -L"]
        );
    }

    #[test]
    fn status_args_are_built_without_running_tmux() {
        assert_eq!(
            set_status_args("right", "#{session_name}"),
            vec!["set-option", "-g", "status-right", "#{session_name}"]
        );
        assert_eq!(
            set_status_position_args("top"),
            vec!["set-option", "-g", "status-position", "top"]
        );
        assert_eq!(
            set_status_interval_args(1),
            vec!["set-option", "-g", "status-interval", "1"]
        );
    }

    #[test]
    fn window_and_send_key_args_are_built_without_running_tmux() {
        assert_eq!(
            new_window_args(&NewWindowOptions {
                name: Some("logs".to_string()),
                cwd: Some("/tmp/work".to_string()),
                command: Some("zsh".to_string()),
            }),
            vec![
                "new-window",
                "-P",
                "-F",
                "#{window_id}",
                "-n",
                "logs",
                "-c",
                "/tmp/work",
                "zsh",
            ]
        );
        assert_eq!(select_window_args("@7"), vec!["select-window", "-t", "@7"]);
        assert_eq!(
            send_keys_args("%1", "npm test", true),
            vec!["send-keys", "-t", "%1", "npm test", "Enter"]
        );
        assert_eq!(
            send_keys_args("%1", "C-c", false),
            vec!["send-keys", "-t", "%1", "C-c"]
        );
    }

    #[test]
    fn split_args_are_built_without_running_tmux() {
        let args = split_args(
            "%1",
            &SplitOptions {
                direction: SplitDirection::Horizontal,
                before: false,
                size: Some("30%".to_string()),
                cwd: Some("/tmp/work".to_string()),
                command: Some("nvim".to_string()),
                detached: true,
                full: true,
            },
        );

        assert_eq!(
            args,
            vec![
                "split-window",
                "-P",
                "-F",
                "#{pane_id}",
                "-t",
                "%1",
                "-h",
                "-d",
                "-f",
                "-l",
                "30%",
                "-c",
                "/tmp/work",
                "nvim",
            ]
        );
    }

    #[test]
    fn join_args_are_built_without_running_tmux() {
        let args = join_args(
            "%2",
            "%1",
            &JoinOptions {
                horizontal: false,
                size: Some("40".to_string()),
                full: true,
            },
        );

        assert_eq!(
            args,
            vec!["join-pane", "-s", "%2", "-t", "%1", "-v", "-f", "-l", "40"]
        );
    }

    #[test]
    fn break_pane_args_are_built_without_running_tmux() {
        assert_eq!(
            break_pane_args("%2", "hidden", "agent"),
            vec![
                "break-pane",
                "-d",
                "-s",
                "%2",
                "-t",
                "hidden",
                "-n",
                "agent"
            ]
        );
    }

    #[test]
    fn kill_pane_args_are_built_without_running_tmux() {
        assert_eq!(kill_pane_args("%9"), vec!["kill-pane", "-t", "%9"]);
    }

    #[test]
    fn pane_title_args_are_built_without_running_tmux() {
        assert_eq!(
            pane_title_args("%1", "agent"),
            vec!["select-pane", "-t", "%1", "-T", "agent"]
        );
    }

    #[test]
    fn stash_arg_builders_are_pure() {
        assert_eq!(hidden_session("@7"), "__tpane-hidden-@7");
        assert_eq!(legacy_hidden_session("@7"), "__pi-hidden-@7");
        assert_eq!(
            new_hidden_session_args("__tpane-hidden-@7", "/tmp/work"),
            vec![
                "new-session",
                "-d",
                "-s",
                "__tpane-hidden-@7",
                "-n",
                "scratch",
                "-c",
                "/tmp/work"
            ]
        );
        assert_eq!(
            stash_break_args("%2", "__tpane-hidden-@7", "agent-sidebar"),
            vec![
                "break-pane",
                "-d",
                "-s",
                "%2",
                "-t",
                "__tpane-hidden-@7:",
                "-n",
                "agent-sidebar"
            ]
        );
        assert_eq!(
            kill_hidden_scratch_args("__tpane-hidden-@7"),
            vec!["kill-window", "-t", "__tpane-hidden-@7:scratch"]
        );
    }
}
