use crate::connections::db_connection::db;
use crate::connections::s3_connection;
use crate::constants;
use crate::models::files::{File, FileID, NewFile};
use crate::models::users::UserID;
use crate::schema::files;
use crate::utils::crypt_utils::sha512;
use crate::utils::time_utils::time;
use diesel::prelude::*;
use mime_guess::Mime;

#[derive(thiserror::Error, Debug)]
enum FilesServiceError {
    #[error("UnknownMimeType!")]
    UnknownMimeType,
    #[error("Could not delete all the files of the user: a file is still referenced!")]
    DeleteAllUserFileStillReferenced,
}

pub async fn create_file_with_file_name(
    user_id: UserID,
    file_name: &str,
    bytes: &[u8],
) -> anyhow::Result<File> {
    let mime = mime_guess::from_path(file_name)
        .first()
        .ok_or(FilesServiceError::UnknownMimeType)?;

    create_file_with_mimetype(user_id, file_name, &mime, bytes).await
}

pub async fn create_file_with_mimetype(
    user_id: UserID,
    file_name: &str,
    mime_type: &Mime,
    bytes: &[u8],
) -> anyhow::Result<File> {
    let sha512 = sha512(bytes);

    if let Ok(f) = get_file_with_hash(user_id, &sha512) {
        return Ok(f);
    }

    let file = NewFile {
        time_create: time() as i64,
        mime_type: mime_type.as_ref(),
        sha512: &sha512,
        file_size: bytes.len() as i32,
        file_name,
        user_id: user_id.0,
    };

    s3_connection::upload_file(&file.file_path(), bytes).await?;

    let res = diesel::insert_into(files::table)
        .values(&file)
        .get_result(&mut db()?)?;

    Ok(res)
}

pub fn get_file_with_hash(user_id: UserID, sha512: &str) -> anyhow::Result<File> {
    Ok(files::table
        .filter(
            files::dsl::sha512
                .eq(sha512)
                .and(files::dsl::user_id.eq(user_id.0)),
        )
        .first(&mut db()?)?)
}

pub fn get_file_with_id(id: FileID) -> anyhow::Result<File> {
    Ok(files::table
        .filter(files::dsl::id.eq(id.0))
        .first(&mut db()?)?)
}

pub async fn get_file_content_by_id(id: FileID) -> anyhow::Result<Vec<u8>> {
    let file = get_file_with_id(id)?;
    get_file_content(&file).await
}

pub async fn get_file_content(file: &File) -> anyhow::Result<Vec<u8>> {
    s3_connection::get_file(&file.file_path()).await
}

/// Delete the file if it is not referenced anymore in the database. Returns true
/// if the file was actually deleted, false otherwise
pub async fn delete_file_if_unused(id: FileID, skip_age_check: bool) -> anyhow::Result<bool> {
    let file = get_file_with_id(id)?;

    // Check if the file is old enough
    if file.time_create as u64 + constants::MIN_FILE_LIFETIME > time() && !skip_age_check {
        return Ok(false);
    }

    let res = diesel::delete(files::dsl::files.filter(files::dsl::id.eq(file.id().0)))
        .execute(&mut db()?);

    match res {
        Ok(_) => {
            s3_connection::delete_file_if_exists(&file.file_path()).await?;
            log::info!("File {:?} was deleted", file.id());

            Ok(true)
        }
        Err(e) => {
            log::info!(
                "File {:?} could not be deleted, it must be used somewhere: {e}",
                file.id()
            );
            Ok(false)
        }
    }
}

/// Get all the files of a user
pub async fn get_all_files_user(user_id: UserID) -> anyhow::Result<Vec<File>> {
    Ok(files::table
        .filter(files::dsl::user_id.eq(user_id.0))
        .load(&mut db()?)?)
}

/// Get the entire list of file
pub async fn get_entire_list() -> anyhow::Result<Vec<File>> {
    Ok(files::table.get_results(&mut db()?)?)
}

/// Remove unused files
pub async fn run_garbage_collector() -> anyhow::Result<usize> {
    let mut count_deleted = 0;

    for file in get_entire_list().await? {
        if delete_file_if_unused(file.id(), false).await? {
            count_deleted += 1;
        }
    }

    Ok(count_deleted)
}

/// Delete all the files of a given user
pub async fn delete_all_user(user_id: UserID) -> anyhow::Result<()> {
    for file in get_all_files_user(user_id).await? {
        if !delete_file_if_unused(file.id(), true).await? {
            return Err(FilesServiceError::DeleteAllUserFileStillReferenced.into());
        }
    }

    Ok(())
}
