//! Code for migrating between different versions of the settings.
//! Migration only supports migrating forward, to newer formats.
//!
//! A settings migration module is responsible for converting
//! from its own version to the next version. So `v3::migrate`
//! migrates from settings version `V3` to `V4` etc.
//!
//! Migration modules may NOT import and use structs that may
//! change. Because then a later change to the current code can break
//! old migrations. The only items a settings migration module may import
//! are anything from `std`, `jnix`, `serde` and the following:
//!
//! ```ignore
//! use super::{Error, Result};
//! use mullvad_types::relay_constraints::Constraint;
//! use mullvad_types::settings::SettingsVersion;
//! ```
//!
//! Any other type must be vendored into the migration module so the format
//! it has is locked over time.
//!
//! There should never be multiple migrations between two official releases. At most one.
//! Between releases, dev builds can break the settings without having a proper migration path.
//!
//! # Creating a migration
//!
//! 1. Copy `vX.rs.template` to `vX.rs` where `X` is the latest settings version right now.
//! 1. Add the new version (`Y = X+1`) to `SettingsVersion` and bump `CURRENT_SETTINGS_VERSION` to
//!    `Y`.
//! 1. Write a comment in the new module about how the format changed, what it needs to migrate.
//! 1. Implement the migration and add adequate tests.
//! 1. Add to the changelog: "Settings format updated to `vY`"

use std::{
    path::Path,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
};
use tokio::{
    fs,
    io::{self, AsyncWriteExt},
};

use crate::{DaemonEventSender, InternalDaemonEvent};

mod account_history;
mod device;
mod v1;
mod v10;
mod v11;
mod v12;
mod v13;
mod v14;
mod v15;
mod v16;
mod v2;
mod v3;
mod v4;
mod v5;
mod v6;
mod v7;
mod v8;
mod v9;

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

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Failed to read the settings")]
    Read(#[source] io::Error),

    #[error("Failed to deserialize settings")]
    Deserialize(#[source] serde_json::Error),

    #[error("Unexpected settings format")]
    InvalidSettingsContent,

    #[error("Missing setting {0}")]
    MissingKey(&'static str),

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

    #[error("Unable to open settings for writing")]
    Open(#[source] io::Error),

    #[error("Unable to write new settings")]
    Write(#[source] io::Error),

    #[error("Unable to sync settings to disk")]
    SyncSettings(#[source] io::Error),

    #[error("Failed to read the account history")]
    ReadHistory(#[source] io::Error),

    #[error("Failed to write new account history")]
    WriteHistory(#[source] io::Error),

    #[error("Failed to parse account history")]
    ParseHistory,

    #[cfg(windows)]
    #[error("Failed to restore Windows update backup")]
    WinMigration(#[source] windows::Error),
}

pub type Result<T> = std::result::Result<T, Error>;

/// Returns whether there is any background work remaining.
#[derive(Clone)]
pub struct MigrationComplete(Arc<AtomicBool>);

impl MigrationComplete {
    pub fn new(state: bool) -> Self {
        Self(Arc::new(AtomicBool::new(state)))
    }

    pub fn is_complete(&self) -> bool {
        self.0.load(Ordering::Relaxed)
    }

    fn set_complete(&mut self) {
        self.0.store(true, Ordering::Relaxed);
    }
}

/// Contains discarded data that may be useful for later work.
pub type MigrationData = v5::MigrationData;

/// Directories that may be passed to the migration logic.
pub struct Directories<'path> {
    cache_dir: &'path Path,
    settings_dir: &'path Path,
}

pub async fn migrate_all(cache_dir: &Path, settings_dir: &Path) -> Result<Option<MigrationData>> {
    #[cfg(windows)]
    windows::migrate_after_windows_update(settings_dir)
        .await
        .map_err(Error::WinMigration)?;

    let path = settings_dir.join(SETTINGS_FILE);

    if !path.is_file() {
        return Ok(None);
    }

    let settings_bytes = fs::read(&path).await.map_err(Error::Read)?;

    let mut settings: serde_json::Value =
        serde_json::from_reader(&settings_bytes[..]).map_err(Error::Deserialize)?;

    let old_settings = settings.clone();
    let directories = Directories {
        cache_dir,
        settings_dir,
    };

    let migration_data = migrate_settings(Some(directories), &mut settings).await?;

    if settings == old_settings {
        // Nothing changed
        return Ok(migration_data);
    }

    let buffer = serde_json::to_string_pretty(&settings).map_err(Error::Serialize)?;

    let mut file = fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(&path)
        .await
        .map_err(Error::Open)?;
    file.write_all(&buffer.into_bytes())
        .await
        .map_err(Error::Write)?;
    file.sync_data().await.map_err(Error::SyncSettings)?;

    log::debug!("Migrated settings. Wrote settings to {}", path.display());

    Ok(migration_data)
}

async fn migrate_settings(
    directories: Option<Directories<'_>>,
    settings: &mut serde_json::Value,
) -> Result<Option<MigrationData>> {
    if !settings.is_object() {
        return Err(Error::InvalidSettingsContent);
    }

    v1::migrate(settings)?;
    v2::migrate(settings)?;
    v3::migrate(settings)?;
    v4::migrate(settings)?;

    if let Some(Directories {
        cache_dir,
        settings_dir,
    }) = directories
    {
        account_history::migrate_location(cache_dir, settings_dir).await;
        account_history::migrate_formats(settings_dir, settings).await?;
    }

    let migration_data = v5::migrate(settings)?;
    v6::migrate(settings)?;
    v7::migrate(settings)?;
    v8::migrate(settings)?;

    v9::migrate(
        settings,
        #[cfg(target_os = "android")]
        directories.map(|directories| v9::Directories {
            settings: directories.settings_dir,
        }),
    )?;

    v10::migrate(settings)?;
    v11::migrate(settings)?;
    v12::migrate(settings)?;
    v13::migrate(settings)?;
    v14::migrate(settings)?;
    v15::migrate(settings)?;
    v16::migrate(settings)?;

    Ok(migration_data)
}

pub(crate) fn migrate_device(
    migration_data: MigrationData,
    rest_handle: mullvad_api::rest::MullvadRestHandle,
    daemon_tx: DaemonEventSender<InternalDaemonEvent>,
) -> MigrationComplete {
    let migration_complete = MigrationComplete::new(false);
    device::generate_device(
        migration_data,
        migration_complete.clone(),
        rest_handle,
        daemon_tx,
    );
    migration_complete
}

#[cfg(windows)]
mod windows {
    use std::{
        ffi::OsStr,
        io,
        os::windows::ffi::OsStrExt,
        path::Path,
        ptr::{self, NonNull},
    };
    use talpid_types::ErrorExt;
    use tokio::fs;
    use windows_sys::Win32::{
        Foundation::{ERROR_SUCCESS, LocalFree},
        Security::{
            Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT, SE_OBJECT_TYPE},
            IsWellKnownSid, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
            SECURITY_DESCRIPTOR, SID, WELL_KNOWN_SID_TYPE, WinBuiltinAdministratorsSid,
            WinLocalSystemSid,
        },
    };

    #[expect(non_camel_case_types)]
    type SECURITY_INFORMATION = u32;

    const MIGRATION_DIRNAME: &str = "windows.old";
    const MIGRATE_FILES: [(&str, bool); 3] = [
        ("settings.json", true),
        ("device.json", true),
        ("account-history.json", false),
    ];

    #[derive(thiserror::Error, Debug)]
    pub enum Error {
        #[error("Unable to find local appdata directory")]
        FindAppData,

        #[error("Could not acquire security descriptor of backup directory")]
        SecurityInformation(#[source] io::Error),

        #[error("Backup directory is not owned by SYSTEM or Built-in Administrators")]
        WrongOwner,

        #[error("Failed to copy files during migration")]
        Io(#[source] io::Error),
    }

    /// Attempts to restore the Mullvad settings from `C:\windows.old` after an update of Windows.
    /// Upon success, it returns `Ok(true)` if the migration succeeded, and `Ok(false)` if no
    /// migration was needed.
    pub async fn migrate_after_windows_update(
        destination_settings_dir: &Path,
    ) -> Result<bool, Error> {
        let system_appdata_dir = dirs::data_local_dir().ok_or(Error::FindAppData)?;
        if !destination_settings_dir.starts_with(system_appdata_dir) {
            return Ok(false);
        }

        let settings_path = destination_settings_dir.join(super::SETTINGS_FILE);
        if settings_path.exists() {
            return Ok(false);
        }

        let mut components = destination_settings_dir.components();
        let prefix = if let Some(prefix) = components.next() {
            prefix
        } else {
            return Ok(false);
        };
        let root = if let Some(root) = components.next() {
            root
        } else {
            return Ok(false);
        };

        let windows_old_dir = Path::new(&prefix).join(root).join(MIGRATION_DIRNAME);
        let source_settings_dir = Path::new(&windows_old_dir).join(&components);
        if !source_settings_dir.exists() {
            return Ok(false);
        }

        let security_info =
            SecurityInformation::from_file(windows_old_dir.as_path(), OWNER_SECURITY_INFORMATION)
                .map_err(Error::SecurityInformation)?;

        let owner_sid = security_info.owner().ok_or(Error::WrongOwner)?;

        if !is_well_known_sid(owner_sid, WinLocalSystemSid)
            && !is_well_known_sid(owner_sid, WinBuiltinAdministratorsSid)
        {
            return Err(Error::WrongOwner);
        }

        if !destination_settings_dir.exists() {
            fs::create_dir_all(destination_settings_dir)
                .await
                .map_err(Error::Io)?;
        }

        let mut result = Ok(true);

        for (file, required) in &MIGRATE_FILES {
            let from = source_settings_dir.join(file);
            let to = destination_settings_dir.join(file);

            log::debug!("Migrating {} to {}", from.display(), to.display());

            match fs::copy(&from, &to).await {
                Ok(_) => {
                    let _ = fs::remove_file(from).await;
                }
                Err(error) => {
                    log::error!(
                        "{}",
                        error.display_chain_with_msg(&format!(
                            "Failed to copy {} to {}",
                            from.display(),
                            to.display()
                        ))
                    );
                    if *required {
                        result = Err(Error::Io(error));
                    }
                }
            }
        }

        if let Err(error) = fs::remove_dir(source_settings_dir).await {
            log::trace!(
                "{}",
                error.display_chain_with_msg("Failed to delete backup directory")
            );
        }

        result
    }

    struct SecurityInformation {
        security_descriptor: NonNull<SECURITY_DESCRIPTOR>,
        owner: Option<NonNull<SID>>,
    }

    impl SecurityInformation {
        pub fn from_file<T: AsRef<OsStr>>(
            path: T,
            security_information: SECURITY_INFORMATION,
        ) -> Result<Self, io::Error> {
            Self::from_object(path, SE_FILE_OBJECT, security_information)
        }

        pub fn from_object<T: AsRef<OsStr>>(
            object_name: T,
            object_type: SE_OBJECT_TYPE,
            security_information: SECURITY_INFORMATION,
        ) -> Result<Self, io::Error> {
            let mut u16_path: Vec<u16> = object_name.as_ref().encode_wide().collect();
            u16_path.push(0u16);

            let mut security_descriptor: PSECURITY_DESCRIPTOR = ptr::null_mut();
            let mut owner: PSID = ptr::null_mut();

            // SAFETY:
            // - u16_path is a null-terminated UTF-16 string
            // - The *mut pointers are allowed to be null
            let status = unsafe {
                GetNamedSecurityInfoW(
                    u16_path.as_ptr(),
                    object_type,
                    security_information,
                    &raw mut owner,
                    ptr::null_mut(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                    &raw mut security_descriptor,
                )
            };

            if status != ERROR_SUCCESS {
                return Err(std::io::Error::from_raw_os_error(status as i32));
            }

            let Some(security_descriptor) = NonNull::new(security_descriptor) else {
                return Err(std::io::Error::other("GetNamedSecurityInfoW returned null"));
            };

            Ok(SecurityInformation {
                security_descriptor: security_descriptor.cast::<SECURITY_DESCRIPTOR>(),
                owner: NonNull::new(owner.cast::<SID>()),
            })
        }

        pub fn owner(&self) -> Option<&SID> {
            // SAFETY: GetNamedSecurityInfoW promises that this pointer was valid,
            // and it should stay valid until we deallocate self.security_descriptor.
            self.owner.map(|ptr| unsafe { ptr.as_ref() })
        }
    }

    impl Drop for SecurityInformation {
        fn drop(&mut self) {
            // SAFETY: GetNamedSecurityInfoW promises that this pointer was valid,
            // and we do not deallocate it before this point. Since we have &mut self,
            // we know that no one else has a reference to security_descriptor.
            unsafe { LocalFree(self.security_descriptor.as_ptr() as PSECURITY_DESCRIPTOR) };
        }
    }

    fn is_well_known_sid(sid: &SID, well_known_sid_type: WELL_KNOWN_SID_TYPE) -> bool {
        // SAFETY: this function doesn't take ownership of sid, and is trivially safe to call.
        unsafe { IsWellKnownSid(sid as *const SID as PSID, well_known_sid_type) == 1 }
    }
}

#[cfg(test)]
mod test {
    use mullvad_types::settings::{CURRENT_SETTINGS_VERSION, Settings};

    use crate::migrations::migrate_settings;

    /// Ensure that no migration logic runs for the default settings by checking whether anything
    /// has changed after running the migration code
    #[tokio::test]
    async fn test_settings_format_version() {
        let default_settings = serde_json::to_value(Settings::default()).unwrap();
        let mut migrated_settings = default_settings.clone();

        migrate_settings(None, &mut migrated_settings)
            .await
            .unwrap();

        assert_eq!(default_settings, migrated_settings);
    }

    /// Ensure that the settings version is correct after running all migration code
    #[tokio::test]
    async fn test_all_migrations() {
        const V1_SETTINGS: &str = include_str!("v1_settings.json");
        let mut settings = serde_json::from_str(V1_SETTINGS).unwrap();
        migrate_settings(None, &mut settings).await.unwrap();
        let deserialized: Settings = serde_json::from_value(settings).unwrap();
        assert_eq!(deserialized.settings_version, CURRENT_SETTINGS_VERSION);
    }
}
