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

use axum::{
    Json, Router,
    extract::{DefaultBodyLimit, Multipart, Path, Query, State},
    response::{IntoResponse, Redirect},
    routing::{get, post},
};
use image::{
    DynamicImage, ImageDecoder, ImageEncoder, ImageFormat, ImageReader,
    codecs::{jpeg::JpegEncoder, png::PngEncoder},
    imageops::FilterType,
};
use serde::Deserialize;
use tower_http::{limit::RequestBodyLimitLayer, services::ServeDir};
use tracing::{debug, trace};

use crate::{
    database::db::Db,
    error::CmsError,
    repositories::image_repository::{
        Folder, FolderId, Image, ImageId, ImageRepository, NewFolder, NewImage,
    },
};

#[derive(Debug, Clone, Deserialize)]
pub struct ImageOptions {
    pub(crate) width: Option<u32>,
    pub(crate) height: Option<u32>,
    pub(crate) quality: Option<u8>,
    pub(crate) rotate: Option<u64>,
    pub(crate) grayscale: Option<bool>,
    pub(crate) imgtype: Option<ImageType>,
}

impl ImageOptions {
    pub(crate) fn no_options_set(&self) -> bool {
        self.width.is_none()
            && self.height.is_none()
            && self.quality.is_none()
            && self.rotate.is_none()
            && self.grayscale.is_none()
            && self.imgtype.is_none()
    }
}

pub trait ToFileName {
    fn to_file_name(&self) -> String;
}

impl<T: ToString> ToFileName for Option<T> {
    fn to_file_name(&self) -> String {
        match self {
            Some(value) => value.to_string(),
            None => "None".to_owned(),
        }
    }
}

impl ImageOptions {
    pub fn get_image_name_with_options(&self, base: &str) -> String {
        let mut s = String::new();

        s.push_str(base);
        s.push('_');

        // version if we would like to add more options later, so we can differentiate
        s.push('v');
        s.push('1');
        s.push('_');

        s.push('w');
        s.push_str(&self.width.to_file_name());
        s.push('_');

        s.push('h');
        s.push_str(&self.height.to_file_name());
        s.push('_');

        s.push('q');
        s.push_str(&self.quality.to_file_name());
        s.push('_');

        s.push('r');
        s.push_str(&self.rotate.to_file_name());
        s.push('_');

        s.push('g');
        s.push_str(&self.grayscale.to_file_name());
        s.push('_');

        s.push('t');
        s.push_str(&self.imgtype.to_file_name());

        s
    }
}

#[derive(Debug, Clone, Deserialize, Copy)]
pub enum ImageType {
    Jpeg,
    Jpg,
    Png,
    Webp,
}

impl ToFileName for Option<ImageType> {
    fn to_file_name(&self) -> String {
        match self {
            Some(image_type) => match image_type {
                ImageType::Jpeg | ImageType::Jpg => "Jpeg".to_owned(),
                ImageType::Png => "Png".to_owned(),
                ImageType::Webp => "Webp".to_owned(),
            },
            None => "None".to_owned(),
        }
    }
}

pub struct ImageState {
    image_repository: ImageRepository,
}

pub fn image_app(db: Db) -> Result<Router, CmsError> {
    debug!("Create Image App");

    let image_folder: PathBuf = "./images".into();
    create_dir_all(&image_folder)?;

    let image_repository = ImageRepository {
        db,
        folder: image_folder,
    };

    let image_state = ImageState { image_repository };
    let image_state = Arc::new(image_state);

    let router = Router::new()
        .route("/cms-api/folder/create", post(create_folder))
        .route("/cms-api/folder", get(get_folders))
        .route(
            "/cms-api/folder/{folder_name}/images",
            get(get_images_of_folder),
        )
        .route("/cms-api/image/{folder_id}", post(create_image))
        .route("/cms-api/images/{image_id}/{image_name}", get(get_image))
        .layer(DefaultBodyLimit::disable())
        .layer(RequestBodyLimitLayer::new(10 * 1024 * 1024)) // 10MB
        .with_state(image_state);

    let serve_dir = ServeDir::new("./images");
    let serve_dir_router = Router::new().nest_service("/cms-api/imagess", serve_dir);
    let router = router.merge(serve_dir_router);

    Ok(router)
}

pub async fn get_image(
    Path((image_id, image_name)): Path<(ImageId, String)>,
    Query(image_options): Query<ImageOptions>,
) -> Result<impl IntoResponse, CmsError> {
    trace!("Image Options {:?}", image_options);

    let base_directory = PathBuf::new().join("./images").join(image_id.to_string());

    if image_options.no_options_set() {
        let url = format!("/cms-api/imagess/{}/{}", image_id, image_name);
        return Ok(Redirect::temporary(&url));
    }

    let base_file_path = base_directory.join(&image_name);
    trace!("Base file path for image: {:?}", base_file_path);
    if base_file_path.exists() {
        let new_file_name = image_options.get_image_name_with_options(&image_name);
        let new_full_file_name = base_directory.join(&new_file_name);

        // only create a new image if it does not already exist
        if !new_full_file_name.exists() {
            create_final_image(&base_file_path, &new_full_file_name, &image_options)?;
        }

        let url = format!("/cms-api/imagess/{}/{}", image_id, new_file_name);
        trace!("Redirect to new filename");
        Ok(Redirect::temporary(&url))
    } else {
        Err(CmsError::Image("Base image does not exist".into()))
    }
}

pub fn create_final_image(
    original_file_path: &PathBuf,
    target_file_path: &PathBuf,
    options: &ImageOptions,
) -> Result<(), CmsError> {
    // load image and apply orientation because image-rs does not write exif data to the image
    let mut decoder = ImageReader::open(original_file_path)?.into_decoder()?;
    let orientation = decoder.orientation()?;

    let image = DynamicImage::from_decoder(decoder)?;

    // apply resize options
    trace!("Apply resize");
    let image = match (options.width, options.height) {
        (Some(width), Some(height)) => image.resize(width, height, FilterType::Lanczos3),
        (Some(width), None) => image.resize(width, u32::MAX, FilterType::Lanczos3),
        (None, Some(height)) => image.resize(u32::MAX, height, FilterType::Lanczos3),
        (None, None) => image,
    };

    // apply rotation
    trace!("Apply rotation");
    let image = match options.rotate {
        Some(rotate) => match rotate {
            90 => image.rotate90(),
            180 => image.rotate180(),
            270 => image.rotate270(),
            _ => {
                return Err(CmsError::Image("Was not able to rotate correctly".into()));
            }
        },
        None => image,
    };

    // apply grayscale
    trace!("Apply grayscale");
    let image = match options.grayscale {
        Some(true) => image.grayscale(),
        _ => image,
    };

    // apply orientation
    trace!("Apply orientation");
    let mut image = image;
    image.apply_orientation(orientation);

    let image_format = match options.imgtype {
        Some(image_type) => match image_type {
            ImageType::Jpeg | ImageType::Jpg => ImageFormat::Jpeg,
            ImageType::Png => ImageFormat::Png,
            ImageType::Webp => ImageFormat::WebP,
        },
        None => ImageFormat::from_path(original_file_path)?,
    };

    // write the file to disk
    let mut file = BufWriter::new(File::create(target_file_path)?);
    match image_format {
        ImageFormat::Jpeg => {
            let encoder = match options.quality {
                Some(quality) => JpegEncoder::new_with_quality(file, quality),
                None => JpegEncoder::new(file),
            };
            encoder.write_image(
                image.as_bytes(),
                image.width(),
                image.height(),
                image.color().into(),
            )?;
        }
        ImageFormat::Png => {
            let encoder = PngEncoder::new(file);

            encoder.write_image(
                image.as_bytes(),
                image.width(),
                image.height(),
                image.color().into(),
            )?;
        }
        ImageFormat::WebP => {
            // this uses the webp crate, because the image crate has only a lossless webp encoder
            let encoder = webp::Encoder::from_image(&image)
                .map_err(|_| CmsError::Image("Was not able to get an WebP encoder".into()))?;

            let webp = match options.quality {
                Some(quality) => encoder.encode(f32::from(quality)),
                None => encoder.encode(75f32),
            };
            file.write_all(&webp)?;
        }
        _ => {
            return Err(CmsError::Image("Unknown Image format".into()));
        }
    }

    Ok(())
}

pub async fn get_images_of_folder(
    State(state): State<Arc<ImageState>>,
    Path(folder_name): Path<String>,
) -> Result<Json<Vec<Image>>, CmsError> {
    let images = state.image_repository.get_images_of_folder(folder_name)?;

    trace!("Found {} images", images.len());

    Ok(Json(images))
}

pub async fn create_folder(
    State(state): State<Arc<ImageState>>,
    Json(new_folder): Json<NewFolder>,
) -> Result<impl IntoResponse, CmsError> {
    state.image_repository.create_folder(new_folder)?;

    Ok(())
}

pub async fn get_folders(
    State(state): State<Arc<ImageState>>,
) -> Result<Json<Vec<Folder>>, CmsError> {
    let folders = state.image_repository.get_folders()?;

    Ok(Json(folders))
}

pub async fn create_image(
    State(state): State<Arc<ImageState>>,
    Path(folder_id): Path<FolderId>,
    mut multipart: Multipart,
) -> Result<impl IntoResponse, CmsError> {
    while let Some(field) = multipart.next_field().await? {
        match &field.file_name() {
            Some(filename) => {
                let new_image = NewImage {
                    image_name: filename.to_string(),
                    folder_id,
                    data: field.bytes().await?.into(),
                };

                trace!(
                    "Got image uploaded with name {} and size {}",
                    new_image.image_name,
                    new_image.data.len()
                );

                state.image_repository.create_image(new_image)?;
            }
            _ => continue,
        }
    }
    Ok(())
}
