use std::{io::Cursor, sync::Arc};

use axum::{
    body::Bytes,
    debug_handler,
    extract::{DefaultBodyLimit, Query, State},
    response::IntoResponse,
    routing::post,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use tracing::info;

use crate::{
    dirs::Dirs,
    vfs::{self, Edit, EditPath, VPathBuf},
};

pub fn router<S>(dirs: Arc<Dirs>) -> Router<S> {
    Router::new()
        .route("/", post(picture_upload))
        .layer(DefaultBodyLimit::disable())
        .with_state(dirs)
}

#[derive(Debug, Deserialize)]
struct PictureUpload {
    label: String,
    format: String,
    compression: Compression,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
enum Compression {
    Lossless,
    GameScreenshot,
}

impl Compression {
    pub fn output_format(self) -> Option<&'static str> {
        match self {
            Compression::Lossless => None,
            Compression::GameScreenshot => Some("image/webp"),
        }
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
enum Response {
    Ulid(String),
    Error(String),
}

fn compress(image_data: &[u8], compression: Compression) -> anyhow::Result<Vec<u8>> {
    match compression {
        Compression::Lossless => Ok(image_data.to_vec()),
        Compression::GameScreenshot => {
            info!("decompressing original image");
            let decompressed = image::ImageReader::new(Cursor::new(image_data))
                .with_guessed_format()?
                .decode()?
                .to_rgba8();

            info!("compressing to webp");
            let compressed = webp::Encoder::new(
                &decompressed,
                webp::PixelLayout::Rgba,
                decompressed.width(),
                decompressed.height(),
            )
            .encode(85.0)
            .to_vec();
            Ok(compressed)
        }
    }
}

async fn write_compressed(
    image_data: &[u8],
    compression: Compression,
    edit_path: EditPath,
) -> anyhow::Result<()> {
    let compressed = compress(image_data, compression)?;
    Edit::Write(edit_path, compressed).apply().await?;
    Ok(())
}

#[debug_handler]
async fn picture_upload(
    State(dirs): State<Arc<Dirs>>,
    Query(mut params): Query<PictureUpload>,
    image: Bytes,
) -> impl IntoResponse {
    info!(?params, "uploading picture");

    let ulid = ulid::Generator::new()
        .generate_with_source(&mut rand::thread_rng())
        .expect("failed to generate ulid");

    if params.label.is_empty() {
        params.label = "untitled".into();
    }

    let file_name = VPathBuf::new(format!(
        "{ulid}-{}.{}",
        params.label,
        get_extension(params.compression.output_format().unwrap_or(&params.format))
            .unwrap_or("unknown")
    ));
    let Some(edit_path) = vfs::query::<EditPath>(&dirs.pic, &file_name) else {
        return Json(Response::Error(format!("{file_name} is not editable")));
    };

    let result = write_compressed(&image, params.compression, edit_path).await;
    info!(?result, "done processing");
    Json(match result {
        Ok(()) => Response::Ulid(ulid.to_string()),
        Err(error) => Response::Error(error.to_string()),
    })
}

fn get_extension(content_type: &str) -> Option<&'static str> {
    match content_type {
        "image/png" => Some("png"),
        "image/jpeg" => Some("jpg"),
        "image/svg+xml" => Some("svg"),
        "image/webp" => Some("webp"),
        _ => None,
    }
}
