use crate::connections::{db_connection, s3_connection};
use crate::constants::{ALLOWED_PHOTOS_MIMETYPES, PHOTOS_MAX_SIZE, THUMB_HEIGHT, THUMB_WIDTH};
use crate::models::{NewPhoto, Photo, PhotoID};
use crate::schema::photos;
use crate::utils::crypt_utils::sha512;
use crate::utils::time_utils::time;
use actix_multipart::form::tempfile::TempFile;
use diesel::prelude::*;
use image::ImageFormat;
use image::imageops::FilterType;
use mime_guess::Mime;
use std::fs::File;
use std::io::{Cursor, Read, Seek, Write};
use uuid::Uuid;

#[derive(thiserror::Error, Debug)]
enum PhotoServiceError {
    #[error("Uploaded file is too large ({0} bytes)!")]
    FileToLarge(usize),

    #[error("Mime type not specified in request!")]
    MissingMimeType,

    #[error("Mimetype forbidden ({0})!")]
    MimeTypeForbidden(String),
}

pub struct UploadedFile {
    pub size: usize,
    pub content_type: Option<Mime>,
    pub file: File,
}

impl From<TempFile> for UploadedFile {
    fn from(value: TempFile) -> Self {
        Self {
            size: value.size,
            content_type: value.content_type,
            file: value.file.into_file(),
        }
    }
}

impl UploadedFile {
    pub fn from_memory(buff: &[u8], content_type: Option<Mime>) -> anyhow::Result<Self> {
        let mut file = tempfile::tempfile()?;
        file.write_all(buff)?;
        file.rewind()?;

        Ok(Self {
            size: buff.len(),
            content_type,
            file,
        })
    }
}

/// Finalize upload of a photo
pub async fn finalize_upload(mut file: UploadedFile) -> anyhow::Result<Photo> {
    // Prerequisite checks
    if file.size > PHOTOS_MAX_SIZE {
        return Err(PhotoServiceError::FileToLarge(file.size).into());
    }

    let mime_type = file
        .content_type
        .ok_or(PhotoServiceError::MissingMimeType)?;

    if !ALLOWED_PHOTOS_MIMETYPES.contains(&mime_type.as_ref()) {
        return Err(PhotoServiceError::MimeTypeForbidden(mime_type.to_string()).into());
    }

    let mut photo_img = Vec::with_capacity(file.size);
    file.file.read_to_end(&mut photo_img)?;

    let thumbnail_image = image::load_from_memory(&photo_img)?.resize(
        THUMB_WIDTH,
        THUMB_HEIGHT,
        FilterType::Triangle,
    );

    let mut thumb_cursor = Cursor::new(vec![]);
    thumbnail_image.write_to(&mut thumb_cursor, ImageFormat::Png)?;
    let thumb_img = thumb_cursor.into_inner();

    let photo = NewPhoto {
        file_id: Uuid::new_v4().to_string(),
        time_create: time() as i64,
        mime_type: mime_type.to_string(),
        sha512: sha512(&photo_img),
        file_size: photo_img.len() as i32,
        thumb_sha512: sha512(&thumb_img),
    };

    s3_connection::upload_file(&photo.photo_path(), &photo_img).await?;
    s3_connection::upload_file(&photo.thumbnail_path(), &thumb_img).await?;

    db_connection::execute(|conn| {
        let res: Photo = diesel::insert_into(photos::table)
            .values(&photo)
            .get_result(conn)?;

        Ok(res)
    })
}

/// Get a photo by its ID
pub async fn get_by_id(id: PhotoID) -> anyhow::Result<Photo> {
    db_connection::execute(|conn| photos::table.filter(photos::dsl::id.eq(id.0)).first(conn))
}

/// Delete a photo
pub async fn delete(id: PhotoID) -> anyhow::Result<()> {
    let photo = get_by_id(id).await?;

    s3_connection::delete_file_if_exists(&photo.photo_path()).await?;
    s3_connection::delete_file_if_exists(&photo.thumbnail_path()).await?;

    db_connection::execute(|conn| {
        diesel::delete(photos::dsl::photos.filter(photos::dsl::id.eq(photo.id().0))).execute(conn)
    })?;

    Ok(())
}
