use std::collections::HashSet;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(super) struct DetectedTerminalProfile {
    pub(super) terminal_id: String,
    pub(super) font_families: Vec<String>,
    pub(super) font_size_px: Option<u16>,
    pub(super) background_opacity_permille: Option<u16>,
    #[serde(default)]
    pub(super) cursor_defaults: CursorDefaults,
    #[serde(default)]
    pub(super) palette_defaults: PaletteDefaults,
    pub(super) source: String,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub(super) struct CursorDefaults {
    pub(super) profile: Option<CursorDefaultProfile>,
    pub(super) shape: Option<CursorDefaultShape>,
    pub(super) blink: Option<CursorDefaultBlink>,
    pub(super) color: Option<String>,
    pub(super) solid_after_input_ms: Option<u32>,
    pub(super) solid_after_output_ms: Option<u32>,
    pub(super) solid_after_cursor_ms: Option<u32>,
    pub(super) paint_mode: Option<CursorDefaultPaintMode>,
    pub(super) text_mode: Option<CursorDefaultTextMode>,
    pub(super) bar_width_pct: Option<u8>,
    pub(super) underline_height_pct: Option<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub(super) struct PaletteDefaults {
    pub(super) foreground: Option<String>,
    pub(super) background: Option<String>,
    #[serde(default)]
    pub(super) colors: Vec<PaletteColorEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(super) struct PaletteColorEntry {
    pub(super) index: u8,
    pub(super) color: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum CursorDefaultProfile {
    Ghostty,
    Generic,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum CursorDefaultShape {
    Block,
    Bar,
    Underline,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum CursorDefaultBlink {
    On,
    Off,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum CursorDefaultPaintMode {
    Invert,
    Fill,
    Outline,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum CursorDefaultTextMode {
    SwapFgBg,
    ForceContrast,
}

trait TerminalProfileProvider {
    fn detect_score(&self, env: &EnvSnapshot) -> u8;
    fn resolve(&self, env: &EnvSnapshot) -> Option<DetectedTerminalProfile>;
}

pub(super) fn detect_render_profile() -> Option<DetectedTerminalProfile> {
    detect_render_profile_for_env(&EnvSnapshot::from_process())
}

fn detect_render_profile_for_env(env: &EnvSnapshot) -> Option<DetectedTerminalProfile> {
    let providers: [&dyn TerminalProfileProvider; 1] = [&GhosttyProvider];
    let mut best = None::<(u8, &dyn TerminalProfileProvider)>;
    for provider in providers {
        let score = provider.detect_score(env);
        if score == 0 {
            continue;
        }
        if best.is_none_or(|(best_score, _)| score > best_score) {
            best = Some((score, provider));
        }
    }
    best.and_then(|(_, provider)| provider.resolve(env))
}

#[derive(Debug, Clone)]
struct EnvSnapshot {
    term_program: Option<String>,
    term: Option<String>,
    home_dir: Option<PathBuf>,
    xdg_config_home: Option<PathBuf>,
}

impl EnvSnapshot {
    fn from_process() -> Self {
        Self {
            term_program: std::env::var("TERM_PROGRAM").ok(),
            term: std::env::var("TERM").ok(),
            home_dir: std::env::var("HOME").ok().map(PathBuf::from),
            xdg_config_home: std::env::var("XDG_CONFIG_HOME").ok().map(PathBuf::from),
        }
    }
}

struct GhosttyProvider;

#[derive(Debug, Default)]
struct GhosttyConfigProfile {
    font_families: Vec<String>,
    font_size_px: Option<u16>,
    background_opacity_permille: Option<u16>,
    cursor_defaults: CursorDefaults,
    palette_defaults: PaletteDefaults,
}

impl GhosttyProvider {
    fn config_paths(env: &EnvSnapshot) -> Vec<PathBuf> {
        let mut candidates = Vec::new();
        if let Some(xdg) = &env.xdg_config_home {
            candidates.push(xdg.join("ghostty/config"));
        }
        if let Some(home) = &env.home_dir {
            candidates.push(home.join(".config/ghostty/config"));
            candidates.push(home.join(".config/nix/configs/ghostty/config"));
        }
        let mut dedupe = HashSet::new();
        candidates
            .into_iter()
            .filter(|path| dedupe.insert(path.clone()))
            .collect()
    }

    fn default_font_families() -> Vec<String> {
        vec![
            "JetBrainsMono Nerd Font".to_string(),
            "JetBrains Mono".to_string(),
            "Symbols Nerd Font Mono".to_string(),
        ]
    }

    fn resolve_from_config(path: &Path) -> Option<DetectedTerminalProfile> {
        let content = std::fs::read_to_string(path).ok()?;
        let parsed = parse_ghostty_config_profile(&content);
        if parsed.font_families.is_empty()
            && parsed.font_size_px.is_none()
            && parsed.background_opacity_permille.is_none()
            && parsed.cursor_defaults == CursorDefaults::default()
            && parsed.palette_defaults == PaletteDefaults::default()
        {
            return None;
        }
        Some(DetectedTerminalProfile {
            terminal_id: "ghostty".to_string(),
            font_families: if parsed.font_families.is_empty() {
                Self::default_font_families()
            } else {
                parsed.font_families
            },
            font_size_px: parsed.font_size_px,
            background_opacity_permille: parsed.background_opacity_permille,
            cursor_defaults: CursorDefaults {
                profile: Some(CursorDefaultProfile::Ghostty),
                solid_after_input_ms: Some(500),
                solid_after_output_ms: Some(500),
                solid_after_cursor_ms: Some(500),
                paint_mode: Some(CursorDefaultPaintMode::Fill),
                text_mode: Some(CursorDefaultTextMode::SwapFgBg),
                bar_width_pct: Some(10),
                underline_height_pct: Some(8),
                ..parsed.cursor_defaults
            },
            palette_defaults: parsed.palette_defaults,
            source: format!("ghostty-config:{}", path.display()),
        })
    }

    fn default_profile() -> DetectedTerminalProfile {
        DetectedTerminalProfile {
            terminal_id: "ghostty".to_string(),
            font_families: Self::default_font_families(),
            font_size_px: None,
            background_opacity_permille: None,
            cursor_defaults: CursorDefaults {
                profile: Some(CursorDefaultProfile::Ghostty),
                solid_after_input_ms: Some(500),
                solid_after_output_ms: Some(500),
                solid_after_cursor_ms: Some(500),
                paint_mode: Some(CursorDefaultPaintMode::Fill),
                text_mode: Some(CursorDefaultTextMode::SwapFgBg),
                bar_width_pct: Some(10),
                underline_height_pct: Some(8),
                ..CursorDefaults::default()
            },
            palette_defaults: PaletteDefaults::default(),
            source: "ghostty-default".to_string(),
        }
    }
}

impl TerminalProfileProvider for GhosttyProvider {
    fn detect_score(&self, env: &EnvSnapshot) -> u8 {
        if env
            .term_program
            .as_deref()
            .is_some_and(|value| value.eq_ignore_ascii_case("ghostty"))
        {
            return 100;
        }
        if env
            .term
            .as_deref()
            .is_some_and(|value| value.contains("ghostty"))
        {
            return 90;
        }
        if Self::config_paths(env).iter().any(|path| path.exists()) {
            return 30;
        }
        0
    }

    fn resolve(&self, env: &EnvSnapshot) -> Option<DetectedTerminalProfile> {
        for path in Self::config_paths(env) {
            if let Some(profile) = Self::resolve_from_config(&path) {
                return Some(profile);
            }
        }
        Some(Self::default_profile())
    }
}

fn parse_ghostty_config_profile(content: &str) -> GhosttyConfigProfile {
    let mut profile = GhosttyConfigProfile::default();
    for raw_line in content.lines() {
        let line = strip_inline_comment(raw_line);
        if line.is_empty() {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let key = key.trim();
        let parsed = trim_config_value(value);
        match key {
            "font-family" => {
                if parsed.is_empty() {
                    profile.font_families.clear();
                    continue;
                }
                profile.font_families.push(parsed);
            }
            "font-size" => {
                if parsed.is_empty() {
                    profile.font_size_px = None;
                    continue;
                }
                if let Some(font_size) = parse_ghostty_font_size_px(&parsed) {
                    profile.font_size_px = Some(font_size);
                }
            }
            "background-opacity" => {
                if parsed.is_empty() {
                    profile.background_opacity_permille = None;
                    continue;
                }
                if let Some(opacity) = parse_ghostty_background_opacity_permille(&parsed) {
                    profile.background_opacity_permille = Some(opacity);
                }
            }
            "foreground" => {
                if parsed.is_empty() {
                    profile.palette_defaults.foreground = None;
                    continue;
                }
                profile.palette_defaults.foreground = Some(parsed);
            }
            "background" => {
                if parsed.is_empty() {
                    profile.palette_defaults.background = None;
                    continue;
                }
                profile.palette_defaults.background = Some(parsed);
            }
            "palette" => {
                if parsed.is_empty() {
                    profile.palette_defaults.colors.clear();
                    continue;
                }
                if let Some(entry) = parse_ghostty_palette_entry(&parsed) {
                    profile.palette_defaults.colors.push(entry);
                }
            }
            "cursor-style" => {
                if parsed.is_empty() {
                    profile.cursor_defaults.shape = None;
                    continue;
                }
                profile.cursor_defaults.shape = parse_ghostty_cursor_style(&parsed);
            }
            "cursor-style-blink" => {
                if parsed.is_empty() {
                    profile.cursor_defaults.blink = None;
                    continue;
                }
                profile.cursor_defaults.blink = parse_ghostty_cursor_blink(&parsed);
            }
            "cursor-color" => {
                if parsed.is_empty() {
                    profile.cursor_defaults.color = None;
                    continue;
                }
                profile.cursor_defaults.color = Some(parsed);
            }
            "cursor-paint-mode" => {
                profile.cursor_defaults.paint_mode = parse_ghostty_cursor_paint_mode(&parsed);
            }
            "cursor-text-mode" => {
                profile.cursor_defaults.text_mode = parse_ghostty_cursor_text_mode(&parsed);
            }
            "cursor-bar-width-pct" => {
                profile.cursor_defaults.bar_width_pct = parsed.parse::<u8>().ok();
            }
            "cursor-underline-height-pct" => {
                profile.cursor_defaults.underline_height_pct = parsed.parse::<u8>().ok();
            }
            _ => {}
        }
    }
    profile
}

fn parse_ghostty_cursor_style(value: &str) -> Option<CursorDefaultShape> {
    match value.trim().to_ascii_lowercase().as_str() {
        "block" | "block_hollow" => Some(CursorDefaultShape::Block),
        "bar" => Some(CursorDefaultShape::Bar),
        "underline" => Some(CursorDefaultShape::Underline),
        _ => None,
    }
}

fn parse_ghostty_cursor_blink(value: &str) -> Option<CursorDefaultBlink> {
    match value.trim().to_ascii_lowercase().as_str() {
        "true" => Some(CursorDefaultBlink::On),
        "false" => Some(CursorDefaultBlink::Off),
        _ => None,
    }
}

fn parse_ghostty_cursor_paint_mode(value: &str) -> Option<CursorDefaultPaintMode> {
    match value.trim().to_ascii_lowercase().as_str() {
        "invert" => Some(CursorDefaultPaintMode::Invert),
        "fill" => Some(CursorDefaultPaintMode::Fill),
        "outline" => Some(CursorDefaultPaintMode::Outline),
        _ => None,
    }
}

fn parse_ghostty_cursor_text_mode(value: &str) -> Option<CursorDefaultTextMode> {
    match value.trim().to_ascii_lowercase().as_str() {
        "swap_fg_bg" | "swap-fg-bg" => Some(CursorDefaultTextMode::SwapFgBg),
        "force_contrast" | "force-contrast" => Some(CursorDefaultTextMode::ForceContrast),
        _ => None,
    }
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // Bounds checked before cast
fn parse_ghostty_font_size_px(value: &str) -> Option<u16> {
    let numeric = value.parse::<f32>().ok()?;
    if numeric <= 0.0 {
        return None;
    }
    let rounded = numeric.round();
    if rounded > f32::from(u16::MAX) {
        return None;
    }
    Some(rounded as u16)
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // Bounds checked before cast
fn parse_ghostty_background_opacity_permille(value: &str) -> Option<u16> {
    let numeric = value.parse::<f32>().ok()?;
    let clamped = numeric.clamp(0.0, 1.0);
    let permille = (clamped * 1000.0).round();
    if permille > f32::from(u16::MAX) {
        return None;
    }
    Some(permille as u16)
}

fn parse_ghostty_palette_entry(value: &str) -> Option<PaletteColorEntry> {
    let (index_raw, color_raw) = value.split_once('=')?;
    let index = parse_ghostty_palette_index(index_raw.trim())?;
    let color = color_raw.trim();
    if color.is_empty() {
        return None;
    }
    Some(PaletteColorEntry {
        index,
        color: color.to_string(),
    })
}

fn parse_ghostty_palette_index(value: &str) -> Option<u8> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return None;
    }
    for (prefix, radix) in [
        ("0x", 16),
        ("0X", 16),
        ("0b", 2),
        ("0B", 2),
        ("0o", 8),
        ("0O", 8),
    ] {
        if let Some(digits) = trimmed.strip_prefix(prefix) {
            if digits.is_empty() {
                return None;
            }
            let parsed = u16::from_str_radix(digits, radix).ok()?;
            return u8::try_from(parsed).ok();
        }
    }
    let parsed = trimmed.parse::<u16>().ok()?;
    u8::try_from(parsed).ok()
}

fn strip_inline_comment(line: &str) -> String {
    let mut in_single = false;
    let mut in_double = false;
    let mut escaped = false;
    let mut out = String::new();
    let mut chars = line.chars().peekable();
    while let Some(ch) = chars.next() {
        if escaped {
            out.push(ch);
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            out.push(ch);
            continue;
        }
        if ch == '\'' && !in_double {
            in_single = !in_single;
            out.push(ch);
            continue;
        }
        if ch == '"' && !in_single {
            in_double = !in_double;
            out.push(ch);
            continue;
        }
        if ch == '#' && !in_single && !in_double {
            let at_line_start = out.trim().is_empty();
            let prev_is_whitespace = out.chars().last().is_none_or(char::is_whitespace);
            let next_is_whitespace_or_eol = chars.peek().is_none_or(|next| next.is_whitespace());
            if at_line_start || (prev_is_whitespace && next_is_whitespace_or_eol) {
                break;
            }
        }
        out.push(ch);
    }
    out.trim().to_string()
}

fn trim_config_value(value: &str) -> String {
    let trimmed = value.trim();
    if trimmed.len() >= 2 {
        let quoted = (trimmed.starts_with('"') && trimmed.ends_with('"'))
            || (trimmed.starts_with('\'') && trimmed.ends_with('\''));
        if quoted {
            return trimmed[1..trimmed.len() - 1].to_string();
        }
    }
    trimmed.to_string()
}

#[cfg(test)]
mod tests {
    #[allow(clippy::wildcard_imports)]
    use super::*;
    #[test]
    fn parse_ghostty_font_families_handles_repeat_and_reset() {
        let parsed = parse_ghostty_config_profile(
            r#"
font-family = "JetBrains Mono"
font-family = "Symbols Nerd Font Mono"
font-family = ""
font-family = "Iosevka"
font-size = 16
background-opacity = 0.9
cursor-style = bar
cursor-style-blink = false
cursor-color = '#33aaee'
cursor-paint-mode = fill
cursor-text-mode = swap_fg_bg
cursor-bar-width-pct = 13
cursor-underline-height-pct = 9
foreground = '#f5f5f5'
background = '#111111'
palette = 5=#bb78d9
palette = 0x09=#ff6655
"#,
        );
        assert_eq!(parsed.font_families, vec!["Iosevka".to_string()]);
        assert_eq!(parsed.font_size_px, Some(16));
        assert_eq!(parsed.background_opacity_permille, Some(900));
        assert_eq!(parsed.cursor_defaults.shape, Some(CursorDefaultShape::Bar));
        assert_eq!(parsed.cursor_defaults.blink, Some(CursorDefaultBlink::Off));
        assert_eq!(parsed.cursor_defaults.color, Some("#33aaee".to_string()));
        assert_eq!(
            parsed.cursor_defaults.paint_mode,
            Some(CursorDefaultPaintMode::Fill)
        );
        assert_eq!(
            parsed.cursor_defaults.text_mode,
            Some(CursorDefaultTextMode::SwapFgBg)
        );
        assert_eq!(parsed.cursor_defaults.bar_width_pct, Some(13));
        assert_eq!(parsed.cursor_defaults.underline_height_pct, Some(9));
        assert_eq!(
            parsed.palette_defaults.foreground,
            Some("#f5f5f5".to_string())
        );
        assert_eq!(
            parsed.palette_defaults.background,
            Some("#111111".to_string())
        );
        assert_eq!(
            parsed.palette_defaults.colors,
            vec![
                PaletteColorEntry {
                    index: 5,
                    color: "#bb78d9".to_string()
                },
                PaletteColorEntry {
                    index: 9,
                    color: "#ff6655".to_string()
                }
            ]
        );
    }

    #[test]
    fn parse_ghostty_profile_keeps_hash_inside_quotes() {
        let parsed = parse_ghostty_config_profile(
            r#"
font-family = "Jet#Brains Mono" # inline comment
"#,
        );
        assert_eq!(parsed.font_families, vec!["Jet#Brains Mono".to_string()]);
    }

    #[test]
    fn parse_ghostty_background_opacity_clamps_to_unit_interval() {
        assert_eq!(parse_ghostty_background_opacity_permille("-1"), Some(0));
        assert_eq!(parse_ghostty_background_opacity_permille("0.5"), Some(500));
        assert_eq!(parse_ghostty_background_opacity_permille("1.5"), Some(1000));
    }

    #[test]
    fn parse_ghostty_palette_index_supports_prefixed_radix() {
        assert_eq!(parse_ghostty_palette_index("15"), Some(15));
        assert_eq!(parse_ghostty_palette_index("0x0f"), Some(15));
        assert_eq!(parse_ghostty_palette_index("0o17"), Some(15));
        assert_eq!(parse_ghostty_palette_index("0b1111"), Some(15));
        assert_eq!(parse_ghostty_palette_index("300"), None);
    }

    #[test]
    fn detect_ghostty_profile_from_term_program() {
        let env = EnvSnapshot {
            term_program: Some("ghostty".to_string()),
            term: None,
            home_dir: None,
            xdg_config_home: None,
        };
        let profile = detect_render_profile_for_env(&env).expect("profile should be detected");
        assert_eq!(profile.terminal_id, "ghostty");
        assert!(!profile.font_families.is_empty());
        assert_eq!(
            profile.cursor_defaults.profile,
            Some(CursorDefaultProfile::Ghostty)
        );
    }

    #[test]
    fn resolve_ghostty_profile_prefers_config_when_available() {
        let suffix = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("clock")
            .as_nanos();
        let temp = std::env::temp_dir().join(format!("bmux-ghostty-test-{suffix}"));
        let config_dir = temp.join(".config/ghostty");
        std::fs::create_dir_all(&config_dir).expect("create config dir");
        std::fs::write(
            config_dir.join("config"),
            "font-family = 'Iosevka Term'\nfont-size = 15\ncursor-style = underline\n",
        )
        .expect("write config");
        let env = EnvSnapshot {
            term_program: Some("ghostty".to_string()),
            term: None,
            home_dir: Some(temp.clone()),
            xdg_config_home: None,
        };
        let profile = detect_render_profile_for_env(&env).expect("profile should be detected");
        assert_eq!(profile.font_families, vec!["Iosevka Term".to_string()]);
        assert_eq!(profile.font_size_px, Some(15));
        assert_eq!(profile.background_opacity_permille, None);
        assert_eq!(
            profile.cursor_defaults.shape,
            Some(CursorDefaultShape::Underline)
        );
        assert!(profile.source.contains("ghostty-config:"));
        let _ = std::fs::remove_dir_all(&temp);
    }
}
