use std::borrow::Cow;
use std::io::Write;

use crate::notifications::tmux;
use crate::terminal::{MultiplexerKind, TerminalContext, TerminalName};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotificationProtocol {
    /// iTerm2/WezTerm/Warp: `\x1b]9;{message}\x07`
    Osc9,
    /// Kitty: `\x1b]99;i=kigi;{message}\x1b\\`
    Osc99,
    /// Ghostty/VTE: `\x1b]777;notify;{title};{body}\x1b\\`
    Osc777,
    /// Universal fallback: `\x07`
    Bel,
    /// No notification capability
    None,
}

impl NotificationProtocol {
    /// Stable lowercase name for telemetry and analytics output.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Osc9 => "osc9",
            Self::Osc99 => "osc99",
            Self::Osc777 => "osc777",
            Self::Bel => "bel",
            Self::None => "none",
        }
    }
}

/// Choose the best notification protocol for the current terminal environment.
pub fn select_protocol(ctx: &TerminalContext) -> NotificationProtocol {
    if ctx.multiplexer == MultiplexerKind::Zellij {
        return NotificationProtocol::Bel;
    }
    match ctx.brand {
        TerminalName::Iterm2 | TerminalName::WezTerm | TerminalName::WarpTerminal => {
            NotificationProtocol::Osc9
        }
        TerminalName::Kitty => NotificationProtocol::Osc99,
        TerminalName::Ghostty
        | TerminalName::Vte
        | TerminalName::Terminator
        | TerminalName::Foot => NotificationProtocol::Osc777,
        TerminalName::KigiDesktop => NotificationProtocol::None,
        TerminalName::AppleTerminal
        | TerminalName::Alacritty
        | TerminalName::Rio
        | TerminalName::VsCode
        | TerminalName::WindowsTerminal
        | TerminalName::JetBrains
        | TerminalName::Cursor
        | TerminalName::Windsurf
        | TerminalName::Zed
        | TerminalName::Otty
        | TerminalName::Unknown => NotificationProtocol::Bel,
    }
}

const BEL_BYTE: &[u8] = b"\x07";

/// Build the escape sequence for a notification, then write it to stderr.
///
/// When running under tmux the sequence is wrapped in DCS passthrough so the
/// outer terminal sees it.
pub fn emit_notification(
    protocol: NotificationProtocol,
    title: &str,
    body: &str,
    ctx: &TerminalContext,
) {
    // For body-only protocols (OSC 9, OSC 99), fold the title (session
    // name) into the body so it's visible.  For OSC 777 (Ghostty), the
    // tab title already appears as the notification subtitle, so we use
    // the app name to avoid showing the session name twice.
    let sequence: Cow<'_, str> = match protocol {
        NotificationProtocol::Osc9 => format!("\x1b]9;{body} \u{b7} {title}\x07").into(),
        NotificationProtocol::Osc99 => format!("\x1b]99;i=kigi;{body} \u{b7} {title}\x1b\\").into(),
        NotificationProtocol::Osc777 => format!("\x1b]777;notify;Kigi;{body}\x1b\\").into(),
        NotificationProtocol::Bel => Cow::Borrowed("\x07"),
        NotificationProtocol::None => return,
    };

    if ctx.is_tmux_backed() {
        let wrapped = tmux::tmux_passthrough(&sequence);
        kigi_shell::util::with_locked_stderr(|stderr| {
            let _ = stderr.write_all(wrapped.as_bytes());
            let _ = stderr.flush();
        });
    } else if matches!(protocol, NotificationProtocol::Bel) {
        kigi_shell::util::with_locked_stderr(|stderr| {
            let _ = stderr.write_all(BEL_BYTE);
            let _ = stderr.flush();
        });
    } else {
        let bytes = sequence.as_bytes();
        kigi_shell::util::with_locked_stderr(|stderr| {
            let _ = stderr.write_all(bytes);
            let _ = stderr.flush();
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::terminal::{MultiplexerKind, TerminalContext, TerminalName};

    fn ctx_with_brand(brand: TerminalName) -> TerminalContext {
        TerminalContext {
            brand,
            ..Default::default()
        }
    }

    fn ctx_with_brand_and_mux(brand: TerminalName, mux: MultiplexerKind) -> TerminalContext {
        TerminalContext {
            brand,
            multiplexer: mux,
            ..Default::default()
        }
    }

    // --- select_protocol: every TerminalName variant ---

    #[test]
    fn select_iterm2_uses_osc9() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::Iterm2)),
            NotificationProtocol::Osc9
        );
    }

    #[test]
    fn select_wezterm_uses_osc9() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::WezTerm)),
            NotificationProtocol::Osc9
        );
    }

    #[test]
    fn select_warp_uses_osc9() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::WarpTerminal)),
            NotificationProtocol::Osc9
        );
    }

    #[test]
    fn select_kitty_uses_osc99() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::Kitty)),
            NotificationProtocol::Osc99
        );
    }

    #[test]
    fn select_ghostty_uses_osc777() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::Ghostty)),
            NotificationProtocol::Osc777
        );
    }

    #[test]
    fn select_vte_uses_osc777() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::Vte)),
            NotificationProtocol::Osc777
        );
    }

    #[test]
    fn select_kigi_desktop_uses_none() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::KigiDesktop)),
            NotificationProtocol::None
        );
    }

    #[test]
    fn select_apple_terminal_uses_bel() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::AppleTerminal)),
            NotificationProtocol::Bel
        );
    }

    #[test]
    fn select_alacritty_uses_bel() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::Alacritty)),
            NotificationProtocol::Bel
        );
    }

    #[test]
    fn select_vscode_uses_bel() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::VsCode)),
            NotificationProtocol::Bel
        );
    }

    #[test]
    fn select_unknown_uses_bel() {
        assert_eq!(
            select_protocol(&ctx_with_brand(TerminalName::Unknown)),
            NotificationProtocol::Bel
        );
    }

    // --- Zellij override ---

    #[test]
    fn zellij_overrides_to_bel_for_kitty() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::Kitty,
                MultiplexerKind::Zellij
            )),
            NotificationProtocol::Bel
        );
    }

    #[test]
    fn zellij_overrides_to_bel_for_ghostty() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::Ghostty,
                MultiplexerKind::Zellij
            )),
            NotificationProtocol::Bel
        );
    }

    #[test]
    fn zellij_overrides_to_bel_for_iterm2() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::Iterm2,
                MultiplexerKind::Zellij
            )),
            NotificationProtocol::Bel
        );
    }

    #[test]
    fn zellij_overrides_to_bel_for_wezterm() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::WezTerm,
                MultiplexerKind::Zellij
            )),
            NotificationProtocol::Bel
        );
    }

    // --- tmux does NOT override protocol selection (passthrough is handled
    //     at emission time, not selection time) ---

    #[test]
    fn tmux_preserves_osc9_for_iterm2() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::Iterm2,
                MultiplexerKind::Tmux
            )),
            NotificationProtocol::Osc9
        );
    }

    #[test]
    fn tmux_preserves_osc99_for_kitty() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::Kitty,
                MultiplexerKind::Tmux
            )),
            NotificationProtocol::Osc99
        );
    }

    // --- screen does not override ---

    #[test]
    fn screen_preserves_osc777_for_ghostty() {
        assert_eq!(
            select_protocol(&ctx_with_brand_and_mux(
                TerminalName::Ghostty,
                MultiplexerKind::Screen
            )),
            NotificationProtocol::Osc777
        );
    }

    // --- emit_notification: verifies None is a no-op (does not panic) ---

    #[test]
    fn emit_none_is_noop() {
        let ctx = ctx_with_brand(TerminalName::KigiDesktop);
        // Should return immediately without writing anything.
        emit_notification(NotificationProtocol::None, "title", "body", &ctx);
    }

    #[test]
    fn emit_bel_does_not_panic() {
        let ctx = ctx_with_brand(TerminalName::Unknown);
        emit_notification(NotificationProtocol::Bel, "", "", &ctx);
    }

    #[test]
    fn emit_osc9_does_not_panic() {
        let ctx = ctx_with_brand(TerminalName::Iterm2);
        emit_notification(NotificationProtocol::Osc9, "title", "body", &ctx);
    }

    #[test]
    fn emit_osc99_does_not_panic() {
        let ctx = ctx_with_brand(TerminalName::Kitty);
        emit_notification(NotificationProtocol::Osc99, "title", "body", &ctx);
    }

    #[test]
    fn emit_osc777_does_not_panic() {
        let ctx = ctx_with_brand(TerminalName::Ghostty);
        emit_notification(NotificationProtocol::Osc777, "title", "body", &ctx);
    }

    // --- exhaustive brand coverage in a table-driven test ---

    #[test]
    fn all_brands_have_defined_protocol() {
        let cases: &[(TerminalName, NotificationProtocol)] = &[
            (TerminalName::Iterm2, NotificationProtocol::Osc9),
            (TerminalName::WezTerm, NotificationProtocol::Osc9),
            (TerminalName::WarpTerminal, NotificationProtocol::Osc9),
            (TerminalName::Kitty, NotificationProtocol::Osc99),
            (TerminalName::Ghostty, NotificationProtocol::Osc777),
            (TerminalName::Vte, NotificationProtocol::Osc777),
            (TerminalName::Foot, NotificationProtocol::Osc777),
            (TerminalName::KigiDesktop, NotificationProtocol::None),
            (TerminalName::AppleTerminal, NotificationProtocol::Bel),
            (TerminalName::Alacritty, NotificationProtocol::Bel),
            (TerminalName::VsCode, NotificationProtocol::Bel),
            (TerminalName::WindowsTerminal, NotificationProtocol::Bel),
            (TerminalName::Unknown, NotificationProtocol::Bel),
        ];

        for &(brand, expected) in cases {
            let ctx = ctx_with_brand(brand);
            assert_eq!(
                select_protocol(&ctx),
                expected,
                "protocol mismatch for {brand:?}"
            );
        }
    }

    #[test]
    fn zellij_forces_bel_for_all_osc_brands() {
        let osc_brands = [
            TerminalName::Iterm2,
            TerminalName::WezTerm,
            TerminalName::WarpTerminal,
            TerminalName::Kitty,
            TerminalName::Ghostty,
            TerminalName::Vte,
        ];
        for brand in osc_brands {
            let ctx = ctx_with_brand_and_mux(brand, MultiplexerKind::Zellij);
            assert_eq!(
                select_protocol(&ctx),
                NotificationProtocol::Bel,
                "zellij should force BEL for {brand:?}"
            );
        }
    }
}
