use crate::data::DirectoryInfo;
use crate::jre::check_jre;
use crate::prelude::ModLoader;
use crate::state;
use crate::state::{
    Credentials, DefaultPage, DeviceToken, DeviceTokenKey, DeviceTokenPair,
    Hooks, LinkedData, MemorySettings, ModrinthCredentials, Profile,
    ProfileInstallStage, Theme, WindowSize,
};
use crate::util::fetch::{read_json, IoSemaphore};
use chrono::{DateTime, Utc};
use p256::ecdsa::SigningKey;
use p256::pkcs8::DecodePrivateKey;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::sync::Semaphore;
use uuid::Uuid;

pub async fn migrate_legacy_data<'a, E>(exec: E) -> crate::Result<()>
where
    E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy,
{
    let mut settings = state::Settings::get(exec).await?;

    if settings.migrated {
        return Ok(());
    };

    let old_launcher_root = if let Some(dir) = default_settings_dir() {
        dir
    } else {
        return Ok(());
    };
    let old_launcher_root_str = old_launcher_root.to_string_lossy().to_string();

    let new_launcher_root = DirectoryInfo::get_initial_settings_dir().ok_or(
        crate::ErrorKind::FSError(
            "Could not find valid config dir".to_string(),
        ),
    )?;
    let new_launcher_root_str = new_launcher_root
        .to_string_lossy()
        .to_string()
        .trim_end_matches('/')
        .trim_end_matches('\\')
        .to_string();

    let io_semaphore = IoSemaphore(Semaphore::new(10));
    let settings_path = old_launcher_root.join("settings.json");

    if let Ok(legacy_settings) =
        read_json::<LegacySettings>(&settings_path, &io_semaphore).await
    {
        settings.max_concurrent_writes = legacy_settings.max_concurrent_writes;
        settings.max_concurrent_downloads =
            legacy_settings.max_concurrent_downloads;
        settings.theme = match legacy_settings.theme {
            LegacyTheme::Dark => Theme::Dark,
            LegacyTheme::Light => Theme::Light,
            LegacyTheme::Oled => Theme::Oled,
        };
        settings.default_page = match legacy_settings.default_page {
            LegacyDefaultPage::Home => DefaultPage::Home,
            LegacyDefaultPage::Library => DefaultPage::Library,
        };
        settings.collapsed_navigation = legacy_settings.collapsed_navigation;
        settings.advanced_rendering = legacy_settings.advanced_rendering;
        settings.native_decorations = legacy_settings.native_decorations;
        settings.telemetry = !legacy_settings.opt_out_analytics;
        settings.discord_rpc = !legacy_settings.disable_discord_rpc;
        settings.developer_mode = legacy_settings.developer_mode;
        settings.onboarded = legacy_settings.fully_onboarded;
        settings.extra_launch_args = legacy_settings.custom_java_args;
        settings.custom_env_vars = legacy_settings.custom_env_args;
        settings.memory.maximum = legacy_settings.memory.maximum;
        settings.force_fullscreen = legacy_settings.force_fullscreen;
        settings.game_resolution.0 = legacy_settings.game_resolution.0;
        settings.game_resolution.1 = legacy_settings.game_resolution.1;
        settings.hide_on_process_start = legacy_settings.hide_on_process;
        settings.hooks.pre_launch = legacy_settings.hooks.pre_launch;
        settings.hooks.wrapper = legacy_settings.hooks.wrapper;
        settings.hooks.post_exit = legacy_settings.hooks.post_exit;

        if let Some(path) = legacy_settings
            .loaded_config_dir
            .clone()
            .and_then(|x| x.to_str().map(|x| x.to_string()))
        {
            if path != old_launcher_root_str {
                settings.custom_dir = Some(path);
            }
        }

        settings.prev_custom_dir = Some(old_launcher_root_str.clone());

        for (_, legacy_version) in legacy_settings.java_globals.0 {
            if let Ok(Some(mut java_version)) =
                check_jre(PathBuf::from(legacy_version.path)).await
            {
                java_version.path = java_version
                    .path
                    .replace(&old_launcher_root_str, &new_launcher_root_str);

                java_version.upsert(exec).await?;
            }
        }

        let modrinth_auth_path =
            old_launcher_root.join("caches/metadata/auth.json");
        if let Ok(creds) = read_json::<LegacyModrinthCredentials>(
            &modrinth_auth_path,
            &io_semaphore,
        )
        .await
        {
            ModrinthCredentials {
                session: creds.session,
                expires: creds.expires_at,
                user_id: creds.user.id,
                active: true,
            }
            .upsert(exec)
            .await?;
        }

        let minecraft_auth_path =
            old_launcher_root.join("caches/metadata/minecraft_auth.json");
        if let Ok(minecraft_auth) = read_json::<LegacyMinecraftAuthStore>(
            &minecraft_auth_path,
            &io_semaphore,
        )
        .await
        {
            let minecraft_users_len = minecraft_auth.users.len();
            for (uuid, credential) in minecraft_auth.users {
                Credentials {
                    id: credential.id,
                    username: credential.username,
                    access_token: credential.access_token,
                    refresh_token: credential.refresh_token,
                    expires: credential.expires,
                    active: minecraft_auth.default_user == Some(uuid)
                        || minecraft_users_len == 1,
                }
                .upsert(exec)
                .await?;
            }

            if let Some(device_token) = minecraft_auth.token {
                if let Ok(private_key) =
                    SigningKey::from_pkcs8_pem(&device_token.private_key)
                {
                    if let Ok(uuid) = Uuid::parse_str(&device_token.id) {
                        DeviceTokenPair {
                            token: DeviceToken {
                                issue_instant: device_token.token.issue_instant,
                                not_after: device_token.token.not_after,
                                token: device_token.token.token,
                                display_claims: device_token
                                    .token
                                    .display_claims,
                            },
                            key: DeviceTokenKey {
                                id: uuid,
                                key: private_key,
                                x: device_token.x,
                                y: device_token.y,
                            },
                        }
                        .upsert(exec)
                        .await?;
                    }
                }
            }
        }

        if let Ok(profiles_dir) = std::fs::read_dir(
            &legacy_settings
                .loaded_config_dir
                .unwrap_or(old_launcher_root)
                .join("profiles"),
        ) {
            for entry in profiles_dir.flatten() {
                if entry.path().is_dir() {
                    let profile_path = entry.path().join("profile.json");

                    if let Ok(profile) =
                        read_json::<LegacyProfile>(&profile_path, &io_semaphore)
                            .await
                    {
                        Profile {
                            path: profile.path,
                            install_stage: match profile.install_stage {
                                LegacyProfileInstallStage::Installed => {
                                    ProfileInstallStage::Installed
                                }
                                LegacyProfileInstallStage::Installing => {
                                    ProfileInstallStage::Installing
                                }
                                LegacyProfileInstallStage::PackInstalling => {
                                    ProfileInstallStage::PackInstalling
                                }
                                LegacyProfileInstallStage::NotInstalled => {
                                    ProfileInstallStage::NotInstalled
                                }
                            },
                            name: profile.metadata.name,
                            icon_path: profile.metadata.icon.map(|x| {
                                x.replace(
                                    &old_launcher_root_str,
                                    &new_launcher_root_str,
                                )
                            }),
                            game_version: profile.metadata.game_version,
                            loader: match profile.metadata.loader {
                                LegacyModLoader::Vanilla => ModLoader::Vanilla,
                                LegacyModLoader::Forge => ModLoader::Forge,
                                LegacyModLoader::Fabric => ModLoader::Fabric,
                                LegacyModLoader::Quilt => ModLoader::Quilt,
                                LegacyModLoader::NeoForge => {
                                    ModLoader::NeoForge
                                }
                            },
                            loader_version: profile
                                .metadata
                                .loader_version
                                .map(|x| x.id),
                            groups: profile.metadata.groups,
                            linked_data: profile.metadata.linked_data.and_then(
                                |x| {
                                    if let Some(project_id) = x.project_id {
                                        if let Some(version_id) = x.version_id {
                                            if let Some(locked) = x.locked {
                                                return Some(LinkedData {
                                                    project_id,
                                                    version_id,
                                                    locked,
                                                });
                                            }
                                        }
                                    }

                                    None
                                },
                            ),
                            created: profile.metadata.date_created,
                            modified: profile.metadata.date_modified,
                            last_played: profile.metadata.last_played,
                            submitted_time_played: profile
                                .metadata
                                .submitted_time_played,
                            recent_time_played: profile
                                .metadata
                                .recent_time_played,
                            java_path: profile.java.as_ref().and_then(|x| {
                                x.override_version.clone().map(|x| {
                                    x.path.replace(
                                        &old_launcher_root_str,
                                        &new_launcher_root_str,
                                    )
                                })
                            }),
                            extra_launch_args: profile
                                .java
                                .as_ref()
                                .and_then(|x| x.extra_arguments.clone()),
                            custom_env_vars: profile
                                .java
                                .and_then(|x| x.custom_env_args),
                            memory: profile
                                .memory
                                .map(|x| MemorySettings { maximum: x.maximum }),
                            force_fullscreen: profile.fullscreen,
                            game_resolution: profile
                                .resolution
                                .map(|x| WindowSize(x.0, x.1)),
                            hooks: Hooks {
                                pre_launch: profile
                                    .hooks
                                    .as_ref()
                                    .and_then(|x| x.pre_launch.clone()),
                                wrapper: profile
                                    .hooks
                                    .as_ref()
                                    .and_then(|x| x.wrapper.clone()),
                                post_exit: profile
                                    .hooks
                                    .and_then(|x| x.post_exit),
                            },
                        }
                        .upsert(exec)
                        .await?;
                    }
                }
            }
        }

        settings.migrated = true;
        settings.update(exec).await?;
    }

    Ok(())
}

#[derive(Deserialize, Debug, Clone)]
struct LegacySettings {
    pub theme: LegacyTheme,
    pub memory: LegacyMemorySettings,
    #[serde(default)]
    pub force_fullscreen: bool,
    pub game_resolution: LegacyWindowSize,
    pub custom_java_args: Vec<String>,
    pub custom_env_args: Vec<(String, String)>,
    pub java_globals: LegacyJavaGlobals,
    pub hooks: LegacyHooks,
    pub max_concurrent_downloads: usize,
    pub max_concurrent_writes: usize,
    pub collapsed_navigation: bool,
    #[serde(default)]
    pub disable_discord_rpc: bool,
    #[serde(default)]
    pub hide_on_process: bool,
    #[serde(default)]
    pub native_decorations: bool,
    #[serde(default)]
    pub default_page: LegacyDefaultPage,
    #[serde(default)]
    pub developer_mode: bool,
    #[serde(default)]
    pub opt_out_analytics: bool,
    #[serde(default)]
    pub advanced_rendering: bool,
    #[serde(default)]
    pub fully_onboarded: bool,
    #[serde(default = "default_settings_dir")]
    pub loaded_config_dir: Option<PathBuf>,
}

fn default_settings_dir() -> Option<PathBuf> {
    Some(dirs::config_dir()?.join("com.modrinth.theseus.offline"))
}

#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LegacyTheme {
    Dark,
    Light,
    Oled,
}

#[derive(Deserialize, Default, Debug, Clone, Copy)]
enum LegacyDefaultPage {
    #[default]
    Home,
    Library,
}

#[derive(Deserialize, Debug, Clone)]
struct LegacyHooks {
    pub pre_launch: Option<String>,
    pub wrapper: Option<String>,
    pub post_exit: Option<String>,
}

#[derive(Deserialize, Debug, Clone, Copy)]
struct LegacyMemorySettings {
    pub maximum: u32,
}

#[derive(Deserialize, Debug, Clone, Copy)]
struct LegacyWindowSize(pub u16, pub u16);

#[derive(Debug, Deserialize, Clone)]
struct LegacyJavaGlobals(HashMap<String, LegacyJavaVersion>);

#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
struct LegacyJavaVersion {
    pub path: String,
    pub version: String,
    pub architecture: String,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyModrinthUser {
    pub id: String,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyModrinthCredentials {
    pub session: String,
    pub expires_at: DateTime<Utc>,
    pub user: LegacyModrinthUser,
}

#[derive(Deserialize, Debug)]
struct LegacyMinecraftAuthStore {
    pub users: HashMap<Uuid, LegacyCredentials>,
    pub token: Option<LegacySaveDeviceToken>,
    pub default_user: Option<Uuid>,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyCredentials {
    pub id: Uuid,
    pub username: String,
    pub access_token: String,
    pub refresh_token: String,
    pub expires: DateTime<Utc>,
}

#[derive(Deserialize, Debug)]
struct LegacySaveDeviceToken {
    pub id: String,
    pub private_key: String,
    pub x: String,
    pub y: String,
    pub token: LegacyDeviceToken,
}

#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
struct LegacyDeviceToken {
    pub issue_instant: DateTime<Utc>,
    pub not_after: DateTime<Utc>,
    pub token: String,
    pub display_claims: HashMap<String, serde_json::Value>,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyProfile {
    #[serde(default)]
    pub install_stage: LegacyProfileInstallStage,
    #[serde(default)]
    pub path: String,
    pub metadata: LegacyProfileMetadata,
    pub java: Option<LegacyJavaSettings>,
    pub memory: Option<LegacyMemorySettings>,
    pub resolution: Option<LegacyWindowSize>,
    pub fullscreen: Option<bool>,
    pub hooks: Option<LegacyHooks>,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyProfileMetadata {
    pub name: String,
    pub icon: Option<String>,
    #[serde(default)]
    pub groups: Vec<String>,

    pub game_version: String,
    #[serde(default)]
    pub loader: LegacyModLoader,
    pub loader_version: Option<LegacyLoaderVersion>,

    pub linked_data: Option<LegacyLinkedData>,

    #[serde(default)]
    pub date_created: DateTime<Utc>,
    #[serde(default)]
    pub date_modified: DateTime<Utc>,
    pub last_played: Option<DateTime<Utc>>,
    #[serde(default)]
    pub submitted_time_played: u64,
    #[serde(default)]
    pub recent_time_played: u64,
}

#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
enum LegacyModLoader {
    #[default]
    Vanilla,
    Forge,
    Fabric,
    Quilt,
    NeoForge,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyLinkedData {
    pub project_id: Option<String>,
    pub version_id: Option<String>,

    #[serde(default = "default_locked")]
    pub locked: Option<bool>,
}

fn default_locked() -> Option<bool> {
    Some(true)
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyJavaSettings {
    pub override_version: Option<LegacyJavaVersion>,
    pub extra_arguments: Option<Vec<String>>,
    pub custom_env_args: Option<Vec<(String, String)>>,
}

#[derive(Deserialize, Clone, Debug)]
struct LegacyLoaderVersion {
    pub id: String,
}

#[derive(Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
enum LegacyProfileInstallStage {
    Installed,
    Installing,
    PackInstalling,
    #[default]
    NotInstalled,
}
