#![allow(dead_code)] // Palette + Capability are consumed by the ratatui draw loop landing in v0.2.

//! Monochrome brand palette mapped to terminal capability.
//!
//! Mirrors `web/src/design/tokens.css`. The web app's strict black + glassy
//! white rule applies here too — never emit a chromatic accent except for
//! `reaction-positive`, which is the only color tolerated in the brand.
//!
//! Capability detection is hand-rolled (10 lines, no extra crate) — read
//! `$COLORTERM` first, fall back to `$TERM`. The third-party `termprofile`
//! crate had API churn between 0.2 and 0.4; not worth pulling in for what
//! amounts to two env var checks.

use ratatui::style::{Color, Style};

/// Capability tier resolved once at startup.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Capability {
    /// 24-bit RGB — full brand fidelity.
    TrueColor,
    /// 256-color palette — degrade glassy whites to ANSI 252-255.
    Indexed,
    /// 16-color or worse — black/white only.
    Basic,
}

impl Capability {
    pub fn detect() -> Self {
        // `COLORTERM=truecolor` (or `24bit`) is the de-facto standard for
        // signalling 24-bit support. Modern terminals (Kitty, WezTerm,
        // Ghostty, iTerm2, Alacritty, Konsole, Windows Terminal) all set it.
        if let Ok(v) = std::env::var("COLORTERM") {
            let v = v.to_ascii_lowercase();
            if v == "truecolor" || v == "24bit" {
                return Self::TrueColor;
            }
        }
        if let Ok(t) = std::env::var("TERM") {
            if t.contains("256color") || t.contains("direct") {
                return Self::Indexed;
            }
            if !t.is_empty() && t != "dumb" {
                return Self::Basic;
            }
        }
        Self::Basic
    }
}

/// Brand palette. `text_primary` returns the right white for the tier;
/// `border_subtle` returns the muted divider color.
pub struct Palette {
    pub cap: Capability,
}

impl Palette {
    pub fn new(cap: Capability) -> Self {
        Self { cap }
    }

    pub fn canvas(&self) -> Color {
        match self.cap {
            Capability::TrueColor => Color::Rgb(10, 10, 10),
            Capability::Indexed => Color::Indexed(233),
            Capability::Basic => Color::Reset,
        }
    }

    pub fn text_primary(&self) -> Color {
        match self.cap {
            Capability::TrueColor => Color::Rgb(245, 245, 247),
            Capability::Indexed => Color::Indexed(255),
            Capability::Basic => Color::White,
        }
    }

    pub fn text_muted(&self) -> Color {
        match self.cap {
            Capability::TrueColor => Color::Rgb(160, 160, 160),
            Capability::Indexed => Color::Indexed(244),
            Capability::Basic => Color::Gray,
        }
    }

    pub fn text_ghost(&self) -> Color {
        match self.cap {
            Capability::TrueColor => Color::Rgb(90, 90, 90),
            Capability::Indexed => Color::Indexed(240),
            Capability::Basic => Color::DarkGray,
        }
    }

    pub fn border_subtle(&self) -> Color {
        match self.cap {
            Capability::TrueColor => Color::Rgb(40, 40, 40),
            Capability::Indexed => Color::Indexed(236),
            Capability::Basic => Color::Reset,
        }
    }

    /// The single chromatic accent — used only for `reaction-positive` signals
    /// (aligned vote, success state). Matches `#4ADE80`.
    pub fn reaction_positive(&self) -> Color {
        match self.cap {
            Capability::TrueColor => Color::Rgb(74, 222, 128),
            Capability::Indexed => Color::Indexed(41),
            Capability::Basic => Color::Green,
        }
    }

    /// Reaction-negative — used only on the hostile sentiment cluster.
    /// Stays in the same restrained register as the rest of the brand:
    /// a dimmed white rather than a saturated red would-be off-brand.
    pub fn reaction_hostile(&self) -> Color {
        // Intentionally mono-side — dim white reads as "muted dissent" without
        // pulling Linear-tier brand into Slack territory.
        self.text_primary()
    }

    pub fn primary_style(&self) -> Style {
        Style::default().fg(self.text_primary())
    }

    pub fn muted_style(&self) -> Style {
        Style::default().fg(self.text_muted())
    }

    pub fn ghost_style(&self) -> Style {
        Style::default().fg(self.text_ghost())
    }
}

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

    /// Helper — tag a `Color` so we can pattern-match it without using
    /// `format!` (Display isn't stable across ratatui versions).
    fn kind(c: Color) -> &'static str {
        match c {
            Color::Reset => "reset",
            Color::Black => "black",
            Color::Red => "red",
            Color::Green => "green",
            Color::Yellow => "yellow",
            Color::Blue => "blue",
            Color::Magenta => "magenta",
            Color::Cyan => "cyan",
            Color::Gray => "gray",
            Color::DarkGray => "darkgray",
            Color::LightRed => "lightred",
            Color::LightGreen => "lightgreen",
            Color::LightYellow => "lightyellow",
            Color::LightBlue => "lightblue",
            Color::LightMagenta => "lightmagenta",
            Color::LightCyan => "lightcyan",
            Color::White => "white",
            Color::Rgb(_, _, _) => "rgb",
            Color::Indexed(_) => "indexed",
        }
    }

    #[test]
    fn truecolor_tier_emits_rgb() {
        let p = Palette::new(Capability::TrueColor);
        for c in [
            p.canvas(),
            p.text_primary(),
            p.text_muted(),
            p.text_ghost(),
            p.border_subtle(),
            p.reaction_positive(),
        ] {
            assert_eq!(kind(c), "rgb", "expected RGB in truecolor tier");
        }
    }

    #[test]
    fn indexed_tier_emits_256_palette() {
        let p = Palette::new(Capability::Indexed);
        for c in [
            p.text_primary(),
            p.text_muted(),
            p.text_ghost(),
            p.border_subtle(),
            p.reaction_positive(),
        ] {
            assert_eq!(kind(c), "indexed", "expected Indexed in 256 tier");
        }
        // canvas may still be Indexed (we picked 233 for dark base).
        assert_eq!(kind(p.canvas()), "indexed");
    }

    #[test]
    fn basic_tier_stays_in_16_color_safe_set() {
        let p = Palette::new(Capability::Basic);
        // Basic must NEVER return Rgb or Indexed — those break on 16-color
        // terms and silently 24-bit-dither on partial terms. Stick to the
        // canonical ANSI 16 names.
        let basic_safe = |c: Color| {
            !matches!(kind(c), "rgb" | "indexed")
        };
        assert!(basic_safe(p.canvas()), "canvas leaked off the 16-color set");
        assert!(basic_safe(p.text_primary()), "text_primary leaked");
        assert!(basic_safe(p.text_muted()), "text_muted leaked");
        assert!(basic_safe(p.text_ghost()), "text_ghost leaked");
        assert!(basic_safe(p.border_subtle()), "border_subtle leaked");
        assert!(basic_safe(p.reaction_positive()), "reaction_positive leaked");

        // Also: confirm the only chromatic accent in basic is plain Green
        // (not LightGreen, not Cyan etc.) — keeps us in the brand's
        // restrained register even on degraded terms.
        assert_eq!(kind(p.reaction_positive()), "green");
    }

    #[test]
    fn capability_detection_defaults_to_basic_when_term_missing() {
        // Snapshot existing env so we can restore.
        let prev_term = std::env::var_os("TERM");
        let prev_color = std::env::var_os("COLORTERM");
        std::env::remove_var("TERM");
        std::env::remove_var("COLORTERM");
        let cap = Capability::detect();
        assert_eq!(cap, Capability::Basic);
        if let Some(v) = prev_term {
            std::env::set_var("TERM", v);
        }
        if let Some(v) = prev_color {
            std::env::set_var("COLORTERM", v);
        }
    }
}
