//! Admin product CRUD.

use axum::extract::{DefaultBodyLimit, Multipart};
use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter,
    QueryOrder, Set,
};
use serde_json::json;

use crate::{
    controllers::{
        admin_form::{read_multipart_form, store_image, MultipartForm},
        i18n::current_lang,
        media::IMAGE_MAX_BYTES,
    },
    shared::{
        guard,
        money::parse_price_to_cents,
        slug::{slugify, unique_slug},
    },
    models::{categories, product_images, products},
    views::shop as view,
};

async fn product_by_id(ctx: &AppContext, id: i32) -> Result<products::Model> {
    products::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)
}

/// Fields parsed from a product form, ready to apply to an active model.
struct ProductFields {
    name: String,
    slug: String,
    description: Option<String>,
    price_cents: i64,
    currency: String,
    sku: Option<String>,
    stock: i32,
    category_id: Option<i32>,
    published: bool,
}

async fn parse_product_fields(
    ctx: &AppContext,
    form: &MultipartForm,
    current_id: Option<i32>,
) -> Result<ProductFields> {
    let name = form
        .text("name")
        .ok_or_else(|| Error::BadRequest("product name is required".to_string()))?;
    let price_cents = parse_price_to_cents(
        form.text("price")
            .ok_or_else(|| Error::BadRequest("price is required".to_string()))?
            .as_str(),
    )?;
    let currency = form.text("currency").unwrap_or_else(|| "EUR".to_string());
    let description = form.text("description");
    let sku = form.text("sku");
    let stock = form
        .text("stock")
        .and_then(|s| s.parse::<i32>().ok())
        .filter(|n| *n >= 0)
        .unwrap_or(0);
    let category_id = form.text("category_id").and_then(|s| s.parse::<i32>().ok());
    let published = form.checked("published");

    let desired = form
        .text("slug")
        .map(|s| slugify(&s))
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| slugify(&name));
    let slug = unique_slug(&desired, |candidate| {
        let ctx = ctx.clone();
        async move {
            let mut query = products::Entity::find().filter(products::Column::Slug.eq(candidate));
            if let Some(id) = current_id {
                query = query.filter(products::Column::Id.ne(id));
            }
            Ok(query.count(&ctx.db).await? > 0)
        }
    })
    .await?;

    Ok(ProductFields {
        name,
        slug,
        description,
        price_cents,
        currency,
        sku,
        stock,
        category_id,
        published,
    })
}

async fn form_context(ctx: &AppContext, jar: &CookieJar) -> Result<serde_json::Value> {
    let categories = categories::Entity::find()
        .order_by_asc(categories::Column::Position)
        .order_by_asc(categories::Column::Name)
        .all(&ctx.db)
        .await?;
    Ok(json!({ "categories": categories, "lang": current_lang(jar) }))
}

#[debug_handler]
async fn index(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let list = products::Entity::find()
        .order_by_desc(products::Column::CreatedAt)
        .all(&ctx.db)
        .await?;
    let mut rows = Vec::new();
    for product in list {
        let image = product_images::first_for(&ctx, product.id).await?;
        let category_name = match product.category_id {
            Some(id) => categories::Entity::find_by_id(id)
                .one(&ctx.db)
                .await?
                .map(|c| c.name),
            None => None,
        };
        rows.push(view::product_card(&product, image, category_name));
    }
    format::view(
        &v,
        "admin/catalog/products.html",
        json!({ "products": rows, "lang": current_lang(&jar) }),
    )
}

#[debug_handler]
async fn new(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let mut context = form_context(&ctx, &jar).await?;
    context["product"] = serde_json::Value::Null;
    format::view(&v, "admin/catalog/product_form.html", context)
}

#[debug_handler]
async fn create(
    auth: auth::JWT,
    State(ctx): State<AppContext>,
    multipart: Multipart,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let form = read_multipart_form(multipart).await?;
    let fields = parse_product_fields(&ctx, &form, None).await?;

    let product = products::ActiveModel {
        name: Set(fields.name),
        slug: Set(fields.slug),
        description: Set(fields.description),
        price_cents: Set(fields.price_cents),
        currency: Set(fields.currency),
        sku: Set(fields.sku),
        stock: Set(fields.stock),
        view_count: Set(0),
        published: Set(fields.published),
        published_at: Set(fields.published.then(|| chrono::Utc::now().into())),
        category_id: Set(fields.category_id),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

    if let Some(data) = form.image {
        let filename = store_image(&ctx, data).await?;
        product_images::ActiveModel {
            product_id: Set(product.id),
            image_id: Set(filename),
            position: Set(0),
            alt: Set(None),
            ..Default::default()
        }
        .insert(&ctx.db)
        .await?;
    }

    format::redirect("/admin/catalog/products")
}

#[debug_handler]
async fn edit(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let product = product_by_id(&ctx, id).await?;
    let image = product_images::first_for(&ctx, id).await?;
    let mut context = form_context(&ctx, &jar).await?;
    context["product"] = view::product_form(&product, image);
    format::view(&v, "admin/catalog/product_form.html", context)
}

#[debug_handler]
async fn update(
    auth: auth::JWT,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    multipart: Multipart,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let existing = product_by_id(&ctx, id).await?;
    let was_published = existing.published;
    let form = read_multipart_form(multipart).await?;
    let fields = parse_product_fields(&ctx, &form, Some(id)).await?;

    let mut product = existing.into_active_model();
    product.name = Set(fields.name);
    product.slug = Set(fields.slug);
    product.description = Set(fields.description);
    product.price_cents = Set(fields.price_cents);
    product.currency = Set(fields.currency);
    product.sku = Set(fields.sku);
    product.stock = Set(fields.stock);
    product.category_id = Set(fields.category_id);
    product.published = Set(fields.published);
    if fields.published && !was_published {
        product.published_at = Set(Some(chrono::Utc::now().into()));
    } else if !fields.published {
        product.published_at = Set(None);
    }
    product.update(&ctx.db).await?;

    if let Some(data) = form.image {
        let filename = store_image(&ctx, data).await?;
        let next_position = product_images::count_for(&ctx, id).await?;
        product_images::ActiveModel {
            product_id: Set(id),
            image_id: Set(filename),
            position: Set(next_position),
            alt: Set(None),
            ..Default::default()
        }
        .insert(&ctx.db)
        .await?;
    }

    format::redirect("/admin/catalog/products")
}

#[debug_handler]
async fn delete(
    auth: auth::JWT,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    product_by_id(&ctx, id).await?.delete(&ctx.db).await?;
    format::redirect("/admin/catalog/products")
}

pub fn routes() -> Routes {
    let image_limit = DefaultBodyLimit::max(IMAGE_MAX_BYTES + 1024 * 1024);
    Routes::new()
        .add("/admin/catalog/products", get(index))
        .add("/admin/catalog/products/new", get(new))
        .add(
            "/admin/catalog/products",
            post(create).layer(image_limit.clone()),
        )
        .add("/admin/catalog/products/{id}/edit", get(edit))
        .add(
            "/admin/catalog/products/{id}",
            post(update).layer(image_limit),
        )
        .add("/admin/catalog/products/{id}/delete", post(delete))
}
