use futures::TryFutureExt;
use mullvad_types::{
    access_method::Error as ApiAccessMethodError,
    custom_list::Error as CustomListError,
    relay_constraints::{RelayConstraints, RelaySettings, WireguardConstraints},
    settings::{DnsState, Settings},
};
use std::{
    fmt::{self, Display},
    ops::Deref,
    path::{Path, PathBuf},
    pin::Pin,
};
use talpid_core::firewall::is_local_address;
use talpid_types::ErrorExt;
use tokio::{
    fs,
    io::{self, AsyncWriteExt},
};

pub mod patch;

const SETTINGS_FILE: &str = "settings.json";

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Unable to read settings file {0}")]
    ReadError(String, #[source] io::Error),

    #[error("Unable to parse settings file")]
    ParseError(#[source] serde_json::Error),

    #[error("Unable to remove settings file {0}")]
    DeleteError(String, #[source] io::Error),

    #[error("Unable to serialize settings to JSON")]
    SerializeError(#[source] serde_json::Error),

    #[error("Unable to write settings to {0}")]
    WriteError(String, #[source] io::Error),

    #[error("Failed to apply settings update")]
    UpdateFailed(Box<dyn std::error::Error + Send + Sync>),

    #[error("Failed to parse IP network from string: {0}")]
    ParseIp(String),
}

/// Converts an [Error] to a management interface status
impl From<Error> for mullvad_management_interface::Status {
    fn from(error: Error) -> mullvad_management_interface::Status {
        use mullvad_management_interface::{Code, Status};
        match error {
            Error::DeleteError(..) | Error::WriteError(..) | Error::ReadError(..) => {
                Status::new(Code::FailedPrecondition, error.to_string())
            }
            Error::UpdateFailed(err)
                if err
                    .downcast_ref::<mullvad_types::custom_list::Error>()
                    .is_some() =>
            {
                let custom_list_err = *err.downcast::<CustomListError>().unwrap();
                handle_custom_list_error(custom_list_err)
            }
            Error::UpdateFailed(err) if err.downcast_ref::<ApiAccessMethodError>().is_some() => {
                let api_access_method_err = *err.downcast::<ApiAccessMethodError>().unwrap();
                handle_api_access_method_error(api_access_method_err)
            }
            Error::SerializeError(..)
            | Error::ParseError(..)
            | Error::UpdateFailed(..)
            | Error::ParseIp(..) => Status::new(Code::Internal, error.to_string()),
        }
    }
}

fn handle_api_access_method_error(
    api_access_method_err: ApiAccessMethodError,
) -> mullvad_management_interface::Status {
    use mullvad_management_interface::{Code, Status};
    match api_access_method_err {
        error @ ApiAccessMethodError::DuplicateName => Status::with_details(
            Code::AlreadyExists,
            error.to_string(),
            mullvad_management_interface::API_ACCESS_METHOD_EXISTS_DETAILS.into(),
        ),
        error => Status::unknown(error.to_string()),
    }
}

fn handle_custom_list_error(
    custom_list_err: CustomListError,
) -> mullvad_management_interface::Status {
    use mullvad_management_interface::{Code, Status};
    match custom_list_err {
        error @ CustomListError::ListExists | error @ CustomListError::DuplicateName => {
            Status::with_details(
                Code::AlreadyExists,
                error.to_string(),
                mullvad_management_interface::CUSTOM_LIST_LIST_EXISTS_DETAILS.into(),
            )
        }
        error @ CustomListError::NameTooLong => Status::with_details(
            Code::InvalidArgument,
            error.to_string(),
            mullvad_management_interface::CUSTOM_LIST_LIST_NAME_TOO_LONG_DETAILS.into(),
        ),
        error @ CustomListError::ListNotFound => Status::with_details(
            Code::NotFound,
            error.to_string(),
            mullvad_management_interface::CUSTOM_LIST_LIST_NOT_FOUND_DETAILS.into(),
        ),
    }
}

type ChangeListener =
    Box<dyn FnMut(&Settings) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> + Send + Sync>;

pub struct SettingsPersister {
    settings: Settings,
    path: PathBuf,
    on_change_listeners: Vec<ChangeListener>,
}

pub type MadeChanges = bool;

impl SettingsPersister {
    /// Loads user settings from file. If it fails, it returns the defaults, and overwrites the old
    /// settings.
    pub async fn load(settings_dir: &Path) -> Self {
        let path = settings_dir.join(SETTINGS_FILE);
        let LoadSettingsResult {
            settings,
            should_save,
        } = Self::load_inner(|| Self::load_from_file(&path)).await;

        let mut persister = SettingsPersister {
            settings,
            path,
            on_change_listeners: vec![],
        };

        if should_save && let Err(error) = persister.save().await {
            log::error!(
                "{}",
                error.display_chain_with_msg("Failed to save updated settings")
            );
        }

        persister
    }

    /// Loads user settings from file. The only difference between this and [Self::load] is that
    /// it is read-only.
    pub async fn read_only(settings_dir: &Path) -> Settings {
        let path = settings_dir.join(SETTINGS_FILE);
        let LoadSettingsResult { settings, .. } =
            Self::load_inner(|| Self::load_from_file(&path)).await;
        settings
    }

    /// Loads user settings, returning default settings if it should fail.
    ///
    /// `load_settings` allows the caller to decide how to load [`Settings`]
    /// from an bitrary resource.
    ///
    /// `load_inner` will always succeed, even in the presence of IO operations.
    /// Errors are handled gracefully by returning the default [`Settings`] if
    /// necessary.
    async fn load_inner<F, R>(load_settings: F) -> LoadSettingsResult
    where
        F: FnOnce() -> R,
        R: Future<Output = Result<Settings, Error>>,
    {
        let mut result = match load_settings().await {
            Ok(settings) => LoadSettingsResult {
                settings,
                should_save: false,
            },
            Err(Error::ReadError(_, err)) if err.kind() == io::ErrorKind::NotFound => {
                log::info!("No settings were found. Using defaults.");
                LoadSettingsResult {
                    settings: Self::default_settings(),
                    should_save: true,
                }
            }
            Err(error) => {
                log::warn!(
                    "{}",
                    error.display_chain_with_msg("Failed to load settings. Using defaults.")
                );

                let settings = Settings {
                    // Protect the user by blocking the internet by default. Previous settings may
                    // not have caused the daemon to enter the non-blocking disconnected state.
                    // On android lockdown mode is handled by the OS so setting this to true
                    // has no effect.
                    #[cfg(not(target_os = "android"))]
                    lockdown_mode: true,
                    ..Self::default_settings()
                };

                LoadSettingsResult {
                    settings,
                    should_save: true,
                }
            }
        };

        if cfg!(target_os = "android") {
            // Auto-connect is managed by Android itself.
            result.settings.auto_connect = false;
        }
        if crate::version::is_beta_version() {
            result.should_save |= !result.settings.show_beta_releases;
            result.settings.show_beta_releases = true;
        }

        result
    }

    async fn load_from_file<P>(path: P) -> Result<Settings, Error>
    where
        P: AsRef<Path> + Clone,
    {
        let display = path.clone();
        log::info!("Loading settings from {}", display.as_ref().display());
        let settings_bytes = fs::read(path)
            .await
            .map_err(|error| Error::ReadError(display.as_ref().display().to_string(), error))?;
        let settings = Self::load_from_bytes(&settings_bytes)?;
        Ok(settings)
    }

    fn load_from_bytes(bytes: &[u8]) -> Result<Settings, Error> {
        serde_json::from_slice(bytes).map_err(Error::ParseError)
    }

    async fn save(&mut self) -> Result<(), Error> {
        Self::save_inner(&self.path, &self.settings).await
    }

    /// Serializes the settings and saves them to the given file.
    async fn save_inner(path: &Path, settings: &Settings) -> Result<(), Error> {
        log::debug!("Writing settings to {}", path.display());

        let buffer = serde_json::to_string_pretty(settings).map_err(Error::SerializeError)?;
        let mut file = mullvad_fs::AtomicFile::new(path)
            .await
            .map_err(|e| Error::WriteError(path.display().to_string(), e))?;
        file.write_all(&buffer.into_bytes())
            .await
            .map_err(|e| Error::WriteError(path.display().to_string(), e))?;
        file.finalize()
            .await
            .map_err(|e| Error::WriteError(path.display().to_string(), e))?;

        Ok(())
    }

    /// Resets to default settings
    pub async fn reset(&mut self) -> Result<(), Error> {
        self.settings = Self::default_settings();
        let path = self.path.clone();
        self.save()
            .or_else(|e| async move {
                log::error!(
                    "{}",
                    e.display_chain_with_msg("Unable to save default settings")
                );
                log::info!("Will attempt to remove settings file");
                match fs::remove_file(&path).await {
                    Ok(()) => Ok(()),
                    Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
                    Err(e) => Err(Error::DeleteError(path.display().to_string(), e)),
                }
            })
            .await?;

        self.notify_listeners().await;

        Ok(())
    }

    pub const fn settings(&self) -> &Settings {
        &self.settings
    }

    pub fn to_settings(&self) -> Settings {
        self.settings.clone()
    }

    /// Modifies `Settings::default()` somewhat, e.g. depending on whether a beta version
    /// is being run or not.
    fn default_settings() -> Settings {
        let mut settings = Settings::default();

        if crate::version::is_beta_version() {
            settings.show_beta_releases = true;
        }

        settings
    }

    /// Edit the settings in a closure and write the changes to disk.
    ///
    /// # On success
    ///
    /// Returns a boolean indicating whether any settings were changed.
    ///
    /// # On failure
    ///
    /// If the settings could not be written to disk, all changes are rolled
    /// back, and an error is returned.
    ///
    /// # Note
    ///
    /// If no settings were changed, no I/O will be performed.
    pub async fn update(
        &mut self,
        update_fn: impl FnOnce(&mut Settings),
    ) -> Result<MadeChanges, Error> {
        self.try_update(|settings| -> Result<(), Error> {
            update_fn(settings);
            Ok(())
        })
        .await
    }

    /// Edit the settings in a closure, and write the changes to disk.
    ///
    /// # On success
    ///
    /// Returns a boolean indicating whether any settings were changed.
    ///
    /// # On failure
    ///
    /// `try_update` may fail in two scenarios
    ///
    /// ## The settings could not be written to disk
    ///
    /// In this case, all changes are rolled back and an error is returned.
    ///
    /// ## `update_fn` failed
    ///
    /// If `update_fn` were to fail the error will be propagated through the
    /// [`Error::UpdateFailed`] error variant. Since the error will be boxed, it
    /// has to be downcasted at runtime using [`Box::downcast`] in case you want
    /// to inspect the error closer.
    ///
    /// ```ignore
    /// #[derive(Debug, thiserror::Error)]
    /// pub enum MyError {
    ///   #[error("Failed for this reason: {0:?}")]
    ///   Failed(String),
    /// }
    ///
    /// let settings = Settings::default_settings();
    /// let err = settings.try_update(|settings| {
    ///   // Perform some update on the settings
    ///   settings.allow_lan = !settings.allow_lan;
    ///   // Fail the update procedure due to some error
    ///   Err(MyError::Failed("No particular reason".to_string()))
    /// });
    ///
    /// matches!(err, Error::UpdateFailed(_)) ;
    /// assert_eq!(settings, Settings::default_settings())
    /// ```
    pub async fn try_update<E>(
        &mut self,
        update_fn: impl FnOnce(&mut Settings) -> Result<(), E>,
    ) -> Result<MadeChanges, Error>
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        let mut new_settings = self.settings.clone();

        update_fn(&mut new_settings)
            .map_err(Box::from)
            .map_err(Error::UpdateFailed)?;

        if self.settings == new_settings {
            return Ok(false);
        }

        Self::save_inner(&self.path, &new_settings).await?;
        self.settings = new_settings;

        self.notify_listeners().await;

        Ok(true)
    }

    /// Return a compact summary of important settings
    pub fn summary(&self) -> SettingsSummary<'_> {
        SettingsSummary {
            settings: &self.settings,
        }
    }

    pub fn register_change_listener(
        &mut self,
        mut change_listener: impl FnMut(&Settings) + Send + Sync + 'static,
    ) {
        self.on_change_listeners.push(Box::new(move |settings| {
            change_listener(settings);
            // lord forgive me
            Box::pin(async move {})
        }));
    }

    pub fn register_change_listener_async<
        F: FnMut(&Settings) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = ()> + Send + Sync + 'static,
    >(
        &mut self,
        mut change_listener: F,
    ) {
        self.on_change_listeners.push(Box::new(move |settings| {
            Box::pin(change_listener(settings))
        }));
    }

    async fn notify_listeners(&mut self) {
        for listener in &mut self.on_change_listeners {
            listener(&self.settings).await;
        }
    }
}

struct LoadSettingsResult {
    settings: Settings,
    should_save: bool,
}

impl Deref for SettingsPersister {
    type Target = Settings;

    fn deref(&self) -> &Self::Target {
        &self.settings
    }
}

/// A compact summary of important settings
pub struct SettingsSummary<'a> {
    settings: &'a Settings,
}

impl Display for SettingsSummary<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let bool_to_label = |state| {
            if state { "on" } else { "off" }
        };

        let relay_settings = self.settings.get_relay_settings();

        write!(f, ", wg mtu: ")?;
        Self::fmt_option(f, self.settings.tunnel_options.wireguard.mtu)?;

        if let RelaySettings::Normal(RelayConstraints {
            wireguard_constraints: WireguardConstraints { ip_version, .. },
            ..
        }) = relay_settings
        {
            write!(f, ", wg ip version: {ip_version}")?;
        }

        let multihop = match relay_settings {
            RelaySettings::Normal(RelayConstraints {
                wireguard_constraints,
                ..
            }) => wireguard_constraints.multihop(),
            _ => false,
        };

        write!(
            f,
            ", multihop: {}, ipv6 (tun): {}, lan: {}, pq: {}, obfs: {}",
            bool_to_label(multihop),
            bool_to_label(self.settings.tunnel_options.generic.enable_ipv6),
            bool_to_label(self.settings.allow_lan),
            self.settings.tunnel_options.wireguard.quantum_resistant,
            self.settings.obfuscation_settings.selected_obfuscation,
        )?;

        // Print DNS options

        write!(f, ", dns: ")?;

        match self.settings.tunnel_options.dns_options.state {
            DnsState::Default => {
                let mut content = vec![];
                let default_options = &self.settings.tunnel_options.dns_options.default_options;

                if default_options.block_ads {
                    content.push("ads");
                }
                if default_options.block_trackers {
                    content.push("trackers");
                }
                if default_options.block_malware {
                    content.push("malware");
                }
                if default_options.block_adult_content {
                    content.push("adult");
                }
                if default_options.block_gambling {
                    content.push("gambling");
                }
                if default_options.block_social_media {
                    content.push("social media");
                }
                if content.is_empty() {
                    content.push("default");
                }
                write!(f, "{}", content.join(" "))?;
            }
            DnsState::Custom => {
                // NOTE: Technically inaccurate, as the gateway IP is a local IP but isn't treated
                // as one.
                let contains_local = self
                    .settings
                    .tunnel_options
                    .dns_options
                    .custom_options
                    .addresses
                    .iter()
                    .copied()
                    .any(is_local_address);
                let contains_public = self
                    .settings
                    .tunnel_options
                    .dns_options
                    .custom_options
                    .addresses
                    .iter()
                    .copied()
                    .any(|addr| !is_local_address(addr));

                match (contains_public, contains_local) {
                    (true, true) => f.write_str("custom, public, local")?,
                    (true, false) => f.write_str("custom, public")?,
                    (false, false) => f.write_str("custom, no addrs")?,
                    (false, true) => f.write_str("custom, local")?,
                }
            }
        }
        Ok(())
    }
}

impl SettingsSummary<'_> {
    fn fmt_option<T: Display>(f: &mut fmt::Formatter<'_>, val: Option<T>) -> fmt::Result {
        match &val {
            Some(inner) => inner.fmt(f),
            _ => f.write_str("unset"),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use mullvad_types::settings::SettingsVersion;

    #[test]
    #[should_panic]
    fn test_deserialization_failure_version_too_small() {
        let _version: SettingsVersion = serde_json::from_str("1").expect("Version too small");
    }

    #[test]
    #[should_panic]
    fn test_deserialization_failure_version_too_big() {
        let _version: SettingsVersion = serde_json::from_str("1000").expect("Version too big");
    }

    #[test]
    fn test_deserialization_success() {
        let _version: SettingsVersion =
            serde_json::from_str("2").expect("Failed to deserialize valid version");
    }

    #[test]
    fn test_serialization_success() {
        let version = SettingsVersion::V2;
        let s = serde_json::to_string(&version).expect("Failed to serialize");
        assert_eq!(s, "2");
    }

    #[test]
    fn test_deserialization() {
        let settings = br#"{
              "account_number": "0000000000000000",
              "relay_settings": {
                "normal": {
                  "location": {
                    "only": {
                      "location": {
                        "country": "gb"
                      }
                    }
                  },
                  "tunnel_protocol": "wireguard",
                  "wireguard_constraints": {
                    "port": "any"
                  },
                  "openvpn_constraints": {
                    "port": "any",
                    "protocol": "any"
                  }
                }
              },
              "bridge_settings": {
                "bridge_type": "normal",
                "normal": {
                  "location": "any"
                },
                "custom": {
                  "socks5_local": {
                    "local_port": 1080,
                    "remote_endpoint": {
                      "address": "1.3.3.7:22",
                      "protocol": "tcp"
                    }
                  }
                }
              },
              "bridge_state": "auto",
              "allow_lan": true,
              "lockdown_mode": false,
              "auto_connect": true,
              "tunnel_options": {
                "openvpn": {
                  "mssfix": null
                },
                "wireguard": {
                  "mtu": null,
                  "rotation_interval": null
                },
                "generic": {
                  "enable_ipv6": true
                }
              },
              "settings_version": 8,
              "show_beta_releases": false,
              "custom_lists": {
                "custom_lists": []
              },
              "recents": [
                {
                  "Multihop": {
                    "entry": {
                      "location": {
                        "country": "se"
                      }
                    },
                    "exit": {
                      "custom_list": {
                        "list_id": "df612270-79a4-47e9-92e7-3405c92f7678"
                      }
                    }
                  }
                },
                {
                  "Singlehop": {
                    "location": {
                      "hostname": [
                        "be",
                        "bru",
                        "be-bru-wg-103"
                      ]
                    }
                  }
                }
              ]
            }"#;

        let _ = SettingsPersister::load_from_bytes(settings).unwrap();
    }

    /// The [`SettingsPersister`] should always succeed when deserializing a
    /// [`Settings`] object from disk. However, there is a distinction between
    /// different error cases.
    ///
    /// If the settings file is missing, it could be because the user starts the
    /// app for the first time. As such, we should simply save the default
    /// [`Settings`] to disk.
    #[tokio::test]
    async fn test_deserialize_missing_settings() {
        let LoadSettingsResult {
            should_save,
            settings,
        } = SettingsPersister::load_inner(|| async {
            Err(Error::ReadError(
                "Settings are missing".to_string(),
                io::ErrorKind::NotFound.into(),
            ))
        })
        .await;

        assert!(
            should_save,
            "Settings should be saved to disk if they didn't exist previously"
        );

        assert!(
            !settings.lockdown_mode,
            "The daemon should not block the internet if settings are missing"
        );
    }

    /// The [`SettingsPersister`] should always succeed when deserializing a
    /// [`Settings`] object from disk. However, there is a distinction between
    /// different error cases.
    ///
    /// If the settings file is corrupt, we can assume that the user has started
    /// the app previously, but we can't know what settings the user have
    /// changed. In this case, we should safeguard against leaks by locking down
    /// the network before the user initiates a connection attempt or change
    /// these settings.
    #[tokio::test]
    async fn test_deserialize_invalid_settings() {
        let LoadSettingsResult {
            should_save,
            settings,
        } = SettingsPersister::load_inner(|| async {
            SettingsPersister::load_from_bytes(b"Not a valid settings file")
        })
        .await;

        assert!(
            should_save,
            "Settings should be saved to disk if they have become corrupt"
        );

        assert!(
            settings.lockdown_mode,
            "The daemon should block the internet if settings are corrupt"
        );
    }

    #[tokio::test]
    async fn test_deserialize_recents() {
        let default: Settings = serde_json::from_str("{}").expect("Failed to deserialize");
        assert_eq!(default.recents, Some(vec![]));

        let disabled: Settings =
            serde_json::from_str(r#"{"recents": null}"#).expect("Failed to deserialize");
        assert_eq!(disabled.recents, None);
    }
}
