use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::io::Read;
use std::path::PathBuf;

use crate::config::Config;
use crate::tmux::Tmux;

/// Cached health data for the switcher to read.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct SessionHealth {
    pub context_pct: u32,
    pub cache_pct: Option<u32>,
    pub cost_usd: f64,
    pub exceeds_200k: bool,
}

/// Health cache directory: ~/.config/muxr/health/
fn health_dir() -> Option<PathBuf> {
    let home = dirs::home_dir()?;
    Some(home.join(".config").join("muxr").join("health"))
}

/// Convert a session name to a safe filename (replace / with --).
fn health_filename(session_name: &str) -> String {
    format!("{}.json", session_name.replace('/', "--"))
}

/// Write health cache for a session.
fn write_health(session_name: &str, health: &SessionHealth) {
    let Some(dir) = health_dir() else { return };
    let _ = std::fs::create_dir_all(&dir);
    let path = dir.join(health_filename(session_name));
    if let Ok(json) = serde_json::to_string(health) {
        let _ = std::fs::write(path, json);
    }
}

/// Read health cache for a session. Returns None if no cache exists.
pub fn read_health(session_name: &str) -> Option<SessionHealth> {
    let dir = health_dir()?;
    let path = dir.join(health_filename(session_name));
    let content = std::fs::read_to_string(path).ok()?;
    serde_json::from_str(&content).ok()
}

// -- ANSI colors (muted palette) --

const RST: &str = "\x1b[0m";
const DIM: &str = "\x1b[2m";
const BOLD: &str = "\x1b[1m";
const WHITE: &str = "\x1b[37m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const RED: &str = "\x1b[31m";
const CYAN: &str = "\x1b[36m";
const MAGENTA: &str = "\x1b[35m";

// Nerd Font GitLab tanuki (U+F296)
const GL_ICON: &str = "\u{f296}";

// -- JSON schema from Claude Code stdin --

#[derive(Deserialize, Default)]
struct StatusInput {
    #[serde(default)]
    model: ModelInfo,
    #[serde(default)]
    context_window: ContextWindow,
    #[serde(default)]
    cost: CostInfo,
    #[serde(default)]
    rate_limits: Option<RateLimits>,
    #[serde(default)]
    workspace: Workspace,
    #[serde(default)]
    worktree: Option<WorktreeInfo>,
    #[serde(default)]
    agent: Option<AgentInfo>,
    #[serde(default)]
    exceeds_200k_tokens: bool,
}

#[derive(Deserialize, Default)]
struct ModelInfo {
    #[serde(default)]
    #[allow(dead_code)]
    id: String,
    #[serde(default)]
    display_name: String,
}

#[derive(Deserialize, Default)]
struct ContextWindow {
    #[serde(default)]
    used_percentage: Option<f64>,
    #[serde(default)]
    #[allow(dead_code)]
    context_window_size: u64,
    #[serde(default)]
    current_usage: Option<CurrentUsage>,
}

#[derive(Deserialize, Default)]
struct CurrentUsage {
    #[serde(default)]
    cache_creation_input_tokens: u64,
    #[serde(default)]
    cache_read_input_tokens: u64,
}

#[derive(Deserialize, Default)]
struct CostInfo {
    #[serde(default)]
    total_cost_usd: f64,
    #[serde(default)]
    total_duration_ms: u64,
    #[serde(default)]
    total_lines_added: u64,
    #[serde(default)]
    total_lines_removed: u64,
}

#[derive(Deserialize, Default)]
struct RateLimits {
    #[serde(default)]
    five_hour: Option<RateWindow>,
    #[serde(default)]
    seven_day: Option<RateWindow>,
}

#[derive(Deserialize, Default)]
struct RateWindow {
    #[serde(default)]
    used_percentage: f64,
}

#[derive(Deserialize, Default)]
struct Workspace {
    #[serde(default)]
    project_dir: String,
    #[serde(default)]
    current_dir: String,
}

#[derive(Deserialize, Default)]
struct WorktreeInfo {
    #[serde(default)]
    name: String,
}

#[derive(Deserialize, Default)]
struct AgentInfo {
    #[serde(default)]
    name: String,
}

// -- Git info via CLI --

struct GitInfo {
    branch: String,
    dirty: bool,
}

fn git_info(project_dir: &str) -> Option<GitInfo> {
    let branch = std::process::Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .current_dir(project_dir)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())?;

    let dirty = std::process::Command::new("git")
        .args(["status", "--porcelain", "-u"])
        .current_dir(project_dir)
        .output()
        .ok()
        .map(|o| !o.stdout.is_empty())
        .unwrap_or(false);

    Some(GitInfo { branch, dirty })
}

// -- Bar rendering --

fn context_bar(used_pct: u32, width: usize) -> String {
    let filled = (used_pct as usize * width / 100).min(width);
    let empty = width - filled;

    let bar_color = if used_pct >= 80 {
        RED
    } else if used_pct >= 50 {
        YELLOW
    } else {
        GREEN
    };

    let mut bar = String::with_capacity(width + 40);
    bar.push_str(bar_color);
    for _ in 0..filled {
        bar.push('\u{2588}'); // █
    }
    bar.push_str(DIM);
    for _ in 0..empty {
        bar.push('\u{2592}'); // ▒
    }
    bar.push_str(RST);
    bar
}

// -- Duration formatting --

fn format_duration(ms: u64) -> String {
    let s = ms / 1000;
    if s >= 3600 {
        let h = s / 3600;
        let m = (s % 3600) / 60;
        if m == 0 { format!("{h}h") } else { format!("{h}h{m}m") }
    } else if s >= 60 {
        let m = s / 60;
        let sec = s % 60;
        if sec == 0 { format!("{m}m") } else { format!("{m}m{sec}s") }
    } else {
        format!("{s}s")
    }
}

// -- Cache ratio --

fn cache_ratio(usage: &Option<CurrentUsage>) -> Option<u32> {
    let u = usage.as_ref()?;
    let total = u.cache_creation_input_tokens + u.cache_read_input_tokens;
    if total == 0 {
        return None;
    }
    Some((u.cache_read_input_tokens * 100 / total) as u32)
}

/// Run the claude-status command. Reads JSON from stdin, outputs formatted status.
pub fn run(tmux: &Tmux) -> Result<()> {
    let mut input = String::new();
    std::io::stdin()
        .read_to_string(&mut input)
        .context("Failed to read stdin")?;

    let status: StatusInput = serde_json::from_str(&input).unwrap_or_default();

    // Get muxr session name from tmux
    let session_name = tmux
        .display_message("#{session_name}")
        .unwrap_or_default();

    // Resolve vertical color from muxr config
    let vertical = session_name.split('/').next().unwrap_or(&session_name);
    let config = Config::load().ok();
    let hex_color = config
        .as_ref()
        .map(|c| c.color_for(vertical).to_string())
        .unwrap_or_else(|| "#8a7f83".to_string());
    let ansi_color = hex_to_ansi(&hex_color);

    // -- Line 1: session identity + git --
    let mut line1 = String::new();

    // Colored dot + session name
    line1.push_str(&ansi_color);
    line1.push_str(GL_ICON);
    line1.push(' ');
    line1.push_str(BOLD);
    line1.push_str(WHITE);
    line1.push_str(&session_name);
    line1.push_str(RST);

    // Git info
    let project_dir = if status.workspace.project_dir.is_empty() {
        &status.workspace.current_dir
    } else {
        &status.workspace.project_dir
    };

    if !project_dir.is_empty()
        && let Some(git) = git_info(project_dir)
    {
        line1.push_str("  ");
        line1.push_str(CYAN);
        line1.push_str(&git.branch);
        line1.push_str(RST);
        if git.dirty {
            line1.push(' ');
            line1.push_str(YELLOW);
            line1.push('*');
            line1.push_str(RST);
        }
    }

    // Lines changed
    if status.cost.total_lines_added > 0 || status.cost.total_lines_removed > 0 {
        line1.push_str("  ");
        line1.push_str(GREEN);
        line1.push_str(&format!("+{}", status.cost.total_lines_added));
        line1.push_str(RST);
        line1.push(' ');
        line1.push_str(RED);
        line1.push_str(&format!("-{}", status.cost.total_lines_removed));
        line1.push_str(RST);
    }

    // Worktree badge
    if let Some(ref wt) = status.worktree
        && !wt.name.is_empty()
    {
        line1.push_str("  ");
        line1.push_str(MAGENTA);
        line1.push_str("wt:");
        line1.push_str(&wt.name);
        line1.push_str(RST);
    }

    // Agent badge
    if let Some(ref agent) = status.agent
        && !agent.name.is_empty()
    {
        line1.push_str("  ");
        line1.push_str(DIM);
        line1.push_str("agent:");
        line1.push_str(&agent.name);
        line1.push_str(RST);
    }

    // -- Line 2: model + context bar + cache + cost + duration + rate limits --
    let mut line2 = String::new();

    // Model name
    line2.push_str(BOLD);
    line2.push_str(WHITE);
    line2.push_str(&status.model.display_name);
    line2.push_str(RST);

    // 1M badge -- only shown when actually past 200k
    if status.exceeds_200k_tokens {
        line2.push_str(" 1M");
    }

    // Context bar
    let used_pct = status.context_window.used_percentage.unwrap_or(0.0) as u32;
    line2.push_str("  ");
    line2.push_str(&context_bar(used_pct, 20));
    line2.push_str(&format!("  {used_pct:>3}%"));

    // Cache ratio
    if let Some(ratio) = cache_ratio(&status.context_window.current_usage) {
        line2.push_str("  ");
        line2.push_str(DIM);
        line2.push_str(&format!("cache {ratio}%"));
        line2.push_str(RST);
    }

    // Cost
    line2.push_str("  ");
    line2.push_str(DIM);
    if status.cost.total_cost_usd > 0.0 {
        line2.push_str(&format!("${:.2}", status.cost.total_cost_usd));
    } else {
        line2.push_str("$0.00");
    }
    line2.push_str(RST);

    // Duration
    line2.push_str("  ");
    line2.push_str(DIM);
    line2.push_str(&format_duration(status.cost.total_duration_ms));
    line2.push_str(RST);

    // Rate limits -- only shown above 50%
    if let Some(ref rl) = status.rate_limits {
        if let Some(ref five) = rl.five_hour {
            let pct = five.used_percentage as u32;
            if pct > 50 {
                let color = if pct >= 80 { RED } else { YELLOW };
                line2.push_str("  ");
                line2.push_str(color);
                line2.push_str(&format!("5h:{pct}%"));
                line2.push_str(RST);
            }
        }
        if let Some(ref seven) = rl.seven_day {
            let pct = seven.used_percentage as u32;
            if pct > 50 {
                let color = if pct >= 80 { RED } else { YELLOW };
                line2.push_str("  ");
                line2.push_str(color);
                line2.push_str(&format!("7d:{pct}%"));
                line2.push_str(RST);
            }
        }
    }

    // -- Cache health for switcher --
    if !session_name.is_empty() {
        write_health(
            &session_name,
            &SessionHealth {
                context_pct: used_pct,
                cache_pct: cache_ratio(&status.context_window.current_usage),
                cost_usd: status.cost.total_cost_usd,
                exceeds_200k: status.exceeds_200k_tokens,
            },
        );
    }

    // -- Output --
    println!("{line1}");
    print!("{line2}");

    Ok(())
}

/// Convert a hex color (#FC6D26) to ANSI 24-bit escape sequence.
pub(crate) fn hex_to_ansi(hex: &str) -> String {
    let hex = hex.trim_start_matches('#');
    if hex.len() != 6 {
        return "\x1b[37m".to_string(); // fallback white
    }
    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
    format!("\x1b[38;2;{r};{g};{b}m")
}

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

    #[test]
    fn hex_to_ansi_valid() {
        assert_eq!(hex_to_ansi("#FC6D26"), "\x1b[38;2;252;109;38m");
        assert_eq!(hex_to_ansi("FC6D26"), "\x1b[38;2;252;109;38m");
        assert_eq!(hex_to_ansi("#000000"), "\x1b[38;2;0;0;0m");
        assert_eq!(hex_to_ansi("#FFFFFF"), "\x1b[38;2;255;255;255m");
    }

    #[test]
    fn hex_to_ansi_invalid_falls_back_to_white() {
        assert_eq!(hex_to_ansi("#FFF"), "\x1b[37m");
        assert_eq!(hex_to_ansi(""), "\x1b[37m");
        assert_eq!(hex_to_ansi("#"), "\x1b[37m");
    }

    #[test]
    fn format_duration_seconds() {
        assert_eq!(format_duration(0), "0s");
        assert_eq!(format_duration(999), "0s");
        assert_eq!(format_duration(1000), "1s");
        assert_eq!(format_duration(59_000), "59s");
    }

    #[test]
    fn format_duration_minutes() {
        assert_eq!(format_duration(60_000), "1m");
        assert_eq!(format_duration(90_000), "1m30s");
        assert_eq!(format_duration(3_599_000), "59m59s");
    }

    #[test]
    fn format_duration_hours() {
        assert_eq!(format_duration(3_600_000), "1h");
        assert_eq!(format_duration(5_400_000), "1h30m");
        assert_eq!(format_duration(7_200_000), "2h");
    }

    #[test]
    fn context_bar_length() {
        let bar = context_bar(50, 20);
        // Bar contains ANSI codes + 20 block characters + reset codes
        assert!(bar.contains('\u{2588}')); // filled
        assert!(bar.contains('\u{2592}')); // empty
    }

    #[test]
    fn context_bar_full() {
        let bar = context_bar(100, 10);
        assert!(!bar.contains('\u{2592}')); // no empty blocks
    }

    #[test]
    fn context_bar_empty() {
        let bar = context_bar(0, 10);
        assert!(!bar.contains('\u{2588}')); // no filled blocks
    }

    #[test]
    fn cache_ratio_none_on_zero_tokens() {
        assert_eq!(cache_ratio(&None), None);
        assert_eq!(
            cache_ratio(&Some(CurrentUsage {
                cache_creation_input_tokens: 0,
                cache_read_input_tokens: 0,
            })),
            None
        );
    }

    #[test]
    fn cache_ratio_computes_percentage() {
        assert_eq!(
            cache_ratio(&Some(CurrentUsage {
                cache_creation_input_tokens: 50,
                cache_read_input_tokens: 50,
            })),
            Some(50)
        );
        assert_eq!(
            cache_ratio(&Some(CurrentUsage {
                cache_creation_input_tokens: 0,
                cache_read_input_tokens: 100,
            })),
            Some(100)
        );
    }

    #[test]
    fn health_filename_replaces_slashes() {
        assert_eq!(health_filename("work/api"), "work--api.json");
        assert_eq!(health_filename("muxr"), "muxr.json");
        assert_eq!(health_filename("work/api/auth"), "work--api--auth.json");
    }
}
