use crate::commands::prelude::*;
use std::cmp::Reverse;

use crate::commands::async_command::{
    AsyncCallResult, AsyncCommandContext, With, async_command, async_command_with_cancel_state,
};
use crate::templates;
use crate::templates::{CreateProjectErr, ProjectTemplateInfo};
use crate::utils::{
    FileSystemTree, collect_notable_project_files_tree, default_project_path,
    find_existing_parent_dir_or_home, project_backup_path, trash_delete,
};
use async_zip::base::read::seek::ZipFileReader;
use futures::future::{join_all, try_join_all};
use futures::prelude::*;
use itertools::Itertools;
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::io;
use std::path::{Component, Path, PathBuf, Prefix};
use std::sync::atomic::AtomicUsize;
use std::time::Instant;
use tauri::{AppHandle, Emitter, Manager, State, Window};
use tauri_plugin_dialog::DialogExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Semaphore;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use vrc_get_vpm::ProjectType;
use vrc_get_vpm::environment::{
    InvalidRealProjectInformation, PackageInstaller, RealProjectInformation, Settings, UserProject,
    ValidRealProjectInformation, VccDatabaseConnection,
};
use vrc_get_vpm::io::{DefaultEnvironmentIo, DefaultProjectIo};
use vrc_get_vpm::version::UnityVersion;

#[derive(Debug, Clone, Serialize, specta::Type)]
pub struct TauriProject {
    // projet information
    name: String,
    path: String,
    project_type: TauriProjectType,
    unity: String,
    unity_revision: Option<String>,
    last_modified: i64,
    created_at: i64,
    favorite: bool,
    is_exists: bool,
    is_valid: Option<bool>,
}

#[derive(Debug, Clone, Serialize, specta::Type)]
pub struct TauriUpdatedRealProjectInfo {
    // project information
    path: String,
    is_valid: bool,
    project_type: TauriProjectType,
    unity: String,
    unity_revision: Option<String>,
}

#[derive(Debug, Clone, Serialize, specta::Type)]
enum TauriProjectType {
    Unknown,
    LegacySdk2,
    LegacyWorlds,
    LegacyAvatars,
    UpmWorlds,
    UpmAvatars,
    UpmStarter,
    Worlds,
    Avatars,
    VpmStarter,
}

impl From<ProjectType> for TauriProjectType {
    fn from(value: ProjectType) -> Self {
        match value {
            ProjectType::Unknown => Self::Unknown,
            ProjectType::LegacySdk2 => Self::LegacySdk2,
            ProjectType::LegacyWorlds => Self::LegacyWorlds,
            ProjectType::LegacyAvatars => Self::LegacyAvatars,
            ProjectType::UpmWorlds => Self::UpmWorlds,
            ProjectType::UpmAvatars => Self::UpmAvatars,
            ProjectType::UpmStarter => Self::UpmStarter,
            ProjectType::Worlds => Self::Worlds,
            ProjectType::Avatars => Self::Avatars,
            ProjectType::VpmStarter => Self::VpmStarter,
        }
    }
}

impl TauriProject {
    fn new(project: &UserProject) -> Self {
        let is_exists = std::fs::metadata(project.path().unwrap())
            .map(|x| x.is_dir())
            .unwrap_or(false);
        Self {
            name: project.name().unwrap().to_string(),
            path: project.path().unwrap().to_string(),
            project_type: project.project_type().into(),
            unity: project
                .unity_version()
                .map(|v| v.to_string())
                .unwrap_or_else(|| "unknown".into()),
            unity_revision: project.unity_revision().map(|x| x.to_string()),
            last_modified: project
                .last_modified()
                .map(|x| x.as_unix_milliseconds())
                .unwrap_or(0),
            created_at: project
                .crated_at()
                .map(|x| x.as_unix_milliseconds())
                .unwrap_or(0),
            favorite: project.favorite(),
            is_exists,
            is_valid: project.is_valid_project(),
        }
    }
}

impl TauriUpdatedRealProjectInfo {
    fn new(project: &ValidRealProjectInformation) -> Self {
        Self {
            path: project.path().into(),
            is_valid: true,
            project_type: project.project_type().into(),
            unity: project.unity_version().to_string(),
            unity_revision: project.unity_revision().map(Into::into),
        }
    }

    fn new_invalid(path: String) -> Self {
        Self {
            path,
            is_valid: false,
            project_type: TauriProjectType::Unknown,
            unity: String::new(),
            unity_revision: None,
        }
    }
}

async fn migrate_sanitize_projects(
    connection: &mut VccDatabaseConnection,
    io: &DefaultEnvironmentIo,
    settings: &Settings,
) -> io::Result<()> {
    info!("migrating projects from settings.json");
    // migrate from settings json
    connection.migrate(settings, io).await?;
    connection.dedup_projects();
    connection.normalize_path();
    Ok(())
}

fn sync_with_real_project_background(projects: &[UserProject], app: &AppHandle) {
    static LAST_UPDATE: std::sync::Mutex<Option<Instant>> = std::sync::Mutex::new(None);
    // update after one minutes.
    let mut lock = LAST_UPDATE.lock().unwrap_or_else(|mut e| {
        **e.get_mut() = None;
        e.into_inner()
    });
    if lock
        .map(|x| x.elapsed() > std::time::Duration::from_secs(60))
        .unwrap_or(true)
    {
        *lock = Some(Instant::now());
        // start update thread
        log::info!("starting sync with real project...");
        tauri::async_runtime::spawn(sync_with_real_project(
            projects
                .iter()
                .map(|x| x.path().unwrap().to_string())
                .collect(),
            app.clone(),
        ));
    } else {
        log::info!("skipped sync with real project since last update was less than 1 minute ago");
    }

    async fn sync_with_real_project(projects: Vec<String>, app: AppHandle) {
        app.emit("projects-update-in-progress", true).ok();

        tokio::time::sleep(std::time::Duration::from_secs(1)).await;

        info!(
            "loading real project information of {} projects",
            projects.len()
        );

        let io = app.state::<DefaultEnvironmentIo>();

        let projects = join_all(projects.into_iter().map(async |project| {
            match ValidRealProjectInformation::load_from_fs(&io, project.to_owned()).await {
                Ok(Some(project)) => {
                    app.emit(
                        "projects-updated",
                        TauriUpdatedRealProjectInfo::new(&project),
                    )
                    .ok();
                    RealProjectInformation::Valid(project)
                }
                Ok(None) => {
                    app.emit(
                        "projects-updated",
                        TauriUpdatedRealProjectInfo::new_invalid(project.clone()),
                    )
                    .ok();
                    RealProjectInformation::Invalid(InvalidRealProjectInformation::new(project))
                }
                Err(err) => {
                    app.emit(
                        "projects-updated",
                        TauriUpdatedRealProjectInfo::new_invalid(project.clone()),
                    )
                    .ok();
                    error!(gui_toast = false; "Error updating project information of {project}: {err}");
                    RealProjectInformation::Invalid(InvalidRealProjectInformation::new(project))
                }
            }
        }))
        .await;
        app.emit("projects-update-in-progress", false).ok();

        info!(
            "updating database real project information of {} projects",
            projects.len()
        );

        let mut connection = match VccDatabaseConnection::connect(io.inner()).await {
            Ok(connection) => connection,
            Err(e) => {
                error!("Error opening database: {e}");
                return;
            }
        };
        connection.sync_with_real_projects_information(projects);
        match connection.save(io.inner()).await {
            Ok(()) => {}
            Err(e) => {
                error!("Error updating database: {e}");
                return;
            }
        }

        info!("updated database based on real project information");
    }
}

#[tauri::command]
#[specta::specta]
pub async fn environment_projects(
    settings: State<'_, SettingsState>,
    io: State<'_, DefaultEnvironmentIo>,
    app: AppHandle,
) -> Result<Vec<TauriProject>, RustError> {
    let mut settings = settings.load_mut(io.inner()).await?;
    let mut connection = VccDatabaseConnection::connect(io.inner()).await?;

    migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;
    settings.load_from_db(&connection)?;
    connection.save(io.inner()).await?;
    settings.save().await?;

    info!("fetching projects");

    let mut projects = connection.get_projects();
    projects.retain(|x| x.path().is_some());

    sync_with_real_project_background(&projects, &app);

    let vec = projects.iter().map(TauriProject::new).collect::<Vec<_>>();

    Ok(vec)
}

#[derive(Serialize, specta::Type)]
pub enum TauriAddProjectWithPickerResult {
    NoFolderSelected,
    InvalidSelection,
    AlreadyAdded,
    Successful,
}

#[derive(Serialize, specta::Type, Clone)]
pub enum TauriRestoreProjectFromBackupResult {
    NoFileSelected,
    InvalidSelection,
    AlreadyExists,
    AlreadyAdded,
    Successful,
}

#[derive(Serialize, specta::Type, Clone)]
pub struct TauriRestoreProjectFromBackupProgress {
    total: usize,
    proceed: usize,
    last_proceed: String,
}

#[tauri::command]
#[specta::specta]
pub async fn environment_add_project_with_picker(
    settings: State<'_, SettingsState>,
    io: State<'_, DefaultEnvironmentIo>,
    window: Window,
) -> Result<TauriAddProjectWithPickerResult, RustError> {
    let mut environment_settings = settings.load_mut(io.inner()).await?;
    let project_dir = default_project_path(&mut environment_settings).to_string();
    environment_settings.maybe_save().await?;

    let Some(project_paths) = window
        .dialog()
        .file()
        .set_parent(&window)
        .set_directory(find_existing_parent_dir_or_home(Path::new(&project_dir)))
        .blocking_pick_folders()
    else {
        return Ok(TauriAddProjectWithPickerResult::NoFolderSelected);
    };

    let Ok(project_paths) = project_paths
        .into_iter()
        .map(|x| x.into_path_buf().map_err(|_| ()))
        .map_ok(|x| x.into_os_string().into_string().map_err(|_| ()))
        .flatten_ok()
        .collect::<Result<Vec<_>, ()>>()
    else {
        return Ok(TauriAddProjectWithPickerResult::InvalidSelection);
    };

    let unity_projects = match try_join_all(
        project_paths
            .into_iter()
            .map(|path| UnityProject::load(DefaultProjectIo::new(PathBuf::from(path).into()))),
    )
    .await
    {
        Ok(unity_projects) => unity_projects,
        Err(e) => {
            error!(gui_toast = false; "Error loading project: {e}");
            return Ok(TauriAddProjectWithPickerResult::InvalidSelection);
        }
    };

    {
        let mut settings = settings.load_mut(io.inner()).await?;
        let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
        migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;

        let projects = connection.get_projects();
        if (projects.iter().cartesian_product(unity_projects.iter()))
            .any(|(in_db, adding)| in_db.path().map(Path::new) == Some(adding.project_dir()))
        {
            return Ok(TauriAddProjectWithPickerResult::AlreadyAdded);
        }
        for unity_project in unity_projects {
            connection.add_project(&unity_project).await?;
        }
        connection.save(io.inner()).await?;
        settings.load_from_db(&connection)?;
        settings.save().await?;
    }

    Ok(TauriAddProjectWithPickerResult::Successful)
}

#[tauri::command]
#[specta::specta]
pub async fn environment_restore_project_from_backup(
    settings: State<'_, SettingsState>,
    io: State<'_, DefaultEnvironmentIo>,
    window: Window,
    channel: String,
) -> Result<
    AsyncCallResult<TauriRestoreProjectFromBackupProgress, TauriRestoreProjectFromBackupResult>,
    RustError,
> {
    let mut environment_settings = settings.load_mut(io.inner()).await?;
    let backup_dir = project_backup_path(&mut environment_settings).to_string();
    let project_dir = default_project_path(&mut environment_settings).to_string();
    environment_settings.maybe_save().await?;

    let Some(backup_path) = window
        .dialog()
        .file()
        .set_parent(&window)
        .set_directory(find_existing_parent_dir_or_home(Path::new(&backup_dir)))
        .add_filter("Zip Archive", &["zip"])
        .blocking_pick_file()
        .map(|x| x.into_path_buf())
        .transpose()?
    else {
        return Ok(AsyncCallResult::Result {
            value: TauriRestoreProjectFromBackupResult::NoFileSelected,
        });
    };

    let Some(project_name) = backup_project_name(&backup_path) else {
        return Ok(AsyncCallResult::Result {
            value: TauriRestoreProjectFromBackupResult::InvalidSelection,
        });
    };
    let restore_path = Path::new(&project_dir).join(project_name);
    if tokio::fs::try_exists(&restore_path).await? {
        return Ok(AsyncCallResult::Result {
            value: TauriRestoreProjectFromBackupResult::AlreadyExists,
        });
    }

    super::super::create_dir_all_with_err(&project_dir).await?;
    tokio::fs::create_dir(&restore_path).await?;

    let backup_path = backup_path.into_os_string().into_string().map_err(|_| {
        RustError::unrecoverable_str("backup archive path is not a valid unicode string")
    })?;
    let restore_path = restore_path
        .into_os_string()
        .into_string()
        .map_err(|_| RustError::unrecoverable_str("restore path is not a valid unicode string"))?;

    async_command(channel, window, async {
        With::<TauriRestoreProjectFromBackupProgress>::continue_async(move |ctx| async move {
            let backup_path = PathBuf::from(backup_path);
            let restore_path = PathBuf::from(restore_path);
            let remove_on_drop = RemoveDirOnDrop::new(&restore_path);

            extract_backup_zip(&backup_path, &restore_path, &ctx).await?;

            let unity_project = match UnityProject::load(DefaultProjectIo::new(
                restore_path.clone().into_boxed_path(),
            ))
            .await
            {
                Ok(unity_project) => unity_project,
                Err(e) => {
                    error!(gui_toast = false; "Error loading restored project: {e}");
                    return Ok(TauriRestoreProjectFromBackupResult::InvalidSelection);
                }
            };

            let settings = ctx.state::<SettingsState>();
            let io = ctx.state::<DefaultEnvironmentIo>();

            {
                let mut settings = settings.load_mut(io.inner()).await?;
                let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
                migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;

                let projects = connection.get_projects();
                if projects
                    .iter()
                    .any(|in_db| in_db.path().map(Path::new) == Some(unity_project.project_dir()))
                {
                    return Ok(TauriRestoreProjectFromBackupResult::AlreadyAdded);
                }

                connection.add_project(&unity_project).await?;
                connection.save(io.inner()).await?;
                settings.load_from_db(&connection)?;
                settings.save().await?;
            }

            remove_on_drop.forget();
            Ok(TauriRestoreProjectFromBackupResult::Successful)
        })
    })
    .await
}

struct RemoveDirOnDrop(PathBuf);

impl RemoveDirOnDrop {
    fn new(path: impl AsRef<Path>) -> Self {
        Self(path.as_ref().to_path_buf())
    }

    fn forget(self) {
        std::mem::forget(self);
    }
}

impl Drop for RemoveDirOnDrop {
    fn drop(&mut self) {
        let path = self.0.clone();
        std::thread::spawn(move || {
            let _ = std::fs::remove_dir_all(path);
        });
    }
}

fn backup_project_name(backup_path: &Path) -> Option<String> {
    Some(backup_path.file_stem()?.to_str()?.to_string())
}

async fn extract_backup_zip(
    backup_path: &Path,
    restore_path: &Path,
    ctx: &AsyncCommandContext<TauriRestoreProjectFromBackupProgress>,
) -> Result<(), RustError> {
    let archive = tokio::fs::File::open(backup_path).await?;
    let archive = BufReader::new(archive).compat();
    let mut reader = ZipFileReader::new(archive).await?;
    let total = reader.file().entries().len();

    ctx.emit(TauriRestoreProjectFromBackupProgress {
        total,
        proceed: 0,
        last_proceed: "Reading backup archive".to_string(),
    })
    .ok();

    for index in 0..total {
        let entry = &reader.file().entries()[index];
        let Some(filename) = entry.filename().as_str().ok() else {
            return Err(RustError::unrecoverable_str(
                "path in backup archive is not utf8",
            ));
        };
        let filename = fix_zip_path_separator(filename);
        let last_proceed = filename.to_string();
        let entry_is_dir = entry.dir()?;
        let filename_path = Path::new(filename.as_ref());
        if !is_complete_relative(filename_path) {
            return Err(RustError::unrecoverable_str(
                "directory traversal detected in backup archive",
            ));
        }

        let path = restore_path.join(filename_path);
        if entry_is_dir {
            tokio::fs::create_dir_all(&path).await?;
        } else {
            let parent = path.parent().ok_or_else(|| {
                RustError::unrecoverable_str("file entry in backup archive has no parent")
            })?;
            tokio::fs::create_dir_all(parent).await?;
            let mut entry_reader = reader.reader_without_entry(index).await?;
            let writer = tokio::fs::OpenOptions::new()
                .write(true)
                .create_new(true)
                .open(&path)
                .await?;
            futures::io::copy(&mut entry_reader, &mut writer.compat_write()).await?;
        }

        ctx.emit(TauriRestoreProjectFromBackupProgress {
            total,
            proceed: index + 1,
            last_proceed,
        })
        .ok();
    }

    Ok(())
}

fn fix_zip_path_separator(path: &str) -> std::borrow::Cow<'_, str> {
    if cfg!(windows) || !path.contains('\\') {
        std::borrow::Cow::Borrowed(path)
    } else {
        std::borrow::Cow::Owned(path.replace('\\', "/"))
    }
}

fn is_complete_relative(path: &Path) -> bool {
    for component in path.components() {
        match component {
            Component::Prefix(_) => return false,
            Component::RootDir => return false,
            Component::ParentDir => return false,
            Component::CurDir => {}
            Component::Normal(_) => {}
        }
    }
    true
}

#[tauri::command]
#[specta::specta]
pub async fn environment_remove_project_by_path(
    settings: State<'_, SettingsState>,
    io: State<'_, DefaultEnvironmentIo>,
    project_path: String,
    directory: bool,
) -> Result<(), RustError> {
    let mut settings = settings.load_mut(io.inner()).await?;
    let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
    migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;
    let Some(project) = connection.find_project(&project_path).unwrap() else {
        return Err(RustError::unrecoverable_str("project not found"));
    };

    if directory {
        let path = project.path().unwrap();
        info!("removing project directory: {path}");

        if let Err(err) = trash_delete(PathBuf::from(path)).await {
            error!("failed to remove project directory: {err}");
            return Err(localizable_error!(
                "projects:toast:remove directory failed by lock",
                err => err
            ));
        }
        info!("removed project directory: {path}");
    }

    connection.remove_project(&project);
    connection.save(io.inner()).await?;
    settings.load_from_db(&connection)?;
    settings.save().await?;

    Ok(())
}

#[derive(Serialize, specta::Type, Clone)]
pub struct TauriCopyProjectProgress {
    total: usize,
    proceed: usize,
    last_proceed: String,
}

#[tauri::command]
#[specta::specta]
pub async fn environment_copy_project_for_migration(
    project_copy: State<'_, ProjectCopyState>,
    window: Window,
    channel: String,
    source_path: String,
) -> Result<AsyncCallResult<TauriCopyProjectProgress, String>, RustError> {
    async fn create_folder(source_path: PathBuf) -> Option<PathBuf> {
        let folder = source_path.parent().unwrap();
        let name = source_path.file_name().unwrap();

        let name = name.to_str().unwrap();
        // first, try `-Migrated`
        let new_path = folder.join(format!("{name}-Migrated"));
        if let Ok(()) = tokio::fs::create_dir(&new_path).await {
            return Some(new_path);
        }

        for i in 1..100 {
            let new_path = folder.join(format!("{name}-Migrated-{i}"));
            if let Ok(()) = tokio::fs::create_dir(&new_path).await {
                return Some(new_path);
            }
        }

        None
    }

    copy_project(
        project_copy.inner().clone(),
        window,
        channel,
        source_path,
        create_folder,
    )
    .await
}

#[tauri::command]
#[specta::specta]
pub async fn environment_copy_project(
    project_copy: State<'_, ProjectCopyState>,
    window: Window,
    channel: String,
    source_path: String,
    new_path: String,
) -> Result<AsyncCallResult<TauriCopyProjectProgress, String>, RustError> {
    copy_project(
        project_copy.inner().clone(),
        window,
        channel,
        source_path,
        async move |_| {
            if let Ok(()) = tokio::fs::create_dir(&new_path).await {
                Some(PathBuf::from(new_path))
            } else {
                None
            }
        },
    )
    .await
}

pub async fn copy_project<F, Fut>(
    project_copy: ProjectCopyState,
    window: Window,
    channel: String,
    source_path: String,
    create_folder: F,
) -> Result<AsyncCallResult<TauriCopyProjectProgress, String>, RustError>
where
    F: FnOnce(PathBuf) -> Fut + Send + Sync,
    Fut: Future<Output = Option<PathBuf>> + Send + Sync,
{
    let project_copy_finish = project_copy.clone();
    async_command_with_cancel_state(
        channel,
        window,
        async {
            let source_path_str = source_path;
            let source_path = Path::new(&source_path_str);

            let Some(new_path) = create_folder(source_path.into()).await else {
                return Err(RustError::unrecoverable_str(
                    "failed to create a new folder for migration",
                ));
            };
            let new_path_str = new_path.into_os_string().into_string().unwrap();

            With::<TauriCopyProjectProgress>::continue_async(move |ctx| async move {
                let source_path = Path::new(&source_path_str);
                let new_path = Path::new(&new_path_str);
                let remove_on_drop = RemoveDirOnDrop::new(new_path);

                info!("copying project for migration: {source_path_str} -> {new_path_str}");

                let file_tree =
                    collect_notable_project_files_tree(PathBuf::from(source_path), false, false)
                        .await?;
                let total_files = file_tree.count_all();

                info!("collecting files for copy finished, total files: {total_files}");

                struct CopyFileContext<'a> {
                    proceed: AtomicUsize,
                    total_files: usize,
                    new_path: &'a Path,
                    semaphore: Semaphore,
                    ctx: &'a AsyncCommandContext<TauriCopyProjectProgress>,
                }

                impl CopyFileContext<'_> {
                    fn on_finish(&self, entry: &FileSystemTree) {
                        let proceed = self
                            .proceed
                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                        let last_proceed = entry.relative_path().to_string();

                        self.ctx
                            .emit(TauriCopyProjectProgress {
                                total: self.total_files,
                                proceed: proceed + 1,
                                last_proceed,
                            })
                            .ok();
                    }

                    async fn process(&self, entry: &FileSystemTree) -> io::Result<()> {
                        let new_entry = self.new_path.join(entry.relative_path());

                        if entry.is_dir() {
                            let permission = self.semaphore.acquire().await.unwrap();
                            if let Err(e) = tokio::fs::create_dir(&new_entry).await
                                && e.kind() != io::ErrorKind::AlreadyExists
                            {
                                return Err(e);
                            }
                            drop(permission);

                            try_join_all(entry.iter().map(|x| self.process(x))).await?;
                        } else {
                            let permission = self.semaphore.acquire().await.unwrap();
                            copy_file_cancellable(entry.absolute_path(), &new_entry).await?;
                            drop(permission);

                            self.on_finish(entry);
                        }

                        Ok(())
                    }
                }

                let parallelism = std::thread::available_parallelism()
                    .map(|x| x.get() * 2)
                    .unwrap_or(4);

                info!("Copying project with parallelism: {parallelism}");

                CopyFileContext {
                    proceed: AtomicUsize::new(0),
                    total_files,
                    new_path,
                    semaphore: Semaphore::new(parallelism),
                    ctx: &ctx,
                }
                .process(&file_tree)
                .await?;

                info!("copied project for migration. adding to project list");

                let unity_project = load_project(new_path_str.clone()).await?;

                let settings = ctx.state::<SettingsState>();
                let io = ctx.state::<DefaultEnvironmentIo>();

                {
                    let mut settings = settings.load_mut(io.inner()).await?;
                    let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
                    migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;
                    connection.add_project(&unity_project).await?;
                    connection.save(io.inner()).await?;
                    settings.load_from_db(&connection)?;
                    settings.save().await?;
                }

                remove_on_drop.forget();
                Ok(new_path_str)
            })
        },
        move |abort| project_copy.start(abort),
        move || project_copy_finish.finish(),
    )
    .await
}

async fn copy_file_cancellable(source: &Path, destination: &Path) -> io::Result<()> {
    let mut source = tokio::fs::File::open(source).await?;
    let mut destination = tokio::fs::File::create(destination).await?;
    let mut buffer = vec![0; 64 * 1024];

    loop {
        let len = source.read(&mut buffer).await?;
        if len == 0 {
            break;
        }
        destination.write_all(&buffer[..len]).await?;
    }

    destination.flush().await
}

#[tauri::command]
#[specta::specta]
pub async fn environment_set_favorite_project(
    io: State<'_, DefaultEnvironmentIo>,
    project_path: String,
    favorite: bool,
) -> Result<(), RustError> {
    let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
    let Some(mut project) = connection.find_project(&project_path).unwrap() else {
        return Err(RustError::unrecoverable_str("project not found"));
    };
    project.set_favorite(favorite);
    connection.update_project(&project);
    connection.save(io.inner()).await?;
    Ok(())
}

#[derive(Serialize, Deserialize, specta::Type)]
pub struct TauriProjectTemplateInfo {
    pub display_name: String,
    pub id: String,
    pub unity_versions: Vec<String>,
    pub update_date: Option<String>,
    pub has_unitypackage: bool,
    pub source_path: Option<String>,
    pub available: bool,
}

impl From<&ProjectTemplateInfo> for TauriProjectTemplateInfo {
    fn from(info: &ProjectTemplateInfo) -> Self {
        Self {
            display_name: info.display_name.clone(),
            id: info.id.clone(),
            unity_versions: info
                .unity_versions
                .iter()
                .sorted_by_key(|&&x| Reverse(x))
                .map(|x| x.to_string())
                .unique()
                .collect(),
            update_date: info.update_date.map(|x| x.to_rfc3339()),
            has_unitypackage: info
                .alcom_template
                .as_ref()
                .map(|x| !x.unity_packages.is_empty())
                .unwrap_or(false),
            source_path: info
                .source_path
                .as_ref()
                .map(|x| x.to_string_lossy().into_owned()),
            available: info.available,
        }
    }
}

#[derive(Serialize, specta::Type)]
pub struct TauriProjectCreationInformation {
    templates: Vec<TauriProjectTemplateInfo>,
    recent_project_locations: Vec<String>,
    favorite_templates: Vec<String>,
    last_used_template: Option<String>,
    templates_version: u32,
    default_path: String,
}

#[tauri::command]
#[specta::specta]
pub async fn environment_project_creation_information(
    settings: State<'_, SettingsState>,
    templates: State<'_, TemplatesState>,
    config: State<'_, GuiConfigState>,
    io: State<'_, DefaultEnvironmentIo>,
) -> Result<TauriProjectCreationInformation, RustError> {
    let unity_paths = {
        let connection = VccDatabaseConnection::connect(io.inner()).await?;

        connection
            .get_unity_installations()
            .iter()
            .filter_map(|unity| unity.version())
            .collect::<Vec<_>>()
    };

    let recent_project_locations = config.get().recent_project_locations.clone();
    let last_used_template = config.get().last_used_template.clone();
    let favorite_templates = config.get().favorite_templates.clone();

    let templates = templates.save(templates::load_resolve_all_templates(&io, &unity_paths).await?);

    let mut settings = settings.load_mut(io.inner()).await?;
    let default_path = default_project_path(&mut settings).to_string();
    settings.maybe_save().await?;

    Ok(TauriProjectCreationInformation {
        templates: templates.iter().map(Into::into).collect(),
        recent_project_locations,
        templates_version: templates.version(),
        default_path,
        last_used_template,
        favorite_templates,
    })
}

#[derive(Serialize, specta::Type)]
pub enum TauriProjectDirCheckResult {
    // path related
    InvalidNameForFolderName,
    MayCompatibilityProblem,
    WideChar,

    AlreadyExists,
    Ok,
}

static WINDOWS_RESERVED_NAMES: &[&str] = &[
    "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
    "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];

static WINDOWS_RESERVED_CHARS: &[char] = &['/', '\\', '<', '>', ':', '"', '|', '?', '*'];

#[tauri::command]
#[specta::specta]
pub async fn environment_check_project_name(
    base_path: String,
    project_name: String,
) -> Result<TauriProjectDirCheckResult, RustError> {
    let project_name = project_name.trim();
    let project_name_upper = project_name.to_ascii_uppercase();

    if project_name.is_empty()
        || project_name.len() > 255
        || WINDOWS_RESERVED_NAMES.contains(&project_name_upper.as_str())
        || project_name.contains(WINDOWS_RESERVED_CHARS)
    {
        return Ok(TauriProjectDirCheckResult::InvalidNameForFolderName);
    }

    let path = Path::new(&base_path).join(project_name);
    if path.exists() {
        return Ok(TauriProjectDirCheckResult::AlreadyExists);
    }

    if cfg!(target_os = "windows") {
        if project_name.contains('%') {
            return Ok(TauriProjectDirCheckResult::MayCompatibilityProblem);
        }

        if project_name.chars().any(|c| c as u32 > 0x7F) {
            return Ok(TauriProjectDirCheckResult::WideChar);
        }
    }

    Ok(TauriProjectDirCheckResult::Ok)
}

#[derive(Serialize, specta::Type)]
pub enum TauriCreateProjectResult {
    AlreadyExists,
    TemplateNotFound,
    Successful,
}

#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)]
pub async fn environment_create_project(
    app_handle: AppHandle,
    base_path: String,
    project_name: String,
    template_id: String,
    template_version: u32,
    unity_version: String,
) -> Result<TauriCreateProjectResult, RustError> {
    let packages_state: State<'_, PackagesState> = app_handle.state();
    let settings: State<'_, SettingsState> = app_handle.state();
    let config: State<'_, GuiConfigState> = app_handle.state();
    let templates: State<'_, TemplatesState> = app_handle.state();
    let io: State<'_, DefaultEnvironmentIo> = app_handle.state();
    let http: State<'_, reqwest::Client> = app_handle.state();

    let templates = templates
        .get_versioned(template_version)
        .ok_or_else(|| RustError::unrecoverable_str("Templates info version mismatch (bug)"))?;

    let unity_version = UnityVersion::parse(&unity_version)
        .ok_or_else(|| RustError::unrecoverable_str("Bad Unity Version (unparsable)"))?;

    let base_path = Path::new(&base_path);
    let base_path = {
        if !base_path.has_root() {
            let mut components = base_path.components().collect::<Vec<_>>();

            match (components.first(), components.get(1)) {
                (Some(Component::Prefix(_)), Some(Component::RootDir)) => {
                    // starts with 'C:/', good!
                }
                (Some(Component::Prefix(prefix)), _) => {
                    if matches!(prefix.kind(), Prefix::Disk(_)) {
                        // starts with 'C:yourpath', we should insert / after prefix
                        components.insert(1, Component::RootDir);
                    } else {
                        // starts with '\\?\', no problem
                    }
                }
                (Some(Component::RootDir), _) => {
                    // starts with '/', good!
                }
                (Some(_), _) => {
                    // starts with 'yourpath', insert '/'
                    components.insert(0, Component::RootDir);
                }
                _ => {}
            }

            components.iter().collect()
        } else {
            base_path.to_path_buf()
        }
    };
    let path = base_path.join(&project_name);

    // update recent locations and last template
    {
        let mut config = config.load_mut().await?;
        let base_path_str = base_path.as_os_str().to_str().unwrap();
        if let Some(path_index) = config
            .recent_project_locations
            .iter()
            .position(|x| x == base_path_str)
        {
            let base_path = config.recent_project_locations.remove(path_index);
            config.recent_project_locations.push(base_path);
        } else {
            let to_remove = config.recent_project_locations.len().saturating_sub(8 - 1);
            config.recent_project_locations.drain(0..to_remove);
            config
                .recent_project_locations
                .push(base_path_str.to_string());
        }
        config.last_used_template = Some(template_id.clone());
        config.save().await?;
    }

    // we split creating folder into two phases
    // because we want to fail if the project folder already exists.

    // create parent directory if not exists (unlikely to happen)
    super::super::create_dir_all_with_err(base_path).await?;

    // create project directory
    match tokio::fs::create_dir(&path).await {
        Ok(()) => {}
        Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
            return Ok(TauriCreateProjectResult::AlreadyExists);
        }
        Err(e) => {
            return Err(e.into());
        }
    }

    let mut unity_project = match templates::create_project(
        &io,
        &templates,
        &template_id,
        &path,
        &project_name,
        unity_version,
    )
    .await
    {
        Ok(unity_project) => unity_project,
        Err(CreateProjectErr::Io(e)) => return Err(e.into()),
        Err(CreateProjectErr::NoSuchTemplate) => {
            return Ok(TauriCreateProjectResult::TemplateNotFound);
        }
    };

    let packages;
    {
        let mut settings = settings.load_mut(io.inner()).await?;
        packages = packages_state
            .load_fully(&settings, io.inner(), http.inner())
            .await?;

        // add the project to listing
        let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
        migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;
        connection.add_project(&unity_project).await?;
        connection.save(io.inner()).await?;
        settings.load_from_db(&connection)?;
        settings.save().await?;
    }

    {
        let installer = PackageInstaller::new(io.inner(), Some(http.inner()));

        // finally, resolve the project folder
        let request = unity_project.resolve_request(packages.collection()).await?;
        unity_project
            .apply_pending_changes(&installer, request)
            .await?;
    }
    Ok(TauriCreateProjectResult::Successful)
}
