use std::{
    fs::{File, create_dir_all},
    io::Write,
    path::PathBuf,
};

use rusqlite::named_params;
use serde::{Deserialize, Serialize};
use tracing::{debug, trace};
use uuid::Uuid;

use crate::{
    database::{
        db::{Db, DbError},
        macros::NewId,
    },
    sql_uuid_newtype,
    types::user::FromRow,
};

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct FolderId(Uuid);
sql_uuid_newtype!(FolderId);

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Folder {
    pub(crate) folder_id: FolderId,
    pub(crate) folder_name: String,
}

impl FromRow for Folder {
    fn try_from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error> {
        let folder = Folder {
            folder_id: row.get("folder_id")?,
            folder_name: row.get("folder_name")?,
        };

        Ok(folder)
    }
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct NewFolder {
    folder_name: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct ImageId(Uuid);
sql_uuid_newtype!(ImageId);

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Image {
    pub(crate) image_id: ImageId,
    pub(crate) image_name: String,
    pub(crate) folder_id: FolderId,
    pub(crate) url: String,
}

impl FromRow for Image {
    fn try_from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error> {
        let image_id = row.get("image_id")?;
        let image_name = row.get("image_name")?;
        let url = format!("/cms-api/images/{}/{}", image_id, image_name);

        let image = Self {
            image_id,
            image_name,
            folder_id: row.get("folder_id")?,
            url,
        };

        Ok(image)
    }
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct NewImage {
    pub(crate) image_name: String,
    pub(crate) folder_id: FolderId,
    pub(crate) data: Vec<u8>,
}

pub struct ImageRepository {
    pub(crate) db: Db,
    pub(crate) folder: PathBuf,
}

impl ImageRepository {
    pub(crate) fn create_folder(&self, folder: NewFolder) -> Result<FolderId, DbError> {
        debug!("Create a new folder");

        let sql = r#"
            INSERT INTO folder
                (folder_id, folder_name)
            VALUES
                (:folder_id, :folder_name)"#;

        let folder_id = FolderId::new_id();

        let params = named_params! {
            ":folder_id": folder_id,
            ":folder_name": folder.folder_name,
        };

        let mut conn = self.db.connection()?;
        let tx = conn.transaction()?;

        tx.execute(sql, params)?;

        tx.commit()?;

        Ok(folder_id)
    }

    pub(crate) fn get_folders(&self) -> Result<Vec<Folder>, DbError> {
        debug!("Get Folders");

        let sql = r#"
            SELECT folder_id, folder_name FROM folder
            "#;

        let conn = self.db.connection()?;

        let mut stmt = conn.prepare(sql)?;

        let folders: Vec<Folder> = stmt
            .query_map({}, Folder::try_from_row)?
            .filter_map(|res| res.ok())
            .collect::<Vec<_>>();

        Ok(folders)
    }

    pub(crate) fn get_images_of_folder(&self, folder_name: String) -> Result<Vec<Image>, DbError> {
        trace!("Get images of folder {:?}", folder_name);

        let sql = r#"
            SELECT
                image.image_id,
                image.image_name,
                image.folder_id,
                folder.folder_id,
                folder.folder_name
            FROM
                image
                INNER JOIN folder ON image.folder_id = folder.folder_id
            WHERE
                folder_name = :folder_name"#;

        let params = named_params! {
            ":folder_name": folder_name
        };

        let conn = self.db.connection()?;
        let mut stmt = conn.prepare(sql)?;

        let images: Vec<Image> = stmt
            .query_map(params, Image::try_from_row)?
            .filter_map(|res| {
                trace!("Result is {:?}", res);
                res.ok()
            })
            .collect::<Vec<_>>();

        Ok(images)
    }

    pub(crate) fn create_image(&self, new_image: NewImage) -> Result<(), DbError> {
        debug!("creating an image");

        let sql = r#"
            INSERT INTO image
                (image_id, image_name, folder_id)
            VALUES
                (:image_id, :image_name, :folder_id)"#;

        let mut conn = self.db.connection()?;

        let tx = conn.transaction()?;

        let image_id = ImageId::new_id();

        let params = named_params! {
            ":image_id": image_id,
            ":image_name": new_image.image_name,
            ":folder_id": new_image.folder_id
        };

        tx.execute(sql, params)?;

        let image_folder = self.folder.join(image_id.to_string());
        trace!("Creating folder {:?}", image_folder);
        create_dir_all(&image_folder)?;

        let filepath = image_folder.join(new_image.image_name);
        trace!("Writing file {:?}", filepath);

        let mut file = File::create(filepath)?;
        file.write_all(&new_image.data)?;

        // commit at the end. After the file was successfully written
        tx.commit()?;

        Ok(())
    }
}
