use crate::shared::guard;
use axum::{
    body::Body,
    extract::{DefaultBodyLimit, Multipart},
    http::header,
};
use bytes::Bytes;
use loco_rs::{config::Config, prelude::*};
use serde::Serialize;
use std::path::{Path as StdPath, PathBuf};
use uuid::Uuid;

pub(crate) const IMAGE_MAX_BYTES: usize = 10 * 1024 * 1024;
pub const IMAGE_STORAGE_DIR: &str = "images";

#[derive(Debug, Serialize)]
struct UploadResponse {
    filename: String,
    url: String,
    size: usize,
}

pub fn uploads_root(config: &Config) -> Result<PathBuf> {
    config
        .settings
        .as_ref()
        .and_then(|settings| settings.get("uploads_root"))
        .and_then(|value| value.as_str())
        .filter(|value| !value.trim().is_empty())
        .map(PathBuf::from)
        .ok_or_else(|| Error::string("settings.uploads_root must be configured"))
}

fn safe_filename(filename: &str) -> Result<&str> {
    if filename.is_empty()
        || filename.contains('/')
        || filename.contains('\\')
        || filename.contains("..")
    {
        return Err(Error::BadRequest("invalid filename".to_string()));
    }
    Ok(filename)
}

fn image_content_type(extension: &str) -> &'static str {
    match extension {
        "gif" => "image/gif",
        "jpg" | "jpeg" => "image/jpeg",
        "png" => "image/png",
        "webp" => "image/webp",
        _ => "application/octet-stream",
    }
}

pub(crate) fn detect_image_extension(data: &[u8]) -> Result<&'static str> {
    if data.len() < 12 {
        return Err(Error::BadRequest("image file is too small".to_string()));
    }
    if data.starts_with(&[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]) {
        return Ok("png");
    }
    if data.starts_with(&[0xff, 0xd8, 0xff]) {
        return Ok("jpg");
    }
    if data.starts_with(b"RIFF") && &data[8..12] == b"WEBP" {
        return Ok("webp");
    }
    if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") {
        return Ok("gif");
    }
    Err(Error::BadRequest("unsupported image format".to_string()))
}

async fn read_multipart_file(mut multipart: Multipart, max_bytes: usize) -> Result<Vec<u8>> {
    while let Some(mut field) = multipart
        .next_field()
        .await
        .map_err(|err| Error::BadRequest(format!("invalid multipart data: {err}")))?
    {
        if field.name() != Some("file") {
            continue;
        }

        let mut data = Vec::new();
        while let Some(chunk) = field
            .chunk()
            .await
            .map_err(|err| Error::BadRequest(format!("invalid multipart chunk: {err}")))?
        {
            data.extend_from_slice(&chunk);
            if data.len() > max_bytes {
                return Err(Error::BadRequest(format!(
                    "file is larger than {} MB",
                    max_bytes / 1024 / 1024
                )));
            }
        }

        if data.is_empty() {
            return Err(Error::BadRequest("empty file upload".to_string()));
        }
        return Ok(data);
    }

    Err(Error::BadRequest(
        "multipart field `file` is required".to_string(),
    ))
}

pub(crate) async fn store_upload(
    ctx: &AppContext,
    folder: &str,
    extension: &str,
    data: Vec<u8>,
) -> Result<String> {
    let filename = format!("{}.{}", Uuid::new_v4(), extension);
    let key = format!("{folder}/{filename}");
    ctx.storage
        .upload(StdPath::new(&key), &Bytes::from(data))
        .await?;
    Ok(filename)
}

#[debug_handler]
async fn image_upload(
    auth: auth::JWT,
    State(ctx): State<AppContext>,
    multipart: Multipart,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let data = read_multipart_file(multipart, IMAGE_MAX_BYTES).await?;
    let extension = detect_image_extension(&data)?;
    let size = data.len();
    let filename = store_upload(&ctx, IMAGE_STORAGE_DIR, extension, data).await?;

    format::json(UploadResponse {
        url: format!("/images/{filename}"),
        filename,
        size,
    })
}

#[debug_handler]
async fn image_serve(
    Path(filename): Path<String>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let filename = safe_filename(&filename)?;
    let extension = filename.rsplit('.').next().unwrap_or("");
    let key = format!("{IMAGE_STORAGE_DIR}/{filename}");
    let body: Vec<u8> = ctx.storage.download(StdPath::new(&key)).await?;
    Response::builder()
        .header(header::CONTENT_TYPE, image_content_type(extension))
        .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
        .body(Body::from(body))
        .map_err(Error::from)
}

pub fn routes() -> Routes {
    Routes::new()
        .add(
            "/images/upload",
            post(image_upload).layer(DefaultBodyLimit::max(IMAGE_MAX_BYTES + 1024 * 1024)),
        )
        .add("/images/{filename}", get(image_serve))
}
