use serde::Deserialize;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use toml_edit::{ArrayOfTables, DocumentMut, Item, Table, value};

use crate::resources::ensure_default_background;
use crate::terminal::{
    DEFAULT_IMAGE_OPACITY, DEFAULT_IMAGE_TERMINAL_OPACITY, DEFAULT_TERMINAL_OPACITY,
    MAX_SCROLLBACK_LINES, TerminalConfig,
};
use crate::theme::TerminalTheme;

const SAMPLE_CONFIG: &str = r##"[window]
title = "Lios"
width = 960
height = 640
decorated = false
renderer = "cairo" # cairo disables GPU; auto/gl use OpenGL, vulkan explicitly tries Vulkan
opacity = 1.00 # master/full-window opacity, 0.0 to 1.0

[terminal]
font = "Monospace 12"
scrollback_lines = 1000

[theme]
name = "xfce"
# foreground = "#ffffff"
# background = "#000000"
# Optional XFCE Red cursor preset values:
# cursor_background = "#e6192e"
# cursor_foreground = "#a51d2d"
# cursor = "#e6192e" # legacy alias for cursor_background
# cursor_match_overlay = false # cursor follows the effective accent overlay when true

# Saved appearance profiles from Preferences use [[theme_profile]] tables.
# [[theme_profile]]
# name = "Ocean Glass"
# theme = "ocean"
# font = "Monospace 12"
# window_opacity = 0.95
# terminal_opacity = 0.50
# overlay_color = "#00b4d8"
# overlay_opacity = 0.12
# random_overlay = false

[background]
# image = "/home/example/Pictures/wallpaper.jpg"
image_opacity = 1.00
terminal_opacity = 1.00
# Use terminal_opacity = 0.50 with an image, or lower values for no-image glass.
# overlay_color = "#7c3aed"
overlay_opacity = 0.00
random_overlay = true
"##;

#[derive(Debug, Clone, Default)]
pub struct AppSettings {
    pub window: WindowSettings,
    pub terminal: TerminalConfig,
    pub theme_profiles: Vec<ThemeProfile>,
}

#[derive(Debug, Clone)]
pub struct ThemeProfile {
    pub name: String,
    pub theme_name: String,
    pub cursor_background: Option<String>,
    pub cursor_foreground: Option<String>,
    pub cursor_match_overlay: bool,
    pub font: String,
    pub decorated: bool,
    pub window_opacity: f64,
    pub background_image: Option<PathBuf>,
    pub image_opacity: f64,
    pub terminal_opacity: f64,
    pub overlay_color: Option<gtk::gdk::RGBA>,
    pub overlay_opacity: f64,
    pub random_overlay: bool,
}

#[derive(Debug, Clone)]
pub struct WindowSettings {
    pub title: String,
    pub default_width: i32,
    pub default_height: i32,
    pub decorated: bool,
    pub renderer: RendererPreference,
    pub opacity: f64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RendererPreference {
    Auto,
    Gl,
    Vulkan,
    Cairo,
}

#[derive(Debug, Clone, Default)]
pub struct ConfigOverrides {
    pub renderer: Option<String>,
    pub theme_name: Option<String>,
    pub font: Option<String>,
    pub background_image: Option<PathBuf>,
    pub background_image_opacity: Option<f64>,
    pub terminal_opacity: Option<f64>,
    pub overlay_color: Option<String>,
    pub overlay_opacity: Option<f64>,
    pub random_overlay: Option<bool>,
    pub decorated: Option<bool>,
    pub window_opacity: Option<f64>,
}

#[derive(Debug, Clone)]
pub enum ConfigCommand {
    Path,
    Sample,
    Init {
        path: Option<PathBuf>,
        force: bool,
    },
    Show {
        path: Option<PathBuf>,
    },
    Set {
        path: Option<PathBuf>,
        key: String,
        value: String,
    },
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct FileConfig {
    window: FileWindow,
    terminal: FileTerminal,
    theme: FileTheme,
    background: FileBackground,
    theme_profile: Vec<FileThemeProfile>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct FileWindow {
    title: Option<String>,
    width: Option<i32>,
    height: Option<i32>,
    decorated: Option<bool>,
    renderer: Option<String>,
    opacity: Option<f64>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct FileTerminal {
    font: Option<String>,
    scrollback_lines: Option<i64>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct FileTheme {
    name: Option<String>,
    foreground: Option<String>,
    background: Option<String>,
    cursor: Option<String>,
    cursor_background: Option<String>,
    cursor_foreground: Option<String>,
    cursor_match_overlay: Option<bool>,
    palette: Option<Vec<String>>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct FileBackground {
    image: Option<PathBuf>,
    image_opacity: Option<f64>,
    terminal_opacity: Option<f64>,
    overlay_color: Option<String>,
    overlay_opacity: Option<f64>,
    random_overlay: Option<bool>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct FileThemeProfile {
    name: Option<String>,
    theme: Option<String>,
    theme_name: Option<String>,
    cursor_background: Option<String>,
    cursor_foreground: Option<String>,
    cursor_match_overlay: Option<bool>,
    font: Option<String>,
    decorated: Option<bool>,
    window_opacity: Option<f64>,
    image: Option<PathBuf>,
    background_image: Option<PathBuf>,
    image_opacity: Option<f64>,
    terminal_opacity: Option<f64>,
    overlay_color: Option<String>,
    overlay_opacity: Option<f64>,
    random_overlay: Option<bool>,
}

impl AppSettings {
    pub fn load(config_path: Option<PathBuf>, overrides: ConfigOverrides) -> Result<Self, String> {
        let mut settings = Self::default();
        let config_path = config_path.or_else(default_existing_config_path);
        let loaded_config = config_path.is_some();

        if let Some(path) = config_path {
            let file = read_config_file(path)?;
            settings.apply_file(file)?;
        }
        if !loaded_config {
            settings.apply_packaged_default_background();
        }

        settings.apply_overrides(overrides)?;
        Ok(settings)
    }

    pub fn persist(&self, path: &Path) -> Result<(), String> {
        let mut doc = DocumentMut::new();

        doc["window"] = Item::Table(Table::new());
        doc["window"]["title"] = value(self.window.title.clone());
        doc["window"]["width"] = value(i64::from(self.window.default_width));
        doc["window"]["height"] = value(i64::from(self.window.default_height));
        doc["window"]["decorated"] = value(self.window.decorated);
        doc["window"]["renderer"] = value(self.window.renderer.as_config());
        doc["window"]["opacity"] = value(self.window.opacity);

        doc["terminal"] = Item::Table(Table::new());
        doc["terminal"]["font"] = value(self.terminal.font.clone());
        doc["terminal"]["scrollback_lines"] = value(self.terminal.scrollback_lines);

        doc["theme"] = Item::Table(Table::new());
        doc["theme"]["name"] = value(self.terminal.theme_name.clone());
        if let Some(color) = &self.terminal.cursor_background {
            doc["theme"]["cursor_background"] = value(color.clone());
        }
        if let Some(color) = &self.terminal.cursor_foreground {
            doc["theme"]["cursor_foreground"] = value(color.clone());
        }
        doc["theme"]["cursor_match_overlay"] = value(self.terminal.cursor_match_overlay);

        doc["background"] = Item::Table(Table::new());
        if let Some(image) = &self.terminal.background.image {
            doc["background"]["image"] = value(image.to_string_lossy().to_string());
        }
        doc["background"]["image_opacity"] = value(self.terminal.background.image_opacity);
        doc["background"]["terminal_opacity"] = value(self.terminal.background.terminal_opacity);
        if let Some(color) = &self.terminal.background.overlay_color {
            doc["background"]["overlay_color"] = value(color_to_hex(color));
        }
        doc["background"]["overlay_opacity"] = value(self.terminal.background.overlay_opacity);
        doc["background"]["random_overlay"] = value(self.terminal.background.random_overlay);

        if !self.theme_profiles.is_empty() {
            let mut profiles = ArrayOfTables::new();
            for profile in &self.theme_profiles {
                profiles.push(profile_table(profile));
            }
            doc["theme_profile"] = Item::ArrayOfTables(profiles);
        }

        write_document(path, doc)
    }

    pub fn upsert_theme_profile(&mut self, profile: ThemeProfile) {
        if let Some(existing) = self
            .theme_profiles
            .iter_mut()
            .find(|existing| existing.name.eq_ignore_ascii_case(&profile.name))
        {
            *existing = profile;
        } else {
            self.theme_profiles.push(profile);
        }
    }

    fn apply_file(&mut self, file: FileConfig) -> Result<(), String> {
        let image_was_configured = file.background.image.is_some();
        let terminal_opacity_was_configured = file.background.terminal_opacity.is_some();

        if let Some(title) = file.window.title {
            self.window.title = title;
        }
        if let Some(width) = file.window.width {
            self.window.default_width = positive_i32(width, "window.width")?;
        }
        if let Some(height) = file.window.height {
            self.window.default_height = positive_i32(height, "window.height")?;
        }
        if let Some(decorated) = file.window.decorated {
            self.window.decorated = decorated;
        }
        if let Some(renderer) = file.window.renderer {
            self.window.renderer = RendererPreference::parse(&renderer)?;
        }
        if let Some(opacity) = file.window.opacity {
            self.window.opacity = unit_interval(opacity, "window.opacity")?;
        }

        if let Some(font) = file.terminal.font {
            self.terminal.font = font;
        }
        if let Some(lines) = file.terminal.scrollback_lines {
            self.terminal.scrollback_lines =
                bounded_scrollback(lines, "terminal.scrollback_lines")?;
        }

        if let Some(name) = file.theme.name {
            let canonical_name = TerminalTheme::canonical_name(&name)?;
            self.terminal.theme_name = canonical_name.to_string();
            self.terminal.theme = TerminalTheme::named(canonical_name)?;
        }
        let cursor_background = file
            .theme
            .cursor_background
            .as_deref()
            .or(file.theme.cursor.as_deref());
        self.terminal.cursor_background = cursor_background.map(|color| color.to_string());
        self.terminal.cursor_foreground = file.theme.cursor_foreground.clone();
        if let Some(cursor_match_overlay) = file.theme.cursor_match_overlay {
            self.terminal.cursor_match_overlay = cursor_match_overlay;
        }
        self.terminal.theme.apply_overrides(
            file.theme.foreground.as_deref(),
            file.theme.background.as_deref(),
            cursor_background,
            file.theme.cursor_foreground.as_deref(),
            file.theme.palette.as_deref(),
        )?;

        if let Some(image) = file.background.image {
            self.terminal.background.image = Some(expand_user_path(image));
        }
        if let Some(opacity) = file.background.image_opacity {
            self.terminal.background.image_opacity =
                unit_interval(opacity, "background.image_opacity")?;
        }
        if let Some(opacity) = file.background.terminal_opacity {
            self.terminal.background.terminal_opacity =
                unit_interval(opacity, "background.terminal_opacity")?;
        }
        if let Some(color) = file.background.overlay_color {
            self.terminal.background.overlay_color = Some(TerminalTheme::parse_color(&color)?);
        }
        if let Some(opacity) = file.background.overlay_opacity {
            self.terminal.background.overlay_opacity =
                unit_interval(opacity, "background.overlay_opacity")?;
        }
        if file.background.random_overlay == Some(true) {
            self.terminal.background.random_overlay = true;
        }
        if image_was_configured
            && (!terminal_opacity_was_configured
                || self.terminal.background.terminal_opacity >= 0.999)
        {
            self.terminal.background.terminal_opacity = DEFAULT_IMAGE_TERMINAL_OPACITY;
        }
        self.theme_profiles = file
            .theme_profile
            .into_iter()
            .map(theme_profile_from_file)
            .collect::<Result<Vec<_>, _>>()?;

        Ok(())
    }

    fn apply_overrides(&mut self, overrides: ConfigOverrides) -> Result<(), String> {
        let image_was_overridden = overrides.background_image.is_some();
        let terminal_opacity_was_overridden = overrides.terminal_opacity.is_some();

        if let Some(renderer) = overrides.renderer {
            self.window.renderer = RendererPreference::parse(&renderer)?;
        }
        if let Some(theme_name) = overrides.theme_name {
            let canonical_name = TerminalTheme::canonical_name(&theme_name)?;
            self.terminal.theme_name = canonical_name.to_string();
            self.terminal.theme = TerminalTheme::named(canonical_name)?;
        }
        if let Some(font) = overrides.font {
            self.terminal.font = font;
        }
        if let Some(image) = overrides.background_image {
            self.terminal.background.image = Some(expand_user_path(image));
        }
        if let Some(opacity) = overrides.background_image_opacity {
            self.terminal.background.image_opacity =
                unit_interval(opacity, "--background-image-opacity")?;
        }
        if let Some(opacity) = overrides.terminal_opacity {
            self.terminal.background.terminal_opacity =
                unit_interval(opacity, "--background-opacity")?;
        }
        if let Some(color) = overrides.overlay_color {
            self.terminal.background.overlay_color = Some(TerminalTheme::parse_color(&color)?);
        }
        if let Some(opacity) = overrides.overlay_opacity {
            self.terminal.background.overlay_opacity = unit_interval(opacity, "--overlay-opacity")?;
        }
        if let Some(random_overlay) = overrides.random_overlay {
            self.terminal.background.random_overlay = random_overlay;
        }
        if let Some(decorated) = overrides.decorated {
            self.window.decorated = decorated;
        }
        if let Some(opacity) = overrides.window_opacity {
            self.window.opacity = unit_interval(opacity, "--opacity")?;
        }
        if image_was_overridden
            && !terminal_opacity_was_overridden
            && self.terminal.background.terminal_opacity >= 0.999
        {
            self.terminal.background.terminal_opacity = DEFAULT_IMAGE_TERMINAL_OPACITY;
        }

        Ok(())
    }

    fn apply_packaged_default_background(&mut self) {
        if self.terminal.background.image.is_some() {
            return;
        }
        if let Ok(path) = ensure_default_background() {
            self.terminal.background.image = Some(path);
            if self.terminal.background.terminal_opacity >= 0.999 {
                self.terminal.background.terminal_opacity = DEFAULT_IMAGE_TERMINAL_OPACITY;
            }
        }
    }
}

impl ThemeProfile {
    pub fn from_settings(name: &str, settings: &AppSettings) -> Result<Self, String> {
        Ok(Self {
            name: normalized_profile_name(name)?,
            theme_name: settings.terminal.theme_name.clone(),
            cursor_background: settings.terminal.cursor_background.clone(),
            cursor_foreground: settings.terminal.cursor_foreground.clone(),
            cursor_match_overlay: settings.terminal.cursor_match_overlay,
            font: settings.terminal.font.clone(),
            decorated: settings.window.decorated,
            window_opacity: settings.window.opacity,
            background_image: settings.terminal.background.image.clone(),
            image_opacity: settings.terminal.background.image_opacity,
            terminal_opacity: settings.terminal.background.terminal_opacity,
            overlay_color: settings.terminal.background.overlay_color,
            overlay_opacity: settings.terminal.background.overlay_opacity,
            random_overlay: settings.terminal.background.random_overlay,
        })
    }

    pub fn apply_to(&self, settings: &mut AppSettings) -> Result<(), String> {
        let canonical_name = TerminalTheme::canonical_name(&self.theme_name)?;
        let mut theme = TerminalTheme::named(canonical_name)?;
        theme.apply_overrides(
            None,
            None,
            self.cursor_background.as_deref(),
            self.cursor_foreground.as_deref(),
            None,
        )?;

        settings.window.decorated = self.decorated;
        settings.window.opacity = self.window_opacity;
        settings.terminal.theme_name = canonical_name.to_string();
        settings.terminal.cursor_background = self.cursor_background.clone();
        settings.terminal.cursor_foreground = self.cursor_foreground.clone();
        settings.terminal.cursor_match_overlay = self.cursor_match_overlay;
        settings.terminal.font = self.font.clone();
        settings.terminal.theme = theme;
        settings.terminal.background.image = self.background_image.clone();
        settings.terminal.background.image_opacity = self.image_opacity;
        settings.terminal.background.terminal_opacity = self.terminal_opacity;
        settings.terminal.background.overlay_color = self.overlay_color;
        settings.terminal.background.overlay_opacity = self.overlay_opacity;
        settings.terminal.background.random_overlay = self.random_overlay;
        Ok(())
    }
}

fn theme_profile_from_file(file: FileThemeProfile) -> Result<ThemeProfile, String> {
    let theme_name = file
        .theme
        .or(file.theme_name)
        .unwrap_or_else(|| "xfce".to_string());
    let canonical_name = TerminalTheme::canonical_name(&theme_name)?;
    let mut theme = TerminalTheme::named(canonical_name)?;
    theme.apply_overrides(
        None,
        None,
        file.cursor_background.as_deref(),
        file.cursor_foreground.as_deref(),
        None,
    )?;
    Ok(ThemeProfile {
        name: normalized_profile_name(file.name.as_deref().unwrap_or_default())?,
        theme_name: canonical_name.to_string(),
        cursor_background: file.cursor_background,
        cursor_foreground: file.cursor_foreground,
        cursor_match_overlay: file.cursor_match_overlay.unwrap_or(false),
        font: file.font.unwrap_or_else(|| "Monospace 12".to_string()),
        decorated: file.decorated.unwrap_or(false),
        window_opacity: unit_interval(
            file.window_opacity.unwrap_or(1.0),
            "theme_profile.window_opacity",
        )?,
        background_image: file.background_image.or(file.image).map(expand_user_path),
        image_opacity: unit_interval(
            file.image_opacity.unwrap_or(DEFAULT_IMAGE_OPACITY),
            "theme_profile.image_opacity",
        )?,
        terminal_opacity: unit_interval(
            file.terminal_opacity.unwrap_or(DEFAULT_TERMINAL_OPACITY),
            "theme_profile.terminal_opacity",
        )?,
        overlay_color: file
            .overlay_color
            .as_deref()
            .map(TerminalTheme::parse_color)
            .transpose()?,
        overlay_opacity: unit_interval(
            file.overlay_opacity.unwrap_or(0.0),
            "theme_profile.overlay_opacity",
        )?,
        random_overlay: file.random_overlay.unwrap_or(false),
    })
}

fn normalized_profile_name(name: &str) -> Result<String, String> {
    let name = name.trim();
    if name.is_empty() {
        Err("theme profile name is required".to_string())
    } else {
        Ok(name.to_string())
    }
}

fn profile_table(profile: &ThemeProfile) -> Table {
    let mut table = Table::new();
    table["name"] = value(profile.name.clone());
    table["theme"] = value(profile.theme_name.clone());
    if let Some(color) = &profile.cursor_background {
        table["cursor_background"] = value(color.clone());
    }
    if let Some(color) = &profile.cursor_foreground {
        table["cursor_foreground"] = value(color.clone());
    }
    table["cursor_match_overlay"] = value(profile.cursor_match_overlay);
    table["font"] = value(profile.font.clone());
    table["decorated"] = value(profile.decorated);
    table["window_opacity"] = value(profile.window_opacity);
    if let Some(image) = &profile.background_image {
        table["background_image"] = value(image.to_string_lossy().to_string());
    }
    table["image_opacity"] = value(profile.image_opacity);
    table["terminal_opacity"] = value(profile.terminal_opacity);
    if let Some(color) = &profile.overlay_color {
        table["overlay_color"] = value(color_to_hex(color));
    }
    table["overlay_opacity"] = value(profile.overlay_opacity);
    table["random_overlay"] = value(profile.random_overlay);
    table
}

impl Default for WindowSettings {
    fn default() -> Self {
        Self {
            title: "Lios".to_string(),
            default_width: 900,
            default_height: 620,
            decorated: false,
            renderer: RendererPreference::Cairo,
            opacity: 1.0,
        }
    }
}

impl RendererPreference {
    pub const GPU_MODE_NAMES: [&'static str; 3] = ["auto", "gl", "vulkan"];

    pub fn parse(value: &str) -> Result<Self, String> {
        match value.trim().to_ascii_lowercase().as_str() {
            "auto" | "default" => Ok(Self::Auto),
            "gl" | "opengl" | "gpu" => Ok(Self::Gl),
            "vulkan" | "vk" => Ok(Self::Vulkan),
            "cairo" | "software" | "cpu" => Ok(Self::Cairo),
            _ => Err(format!(
                "unknown renderer '{value}'. Use auto, gl, vulkan, or cairo"
            )),
        }
    }

    pub fn parse_gpu_mode(value: &str) -> Result<Self, String> {
        match value.trim().to_ascii_lowercase().as_str() {
            "auto" | "default" => Ok(Self::Auto),
            "gl" | "opengl" => Ok(Self::Gl),
            "vulkan" | "vk" => Ok(Self::Vulkan),
            _ => Err(format!(
                "unknown GPU mode '{value}'. Use auto, gl, or vulkan"
            )),
        }
    }

    pub fn from_gpu_settings(enabled: bool, mode: &str) -> Result<Self, String> {
        if enabled {
            Self::parse_gpu_mode(mode)
        } else {
            Ok(Self::Cairo)
        }
    }

    pub fn gpu_enabled(self) -> bool {
        !matches!(self, Self::Cairo)
    }

    pub fn gpu_mode_config(self) -> &'static str {
        match self {
            Self::Auto | Self::Cairo => "auto",
            Self::Gl => "gl",
            Self::Vulkan => "vulkan",
        }
    }

    pub fn as_config(self) -> &'static str {
        match self {
            Self::Auto => "auto",
            Self::Gl => "gl",
            Self::Vulkan => "vulkan",
            Self::Cairo => "cairo",
        }
    }

    pub fn gsk_renderer(self) -> Option<&'static str> {
        match self {
            Self::Auto => Some("gl"),
            Self::Gl => Some("gl"),
            Self::Vulkan => Some("vulkan"),
            Self::Cairo => Some("cairo"),
        }
    }
}

pub fn run_config_command(command: ConfigCommand) -> Result<String, String> {
    match command {
        ConfigCommand::Path => default_config_path()
            .map(|path| format!("{}\n", path.display()))
            .ok_or_else(|| {
                "unable to determine config path; set HOME or XDG_CONFIG_HOME".to_string()
            }),
        ConfigCommand::Sample => Ok(sample_config().to_string()),
        ConfigCommand::Init { path, force } => {
            let path = config_path_for_write(path)?;
            if path.exists() && !force {
                return Err(format!(
                    "config already exists at '{}'; use --force to overwrite",
                    path.display()
                ));
            }

            write_text(&path, sample_config())?;
            Ok(format!("wrote {}\n", path.display()))
        }
        ConfigCommand::Show { path } => {
            let path = config_path_for_write(path)?;
            fs::read_to_string(&path)
                .map_err(|error| format!("failed to read config '{}': {error}", path.display()))
        }
        ConfigCommand::Set { path, key, value } => {
            let path = config_path_for_write(path)?;
            set_config_value(&path, &key, &value)?;
            Ok(format!("set {key} in {}\n", path.display()))
        }
    }
}

pub fn sample_config() -> &'static str {
    SAMPLE_CONFIG
}

pub fn default_config_path() -> Option<PathBuf> {
    if let Ok(config_home) = env::var("XDG_CONFIG_HOME") {
        if !config_home.is_empty() {
            return Some(PathBuf::from(config_home).join("lios/config.toml"));
        }
    }

    env::var("HOME")
        .ok()
        .filter(|home| !home.is_empty())
        .map(|home| PathBuf::from(home).join(".config/lios/config.toml"))
}

pub fn config_path_for_write(path: Option<PathBuf>) -> Result<PathBuf, String> {
    path.or_else(default_config_path)
        .ok_or_else(|| "unable to determine config path; set HOME or XDG_CONFIG_HOME".to_string())
}

pub fn expand_user_path(path: PathBuf) -> PathBuf {
    let Some(text) = path.to_str() else {
        return path;
    };
    let Some(home) = env::var_os("HOME") else {
        return path;
    };

    if text == "~" {
        PathBuf::from(home)
    } else if let Some(rest) = text.strip_prefix("~/") {
        PathBuf::from(home).join(rest)
    } else {
        path
    }
}

fn default_existing_config_path() -> Option<PathBuf> {
    default_config_path().filter(|path| path.exists())
}

fn read_config_file(path: PathBuf) -> Result<FileConfig, String> {
    let text = fs::read_to_string(&path)
        .map_err(|error| format!("failed to read config '{}': {error}", path.display()))?;

    toml::from_str(&text)
        .map_err(|error| format!("failed to parse config '{}': {error}", path.display()))
}

fn set_config_value(path: &Path, key: &str, raw_value: &str) -> Result<(), String> {
    let mut doc = read_document(path)?;
    let (section, field) = config_key_path(key)?;
    ensure_table(&mut doc, section);

    doc[section][field] = match (section, field) {
        ("theme", "name") => {
            let canonical_name = TerminalTheme::canonical_name(raw_value)?;
            value(canonical_name)
        }
        ("window", "renderer") => {
            let renderer = renderer_from_config_value(key, raw_value)?;
            value(renderer.as_config())
        }
        ("theme", "foreground")
        | ("theme", "background")
        | ("theme", "cursor")
        | ("theme", "cursor_background")
        | ("theme", "cursor_foreground")
        | ("background", "overlay_color") => {
            TerminalTheme::parse_color(raw_value)?;
            value(raw_value)
        }
        ("window", "decorated")
        | ("theme", "cursor_match_overlay")
        | ("background", "random_overlay") => value(parse_bool(raw_value)?),
        ("terminal", "scrollback_lines") => {
            let parsed = raw_value
                .parse::<i64>()
                .map_err(|_| format!("{key} requires an integer"))?;
            value(bounded_scrollback(parsed, key)?)
        }
        ("window", "width") | ("window", "height") => value(
            raw_value
                .parse::<i64>()
                .map_err(|_| format!("{key} requires an integer"))?,
        ),
        ("window", "opacity")
        | ("background", "image_opacity")
        | ("background", "terminal_opacity")
        | ("background", "overlay_opacity") => {
            let parsed = raw_value
                .parse::<f64>()
                .map_err(|_| format!("{key} requires a number"))?;
            if !(0.0..=1.0).contains(&parsed) {
                return Err(format!("{key} must be between 0.0 and 1.0"));
            }
            value(parsed)
        }
        _ => value(raw_value),
    };

    write_document(path, doc)
}

fn config_key_path(key: &str) -> Result<(&'static str, &'static str), String> {
    match key.trim().to_ascii_lowercase().replace('-', "_").as_str() {
        "theme" | "theme.name" | "theme_name" => Ok(("theme", "name")),
        "foreground" | "theme.foreground" => Ok(("theme", "foreground")),
        "theme.background" => Ok(("theme", "background")),
        "cursor" | "theme.cursor" => Ok(("theme", "cursor")),
        "cursor_background" | "cursor_bg" | "theme.cursor_background" => {
            Ok(("theme", "cursor_background"))
        }
        "cursor_foreground" | "cursor_fg" | "theme.cursor_foreground" => {
            Ok(("theme", "cursor_foreground"))
        }
        "cursor_match_overlay"
        | "cursor_match_accent"
        | "match_accent"
        | "theme.cursor_match_overlay" => Ok(("theme", "cursor_match_overlay")),
        "font" | "terminal.font" => Ok(("terminal", "font")),
        "scrollback" | "scrollback_lines" | "terminal.scrollback_lines" => {
            Ok(("terminal", "scrollback_lines"))
        }
        "title" | "window.title" => Ok(("window", "title")),
        "width" | "window.width" => Ok(("window", "width")),
        "height" | "window.height" => Ok(("window", "height")),
        "renderer"
        | "gpu"
        | "gpu_acceleration"
        | "gpu_mode"
        | "window.renderer"
        | "window.gpu"
        | "window.gpu_acceleration"
        | "window.gpu_mode" => Ok(("window", "renderer")),
        "opacity" | "window.opacity" | "window_opacity" | "total_opacity" | "master_opacity" => {
            Ok(("window", "opacity"))
        }
        "topbar" | "titlebar" | "decorated" | "window.decorated" => Ok(("window", "decorated")),
        "background_image" | "background.image" | "background_image_path" => {
            Ok(("background", "image"))
        }
        "background_image_opacity" | "background.image_opacity" => {
            Ok(("background", "image_opacity"))
        }
        "background_opacity" | "terminal_opacity" | "background.terminal_opacity" => {
            Ok(("background", "terminal_opacity"))
        }
        "overlay_color" | "background.overlay_color" => Ok(("background", "overlay_color")),
        "overlay_opacity" | "background.overlay_opacity" => Ok(("background", "overlay_opacity")),
        "random_overlay" | "background.random_overlay" => Ok(("background", "random_overlay")),
        _ => Err(format!(
            "unknown config key '{key}'. Try gpu, gpu_mode, theme, font, cursor_background, cursor_foreground, cursor_match_overlay, opacity, background.image, background_opacity, overlay_color, overlay_opacity, random_overlay, or topbar"
        )),
    }
}

fn renderer_from_config_value(key: &str, raw_value: &str) -> Result<RendererPreference, String> {
    match key.trim().to_ascii_lowercase().replace('-', "_").as_str() {
        "gpu" | "gpu_acceleration" | "window.gpu" | "window.gpu_acceleration" => {
            if let Ok(enabled) = parse_bool(raw_value) {
                Ok(if enabled {
                    RendererPreference::Auto
                } else {
                    RendererPreference::Cairo
                })
            } else {
                RendererPreference::parse(raw_value)
            }
        }
        "gpu_mode" | "window.gpu_mode" => RendererPreference::parse_gpu_mode(raw_value),
        _ => RendererPreference::parse(raw_value),
    }
}

fn read_document(path: &Path) -> Result<DocumentMut, String> {
    let text = if path.exists() {
        fs::read_to_string(path)
            .map_err(|error| format!("failed to read config '{}': {error}", path.display()))?
    } else {
        sample_config().to_string()
    };

    text.parse::<DocumentMut>()
        .map_err(|error| format!("failed to parse config '{}': {error}", path.display()))
}

fn write_document(path: &Path, doc: DocumentMut) -> Result<(), String> {
    write_text(path, &doc.to_string())
}

fn write_text(path: &Path, text: &str) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            format!(
                "failed to create config directory '{}': {error}",
                parent.display()
            )
        })?;
    }

    fs::write(path, text)
        .map_err(|error| format!("failed to write config '{}': {error}", path.display()))
}

fn ensure_table(doc: &mut DocumentMut, section: &str) {
    if !doc[section].is_table() {
        doc[section] = Item::Table(Table::new());
    }
}

fn parse_bool(value: &str) -> Result<bool, String> {
    match value.trim().to_ascii_lowercase().as_str() {
        "1" | "true" | "yes" | "on" => Ok(true),
        "0" | "false" | "no" | "off" => Ok(false),
        _ => Err(format!("'{value}' is not a boolean")),
    }
}

fn unit_interval(value: f64, name: &str) -> Result<f64, String> {
    if (0.0..=1.0).contains(&value) {
        Ok(value)
    } else {
        Err(format!("{name} must be between 0.0 and 1.0"))
    }
}

fn positive_i32(value: i32, name: &str) -> Result<i32, String> {
    if value > 0 {
        Ok(value)
    } else {
        Err(format!("{name} must be greater than 0"))
    }
}

fn bounded_scrollback(value: i64, name: &str) -> Result<i64, String> {
    if !(0..=MAX_SCROLLBACK_LINES).contains(&value) {
        Err(format!(
            "{name} must be between 0 and {MAX_SCROLLBACK_LINES}"
        ))
    } else {
        Ok(value)
    }
}

fn color_to_hex(color: &gtk::gdk::RGBA) -> String {
    let red = (color.red().clamp(0.0, 1.0) * 255.0).round() as u8;
    let green = (color.green().clamp(0.0, 1.0) * 255.0).round() as u8;
    let blue = (color.blue().clamp(0.0, 1.0) * 255.0).round() as u8;
    format!("#{red:02x}{green:02x}{blue:02x}")
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn empty_config_uses_reference_dark_default() {
        let path = write_temp_config("");
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(settings.terminal.theme_name, "xfce");
        assert!(!settings.window.decorated);
        assert_eq!(settings.window.renderer, RendererPreference::Cairo);
        assert_eq!(settings.terminal.background.terminal_opacity, 1.0);
        assert!(settings.terminal.background.random_overlay);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn image_config_with_opaque_terminal_uses_reference_shade() {
        let path = write_temp_config(
            r#"
[background]
image = "/tmp/wallpaper.jpg"
terminal_opacity = 1.00
"#,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(
            settings.terminal.background.terminal_opacity,
            DEFAULT_IMAGE_TERMINAL_OPACITY
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn image_override_lowers_default_terminal_opacity() {
        let path = write_temp_config("");
        let overrides = ConfigOverrides {
            background_image: Some(PathBuf::from("/tmp/wallpaper.jpg")),
            ..ConfigOverrides::default()
        };
        let settings = AppSettings::load(Some(path.clone()), overrides).unwrap();

        assert_eq!(
            settings.terminal.background.terminal_opacity,
            DEFAULT_IMAGE_TERMINAL_OPACITY
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn window_opacity_loads_from_config() {
        let path = write_temp_config(
            r#"
[window]
opacity = 0.72
"#,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(settings.window.opacity, 0.72);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn window_opacity_override_applies() {
        let path = write_temp_config("");
        let overrides = ConfigOverrides {
            window_opacity: Some(0.66),
            ..ConfigOverrides::default()
        };
        let settings = AppSettings::load(Some(path.clone()), overrides).unwrap();

        assert_eq!(settings.window.opacity, 0.66);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn paired_cursor_colors_load_from_config() {
        let path = write_temp_config(
            r##"
[theme]
name = "xfce"
cursor_background = "#112233"
cursor_foreground = "#445566"
"##,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_background_color().unwrap()),
            "#112233"
        );
        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_foreground_color().unwrap()),
            "#445566"
        );
        assert_eq!(
            settings.terminal.cursor_background.as_deref(),
            Some("#112233")
        );
        assert_eq!(
            settings.terminal.cursor_foreground.as_deref(),
            Some("#445566")
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn legacy_cursor_alias_loads_as_cursor_background() {
        let path = write_temp_config(
            r##"
[theme]
cursor = "#778899"
"##,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_background_color().unwrap()),
            "#778899"
        );
        assert_eq!(
            settings.terminal.cursor_background.as_deref(),
            Some("#778899")
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn cursor_background_takes_precedence_over_legacy_cursor_alias() {
        let path = write_temp_config(
            r##"
[theme]
cursor = "#778899"
cursor_background = "#112233"
"##,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_background_color().unwrap()),
            "#112233"
        );
        assert_eq!(
            settings.terminal.cursor_background.as_deref(),
            Some("#112233")
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn persist_writes_paired_cursor_colors() {
        let path = write_temp_config("");
        let mut settings = AppSettings::default();
        settings.terminal.cursor_background = Some("#112233".to_string());
        settings.terminal.cursor_foreground = Some("#445566".to_string());
        settings.terminal.cursor_match_overlay = true;

        settings.persist(&path).unwrap();
        let text = fs::read_to_string(&path).unwrap();

        assert!(text.contains("cursor_background = \"#112233\""));
        assert!(text.contains("cursor_foreground = \"#445566\""));
        assert!(text.contains("cursor_match_overlay = true"));

        let _ = fs::remove_file(path);
    }

    #[test]
    fn cursor_match_overlay_loads_from_config() {
        let path = write_temp_config(
            r##"
[theme]
cursor_match_overlay = true
"##,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert!(settings.terminal.cursor_match_overlay);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn theme_profiles_load_from_config() {
        let path = write_temp_config(
            r##"
[[theme_profile]]
name = "Dracula Glass"
theme = "dracula"
cursor_background = "#bd93f9"
cursor_foreground = "#282a36"
cursor_match_overlay = true
font = "Monospace 13"
decorated = true
window_opacity = 0.91
background_image = "~/wallpaper.png"
image_opacity = 0.8
terminal_opacity = 0.5
overlay_color = "#bd93f9"
overlay_opacity = 0.12
random_overlay = false
"##,
        );
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(settings.theme_profiles.len(), 1);
        let profile = &settings.theme_profiles[0];
        assert_eq!(profile.name, "Dracula Glass");
        assert_eq!(profile.theme_name, "dracula");
        assert_eq!(profile.cursor_background.as_deref(), Some("#bd93f9"));
        assert!(profile.cursor_match_overlay);
        assert_eq!(profile.font, "Monospace 13");
        assert!(profile.decorated);
        assert_eq!(profile.window_opacity, 0.91);
        assert_eq!(profile.terminal_opacity, 0.5);
        assert_eq!(color_to_hex(&profile.overlay_color.unwrap()), "#bd93f9");

        let _ = fs::remove_file(path);
    }

    #[test]
    fn persist_writes_theme_profiles() {
        let path = write_temp_config("");
        let mut settings = AppSettings::default();
        settings.terminal.theme_name = "dracula".to_string();
        settings.terminal.theme = TerminalTheme::named("dracula").unwrap();
        settings.terminal.background.overlay_color =
            Some(TerminalTheme::parse_color("#bd93f9").unwrap());
        settings.terminal.background.overlay_opacity = 0.12;
        settings
            .upsert_theme_profile(ThemeProfile::from_settings("Dracula Glass", &settings).unwrap());

        settings.persist(&path).unwrap();
        let text = fs::read_to_string(&path).unwrap();

        assert!(text.contains("[[theme_profile]]"));
        assert!(text.contains("name = \"Dracula Glass\""));
        assert!(text.contains("theme = \"dracula\""));
        assert!(text.contains("overlay_color = \"#bd93f9\""));

        let _ = fs::remove_file(path);
    }

    #[test]
    fn theme_profile_apply_updates_appearance_settings() {
        let mut settings = AppSettings::default();
        let mut profile = ThemeProfile::from_settings("Dracula", &settings).unwrap();
        profile.theme_name = "dracula".to_string();
        profile.font = "Monospace 13".to_string();
        profile.window_opacity = 0.91;
        profile.overlay_color = Some(TerminalTheme::parse_color("#bd93f9").unwrap());
        profile.overlay_opacity = 0.12;

        profile.apply_to(&mut settings).unwrap();

        assert_eq!(settings.terminal.theme_name, "dracula");
        assert_eq!(settings.terminal.font, "Monospace 13");
        assert_eq!(settings.window.opacity, 0.91);
        assert_eq!(
            color_to_hex(&settings.terminal.background.overlay_color.unwrap()),
            "#bd93f9"
        );
    }

    #[test]
    fn theme_profile_upsert_replaces_same_name() {
        let mut settings = AppSettings::default();
        let mut first = ThemeProfile::from_settings("Night", &settings).unwrap();
        first.theme_name = "xfce".to_string();
        let mut second = first.clone();
        second.theme_name = "dracula".to_string();

        settings.upsert_theme_profile(first);
        settings.upsert_theme_profile(second);

        assert_eq!(settings.theme_profiles.len(), 1);
        assert_eq!(settings.theme_profiles[0].theme_name, "dracula");
    }

    #[test]
    fn renderer_gpu_settings_map_to_expected_backends() {
        assert_eq!(
            RendererPreference::from_gpu_settings(false, "vulkan").unwrap(),
            RendererPreference::Cairo
        );
        assert_eq!(
            RendererPreference::from_gpu_settings(true, "auto").unwrap(),
            RendererPreference::Auto
        );
        assert_eq!(
            RendererPreference::from_gpu_settings(true, "gl").unwrap(),
            RendererPreference::Gl
        );
        assert_eq!(
            RendererPreference::from_gpu_settings(true, "vulkan").unwrap(),
            RendererPreference::Vulkan
        );
        assert!(!RendererPreference::Cairo.gpu_enabled());
        assert!(RendererPreference::Vulkan.gpu_enabled());
        assert_eq!(RendererPreference::Cairo.gpu_mode_config(), "auto");
        assert_eq!(RendererPreference::Auto.gsk_renderer(), Some("gl"));
        assert_eq!(RendererPreference::Vulkan.gsk_renderer(), Some("vulkan"));
        assert_eq!(RendererPreference::Cairo.gsk_renderer(), Some("cairo"));
    }

    #[test]
    fn config_set_gpu_alias_writes_renderer() {
        let path = write_temp_config(sample_config());

        set_config_value(&path, "gpu", "on").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();
        assert_eq!(settings.window.renderer, RendererPreference::Auto);

        set_config_value(&path, "gpu", "off").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();
        assert_eq!(settings.window.renderer, RendererPreference::Cairo);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn config_set_gpu_mode_writes_renderer() {
        let path = write_temp_config(sample_config());

        set_config_value(&path, "gpu_mode", "vulkan").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();
        assert_eq!(settings.window.renderer, RendererPreference::Vulkan);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn config_set_opacity_writes_window_opacity() {
        let path = write_temp_config(sample_config());

        set_config_value(&path, "opacity", "0.73").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(settings.window.opacity, 0.73);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn config_set_background_opacity_writes_terminal_shade() {
        let path = write_temp_config(sample_config());

        set_config_value(&path, "background_opacity", "0.41").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(settings.terminal.background.terminal_opacity, 0.41);

        let _ = fs::remove_file(path);
    }

    #[test]
    fn config_set_writes_paired_cursor_colors() {
        let path = write_temp_config(sample_config());

        set_config_value(&path, "cursor_background", "#112233").unwrap();
        set_config_value(&path, "cursor_foreground", "#445566").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_background_color().unwrap()),
            "#112233"
        );
        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_foreground_color().unwrap()),
            "#445566"
        );

        let _ = fs::remove_file(path);
    }

    #[test]
    fn config_set_writes_cursor_match_overlay() {
        let path = write_temp_config(sample_config());

        set_config_value(&path, "cursor_match_overlay", "on").unwrap();
        let settings = AppSettings::load(Some(path.clone()), ConfigOverrides::default()).unwrap();

        assert!(settings.terminal.cursor_match_overlay);

        let _ = fs::remove_file(path);
    }

    fn write_temp_config(contents: &str) -> PathBuf {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = env::temp_dir().join(format!("lios-test-{}-{nonce}.toml", std::process::id()));
        fs::write(&path, contents).unwrap();
        path
    }
}
