//! # Key-value configuration management.

use std::env;
use std::path::Path;
use std::str::FromStr;

use anyhow::{Context as _, Result, bail, ensure};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;

use crate::blob::BlobObject;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport};
use crate::{constants, stats};

/// The available configuration keys.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Display,
    EnumString,
    AsRefStr,
    EnumIter,
    EnumProperty,
    PartialOrd,
    Ord,
    Serialize,
    Deserialize,
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
    /// Email address, used in the `From:` field.
    Addr,

    /// IMAP server hostname.
    MailServer,

    /// IMAP server username.
    MailUser,

    /// IMAP server password.
    MailPw,

    /// IMAP server port.
    MailPort,

    /// IMAP server security (e.g. TLS, STARTTLS).
    MailSecurity,

    /// How to check TLS certificates.
    ///
    /// "IMAP" in the name is for compatibility,
    /// this actually applies to both IMAP and SMTP connections.
    ImapCertificateChecks,

    /// SMTP server hostname.
    SendServer,

    /// SMTP server username.
    SendUser,

    /// SMTP server password.
    SendPw,

    /// SMTP server port.
    SendPort,

    /// SMTP server security (e.g. TLS, STARTTLS).
    SendSecurity,

    /// Deprecated option for backwards compatibility.
    ///
    /// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
    SmtpCertificateChecks,

    /// Whether to use OAuth 2.
    ///
    /// Historically contained other bitflags, which are now deprecated.
    /// Should not be extended in the future, create new config keys instead.
    ServerFlags,

    /// True if proxy is enabled.
    ///
    /// Can be used to disable proxy without erasing known URLs.
    ProxyEnabled,

    /// Proxy URL.
    ///
    /// Supported URLs schemes are `http://` (HTTP), `https://` (HTTPS),
    /// `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
    ///
    /// May contain multiple URLs separated by newline, in which case the first one is used.
    ProxyUrl,

    /// True if SOCKS5 is enabled.
    ///
    /// Can be used to disable SOCKS5 without erasing SOCKS5 configuration.
    ///
    /// Deprecated in favor of `ProxyEnabled`.
    Socks5Enabled,

    /// SOCKS5 proxy server hostname or address.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5Host,

    /// SOCKS5 proxy server port.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5Port,

    /// SOCKS5 proxy server username.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5User,

    /// SOCKS5 proxy server password.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5Password,

    /// Own name to use in the `From:` field when sending messages.
    Displayname,

    /// Own status to display, sent in message footer.
    Selfstatus,

    /// Own avatar filename.
    Selfavatar,

    /// Send BCC copy to self.
    ///
    /// Should be enabled for multi-device setups.
    ///
    /// This is automatically enabled when importing/exporting a backup,
    /// setting up a second device, or receiving a sync message.
    #[strum(props(default = "0"))]
    BccSelf,

    /// True if Message Delivery Notifications (read receipts) should
    /// be sent and requested.
    #[strum(props(default = "1"))]
    MdnsEnabled,

    /// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
    /// ones are moved there anyway.
    #[strum(props(default = "1"))]
    MvboxMove,

    /// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
    ///
    /// This will not entirely disable other folders, e.g. the spam folder will also still
    /// be watched for new messages.
    #[strum(props(default = "0"))]
    OnlyFetchMvbox,

    /// Whether to show classic emails or only chat messages.
    #[strum(props(default = "2"))] // also change ShowEmails.default() on changes
    ShowEmails,

    /// Quality of the media files to send.
    #[strum(props(default = "0"))] // also change MediaQuality.default() on changes
    MediaQuality,

    /// If set to "1", then existing messages are considered to be already fetched.
    /// This flag is reset after successful configuration.
    #[strum(props(default = "1"))]
    FetchedExistingMsgs,

    /// Timer in seconds after which the message is deleted from the
    /// server.
    ///
    /// 0 means messages are never deleted by Delta Chat.
    ///
    /// Value 1 is treated as "delete at once": messages are deleted
    /// immediately, without moving to DeltaChat folder.
    ///
    /// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
    DeleteServerAfter,

    /// Timer in seconds after which the message is deleted from the
    /// device.
    ///
    /// Equals to 0 by default, which means the message is never
    /// deleted.
    #[strum(props(default = "0"))]
    DeleteDeviceAfter,

    /// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
    /// `ProviderOptions::delete_to_trash`.
    DeleteToTrash,

    /// The primary email address.
    ConfiguredAddr,

    /// List of configured IMAP servers as a JSON array.
    ConfiguredImapServers,

    /// Configured IMAP server hostname.
    ///
    /// This is replaced by `configured_imap_servers` for new configurations.
    ConfiguredMailServer,

    /// Configured IMAP server port.
    ///
    /// This is replaced by `configured_imap_servers` for new configurations.
    ConfiguredMailPort,

    /// Configured IMAP server security (e.g. TLS, STARTTLS).
    ///
    /// This is replaced by `configured_imap_servers` for new configurations.
    ConfiguredMailSecurity,

    /// Configured IMAP server username.
    ///
    /// This is set if user has configured username manually.
    ConfiguredMailUser,

    /// Configured IMAP server password.
    ConfiguredMailPw,

    /// Configured TLS certificate checks.
    /// This option is saved on successful configuration
    /// and should not be modified manually.
    ///
    /// This actually applies to both IMAP and SMTP connections,
    /// but has "IMAP" in the name for backwards compatibility.
    ConfiguredImapCertificateChecks,

    /// List of configured SMTP servers as a JSON array.
    ConfiguredSmtpServers,

    /// Configured SMTP server hostname.
    ///
    /// This is replaced by `configured_smtp_servers` for new configurations.
    ConfiguredSendServer,

    /// Configured SMTP server port.
    ///
    /// This is replaced by `configured_smtp_servers` for new configurations.
    ConfiguredSendPort,

    /// Configured SMTP server security (e.g. TLS, STARTTLS).
    ///
    /// This is replaced by `configured_smtp_servers` for new configurations.
    ConfiguredSendSecurity,

    /// Configured SMTP server username.
    ///
    /// This is set if user has configured username manually.
    ConfiguredSendUser,

    /// Configured SMTP server password.
    ConfiguredSendPw,

    /// Deprecated, stored for backwards compatibility.
    ///
    /// ConfiguredImapCertificateChecks is actually used.
    ConfiguredSmtpCertificateChecks,

    /// Whether OAuth 2 is used with configured provider.
    ConfiguredServerFlags,

    /// Configured folder for incoming messages.
    ConfiguredInboxFolder,

    /// Configured folder for chat messages.
    ConfiguredMvboxFolder,

    /// Configured "Trash" folder.
    ConfiguredTrashFolder,

    /// Unix timestamp of the last successful configuration.
    ConfiguredTimestamp,

    /// ID of the configured provider from the provider database.
    ConfiguredProvider,

    /// True if account is configured.
    Configured,

    /// True if account is a chatmail account.
    IsChatmail,

    /// True if `IsChatmail` mustn't be autoconfigured. For tests.
    FixIsChatmail,

    /// True if account is muted.
    IsMuted,

    /// Optional tag as "Work", "Family".
    /// Meant to help profile owner to differ between profiles with similar names.
    PrivateTag,

    /// Read-only core version string.
    #[strum(serialize = "sys.version")]
    SysVersion,

    /// Maximal recommended attachment size in bytes.
    #[strum(serialize = "sys.msgsize_max_recommended")]
    SysMsgsizeMaxRecommended,

    /// Space separated list of all config keys available.
    #[strum(serialize = "sys.config_keys")]
    SysConfigKeys,

    /// True if it is a bot account.
    Bot,

    /// True when to skip initial start messages in groups.
    #[strum(props(default = "0"))]
    SkipStartMessages,

    /// Whether we send a warning if the password is wrong (set to false when we send a warning
    /// because we do not want to send a second warning)
    #[strum(props(default = "0"))]
    NotifyAboutWrongPw,

    /// If a warning about exceeding quota was shown recently,
    /// this is the percentage of quota at the time the warning was given.
    /// Unset, when quota falls below minimal warning threshold again.
    QuotaExceeding,

    /// Timestamp of the last time housekeeping was run
    LastHousekeeping,

    /// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
    LastCantDecryptOutgoingMsgs,

    /// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
    #[strum(props(default = "60"))]
    ScanAllFoldersDebounceSecs,

    /// Whether to avoid using IMAP IDLE even if the server supports it.
    ///
    /// This is a developer option for testing "fake idle".
    #[strum(props(default = "0"))]
    DisableIdle,

    /// Timestamp of the next check for donation request need.
    DonationRequestNextCheck,

    /// Defines the max. size (in bytes) of messages downloaded automatically.
    /// 0 = no limit.
    #[strum(props(default = "0"))]
    DownloadLimit,

    /// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set
    /// and `Bot` unset.
    ///
    /// On real devices, this is usually always enabled and `BccSelf` is the only setting
    /// that controls whether sync messages are sent.
    ///
    /// In tests, this is usually disabled.
    #[strum(props(default = "1"))]
    SyncMsgs,

    /// Space-separated list of all the authserv-ids which we believe
    /// may be the one of our email server.
    ///
    /// See `crate::authres::update_authservid_candidates`.
    AuthservIdCandidates,

    /// Make all outgoing messages with Autocrypt header "multipart/signed".
    SignUnencrypted,

    /// Let the core save all events to the database.
    /// This value is used internally to remember the MsgId of the logging xdc
    #[strum(props(default = "0"))]
    DebugLogging,

    /// Last message processed by the bot.
    LastMsgId,

    /// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by
    /// default.
    ///
    /// This is not supposed to be changed by UIs and only used for testing.
    #[strum(props(default = "172800"))]
    GossipPeriod,

    /// Row ID of the key in the `keypairs` table
    /// used for signatures, encryption to self and included in `Autocrypt` header.
    KeyId,

    /// Send statistics to Delta Chat's developers.
    /// Can be exposed to the user as a setting.
    StatsSending,

    /// Last time statistics were sent to Delta Chat's developers
    StatsLastSent,

    /// Last time `update_message_stats()` was called
    StatsLastUpdate,

    /// This key is sent to the statistics bot so that the bot can recognize the user
    /// without storing the email address
    StatsId,

    /// The last contact id that already existed when statistics-sending was enabled for the first time.
    StatsLastOldContactId,

    /// MsgId of webxdc map integration.
    WebxdcIntegration,

    /// Enable webxdc realtime features.
    #[strum(props(default = "1"))]
    WebxdcRealtimeEnabled,

    /// Last device token stored on the chatmail server.
    ///
    /// If it has not changed, we do not store
    /// the device token again.
    DeviceToken,

    /// Device token encrypted with OpenPGP.
    ///
    /// We store encrypted token next to `device_token`
    /// to avoid encrypting it differently and
    /// storing the same token multiple times on the server.
    EncryptedDeviceToken,

    /// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`.
    /// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
    /// using this still run unmodified code.
    TestHooks,

    /// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
    FailOnReceivingFullMsg,

    /// Enable composing emails with Header Protection as defined in
    /// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
    /// Protected Email".
    #[strum(props(default = "1"))]
    StdHeaderProtectionComposing,
}

impl Config {
    /// Whether the config option is synced across devices.
    ///
    /// This must be checked on both sides so that if there are different client versions, the
    /// synchronisation of a particular option is either done or not done in both directions.
    /// Moreover, receivers of a config value need to check if a key can be synced because if it is
    /// a file path, it could otherwise lead to exfiltration of files from a receiver's
    /// device if we assume an attacker to have control of a device in a multi-device setting or if
    /// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
    /// mustn't be controlled by other devices.
    pub(crate) fn is_synced(&self) -> bool {
        matches!(
            self,
            Self::Displayname
                | Self::MdnsEnabled
                | Self::MvboxMove
                | Self::ShowEmails
                | Self::Selfavatar
                | Self::Selfstatus,
        )
    }

    /// Whether the config option needs an IO scheduler restart to take effect.
    pub(crate) fn needs_io_restart(&self) -> bool {
        matches!(
            self,
            Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
        )
    }
}

impl Context {
    /// Returns true if configuration value is set in the db for the given key.
    ///
    /// NB: Don't use this to check if the key is configured because this doesn't look into
    /// environment. The proper use of this function is e.g. checking a key before setting it.
    pub(crate) async fn config_exists(&self, key: Config) -> Result<bool> {
        Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
    }

    /// Get a config key value. Returns `None` if no value is set.
    pub(crate) async fn get_config_opt(&self, key: Config) -> Result<Option<String>> {
        let env_key = format!("DELTACHAT_{}", key.as_ref().to_uppercase());
        if let Ok(value) = env::var(env_key) {
            return Ok(Some(value));
        }

        let value = match key {
            Config::Selfavatar => {
                let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
                rel_path.map(|p| {
                    get_abs_path(self, Path::new(&p))
                        .to_string_lossy()
                        .into_owned()
                })
            }
            Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
            Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
            Config::SysConfigKeys => Some(get_config_keys_string()),
            _ => self.sql.get_raw_config(key.as_ref()).await?,
        };
        Ok(value)
    }

    /// Get a config key value if set, or a default value. Returns `None` if no value exists.
    pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
        let value = self.get_config_opt(key).await?;
        if value.is_some() {
            return Ok(value);
        }

        // Default values
        let val = match key {
            Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
            Config::DeleteServerAfter => {
                match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
                    && Box::pin(self.is_chatmail()).await?
                {
                    true => Some("1".to_string()),
                    false => Some("0".to_string()),
                }
            }
            Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
            _ => key.get_str("default").map(|s| s.to_string()),
        };
        Ok(val)
    }

    /// Returns Some(T) if a value for the given key is set and was successfully parsed.
    /// Returns None if could not parse.
    pub(crate) async fn get_config_opt_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
        self.get_config_opt(key)
            .await
            .map(|s: Option<String>| s.and_then(|s| s.parse().ok()))
    }

    /// Returns Some(T) if a value for the given key exists (incl. default value) and was
    /// successfully parsed.
    /// Returns None if could not parse.
    pub async fn get_config_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
        self.get_config(key)
            .await
            .map(|s: Option<String>| s.and_then(|s| s.parse().ok()))
    }

    /// Returns 32-bit signed integer configuration value for the given key.
    pub async fn get_config_int(&self, key: Config) -> Result<i32> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns 32-bit unsigned integer configuration value for the given key.
    pub async fn get_config_u32(&self, key: Config) -> Result<u32> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns 64-bit signed integer configuration value for the given key.
    pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns 64-bit unsigned integer configuration value for the given key.
    pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns boolean configuration value (if set) for the given key.
    pub(crate) async fn get_config_bool_opt(&self, key: Config) -> Result<Option<bool>> {
        Ok(self
            .get_config_opt_parsed::<i32>(key)
            .await?
            .map(|x| x != 0))
    }

    /// Returns boolean configuration value for the given key.
    pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
        Ok(self
            .get_config(key)
            .await?
            .and_then(|s| s.parse::<i32>().ok())
            .map(|x| x != 0)
            .unwrap_or_default())
    }

    /// Returns true if movebox ("DeltaChat" folder) should be watched.
    pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::MvboxMove).await?
            || self.get_config_bool(Config::OnlyFetchMvbox).await?
            || !self.get_config_bool(Config::IsChatmail).await?)
    }

    /// Returns true if sync messages should be sent.
    pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::SyncMsgs).await?
            && self.get_config_bool(Config::BccSelf).await?
            && !self.get_config_bool(Config::Bot).await?)
    }

    /// Returns whether sync messages should be uploaded to the mvbox.
    pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::MvboxMove).await?
            || !self.get_config_bool(Config::IsChatmail).await?)
    }

    /// Returns whether MDNs should be requested.
    pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
        match self.get_config_bool_opt(Config::MdnsEnabled).await? {
            Some(val) => Ok(val),
            None => Ok(!self.get_config_bool(Config::Bot).await?),
        }
    }

    /// Returns whether MDNs should be sent.
    pub(crate) async fn should_send_mdns(&self) -> Result<bool> {
        self.get_config_bool(Config::MdnsEnabled).await
    }

    /// Gets configured "delete_server_after" value.
    ///
    /// `None` means never delete the message, `Some(0)` means delete
    /// at once, `Some(x)` means delete after `x` seconds.
    pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
        let val = match self
            .get_config_parsed::<i64>(Config::DeleteServerAfter)
            .await?
            .unwrap_or(0)
        {
            0 => None,
            1 => Some(0),
            x => Some(x),
        };
        Ok(val)
    }

    /// Gets the configured provider.
    ///
    /// The provider is determined by the current primary transport.
    pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
        let provider = ConfiguredLoginParam::load(self)
            .await?
            .and_then(|(_transport_id, param)| param.provider);
        Ok(provider)
    }

    /// Gets configured "delete_device_after" value.
    ///
    /// `None` means never delete the message, `Some(x)` means delete
    /// after `x` seconds.
    pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
        match self.get_config_int(Config::DeleteDeviceAfter).await? {
            0 => Ok(None),
            x => Ok(Some(i64::from(x))),
        }
    }

    /// Executes [`SyncData::Config`] item sent by other device.
    pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> {
        let config_value;
        let value = match key {
            Config::Selfavatar if value.is_empty() => None,
            Config::Selfavatar => {
                config_value = BlobObject::store_from_base64(self, value)?;
                config_value.as_deref()
            }
            _ => Some(value),
        };
        match key.is_synced() {
            true => self.set_config_ex(Nosync, *key, value).await,
            false => Ok(()),
        }
    }

    fn check_config(key: Config, value: Option<&str>) -> Result<()> {
        match key {
            Config::Socks5Enabled
            | Config::ProxyEnabled
            | Config::BccSelf
            | Config::MdnsEnabled
            | Config::MvboxMove
            | Config::OnlyFetchMvbox
            | Config::DeleteToTrash
            | Config::Configured
            | Config::Bot
            | Config::NotifyAboutWrongPw
            | Config::SyncMsgs
            | Config::SignUnencrypted
            | Config::DisableIdle => {
                ensure!(
                    matches!(value, None | Some("0") | Some("1")),
                    "Boolean value must be either 0 or 1"
                );
            }
            _ => (),
        }
        Ok(())
    }

    /// Set the given config key and make it effective.
    /// This may restart the IO scheduler. If `None` is passed as a value the value is cleared and
    /// set to the default if there is one.
    pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
        Self::check_config(key, value)?;

        let n_transports = self.count_transports().await?;
        if n_transports > 1
            && matches!(
                key,
                Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
            )
        {
            bail!("Cannot reconfigure {key} when multiple transports are configured");
        }

        let _pause = match key.needs_io_restart() {
            true => self.scheduler.pause(self).await?,
            _ => Default::default(),
        };
        if key == Config::StatsSending {
            let old_value = self.get_config(key).await?;
            let old_value = bool_from_config(old_value.as_deref());
            let new_value = bool_from_config(value);
            stats::pre_sending_config_change(self, old_value, new_value).await?;
        }
        self.set_config_internal(key, value).await?;
        if key == Config::StatsSending {
            stats::maybe_send_stats(self).await?;
        }
        Ok(())
    }

    pub(crate) async fn set_config_internal(&self, key: Config, value: Option<&str>) -> Result<()> {
        self.set_config_ex(Sync, key, value).await
    }

    pub(crate) async fn set_config_ex(
        &self,
        sync: sync::Sync,
        key: Config,
        mut value: Option<&str>,
    ) -> Result<()> {
        Self::check_config(key, value)?;
        let sync = sync == Sync && key.is_synced() && self.is_configured().await?;
        let better_value;

        match key {
            Config::Selfavatar => {
                self.sql
                    .execute("UPDATE contacts SET selfavatar_sent=0;", ())
                    .await?;
                match value {
                    Some(path) => {
                        let path = get_abs_path(self, Path::new(path));
                        let mut blob = BlobObject::create_and_deduplicate(self, &path, &path)?;
                        blob.recode_to_avatar_size(self).await?;
                        self.sql
                            .set_raw_config(key.as_ref(), Some(blob.as_name()))
                            .await?;
                        if sync {
                            let buf = fs::read(blob.to_abs_path()).await?;
                            better_value = base64::engine::general_purpose::STANDARD.encode(buf);
                            value = Some(&better_value);
                        }
                    }
                    None => {
                        self.sql.set_raw_config(key.as_ref(), None).await?;
                        if sync {
                            better_value = String::new();
                            value = Some(&better_value);
                        }
                    }
                }
                self.emit_event(EventType::SelfavatarChanged);
            }
            Config::DeleteDeviceAfter => {
                let ret = self.sql.set_raw_config(key.as_ref(), value).await;
                // Interrupt ephemeral loop to delete old messages immediately.
                self.scheduler.interrupt_ephemeral_task().await;
                ret?
            }
            Config::Displayname => {
                if let Some(v) = value {
                    better_value = sanitize_single_line(v);
                    value = Some(&better_value);
                }
                self.sql.set_raw_config(key.as_ref(), value).await?;
            }
            Config::Addr => {
                self.sql
                    .set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
                    .await?;
            }
            Config::MvboxMove => {
                self.sql.set_raw_config(key.as_ref(), value).await?;
                self.sql
                    .set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
                    .await?;
            }
            Config::ConfiguredAddr => {
                let Some(addr) = value else {
                    bail!("Cannot unset configured_addr");
                };

                if !self.is_configured().await? {
                    info!(
                        self,
                        "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
                    );
                    add_pseudo_transport(self, addr).await?;
                    self.sql
                        .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
                        .await?;
                }
                self.sql
                    .transaction(|transaction| {
                        if transaction.query_row(
                            "SELECT COUNT(*) FROM transports WHERE addr=?",
                            (addr,),
                            |row| {
                                let res: i64 = row.get(0)?;
                                Ok(res)
                            },
                        )? == 0
                        {
                            bail!("Address does not belong to any transport.");
                        }
                        transaction.execute(
                            "UPDATE config SET value=? WHERE keyname='configured_addr'",
                            (addr,),
                        )?;

                        // Clean up SMTP and IMAP APPEND queue.
                        //
                        // The messages in the queue have a different
                        // From address so we cannot send them over
                        // the new SMTP transport.
                        transaction.execute("DELETE FROM smtp", ())?;
                        transaction.execute("DELETE FROM imap_send", ())?;

                        Ok(())
                    })
                    .await?;
                self.sql.uncache_raw_config("configured_addr").await;
            }
            _ => {
                self.sql.set_raw_config(key.as_ref(), value).await?;
            }
        }
        if matches!(
            key,
            Config::Displayname | Config::Selfavatar | Config::PrivateTag
        ) {
            self.emit_event(EventType::AccountsItemChanged);
        }
        if key.is_synced() {
            self.emit_event(EventType::ConfigSynced { key });
        }
        if !sync {
            return Ok(());
        }
        let Some(val) = value else {
            return Ok(());
        };
        let val = val.to_string();
        if self
            .add_sync_item(SyncData::Config { key, val })
            .await
            .log_err(self)
            .is_err()
        {
            return Ok(());
        }
        self.scheduler.interrupt_inbox().await;
        Ok(())
    }

    /// Set the given config to an unsigned 32-bit integer value.
    pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> {
        self.set_config(key, Some(&value.to_string())).await?;
        Ok(())
    }

    /// Set the given config to a boolean value.
    pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
        self.set_config(key, from_bool(value)).await?;
        Ok(())
    }

    /// Sets an ui-specific key-value pair.
    /// Keys must be prefixed by `ui.`
    /// and should be followed by the name of the system and maybe subsystem,
    /// eg. `ui.desktop.linux.foo`, `ui.desktop.macos.bar`, `ui.ios.foobar`.
    pub async fn set_ui_config(&self, key: &str, value: Option<&str>) -> Result<()> {
        ensure!(key.starts_with("ui."), "set_ui_config(): prefix missing.");
        self.sql.set_raw_config(key, value).await
    }

    /// Gets an ui-specific value set by set_ui_config().
    pub async fn get_ui_config(&self, key: &str) -> Result<Option<String>> {
        ensure!(key.starts_with("ui."), "get_ui_config(): prefix missing.");
        self.sql.get_raw_config(key).await
    }
}

/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
    Some(if val { "1" } else { "0" })
}

pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
    config.is_some_and(|v| v.parse::<i32>().unwrap_or_default() != 0)
}

// Separate impl block for self address handling
impl Context {
    /// Determine whether the specified addr maps to the/a self addr.
    /// Returns `false` if no addresses are configured.
    pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
        Ok(self
            .get_config(Config::ConfiguredAddr)
            .await?
            .iter()
            .any(|a| addr_cmp(addr, a))
            || self
                .get_secondary_self_addrs()
                .await?
                .iter()
                .any(|a| addr_cmp(addr, a)))
    }

    /// Sets `primary_new` as the new primary self address and saves the old
    /// primary address (if exists) as a secondary address.
    ///
    /// This should only be used by test code and during configure.
    #[cfg(test)] // AEAP is disabled, but there are still tests for it
    pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
        self.quota.write().await.take();

        self.sql
            .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
            .await?;
        self.emit_event(EventType::ConnectivityChanged);
        Ok(())
    }

    /// Returns all primary and secondary self addresses.
    pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
        let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
        let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();

        Ok(primary_addrs.chain(secondary_addrs).collect())
    }

    /// Returns all secondary self addresses.
    pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
        self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
            let addr: String = row.get(0)?;
            Ok(addr)
        }).await
    }

    /// Returns the primary self address.
    /// Returns an error if no self addr is configured.
    pub async fn get_primary_self_addr(&self) -> Result<String> {
        self.get_config(Config::ConfiguredAddr)
            .await?
            .context("No self addr configured")
    }
}

/// Returns all available configuration keys concated together.
fn get_config_keys_string() -> String {
    let keys = Config::iter().fold(String::new(), |mut acc, key| {
        acc += key.as_ref();
        acc += " ";
        acc
    });

    format!(" {keys} ")
}

#[cfg(test)]
mod config_tests;
