//! Strict, secret-free configuration shared by every frontend.

use std::{
    collections::BTreeSet,
    env,
    error::Error,
    fmt, fs,
    io::Write,
    path::{Component, Path, PathBuf},
    time::Duration,
};

use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
use serde::Deserialize;
use url::Url;

use crate::authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT};
use crate::presentation::{ClipboardTimeout, DEFAULT_CLIPBOARD_TIMEOUT};

const APPLICATION_DIRECTORY: &str = "ironstorage";
const CONFIG_FILE: &str = "config.toml";
const MAX_CONFIG_BYTES: u64 = 1024 * 1024;

/// Validated application configuration.
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
    source: PathBuf,
    document: toml::Value,
    vault: PathBuf,
    default_key: KeyIdentity,
    key_material: PathBuf,
    editor: Option<EditorCommand>,
    clipboard_timeout: ClipboardTimeout,
    authentication_timeout: AuthenticationTimeout,
    git_remotes: Vec<GitRemote>,
}

// Accepted fields contain no floating-point TOML values, so validated documents
// preserve the equivalence relation required by `Eq`.
impl Eq for Config {}

/// Editable, secret-free values shared by every frontend.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigSettings {
    vault: PathBuf,
    default_key: String,
    editor: Option<Vec<String>>,
    authentication_timeout: Duration,
}

impl ConfigSettings {
    pub fn vault(&self) -> &Path {
        &self.vault
    }

    pub fn set_vault(&mut self, vault: PathBuf) {
        self.vault = vault;
    }

    pub fn default_key(&self) -> &str {
        &self.default_key
    }

    pub fn set_default_key(&mut self, default_key: String) {
        self.default_key = default_key;
    }

    pub fn editor(&self) -> Option<&[String]> {
        self.editor.as_deref()
    }

    pub fn set_editor(&mut self, editor: Option<Vec<String>>) {
        self.editor = editor;
    }

    pub fn authentication_timeout(&self) -> Duration {
        self.authentication_timeout
    }

    pub fn set_authentication_timeout(&mut self, timeout: Duration) {
        self.authentication_timeout = timeout;
    }
}

impl Config {
    /// Load an explicit configuration file, or the native per-user default.
    pub fn load(explicit: Option<&Path>) -> Result<Self, ConfigError> {
        ConfigLoader::system()?.load(explicit)
    }

    pub fn source(&self) -> &Path {
        &self.source
    }

    pub fn vault(&self) -> &Path {
        &self.vault
    }

    pub fn default_key(&self) -> &KeyIdentity {
        &self.default_key
    }

    pub fn key_material(&self) -> &Path {
        &self.key_material
    }

    pub fn configured_editor(&self) -> Option<&EditorCommand> {
        self.editor.as_ref()
    }

    pub fn clipboard_timeout(&self) -> ClipboardTimeout {
        self.clipboard_timeout
    }

    /// Shared inactivity lease used by every interactive frontend.
    pub fn authentication_timeout(&self) -> AuthenticationTimeout {
        self.authentication_timeout
    }

    pub fn git_remotes(&self) -> &[GitRemote] {
        &self.git_remotes
    }

    pub fn settings(&self) -> ConfigSettings {
        let editor = self.editor.as_ref().map(|editor| {
            std::iter::once(editor.program.clone())
                .chain(editor.arguments.iter().cloned())
                .collect()
        });
        ConfigSettings {
            vault: self.vault.clone(),
            default_key: self.default_key.0.clone(),
            editor,
            authentication_timeout: self.authentication_timeout.duration(),
        }
    }

    /// Select a configured remote by name, or the configured default (first
    /// remote) when no name was requested.
    pub fn git_remote(&self, requested: Option<&str>) -> Option<&GitRemote> {
        match requested {
            Some(name) => self
                .git_remotes
                .iter()
                .find(|remote| remote.name().as_str() == name),
            None => self.git_remotes.first(),
        }
    }

    /// Resolve the configured editor, then `$VISUAL`, `$EDITOR`, and finally `vim`.
    pub fn resolve_editor(&self) -> Result<ResolvedEditor, EditorError> {
        self.resolve_editor_from(
            env::var_os("VISUAL").as_deref(),
            env::var_os("EDITOR").as_deref(),
        )
    }

    /// Deterministic editor resolution used by adapters and tests.
    pub fn resolve_editor_from(
        &self,
        visual: Option<&std::ffi::OsStr>,
        editor: Option<&std::ffi::OsStr>,
    ) -> Result<ResolvedEditor, EditorError> {
        if let Some(command) = &self.editor {
            return Ok(ResolvedEditor {
                command: command.clone(),
                source: EditorSource::Configuration,
            });
        }
        if let Some(command) = parse_environment_editor("VISUAL", visual)? {
            return Ok(ResolvedEditor {
                command,
                source: EditorSource::VisualEnvironment,
            });
        }
        if let Some(command) = parse_environment_editor("EDITOR", editor)? {
            return Ok(ResolvedEditor {
                command,
                source: EditorSource::EditorEnvironment,
            });
        }
        Ok(ResolvedEditor {
            command: EditorCommand {
                program: "vim".to_owned(),
                arguments: Vec::new(),
            },
            source: EditorSource::Fallback,
        })
    }

    pub(crate) fn with_settings(&self, settings: ConfigSettings) -> Result<Self, ConfigError> {
        let vault =
            fs::canonicalize(&settings.vault).map_err(|_| ConfigError::VaultUnavailable {
                path: settings.vault.clone(),
            })?;
        if !vault.is_dir() {
            return Err(ConfigError::VaultIsNotDirectory { path: vault });
        }
        let vault = vault
            .to_str()
            .ok_or(ConfigError::InvalidField { field: "vault" })?;
        let mut document = self.document.clone();
        let root = document
            .as_table_mut()
            .ok_or_else(|| ConfigError::Malformed {
                path: self.source.clone(),
            })?;
        root.insert("vault".to_owned(), toml::Value::String(vault.to_owned()));
        root.insert(
            "default_key".to_owned(),
            toml::Value::String(settings.default_key),
        );
        match settings.editor {
            Some(editor) => {
                root.insert(
                    "editor".to_owned(),
                    toml::Value::Array(editor.into_iter().map(toml::Value::String).collect()),
                );
            }
            None => {
                root.remove("editor");
            }
        }
        if settings.authentication_timeout.subsec_nanos() != 0 {
            return Err(ConfigError::InvalidField {
                field: "security.inactivity_timeout_seconds",
            });
        }
        let authentication_timeout = i64::try_from(settings.authentication_timeout.as_secs())
            .map_err(|_| ConfigError::InvalidField {
                field: "security.inactivity_timeout_seconds",
            })?;
        let security = root
            .entry("security")
            .or_insert_with(|| toml::Value::Table(toml::Table::new()))
            .as_table_mut()
            .ok_or(ConfigError::InvalidField { field: "security" })?;
        security.insert(
            "inactivity_timeout_seconds".to_owned(),
            toml::Value::Integer(authentication_timeout),
        );
        let raw = document
            .clone()
            .try_into::<RawConfig>()
            .map_err(|_| ConfigError::Malformed {
                path: self.source.clone(),
            })?;
        validate_config(self.source.clone(), document, raw)
    }

    pub(crate) fn persist(&self) -> Result<(), ConfigError> {
        let parent = self.source.parent().ok_or_else(|| ConfigError::Write {
            path: self.source.clone(),
        })?;
        let name = self.source.file_name().ok_or_else(|| ConfigError::Write {
            path: self.source.clone(),
        })?;
        let directory =
            Dir::open_ambient_dir(parent, ambient_authority()).map_err(|_| ConfigError::Write {
                path: self.source.clone(),
            })?;
        let permissions = directory
            .metadata(name)
            .map_err(|_| ConfigError::Write {
                path: self.source.clone(),
            })?
            .permissions();
        let contents = toml::to_string_pretty(&self.document).map_err(|_| ConfigError::Write {
            path: self.source.clone(),
        })?;
        let mut temporary = TempFile::new(&directory).map_err(|_| ConfigError::Write {
            path: self.source.clone(),
        })?;
        temporary
            .as_file()
            .set_permissions(permissions)
            .and_then(|()| temporary.write_all(contents.as_bytes()))
            .and_then(|()| temporary.as_file().sync_all())
            .and_then(|()| temporary.replace(name))
            .map_err(|_| ConfigError::Write {
                path: self.source.clone(),
            })
    }
}

/// Deterministic path context for configuration loading.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigLoader {
    current_directory: PathBuf,
    native_config_directory: PathBuf,
}

impl ConfigLoader {
    pub fn system() -> Result<Self, ConfigError> {
        let current_directory = env::current_dir().map_err(|_| ConfigError::CurrentDirectory)?;
        let native_config_directory =
            native_config_directory().ok_or(ConfigError::NativeConfigDirectory)?;
        Ok(Self::new(current_directory, native_config_directory))
    }

    pub fn new(current_directory: PathBuf, native_config_directory: PathBuf) -> Self {
        Self {
            current_directory: normalize_absolute(current_directory),
            native_config_directory: normalize_absolute(native_config_directory),
        }
    }

    pub fn default_path(&self) -> PathBuf {
        self.native_config_directory
            .join(APPLICATION_DIRECTORY)
            .join(CONFIG_FILE)
    }

    pub fn load(&self, explicit: Option<&Path>) -> Result<Config, ConfigError> {
        let requested = explicit
            .map(|path| resolve_path(&self.current_directory, path))
            .unwrap_or_else(|| self.default_path());
        let metadata = fs::metadata(&requested).map_err(|error| {
            if error.kind() == std::io::ErrorKind::NotFound {
                ConfigError::NotFound {
                    path: requested.clone(),
                }
            } else {
                ConfigError::Read {
                    path: requested.clone(),
                }
            }
        })?;
        if !metadata.is_file() || metadata.len() > MAX_CONFIG_BYTES {
            return Err(ConfigError::InvalidFile {
                path: requested.clone(),
            });
        }
        let source = fs::canonicalize(&requested).map_err(|_| ConfigError::Read {
            path: requested.clone(),
        })?;
        let contents = fs::read_to_string(&source).map_err(|_| ConfigError::Read {
            path: source.clone(),
        })?;
        let value =
            toml::from_str::<toml::Value>(&contents).map_err(|_| ConfigError::Malformed {
                path: source.clone(),
            })?;
        reject_insecure_fields(&value, "")?;
        validate_known_fields(&value, &source)?;
        let raw = value
            .clone()
            .try_into::<RawConfig>()
            .map_err(|_| ConfigError::Malformed {
                path: source.clone(),
            })?;
        validate_config(source, value, raw)
    }
}

#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct KeyIdentity(String);

impl KeyIdentity {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for KeyIdentity {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.0)
    }
}

macro_rules! identifier_type {
    ($name:ident) => {
        #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
        pub struct $name(String);

        impl $name {
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(&self.0)
            }
        }
    };
}

identifier_type!(RemoteName);
identifier_type!(ServerId);
identifier_type!(ApplicationId);

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitRemote {
    name: RemoteName,
    url: Url,
    server_id: ServerId,
    application_id: ApplicationId,
}

impl GitRemote {
    pub fn name(&self) -> &RemoteName {
        &self.name
    }

    pub fn url(&self) -> &Url {
        &self.url
    }

    pub fn server_id(&self) -> &ServerId {
        &self.server_id
    }

    pub fn application_id(&self) -> &ApplicationId {
        &self.application_id
    }
}

/// An executable and arguments. It is never interpreted by a shell.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EditorCommand {
    program: String,
    arguments: Vec<String>,
}

impl EditorCommand {
    pub fn program(&self) -> &str {
        &self.program
    }

    pub fn arguments(&self) -> &[String] {
        &self.arguments
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EditorSource {
    Configuration,
    VisualEnvironment,
    EditorEnvironment,
    Fallback,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedEditor {
    command: EditorCommand,
    source: EditorSource,
}

impl ResolvedEditor {
    pub fn command(&self) -> &EditorCommand {
        &self.command
    }

    pub fn source(&self) -> EditorSource {
        self.source
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConfigError {
    CurrentDirectory,
    NativeConfigDirectory,
    NotFound { path: PathBuf },
    Read { path: PathBuf },
    InvalidFile { path: PathBuf },
    Malformed { path: PathBuf },
    UnknownField { field: String },
    InsecureField { field: String },
    MissingField { field: &'static str },
    InvalidField { field: &'static str },
    VaultUnavailable { path: PathBuf },
    VaultIsNotDirectory { path: PathBuf },
    Write { path: PathBuf },
    KeyMaterialNotFound { path: PathBuf },
    InvalidKeyMaterial { path: PathBuf },
    DuplicateRemote { name: String },
    DuplicateCredentialReference,
    InvalidRemoteUrl { name: String },
}

impl fmt::Display for ConfigError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::CurrentDirectory => formatter.write_str("cannot determine the current directory"),
            Self::NativeConfigDirectory => {
                formatter.write_str("cannot determine the native user configuration directory")
            }
            Self::NotFound { path } => {
                write!(
                    formatter,
                    "configuration file not found: {}",
                    path.display()
                )
            }
            Self::Read { path } => {
                write!(
                    formatter,
                    "cannot read configuration file: {}",
                    path.display()
                )
            }
            Self::InvalidFile { path } => write!(
                formatter,
                "configuration path is not a small regular file: {}",
                path.display()
            ),
            Self::Malformed { path } => write!(
                formatter,
                "configuration is malformed; its contents were redacted: {}",
                path.display()
            ),
            Self::UnknownField { field } => {
                write!(formatter, "unknown configuration field: {field}")
            }
            Self::InsecureField { field } => write!(
                formatter,
                "secret values are forbidden in configuration field: {field}"
            ),
            Self::MissingField { field } => {
                write!(formatter, "missing required configuration field: {field}")
            }
            Self::InvalidField { field } => {
                write!(formatter, "invalid configuration field: {field}")
            }
            Self::VaultUnavailable { path } => write!(
                formatter,
                "vault folder is missing or inaccessible: {}",
                path.display()
            ),
            Self::VaultIsNotDirectory { path } => {
                write!(
                    formatter,
                    "vault path is not a directory: {}",
                    path.display()
                )
            }
            Self::Write { path } => {
                write!(
                    formatter,
                    "cannot update configuration file: {}",
                    path.display()
                )
            }
            Self::KeyMaterialNotFound { path } => write!(
                formatter,
                "exported key material does not exist: {}",
                path.display()
            ),
            Self::InvalidKeyMaterial { path } => write!(
                formatter,
                "exported key material is not a file or directory: {}",
                path.display()
            ),
            Self::DuplicateRemote { name } => {
                write!(formatter, "duplicate Git remote name: {name}")
            }
            Self::DuplicateCredentialReference => {
                formatter.write_str("duplicate Git server/application credential reference")
            }
            Self::InvalidRemoteUrl { name } => write!(
                formatter,
                "Git remote {name} must be an HTTPS URL without embedded credentials, query, or fragment"
            ),
        }
    }
}

impl Error for ConfigError {}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EditorError {
    NonUnicode { variable: &'static str },
    InvalidCommand { source: &'static str },
}

impl fmt::Display for EditorError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NonUnicode { variable } => {
                write!(formatter, "{variable} is not valid Unicode")
            }
            Self::InvalidCommand { source } => {
                write!(
                    formatter,
                    "{source} does not contain a valid editor command"
                )
            }
        }
    }
}

impl Error for EditorError {}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawConfig {
    vault: Option<PathBuf>,
    default_key: Option<String>,
    key_material: Option<PathBuf>,
    editor: Option<RawEditor>,
    clipboard_timeout_seconds: Option<u64>,
    #[serde(default)]
    security: RawSecurity,
    #[serde(default)]
    git: RawGit,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawSecurity {
    inactivity_timeout_seconds: Option<u64>,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum RawEditor {
    CommandLine(String),
    Arguments(Vec<String>),
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawGit {
    #[serde(default)]
    remotes: Vec<RawGitRemote>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawGitRemote {
    name: String,
    url: String,
    server_id: String,
    application_id: String,
}

fn validate_config(
    source: PathBuf,
    document: toml::Value,
    raw: RawConfig,
) -> Result<Config, ConfigError> {
    let base = source
        .parent()
        .ok_or(ConfigError::InvalidField { field: "source" })?;
    let vault = resolve_required_path(base, raw.vault, "vault")?;
    if vault.exists() && !vault.is_dir() {
        return Err(ConfigError::VaultIsNotDirectory { path: vault });
    }

    let key_material = resolve_required_path(base, raw.key_material, "key_material")?;
    if !key_material.exists() {
        return Err(ConfigError::KeyMaterialNotFound { path: key_material });
    }
    let key_metadata =
        fs::metadata(&key_material).map_err(|_| ConfigError::KeyMaterialNotFound {
            path: key_material.clone(),
        })?;
    if !key_metadata.is_file() && !key_metadata.is_dir() {
        return Err(ConfigError::InvalidKeyMaterial { path: key_material });
    }
    let key_material = fs::canonicalize(&key_material)
        .map_err(|_| ConfigError::InvalidKeyMaterial { path: key_material })?;

    let default_key = raw
        .default_key
        .ok_or(ConfigError::MissingField {
            field: "default_key",
        })
        .and_then(validate_key_identity)?;
    let editor = raw.editor.map(validate_editor).transpose()?;
    let clipboard_timeout = ClipboardTimeout::new(Duration::from_secs(
        raw.clipboard_timeout_seconds
            .unwrap_or(DEFAULT_CLIPBOARD_TIMEOUT.as_secs()),
    ))
    .map_err(|_| ConfigError::InvalidField {
        field: "clipboard_timeout_seconds",
    })?;
    let authentication_timeout = AuthenticationTimeout::new(Duration::from_secs(
        raw.security
            .inactivity_timeout_seconds
            .unwrap_or(DEFAULT_AUTHENTICATION_TIMEOUT.as_secs()),
    ))
    .map_err(|_| ConfigError::InvalidField {
        field: "security.inactivity_timeout_seconds",
    })?;
    let git_remotes = validate_remotes(raw.git.remotes)?;

    Ok(Config {
        source,
        document,
        vault,
        default_key,
        key_material,
        editor,
        clipboard_timeout,
        authentication_timeout,
        git_remotes,
    })
}

fn resolve_required_path(
    base: &Path,
    value: Option<PathBuf>,
    field: &'static str,
) -> Result<PathBuf, ConfigError> {
    let value = value.ok_or(ConfigError::MissingField { field })?;
    if value.as_os_str().is_empty() {
        return Err(ConfigError::InvalidField { field });
    }
    Ok(resolve_path(base, &value))
}

fn validate_key_identity(value: String) -> Result<KeyIdentity, ConfigError> {
    let trimmed = value.trim();
    if trimmed.is_empty()
        || trimmed.len() > 512
        || trimmed.chars().any(char::is_control)
        || trimmed != value
    {
        return Err(ConfigError::InvalidField {
            field: "default_key",
        });
    }
    Ok(KeyIdentity(value))
}

fn validate_editor(raw: RawEditor) -> Result<EditorCommand, ConfigError> {
    let words = match raw {
        RawEditor::CommandLine(command) => {
            shlex::split(&command).ok_or(ConfigError::InvalidField { field: "editor" })?
        }
        RawEditor::Arguments(arguments) => arguments,
    };
    editor_from_words(words).map_err(|_| ConfigError::InvalidField { field: "editor" })
}

fn editor_from_words(mut words: Vec<String>) -> Result<EditorCommand, ()> {
    if words.is_empty() || words.iter().any(|word| word.contains('\0')) {
        return Err(());
    }
    let program = words.remove(0);
    if program.is_empty() {
        return Err(());
    }
    Ok(EditorCommand {
        program,
        arguments: words,
    })
}

fn parse_environment_editor(
    variable: &'static str,
    value: Option<&std::ffi::OsStr>,
) -> Result<Option<EditorCommand>, EditorError> {
    let Some(value) = value else {
        return Ok(None);
    };
    if value.is_empty() {
        return Ok(None);
    }
    let value = value.to_str().ok_or(EditorError::NonUnicode { variable })?;
    let words = shlex::split(value).ok_or(EditorError::InvalidCommand { source: variable })?;
    editor_from_words(words)
        .map(Some)
        .map_err(|()| EditorError::InvalidCommand { source: variable })
}

fn validate_remotes(raw: Vec<RawGitRemote>) -> Result<Vec<GitRemote>, ConfigError> {
    let mut names = BTreeSet::new();
    let mut references = BTreeSet::new();
    let mut remotes = Vec::with_capacity(raw.len());
    for remote in raw {
        let name = RemoteName(validate_identifier("git.remotes.name", remote.name)?);
        let server_id = ServerId(validate_identifier(
            "git.remotes.server_id",
            remote.server_id,
        )?);
        let application_id = ApplicationId(validate_identifier(
            "git.remotes.application_id",
            remote.application_id,
        )?);
        if !names.insert(name.clone()) {
            return Err(ConfigError::DuplicateRemote {
                name: name.0.clone(),
            });
        }
        if !references.insert((server_id.clone(), application_id.clone())) {
            return Err(ConfigError::DuplicateCredentialReference);
        }
        let url = Url::parse(&remote.url).map_err(|_| ConfigError::InvalidRemoteUrl {
            name: name.0.clone(),
        })?;
        if url.scheme() != "https"
            || url.host_str().is_none()
            || !url.username().is_empty()
            || url.password().is_some()
            || url.query().is_some()
            || url.fragment().is_some()
        {
            return Err(ConfigError::InvalidRemoteUrl {
                name: name.0.clone(),
            });
        }
        remotes.push(GitRemote {
            name,
            url,
            server_id,
            application_id,
        });
    }
    Ok(remotes)
}

fn validate_identifier(field: &'static str, value: String) -> Result<String, ConfigError> {
    if value.is_empty()
        || value.len() > 128
        || !value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
    {
        return Err(ConfigError::InvalidField { field });
    }
    Ok(value)
}

fn validate_known_fields(value: &toml::Value, source: &Path) -> Result<(), ConfigError> {
    let root = value.as_table().ok_or_else(|| ConfigError::UnknownField {
        field: "<root>".to_owned(),
    })?;
    validate_table(
        root,
        "",
        &[
            "vault",
            "default_key",
            "key_material",
            "editor",
            "clipboard_timeout_seconds",
            "security",
            "git",
        ],
    )?;
    if let Some(security) = root.get("security") {
        let security = security.as_table().ok_or_else(|| ConfigError::Malformed {
            path: source.to_owned(),
        })?;
        validate_table(security, "security", &["inactivity_timeout_seconds"])?;
    }
    let Some(git) = root.get("git") else {
        return Ok(());
    };
    let git = git.as_table().ok_or_else(|| ConfigError::Malformed {
        path: source.to_owned(),
    })?;
    validate_table(git, "git", &["remotes"])?;
    if let Some(remotes) = git.get("remotes") {
        let remotes = remotes.as_array().ok_or_else(|| ConfigError::Malformed {
            path: source.to_owned(),
        })?;
        for (index, remote) in remotes.iter().enumerate() {
            let remote = remote.as_table().ok_or_else(|| ConfigError::Malformed {
                path: source.to_owned(),
            })?;
            validate_table(
                remote,
                &format!("git.remotes[{index}]"),
                &["name", "url", "server_id", "application_id"],
            )?;
        }
    }
    Ok(())
}

fn validate_table(table: &toml::Table, prefix: &str, allowed: &[&str]) -> Result<(), ConfigError> {
    for key in table.keys() {
        if !allowed.contains(&key.as_str()) {
            let field = if prefix.is_empty() {
                key.clone()
            } else {
                format!("{prefix}.{key}")
            };
            return Err(ConfigError::UnknownField { field });
        }
    }
    Ok(())
}

fn reject_insecure_fields(value: &toml::Value, prefix: &str) -> Result<(), ConfigError> {
    match value {
        toml::Value::Table(table) => {
            for (key, nested) in table {
                let path = if prefix.is_empty() {
                    key.clone()
                } else {
                    format!("{prefix}.{key}")
                };
                let normalized = key.to_ascii_lowercase().replace('-', "_");
                if [
                    "password",
                    "passphrase",
                    "token",
                    "secret",
                    "credential",
                    "credentials",
                    "private_key",
                ]
                .contains(&normalized.as_str())
                {
                    return Err(ConfigError::InsecureField { field: path });
                }
                reject_insecure_fields(nested, &path)?;
            }
        }
        toml::Value::Array(values) => {
            for (index, nested) in values.iter().enumerate() {
                reject_insecure_fields(nested, &format!("{prefix}[{index}]"))?;
            }
        }
        _ => {}
    }
    Ok(())
}

fn resolve_path(base: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        normalize_absolute(path.to_owned())
    } else {
        normalize_absolute(base.join(path))
    }
}

fn normalize_absolute(path: PathBuf) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::ParentDir => {
                if !normalized.pop() {
                    normalized.push(component.as_os_str());
                }
            }
            _ => normalized.push(component.as_os_str()),
        }
    }
    normalized
}

#[cfg(target_os = "windows")]
fn native_config_directory() -> Option<PathBuf> {
    env::var_os("APPDATA").map(PathBuf::from)
}

#[cfg(target_os = "macos")]
fn native_config_directory() -> Option<PathBuf> {
    env::var_os("HOME")
        .map(PathBuf::from)
        .map(|home| home.join("Library/Application Support"))
}

#[cfg(all(unix, not(target_os = "macos")))]
fn native_config_directory() -> Option<PathBuf> {
    match env::var_os("XDG_CONFIG_HOME") {
        Some(path) if !path.is_empty() && Path::new(&path).is_absolute() => {
            Some(PathBuf::from(path))
        }
        _ => env::var_os("HOME")
            .filter(|home| !home.is_empty())
            .map(PathBuf::from)
            .map(|home| home.join(".config")),
    }
}

#[cfg(not(any(unix, target_os = "windows")))]
fn native_config_directory() -> Option<PathBuf> {
    None
}
