use anyhow::{anyhow, ensure, Context, Result};
use std::{
    fs,
    path::{Path, PathBuf},
};
use unicode_width::UnicodeWidthChar;

pub const ELEMENTS: [&str; 7] = [
    "Menü",
    "Desktop",
    "Code",
    "Aktiver Titel",
    "Rahmen",
    "Dialog/Projekt",
    "Status",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Options {
    pub colors: [(u8, u8); 7],
    pub desktop: char,
    pub tab_width: usize,
    pub include_paths: Vec<PathBuf>,
    pub source_dir: PathBuf,
    pub output_dir: PathBuf,
    pub right_help: bool,
    pub syntax_checking: bool,
}
impl Default for Options {
    fn default() -> Self {
        Self {
            colors: [(0, 7), (7, 0), (7, 1), (15, 5), (0, 7), (0, 7), (0, 3)],
            desktop: '▒',
            tab_width: 8,
            include_paths: vec![],
            source_dir: PathBuf::new(),
            output_dir: PathBuf::new(),
            right_help: true,
            syntax_checking: true,
        }
    }
}
pub fn config_path() -> Result<PathBuf> {
    let base = if cfg!(windows) {
        std::env::var_os("APPDATA").map(PathBuf::from)
    } else if let Some(path) = std::env::var_os("XDG_CONFIG_HOME") {
        Some(PathBuf::from(path))
    } else {
        std::env::var_os("HOME").map(|p| {
            PathBuf::from(p).join(if cfg!(target_os = "macos") {
                "Library/Application Support"
            } else {
                ".config"
            })
        })
    }
    .ok_or_else(|| anyhow!("Benutzer-Konfigurationsverzeichnis fehlt"))?;
    ensure!(
        base.is_absolute(),
        "Konfigurationsverzeichnis muss absolut sein"
    );
    Ok(base.join("TerminalBasic/options.ini"))
}
impl Options {
    pub fn validate(&self) -> Result<()> {
        ensure!(
            self.colors.iter().all(|(fg, bg)| *fg < 16 && *bg < 16),
            "Farben müssen zwischen 0 und 15 liegen"
        );
        ensure!(
            (1..=32).contains(&self.tab_width),
            "Tabweite muss zwischen 1 und 32 liegen"
        );
        ensure!(
            self.desktop.width() == Some(1) && !self.desktop.is_control(),
            "Desktopzeichen muss genau eine Terminalzelle belegen"
        );
        for path in self
            .include_paths
            .iter()
            .chain([&self.source_dir, &self.output_dir])
        {
            if path.as_os_str().is_empty() {
                continue;
            }
            ensure!(
                path.is_absolute()
                    && path
                        .to_str()
                        .is_some_and(|s| !s.contains(['\n', '\r', '\0', '|'])),
                "Optionspfade müssen absolute Verzeichnispfade sein (ohne | oder Zeilenumbruch)"
            );
        }
        Ok(())
    }
    pub fn load(path: &Path) -> (Self, Vec<String>, Option<Vec<u8>>) {
        let bytes = match fs::read(path) {
            Ok(b) => b,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
                return (Self::default(), vec![], None)
            }
            Err(e) => {
                return (
                    Self::default(),
                    vec![format!("{}: {e}", path.display())],
                    None,
                )
            }
        };
        let Ok(text) = std::str::from_utf8(&bytes) else {
            return (
                Self::default(),
                vec!["Optionsdatei ist nicht UTF-8; Defaults aktiv".into()],
                Some(bytes),
            );
        };
        let mut value = Self::default();
        let mut errors = vec![];
        if !text.lines().any(|l| l == "version=1") {
            return (
                value,
                vec!["Unbekannte Optionsversion; Defaults aktiv".into()],
                Some(bytes),
            );
        }
        for line in text
            .lines()
            .filter(|l| !l.is_empty() && !l.starts_with('#'))
        {
            let Some((key, s)) = line.split_once('=') else {
                errors.push(format!("Ungültige Optionszeile: {line}"));
                continue;
            };
            let mut candidate = value.clone();
            let result = (|| -> Result<()> {
                match key {
                    "version" => ensure!(s == "1", "Unbekannte Version"),
                    "desktop" => {
                        let mut chars = s.chars();
                        candidate.desktop = chars
                            .next()
                            .ok_or_else(|| anyhow!("Desktopzeichen fehlt"))?;
                        ensure!(chars.next().is_none(), "Ein Desktopzeichen erforderlich");
                    }
                    "tab_width" => candidate.tab_width = s.parse()?,
                    "right_help" => candidate.right_help = s.parse()?,
                    "syntax_checking" => candidate.syntax_checking = s.parse()?,
                    "source_dir" => candidate.source_dir = s.into(),
                    "output_dir" => candidate.output_dir = s.into(),
                    "include_paths" => {
                        candidate.include_paths = s
                            .split('|')
                            .filter(|s| !s.is_empty())
                            .map(PathBuf::from)
                            .collect()
                    }
                    _ if key.starts_with("color.") => {
                        let i: usize = key[6..].parse()?;
                        ensure!(i < 7, "Unbekanntes Farbelement");
                        let (a, b) = s.split_once(',').ok_or_else(|| anyhow!("Farbpaar fehlt"))?;
                        candidate.colors[i] = (a.parse()?, b.parse()?);
                    }
                    _ => {
                        errors.push(format!("Unbekannte Option {key} ignoriert"));
                        return Ok(());
                    }
                }
                candidate.validate()
            })();
            match result {
                Ok(()) => value = candidate,
                Err(e) => errors.push(format!("Option {key}: {e}; Vorgabe beibehalten")),
            }
        }
        (value, errors, Some(bytes))
    }
    pub fn save(&self, path: &Path, expected: Option<&[u8]>) -> Result<Vec<u8>> {
        self.validate()?;
        let mut text=format!("version=1\ndesktop={}\ntab_width={}\nright_help={}\nsyntax_checking={}\nsource_dir={}\noutput_dir={}\ninclude_paths={}\n",self.desktop,self.tab_width,self.right_help,self.syntax_checking,self.source_dir.display(),self.output_dir.display(),self.include_paths.iter().map(|p|p.to_str().unwrap()).collect::<Vec<_>>().join("|"));
        for (i, (fg, bg)) in self.colors.iter().enumerate() {
            text.push_str(&format!("color.{i}={fg},{bg}\n"));
        }
        fs::create_dir_all(path.parent().ok_or_else(|| anyhow!("Optionspfad fehlt"))?)
            .context("Optionsverzeichnis erstellen")?;
        crate::documents::atomic_write(path, text.as_bytes(), expected, false)?;
        Ok(text.into_bytes())
    }
}
