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

use crate::terminal::TerminalConfig;
use crate::theme::TerminalTheme;

const SAMPLE_CONFIG: &str = r##"[window]
title = "Lios"
width = 960
height = 640
decorated = true

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

[theme]
name = "dark-pastels"
# foreground = "#f8fafc"
# background = "#020617"
# cursor = "#a78bfa"

[background]
# image = "/home/you/Pictures/wallpaper.jpg"
image_opacity = 0.70
terminal_opacity = 0.82
overlay_color = "#7c3aed"
overlay_opacity = 0.18
random_overlay = false
"##;

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

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

#[derive(Debug, Clone, Default)]
pub struct ConfigOverrides {
    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>,
}

#[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,
}

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

#[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>,
    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>,
}

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

        if let Some(path) = config_path.or_else(default_existing_config_path) {
            let file = read_config_file(path)?;
            settings.apply_file(file)?;
        }

        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["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());

        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);

        write_document(path, doc)
    }

    fn apply_file(&mut self, file: FileConfig) -> Result<(), String> {
        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(font) = file.terminal.font {
            self.terminal.font = font;
        }
        if let Some(lines) = file.terminal.scrollback_lines {
            self.terminal.scrollback_lines = non_negative_i64(lines, "terminal.scrollback_lines")?;
        }

        if let Some(name) = file.theme.name {
            self.terminal.theme_name = name.clone();
            self.terminal.theme = TerminalTheme::named(&name)?;
        }
        self.terminal.theme.apply_overrides(
            file.theme.foreground.as_deref(),
            file.theme.background.as_deref(),
            file.theme.cursor.as_deref(),
            file.theme.palette.as_deref(),
        )?;

        if let Some(image) = file.background.image {
            self.terminal.background.image = Some(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 let Some(random_overlay) = file.background.random_overlay {
            self.terminal.background.random_overlay = random_overlay;
        }

        Ok(())
    }

    fn apply_overrides(&mut self, overrides: ConfigOverrides) -> Result<(), String> {
        if let Some(theme_name) = overrides.theme_name {
            self.terminal.theme_name = theme_name.clone();
            self.terminal.theme = TerminalTheme::named(&theme_name)?;
        }
        if let Some(font) = overrides.font {
            self.terminal.font = font;
        }
        if let Some(image) = overrides.background_image {
            self.terminal.background.image = Some(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;
        }

        Ok(())
    }
}

impl Default for WindowSettings {
    fn default() -> Self {
        Self {
            title: "Lios".to_string(),
            default_width: 900,
            default_height: 620,
            decorated: true,
        }
    }
}

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())
}

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") => {
            TerminalTheme::named(raw_value)?;
            value(raw_value)
        }
        ("background", "overlay_color") => {
            TerminalTheme::parse_color(raw_value)?;
            value(raw_value)
        }
        ("window", "decorated") | ("background", "random_overlay") => value(parse_bool(raw_value)?),
        ("window", "width") | ("window", "height") | ("terminal", "scrollback_lines") => value(
            raw_value
                .parse::<i64>()
                .map_err(|_| format!("{key} requires an integer"))?,
        ),
        ("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")),
        "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")),
        "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 theme, font, background.image, background_opacity, overlay_color, overlay_opacity, random_overlay, or topbar"
        )),
    }
}

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 non_negative_i64(value: i64, name: &str) -> Result<i64, String> {
    if value >= 0 {
        Ok(value)
    } else {
        Err(format!("{name} must be 0 or greater"))
    }
}

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}")
}
