//! `tear-tmux-backend` — typed-profile → `tmux.conf` renderer.
//!
//! Operators authoring a tear config get the SAME shikumi-driven
//! authoring experience whether the backend is in-process or
//! delegated to vanilla tmux: this crate's [`render_tmux_conf`]
//! reduces a typed [`tear_config::TearConfig`] to a tmux config
//! string. The escape hatch — for remote hosts with tmux but not
//! tear — is permanent, not transitional.
//!
//! ## What renders
//!
//! - `prefix` → `set -g prefix <chord>` + `unbind C-b` + `bind <chord> send-prefix`
//! - `default_shell` → `set -g default-shell <path>`
//! - `mouse` → `set -g mouse on|off`
//! - `base_index` → `set -g base-index <n>` + `set -g pane-base-index <n>`
//! - `keys` → `bind-key` lines (one per [`tear_types::KeyBind`])
//! - `status` → `set -g status-left/right` with `#{...}` format strings
//! - `theme` → `set -g status-style` + color statements
//!
//! Segments that tmux can't express natively (e.g. typed
//! conditionals) fall back to literal text and surface a comment
//! line so the operator sees what was dropped.

#![forbid(unsafe_code)]

use tear_config::TearConfig;
use tear_types::{Action, Direction, KeyBind, Segment, StatusBar, TearTheme};

/// Render a `TearConfig` to a tmux config string. Pure function —
/// deterministic, easy to snapshot-test.
#[must_use]
pub fn render_tmux_conf(cfg: &TearConfig) -> String {
    let mut out = String::new();
    out.push_str("# Generated by tear-tmux-backend.\n");
    out.push_str("# Source: ~/.config/tear/tear.yaml\n");
    out.push_str("# Edit the YAML, not this file.\n\n");

    render_prefix(&cfg.prefix, &mut out);
    render_shell(&cfg.default_shell, &mut out);
    render_mouse(cfg.mouse, &mut out);
    render_base_index(cfg.base_index, &mut out);
    out.push('\n');

    render_keys(&cfg.keys, &mut out);
    out.push('\n');

    render_status(&cfg.status, &cfg.theme, &mut out);
    out.push('\n');

    render_theme(&cfg.theme, &mut out);

    out
}

fn render_prefix(prefix: &str, out: &mut String) {
    if prefix.is_empty() {
        out.push_str("unbind C-b\n");
        return;
    }
    let chord = chord_to_tmux(prefix);
    out.push_str("unbind C-b\n");
    out.push_str(&format!("set -g prefix {chord}\n"));
    out.push_str(&format!("bind {chord} send-prefix\n"));
}

fn render_shell(shell: &str, out: &mut String) {
    out.push_str(&format!("set -g default-shell {shell}\n"));
    out.push_str(&format!("set -g default-command {shell}\n"));
}

fn render_mouse(mouse: bool, out: &mut String) {
    let v = if mouse { "on" } else { "off" };
    out.push_str(&format!("set -g mouse {v}\n"));
}

fn render_base_index(idx: u16, out: &mut String) {
    out.push_str(&format!("set -g base-index {idx}\n"));
    out.push_str(&format!("set -g pane-base-index {idx}\n"));
}

fn render_keys(keys: &[KeyBind], out: &mut String) {
    out.push_str("# ── bindings ──────────────────────────────────────\n");
    for kb in keys {
        let chord = chord_to_tmux(&kb.chord.0);
        let cmd = action_to_tmux(&kb.action);
        if !kb.note.is_empty() {
            out.push_str(&format!("# {}\n", kb.note));
        }
        out.push_str(&format!("bind {chord} {cmd}\n"));
    }
}

fn render_status(bar: &StatusBar, _theme: &TearTheme, out: &mut String) {
    out.push_str("# ── status bar ────────────────────────────────────\n");
    let v = if bar.visible { "on" } else { "off" };
    out.push_str(&format!("set -g status {v}\n"));
    out.push_str(&format!(
        "set -g status-interval {}\n",
        bar.refresh_interval_seconds
    ));
    out.push_str(&format!(
        "set -g status-left \"{}\"\n",
        segments_to_format(&bar.left)
    ));
    out.push_str(&format!(
        "set -g status-right \"{}\"\n",
        segments_to_format(&bar.right)
    ));
    if !bar.center.is_empty() {
        out.push_str(&format!(
            "# center segments don't have a direct tmux equivalent — rendered via window-status-format\n"
        ));
        out.push_str(&format!(
            "set -g window-status-format \"{}\"\n",
            segments_to_format(&bar.center)
        ));
    }
}

fn render_theme(theme: &TearTheme, out: &mut String) {
    out.push_str("# ── theme ─────────────────────────────────────────\n");
    out.push_str(&format!(
        "set -g status-style \"fg={},bg={}\"\n",
        theme.fg.0, theme.bg.0
    ));
    out.push_str(&format!(
        "set -g window-status-current-style \"fg={},bg={},bold\"\n",
        theme.active_fg.0, theme.active_bg.0
    ));
    out.push_str(&format!(
        "set -g window-status-style \"fg={},bg={}\"\n",
        theme.inactive_fg.0, theme.inactive_bg.0
    ));
    out.push_str(&format!(
        "set -g pane-border-style \"fg={}\"\n",
        theme.border_inactive.0
    ));
    out.push_str(&format!(
        "set -g pane-active-border-style \"fg={}\"\n",
        theme.border_active.0
    ));
    out.push_str(&format!(
        "set -g message-style \"fg={},bg={}\"\n",
        theme.message_fg.0, theme.message_bg.0
    ));
}

/// Translate our normalised `"ctrl+a"` style into tmux's `C-a`.
fn chord_to_tmux(chord: &str) -> String {
    // Drop the `C-b ` prefix tmux applies via send-prefix; the actual
    // bind takes only the chord that follows the prefix.
    let chord = chord
        .strip_prefix("ctrl+b ")
        .or_else(|| chord.strip_prefix("C-b "))
        .unwrap_or(chord);
    let mut parts: Vec<String> = Vec::new();
    let mut key = String::new();
    for seg in chord.split('+') {
        match seg.to_ascii_lowercase().as_str() {
            "ctrl" => parts.push("C".into()),
            "alt" => parts.push("M".into()),
            "shift" => parts.push("S".into()),
            "super" | "cmd" => parts.push("D".into()),
            other => key = other.into(),
        }
    }
    parts.push(key);
    parts.join("-")
}

fn action_to_tmux(a: &Action) -> String {
    match a {
        Action::SplitPane { direction } => match direction {
            Direction::Right => "split-window -h".into(),
            Direction::Left => "split-window -h -b".into(),
            Direction::Below => "split-window -v".into(),
            Direction::Above => "split-window -v -b".into(),
        },
        Action::SendKeys { keys } => format!("send-keys \"{keys}\""),
        Action::Command { cmd } => cmd.clone(),
        Action::SelectPane { direction } => match direction {
            Direction::Right => "select-pane -R".into(),
            Direction::Left => "select-pane -L".into(),
            Direction::Below => "select-pane -D".into(),
            Direction::Above => "select-pane -U".into(),
        },
        Action::NextWindow => "next-window".into(),
        Action::PreviousWindow => "previous-window".into(),
        Action::NewWindow => "new-window".into(),
        Action::KillPane => "kill-pane".into(),
        Action::KillWindow => "kill-window".into(),
        Action::KillSession => "kill-session".into(),
        Action::Detach => "detach-client".into(),
        Action::ReloadConfig => "source-file ~/.tmux.conf".into(),
        Action::EnterTable { table } => format!("switch-client -T {}", table.0),
    }
}

fn segments_to_format(segments: &[Segment]) -> String {
    let mut out = String::new();
    for s in segments {
        out.push_str(&segment_to_format(s));
    }
    out
}

fn segment_to_format(s: &Segment) -> String {
    match s {
        Segment::Text { value } => value.clone(),
        Segment::SessionName => "#S".into(),
        Segment::WindowName => "#W".into(),
        Segment::PaneCommand => "#{pane_current_command}".into(),
        Segment::PaneCwdBasename => "#{b:pane_current_path}".into(),
        Segment::Time { format } => format!("#({})", time_format_to_shell(format)),
        Segment::Hostname { short } => {
            if *short {
                "#h".into()
            } else {
                "#H".into()
            }
        }
        Segment::Shell { cmd, interval_seconds } => {
            // tmux re-runs `#(...)` every status-interval, so the
            // segment's own interval is informational here.
            let _ = interval_seconds;
            format!("#({cmd})")
        }
        Segment::If { cond, then, otherwise } => {
            format!(
                "#{{?{},{},{}}}",
                cond,
                segment_to_format(then),
                segment_to_format(otherwise)
            )
        }
        // A fleet signal renders to its literal glyph — tmux treats it as
        // verbatim status text. The mark itself is the fleet-uniform
        // vocabulary from the ishou atlas; *when* it shows (only while the
        // state is active) is the surrounding `If`'s `#{?...}` concern.
        Segment::Signal { signal, mode } => signal.render(*mode).to_string(),
    }
}

fn time_format_to_shell(strftime: &str) -> String {
    format!("date +'{strftime}'")
}

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

    #[test]
    fn default_config_renders_a_complete_tmux_conf() {
        let cfg = TearConfig::default();
        let s = render_tmux_conf(&cfg);
        assert!(s.contains("set -g prefix"));
        assert!(s.contains("set -g mouse on"));
        assert!(s.contains("set -g status"));
        assert!(s.contains("set -g status-left"));
        assert!(s.contains("set -g pane-active-border-style"));
    }

    #[test]
    fn chord_normalisation_matches_tmux_syntax() {
        assert_eq!(chord_to_tmux("ctrl+a"), "C-a");
        assert_eq!(chord_to_tmux("alt+left"), "M-left");
        assert_eq!(chord_to_tmux("ctrl+b %"), "%");
    }
}
