use crate::file_ops::{DeckBackupPolicy, write_text_file_durably};
use crate::learning_dashboard_model::CalibrationRecallRule;
use crate::localization::{self, Localizer};
use crate::ready_list::{ReadyCardFilter, ReadyCardSort, ReadyCardSortDirection};
use std::fs;
use std::path::{Path, PathBuf};

pub(crate) const CONFIG_PATH: &str = "cosimo.ini";
pub(crate) const LEGACY_SETTINGS_PATH: &str = "cosimo-settings.txt";
pub(crate) const LEGACY_LAST_OPEN_DECK_PATH_FILE: &str = "cosimo-last-open-deck.txt";
pub(crate) const MAX_RECENT_DECKS: usize = 8;

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum ReviewDbBackupPolicy {
    #[default]
    OnSessionStart,
    Off,
}

impl ReviewDbBackupPolicy {
    fn from_code(code: &str) -> Option<Self> {
        match code {
            "on-session-start" => Some(Self::OnSessionStart),
            "off" => Some(Self::Off),
            _ => None,
        }
    }

    fn code(self) -> &'static str {
        match self {
            Self::OnSessionStart => "on-session-start",
            Self::Off => "off",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum SoundNotificationPolicy {
    Always,
    WhenVisible,
    WhenInTray,
    #[default]
    Never,
}

impl SoundNotificationPolicy {
    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "always" => Some(Self::Always),
            "when-visible" => Some(Self::WhenVisible),
            "when-in-tray" => Some(Self::WhenInTray),
            "never" => Some(Self::Never),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Always => "always",
            Self::WhenVisible => "when-visible",
            Self::WhenInTray => "when-in-tray",
            Self::Never => "never",
        }
    }

    pub(crate) fn allows_notification(self, app_visible: bool, in_tray: bool) -> bool {
        match self {
            Self::Always => true,
            Self::WhenVisible => app_visible,
            Self::WhenInTray => in_tray,
            Self::Never => false,
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum ReviewAudioPolicy {
    Automatic,
    Manual,
    #[default]
    Off,
}

impl ReviewAudioPolicy {
    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "automatic" => Some(Self::Automatic),
            "manual" => Some(Self::Manual),
            "off" => Some(Self::Off),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Automatic => "automatic",
            Self::Manual => "manual",
            Self::Off => "off",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AppSettings {
    pub(crate) language: localization::Language,
    pub(crate) minimum_cards_per_session: usize,
    pub(crate) deck_backup_policy: DeckBackupPolicy,
    pub(crate) review_db_backup_policy: ReviewDbBackupPolicy,
    pub(crate) sound_notification_policy: SoundNotificationPolicy,
    pub(crate) review_audio_policy: ReviewAudioPolicy,
    pub(crate) calibration_recall_rule: CalibrationRecallRule,
    pub(crate) minimize_to_tray: bool,
}

impl Default for AppSettings {
    fn default() -> Self {
        Self {
            language: localization::Language::English,
            minimum_cards_per_session: 0,
            deck_backup_policy: DeckBackupPolicy::Full,
            review_db_backup_policy: ReviewDbBackupPolicy::OnSessionStart,
            sound_notification_policy: SoundNotificationPolicy::Never,
            review_audio_policy: ReviewAudioPolicy::Off,
            calibration_recall_rule: CalibrationRecallRule::default(),
            minimize_to_tray: false,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ParsedAppSettings {
    pub(crate) language: Option<localization::Language>,
    pub(crate) minimum_cards_per_session: usize,
    pub(crate) deck_backup_policy: DeckBackupPolicy,
    pub(crate) review_db_backup_policy: ReviewDbBackupPolicy,
    pub(crate) sound_notification_policy: SoundNotificationPolicy,
    pub(crate) review_audio_policy: ReviewAudioPolicy,
    pub(crate) calibration_recall_rule: CalibrationRecallRule,
    pub(crate) minimize_to_tray: bool,
}

impl Default for ParsedAppSettings {
    fn default() -> Self {
        Self {
            language: None,
            minimum_cards_per_session: 0,
            deck_backup_policy: DeckBackupPolicy::Full,
            review_db_backup_policy: ReviewDbBackupPolicy::OnSessionStart,
            sound_notification_policy: SoundNotificationPolicy::Never,
            review_audio_policy: ReviewAudioPolicy::Off,
            calibration_recall_rule: CalibrationRecallRule::default(),
            minimize_to_tray: false,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AppConfig {
    pub(crate) settings: ParsedAppSettings,
    pub(crate) last_open_deck_path: Option<PathBuf>,
    pub(crate) recent_deck_paths: Vec<PathBuf>,
    pub(crate) ready_card_filter: ReadyCardFilter,
    pub(crate) ready_card_sort: ReadyCardSort,
    pub(crate) ready_card_sort_direction: ReadyCardSortDirection,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            settings: ParsedAppSettings::default(),
            last_open_deck_path: None,
            recent_deck_paths: Vec::new(),
            ready_card_filter: ReadyCardFilter::All,
            ready_card_sort: ReadyCardSort::DeckOrder,
            ready_card_sort_direction: ReadyCardSort::DeckOrder.default_direction(),
        }
    }
}

#[cfg(test)]
pub(crate) fn parse_app_settings(settings_text: &str) -> AppSettings {
    let parsed = parse_app_settings_without_default_language(settings_text);
    AppSettings {
        language: parsed.language.unwrap_or(localization::Language::English),
        minimum_cards_per_session: parsed.minimum_cards_per_session,
        deck_backup_policy: parsed.deck_backup_policy,
        review_db_backup_policy: parsed.review_db_backup_policy,
        sound_notification_policy: parsed.sound_notification_policy,
        review_audio_policy: parsed.review_audio_policy,
        calibration_recall_rule: parsed.calibration_recall_rule,
        minimize_to_tray: parsed.minimize_to_tray,
    }
}

pub(crate) fn parse_app_settings_without_default_language(
    settings_text: &str,
) -> ParsedAppSettings {
    let mut settings = ParsedAppSettings::default();

    for line in settings_text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };

        match key.trim() {
            "language" => {
                if let Some(language) = localization::Language::from_code(value.trim()) {
                    settings.language = Some(language);
                }
            }
            "minimum_cards_per_session" => {
                if let Ok(minimum_cards_per_session) = value.trim().parse::<usize>() {
                    settings.minimum_cards_per_session = minimum_cards_per_session;
                }
            }
            "deck_backup_policy" => {
                if let Some(deck_backup_policy) = DeckBackupPolicy::from_code(value.trim()) {
                    settings.deck_backup_policy = deck_backup_policy;
                }
            }
            "review_db_backup_policy" => {
                if let Some(review_db_backup_policy) = ReviewDbBackupPolicy::from_code(value.trim())
                {
                    settings.review_db_backup_policy = review_db_backup_policy;
                }
            }
            "sound_notification_policy" => {
                if let Some(policy) = SoundNotificationPolicy::from_code(value.trim()) {
                    settings.sound_notification_policy = policy;
                }
            }
            "review_audio_policy" => {
                if let Some(policy) = ReviewAudioPolicy::from_code(value.trim()) {
                    settings.review_audio_policy = policy;
                }
            }
            "calibration_recall_rule" => {
                if let Some(rule) = CalibrationRecallRule::from_code(value.trim()) {
                    settings.calibration_recall_rule = rule;
                }
            }
            "sound_notifications" => {
                settings.sound_notification_policy = legacy_sound_notification_policy(value.trim());
            }
            "minimize_to_tray" => {
                settings.minimize_to_tray = parse_bool_setting(value.trim());
            }
            _ => {}
        }
    }

    settings
}

#[cfg(test)]
pub(crate) fn format_app_settings(settings: AppSettings) -> String {
    format!(
        "language={}\nminimum_cards_per_session={}\ndeck_backup_policy={}\nreview_db_backup_policy={}\nsound_notification_policy={}\nreview_audio_policy={}\ncalibration_recall_rule={}\nminimize_to_tray={}\n",
        settings.language.code(),
        settings.minimum_cards_per_session,
        settings.deck_backup_policy.code(),
        settings.review_db_backup_policy.code(),
        settings.sound_notification_policy.code(),
        settings.review_audio_policy.code(),
        settings.calibration_recall_rule.code(),
        format_bool_setting(settings.minimize_to_tray)
    )
}

pub(crate) fn parse_app_config(config_text: &str) -> AppConfig {
    let mut config = AppConfig::default();
    let mut section = "";
    let mut explicit_sort_direction = None;

    for line in config_text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
            continue;
        }
        if let Some(section_name) = line
            .strip_prefix('[')
            .and_then(|line| line.strip_suffix(']'))
        {
            section = section_name.trim();
            continue;
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let key = key.trim();
        let value = value.trim();

        match (section, key) {
            ("settings" | "", "language") => {
                if let Some(language) = localization::Language::from_code(value) {
                    config.settings.language = Some(language);
                }
            }
            ("settings" | "", "minimum_cards_per_session") => {
                if let Ok(minimum_cards_per_session) = value.parse::<usize>() {
                    config.settings.minimum_cards_per_session = minimum_cards_per_session;
                }
            }
            ("settings" | "", "deck_backup_policy") => {
                if let Some(deck_backup_policy) = DeckBackupPolicy::from_code(value) {
                    config.settings.deck_backup_policy = deck_backup_policy;
                }
            }
            ("settings" | "", "review_db_backup_policy") => {
                if let Some(review_db_backup_policy) = ReviewDbBackupPolicy::from_code(value) {
                    config.settings.review_db_backup_policy = review_db_backup_policy;
                }
            }
            ("settings" | "", "sound_notification_policy") => {
                if let Some(policy) = SoundNotificationPolicy::from_code(value) {
                    config.settings.sound_notification_policy = policy;
                }
            }
            ("settings" | "", "review_audio_policy") => {
                if let Some(policy) = ReviewAudioPolicy::from_code(value) {
                    config.settings.review_audio_policy = policy;
                }
            }
            ("settings" | "", "calibration_recall_rule") => {
                if let Some(rule) = CalibrationRecallRule::from_code(value) {
                    config.settings.calibration_recall_rule = rule;
                }
            }
            ("settings" | "", "sound_notifications") => {
                config.settings.sound_notification_policy = legacy_sound_notification_policy(value);
            }
            ("settings" | "", "minimize_to_tray") => {
                config.settings.minimize_to_tray = parse_bool_setting(value);
            }
            ("state" | "", "last_open_deck") if !value.is_empty() => {
                config.last_open_deck_path = Some(PathBuf::from(value));
            }
            ("state" | "", "recent_deck") if !value.is_empty() => {
                append_recent_deck_path(&mut config.recent_deck_paths, PathBuf::from(value));
            }
            ("state" | "", "ready_card_filter") => {
                if let Some(filter) = ReadyCardFilter::from_code(value) {
                    config.ready_card_filter = filter;
                }
            }
            ("state" | "", "ready_card_sort") => {
                if let Some(sort) = ReadyCardSort::from_code(value) {
                    config.ready_card_sort = sort;
                    config.ready_card_sort_direction = sort.default_direction();
                }
            }
            ("state" | "", "ready_card_sort_direction") => {
                if let Some(direction) = ReadyCardSortDirection::from_code(value) {
                    explicit_sort_direction = Some(direction);
                }
            }
            _ => {}
        }
    }

    if let Some(direction) = explicit_sort_direction {
        config.ready_card_sort_direction = direction;
    }

    if config.recent_deck_paths.is_empty()
        && let Some(last_open_deck_path) = config.last_open_deck_path.clone()
    {
        push_recent_deck_path(&mut config.recent_deck_paths, last_open_deck_path);
    }

    config
}

pub(crate) fn push_recent_deck_path(recent_deck_paths: &mut Vec<PathBuf>, deck_path: PathBuf) {
    recent_deck_paths.retain(|existing| existing != &deck_path);
    recent_deck_paths.insert(0, deck_path);
    recent_deck_paths.truncate(MAX_RECENT_DECKS);
}

fn append_recent_deck_path(recent_deck_paths: &mut Vec<PathBuf>, deck_path: PathBuf) {
    if recent_deck_paths.len() >= MAX_RECENT_DECKS
        || recent_deck_paths
            .iter()
            .any(|existing| existing == &deck_path)
    {
        return;
    }
    recent_deck_paths.push(deck_path);
}

#[cfg(test)]
pub(crate) fn format_app_config(
    settings: AppSettings,
    last_open_deck_path: Option<&Path>,
) -> String {
    format_parsed_app_config(
        ParsedAppSettings {
            language: Some(settings.language),
            minimum_cards_per_session: settings.minimum_cards_per_session,
            deck_backup_policy: settings.deck_backup_policy,
            review_db_backup_policy: settings.review_db_backup_policy,
            sound_notification_policy: settings.sound_notification_policy,
            review_audio_policy: settings.review_audio_policy,
            calibration_recall_rule: settings.calibration_recall_rule,
            minimize_to_tray: settings.minimize_to_tray,
        },
        last_open_deck_path,
        &[],
        ReadyCardFilter::All,
        ReadyCardSort::DeckOrder,
        ReadyCardSort::DeckOrder.default_direction(),
    )
}

fn format_parsed_app_config(
    settings: ParsedAppSettings,
    last_open_deck_path: Option<&Path>,
    recent_deck_paths: &[PathBuf],
    ready_card_filter: ReadyCardFilter,
    ready_card_sort: ReadyCardSort,
    ready_card_sort_direction: ReadyCardSortDirection,
) -> String {
    let mut config_text = format!(
        "[settings]\nminimum_cards_per_session={}\ndeck_backup_policy={}\nreview_db_backup_policy={}\nsound_notification_policy={}\nreview_audio_policy={}\ncalibration_recall_rule={}\nminimize_to_tray={}\n",
        settings.minimum_cards_per_session,
        settings.deck_backup_policy.code(),
        settings.review_db_backup_policy.code(),
        settings.sound_notification_policy.code(),
        settings.review_audio_policy.code(),
        settings.calibration_recall_rule.code(),
        format_bool_setting(settings.minimize_to_tray)
    );
    if let Some(language) = settings.language {
        config_text.insert_str(
            "[settings]\n".len(),
            &format!("language={}\n", language.code()),
        );
    }
    config_text.push_str("\n[state]\n");
    if let Some(last_open_deck_path) = last_open_deck_path {
        config_text.push_str(&format!(
            "last_open_deck={}\n",
            last_open_deck_path.display()
        ));
    }
    for recent_deck_path in recent_deck_paths.iter().take(MAX_RECENT_DECKS) {
        config_text.push_str(&format!("recent_deck={}\n", recent_deck_path.display()));
    }
    config_text.push_str(&format!("ready_card_filter={}\n", ready_card_filter.code()));
    config_text.push_str(&format!("ready_card_sort={}\n", ready_card_sort.code()));
    config_text.push_str(&format!(
        "ready_card_sort_direction={}\n",
        ready_card_sort_direction.code()
    ));
    config_text
}

pub(crate) fn load_app_config(config_path: impl AsRef<Path>) -> Result<AppConfig, String> {
    let config_path = config_path.as_ref();
    match fs::read_to_string(config_path) {
        Ok(config_text) => Ok(parse_app_config(&config_text)),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(AppConfig::default()),
        Err(error) => {
            Err(Localizer::default().could_not_read_path(config_path, &error.to_string()))
        }
    }
}

pub(crate) fn effective_settings_from_config(
    config: &AppConfig,
    default_language: localization::Language,
) -> AppSettings {
    effective_app_settings_from(config.settings, default_language)
}

#[cfg(test)]
pub(crate) fn load_app_settings(settings_path: impl AsRef<Path>) -> Result<AppSettings, String> {
    load_app_config(settings_path)
        .map(|config| effective_settings_from_config(&config, localization::Language::English))
}

pub(crate) fn persist_app_settings(
    config_path: impl AsRef<Path>,
    settings: AppSettings,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.settings = ParsedAppSettings {
        language: Some(settings.language),
        minimum_cards_per_session: settings.minimum_cards_per_session,
        deck_backup_policy: settings.deck_backup_policy,
        review_db_backup_policy: settings.review_db_backup_policy,
        sound_notification_policy: settings.sound_notification_policy,
        review_audio_policy: settings.review_audio_policy,
        calibration_recall_rule: settings.calibration_recall_rule,
        minimize_to_tray: settings.minimize_to_tray,
    };
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn load_effective_app_settings(config_path: impl AsRef<Path>) -> AppSettings {
    let config = load_app_config(config_path).unwrap_or_default();
    let mut settings = effective_settings_from_config(&config, system_default_language());

    if let Ok(value) = std::env::var("COSIMO_LANGUAGE")
        && let Some(language) = localization::Language::from_code(&value)
    {
        settings.language = language;
    }

    settings
}

pub(crate) fn migrate_legacy_portable_config(
    config_path: impl AsRef<Path>,
    legacy_settings_path: impl AsRef<Path>,
    legacy_last_open_deck_path: impl AsRef<Path>,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let legacy_settings_path = legacy_settings_path.as_ref();
    let legacy_last_open_deck_path = legacy_last_open_deck_path.as_ref();
    let legacy_settings_exists = legacy_settings_path.exists();
    let legacy_last_open_deck_exists = legacy_last_open_deck_path.exists();

    if !legacy_settings_exists && !legacy_last_open_deck_exists {
        return Ok(());
    }

    let mut config = load_app_config(config_path)?;
    if legacy_settings_exists {
        let settings_text = fs::read_to_string(legacy_settings_path).map_err(|error| {
            Localizer::default().could_not_read_path(legacy_settings_path, &error.to_string())
        })?;
        config.settings = parse_app_settings_without_default_language(&settings_text);
    }
    if legacy_last_open_deck_exists {
        config.last_open_deck_path = read_persisted_open_deck_path(legacy_last_open_deck_path);
        if let Some(deck_path) = config.last_open_deck_path.clone() {
            push_recent_deck_path(&mut config.recent_deck_paths, deck_path);
        }
    }

    write_text_file_durably(
        config_path,
        &format_parsed_app_config(
            config.settings,
            config.last_open_deck_path.as_deref(),
            &config.recent_deck_paths,
            config.ready_card_filter,
            config.ready_card_sort,
            config.ready_card_sort_direction,
        ),
    )
    .map_err(|error| Localizer::default().could_not_write_path(config_path, &error.to_string()))?;

    if legacy_settings_exists {
        fs::remove_file(legacy_settings_path).map_err(|error| {
            Localizer::default().could_not_write_path(legacy_settings_path, &error.to_string())
        })?;
    }
    if legacy_last_open_deck_exists {
        fs::remove_file(legacy_last_open_deck_path).map_err(|error| {
            Localizer::default()
                .could_not_write_path(legacy_last_open_deck_path, &error.to_string())
        })?;
    }

    Ok(())
}

pub(crate) fn persist_open_deck_path(
    config_path: impl AsRef<Path>,
    deck_path: impl AsRef<Path>,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let deck_path = deck_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.last_open_deck_path = Some(deck_path.to_path_buf());
    push_recent_deck_path(
        &mut updated_config.recent_deck_paths,
        deck_path.to_path_buf(),
    );
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn persist_ready_view_state(
    config_path: impl AsRef<Path>,
    ready_card_filter: ReadyCardFilter,
    ready_card_sort: ReadyCardSort,
    ready_card_sort_direction: ReadyCardSortDirection,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.ready_card_filter = ready_card_filter;
    updated_config.ready_card_sort = ready_card_sort;
    updated_config.ready_card_sort_direction = ready_card_sort_direction;
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

fn write_app_config_if_changed(
    config_path: &Path,
    current_config: &AppConfig,
    updated_config: &AppConfig,
    localizer: Localizer,
) -> Result<(), String> {
    // Runtime view state can change often, so compare parsed settings before
    // writing. An unchanged file is left byte-for-byte alone, including any
    // user comments that would be lost when we reformat the ini canonically.
    if current_config == updated_config {
        return Ok(());
    }

    write_text_file_durably(
        config_path,
        &format_parsed_app_config(
            updated_config.settings,
            updated_config.last_open_deck_path.as_deref(),
            &updated_config.recent_deck_paths,
            updated_config.ready_card_filter,
            updated_config.ready_card_sort,
            updated_config.ready_card_sort_direction,
        ),
    )
    .map_err(|error| localizer.could_not_write_path(config_path, &error.to_string()))
}

fn read_persisted_open_deck_path(state_path: impl AsRef<Path>) -> Option<PathBuf> {
    let state_path = state_path.as_ref();
    let saved_path = fs::read_to_string(state_path).ok()?;
    let saved_path = saved_path.trim();
    if saved_path.is_empty() {
        None
    } else {
        Some(PathBuf::from(saved_path))
    }
}

pub(crate) fn effective_app_settings_from(
    parsed_settings: ParsedAppSettings,
    default_language: localization::Language,
) -> AppSettings {
    AppSettings {
        language: parsed_settings.language.unwrap_or(default_language),
        minimum_cards_per_session: parsed_settings.minimum_cards_per_session,
        deck_backup_policy: parsed_settings.deck_backup_policy,
        review_db_backup_policy: parsed_settings.review_db_backup_policy,
        sound_notification_policy: parsed_settings.sound_notification_policy,
        review_audio_policy: parsed_settings.review_audio_policy,
        calibration_recall_rule: parsed_settings.calibration_recall_rule,
        minimize_to_tray: parsed_settings.minimize_to_tray,
    }
}

fn legacy_sound_notification_policy(value: &str) -> SoundNotificationPolicy {
    if parse_bool_setting(value) {
        SoundNotificationPolicy::Always
    } else {
        SoundNotificationPolicy::Never
    }
}

fn parse_bool_setting(value: &str) -> bool {
    matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "true" | "yes" | "on" | "1"
    )
}

fn format_bool_setting(value: bool) -> &'static str {
    if value { "true" } else { "false" }
}

fn system_default_language() -> localization::Language {
    language_for_locale_codes(system_locale_codes())
}

pub(crate) fn language_for_locale_codes(
    codes: impl IntoIterator<Item = String>,
) -> localization::Language {
    codes
        .into_iter()
        .find_map(|code| localization::Language::from_code(&code))
        .unwrap_or(localization::Language::English)
}

#[cfg(windows)]
fn system_locale_codes() -> Vec<String> {
    windows_user_locale_name().into_iter().collect()
}

#[cfg(windows)]
fn windows_user_locale_name() -> Option<String> {
    use windows_sys::Win32::Globalization::GetUserDefaultLocaleName;
    use windows_sys::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;

    let mut buffer = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
    let length =
        unsafe { GetUserDefaultLocaleName(buffer.as_mut_ptr(), LOCALE_NAME_MAX_LENGTH as i32) };
    if length <= 1 {
        return None;
    }

    Some(String::from_utf16_lossy(&buffer[..length as usize - 1]))
}

#[cfg(not(windows))]
fn system_locale_codes() -> Vec<String> {
    let mut codes = Vec::new();
    for variable in ["LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"] {
        let Ok(value) = std::env::var(variable) else {
            continue;
        };

        codes.extend(
            value
                .split(':')
                .map(normalize_locale_code)
                .filter(|code| !code.is_empty()),
        );
    }
    codes
}

#[cfg(not(windows))]
fn normalize_locale_code(code: &str) -> String {
    code.split('.').next().unwrap_or(code).trim().to_string()
}
