//! Multipart form handling shared by the product and category admin forms.
//!
//! Both forms submit a mix of text fields and `image` file part(s); this
//! collects them into an easy-to-query [`MultipartForm`] and stores any
//! uploaded image through the configured storage driver. The product form can
//! upload several images at once and submits a unified gallery order as
//! repeated `image_order` fields — each either an existing image's id or the
//! literal `new` (a placeholder consumed, in order, from the uploaded files).

use std::collections::HashMap;

use axum::extract::Multipart;
use loco_rs::prelude::*;

use crate::controllers::media::{detect_image_extension, store_upload, IMAGE_MAX_BYTES, IMAGE_STORAGE_DIR};

fn normalize_empty(value: Option<String>) -> Option<String> {
    value.and_then(|value| {
        let value = value.trim().to_string();
        (!value.is_empty()).then_some(value)
    })
}

/// One slot in the unified gallery order submitted by the product form.
#[derive(Debug, Clone)]
pub(crate) enum ImageSlot {
    /// An existing image kept in the gallery.
    Existing(i32),
    /// A placeholder for one newly-uploaded file, consumed from [`MultipartForm::images`]
    /// in the order these slots appear.
    New,
}

/// Collected multipart form: text fields keyed by name, the raw bytes of every
/// `image` file part uploaded (empty file inputs are ignored, submission order
/// preserved), and the full gallery order as repeated `image_order` fields —
/// each either an existing image's id or the literal `new`.
pub(crate) struct MultipartForm {
    fields: HashMap<String, String>,
    pub(crate) images: Vec<Vec<u8>>,
    pub(crate) image_order: Vec<ImageSlot>,
}

impl MultipartForm {
    /// Trimmed value of a text field, `None` when missing or blank.
    pub(crate) fn text(&self, key: &str) -> Option<String> {
        normalize_empty(self.fields.get(key).cloned())
    }

    /// The single uploaded image, for forms (like categories) that accept only
    /// one. Consumes the first uploaded part; any extras are ignored.
    pub(crate) fn single_image(self) -> Option<Vec<u8>> {
        self.images.into_iter().next()
    }

    /// Whether a checkbox-style field is checked.
    pub(crate) fn checked(&self, key: &str) -> bool {
        matches!(
            self.fields.get(key).map(String::as_str),
            Some("on" | "true" | "1")
        )
    }

    /// The distinct row indices `N` present among `variants[N][...]` fields,
    /// sorted ascending. Used to read the repeated variant rows of the product
    /// form (each row's fields are uniquely keyed, so the HashMap keeps them all).
    pub(crate) fn variant_indices(&self) -> Vec<usize> {
        let mut idx: Vec<usize> = self
            .fields
            .keys()
            .filter_map(|k| {
                let rest = k.strip_prefix("variants[")?;
                rest.split(']').next()?.parse::<usize>().ok()
            })
            .collect();
        idx.sort_unstable();
        idx.dedup();
        idx
    }
}

pub(crate) async fn read_multipart_form(mut multipart: Multipart) -> Result<MultipartForm> {
    let mut fields = HashMap::new();
    let mut images = Vec::new();
    let mut image_order = Vec::new();

    while let Some(mut field) = multipart
        .next_field()
        .await
        .map_err(|err| Error::BadRequest(format!("invalid multipart data: {err}")))?
    {
        let name = field.name().unwrap_or("").to_string();
        if name == "image" {
            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() > IMAGE_MAX_BYTES {
                    return Err(Error::BadRequest(format!(
                        "image is larger than {} MB",
                        IMAGE_MAX_BYTES / 1024 / 1024
                    )));
                }
            }
            // An empty file part (no file chosen in a slot) is ignored.
            if !data.is_empty() {
                images.push(data);
            }
        } else if name == "image_order" {
            let value = field
                .text()
                .await
                .map_err(|err| Error::BadRequest(format!("invalid multipart field: {err}")))?;
            let trimmed = value.trim();
            if trimmed == "new" {
                image_order.push(ImageSlot::New);
            } else if let Ok(id) = trimmed.parse::<i32>() {
                image_order.push(ImageSlot::Existing(id));
            }
        } else {
            let value = field
                .text()
                .await
                .map_err(|err| Error::BadRequest(format!("invalid multipart field: {err}")))?;
            fields.insert(name, value);
        }
    }

    Ok(MultipartForm {
        fields,
        images,
        image_order,
    })
}

/// Store an uploaded image's bytes and return its generated filename.
pub(crate) async fn store_image(ctx: &AppContext, data: Vec<u8>) -> Result<String> {
    let extension = detect_image_extension(&data)?;
    store_upload(ctx, IMAGE_STORAGE_DIR, extension, data).await
}
