//! Public storefront: product listings, product detail, category pages and the
//! lazily-loaded category sidebar.

use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set};
use serde_json::json;

use crate::{
    controllers::i18n::current_lang,
    shared::{guard, pricing},
    models::{categories, product_images, product_variants, products, users},
    views::shop as view,
};

/// Shape a list of products into card rows for `user` (None = public). Each card
/// shows the resolved price of the product's representative (first) variant; the
/// `variant_count` lets the template render "from {price}" for multi-variant
/// products. Products with no variants are skipped (not purchasable).
async fn product_rows(
    ctx: &AppContext,
    user: Option<&users::Model>,
    list: Vec<products::Model>,
) -> Result<Vec<serde_json::Value>> {
    let ids: Vec<i32> = list.iter().map(|p| p.id).collect();
    let grouped = product_variants::Entity::grouped_for_products(&ctx.db, &ids).await?;

    // Representative (first) variant per product, in list order, dropping any
    // product that has no variants.
    let mut entries: Vec<(&products::Model, product_variants::Model, usize)> = Vec::new();
    for product in &list {
        if let Some(variants) = grouped.get(&product.id) {
            if let Some(first) = variants.first() {
                entries.push((product, first.clone(), variants.len()));
            }
        }
    }

    let reps: Vec<product_variants::Model> = entries.iter().map(|(_, v, _)| v.clone()).collect();
    let priced = pricing::price_variants(ctx, &reps, user).await?;

    let mut rows = Vec::with_capacity(entries.len());
    for ((product, rep, count), priced) in entries.iter().zip(priced.iter()) {
        let image = product_images::first_for(ctx, product.id).await?;
        rows.push(view::product_card(product, rep, priced, *count, image, None));
    }
    Ok(rows)
}

/// Latest published products (with primary image), shaped for templates. Reused
/// by the home-page landing grid.
pub(crate) async fn featured_products(
    ctx: &AppContext,
    user: Option<&users::Model>,
    limit: u64,
) -> Result<Vec<serde_json::Value>> {
    let list = products::Entity::find()
        .filter(products::Column::Published.eq(true))
        .order_by_desc(products::Column::PublishedAt)
        .limit(limit)
        .all(&ctx.db)
        .await?;
    product_rows(ctx, user, list).await
}

/// The site-wide category sidebar, loaded lazily via htmx by the base layout so
/// every page gets it without each handler having to supply category data.
#[debug_handler]
async fn category_sidebar(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let published = categories::published(&ctx).await?;
    format::view(
        &v,
        "shop/_sidebar.html",
        json!({
            "category_groups": view::sidebar_groups(&published),
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn index(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let list = products::Entity::find()
        .filter(products::Column::Published.eq(true))
        .order_by_desc(products::Column::PublishedAt)
        .all(&ctx.db)
        .await?;

    let user = guard::current_user(&ctx, &jar).await;
    let c = guard::chrome_from(&ctx, user.as_ref());
    format::view(
        &v,
        "shop/index.html",
        json!({
            "products": product_rows(&ctx, user.as_ref(), list).await?,
            "logged_in_admin": c.logged_in_admin,
            "logged_in_customer": c.logged_in_customer,
            "customer_name": c.customer_name,
            "customer_account_type": c.customer_account_type,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn show(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path(slug): Path<String>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let product = products::Entity::find()
        .filter(products::Column::Slug.eq(slug))
        .filter(products::Column::Published.eq(true))
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;

    let mut active = product.clone().into_active_model();
    active.view_count = Set(product.view_count + 1);
    let product = active.update(&ctx.db).await?;

    let images = product_images::Entity::find()
        .filter(product_images::Column::ProductId.eq(product.id))
        .order_by_asc(product_images::Column::Position)
        .all(&ctx.db)
        .await?;
    let category = match product.category_id {
        Some(id) => categories::Entity::find_by_id(id).one(&ctx.db).await?,
        None => None,
    };

    let user = guard::current_user(&ctx, &jar).await;
    let variants = product_variants::Entity::for_product(&ctx.db, product.id).await?;
    let variant_prices = pricing::price_variants(&ctx, &variants, user.as_ref()).await?;
    let options: Vec<serde_json::Value> = variants
        .iter()
        .zip(variant_prices.iter())
        .map(|(variant, priced)| view::variant_option(variant, priced))
        .collect();
    // The card header uses the representative (first) variant for its headline
    // price; the picker below lets the customer switch.
    let representative = variants.first();
    let priced = variant_prices.first().copied();
    let card = match (representative, priced) {
        (Some(rep), Some(priced)) => view::product_card(
            &product,
            rep,
            &priced,
            variants.len(),
            None,
            category.as_ref().map(|c| c.name.clone()),
        ),
        // A product with no variants isn't purchasable; show it without a price.
        _ => serde_json::json!({
            "id": product.id,
            "name": product.name,
            "slug": product.slug,
            "description": product.description,
            "currency": product.currency,
            "variant_count": 0,
            "has_options": false,
        }),
    };
    let c = guard::chrome_from(&ctx, user.as_ref());
    format::view(
        &v,
        "shop/show.html",
        json!({
            "product": card,
            "variants": options,
            "images": images.iter().map(|i| i.image_id.clone()).collect::<Vec<_>>(),
            "category": category,
            "logged_in_admin": c.logged_in_admin,
            "logged_in_customer": c.logged_in_customer,
            "customer_name": c.customer_name,
            "customer_account_type": c.customer_account_type,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn category(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path(slug): Path<String>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let published = categories::published(&ctx).await?;
    let category = published
        .iter()
        .find(|c| c.slug == slug)
        .cloned()
        .ok_or_else(|| Error::NotFound)?;

    let breadcrumbs = categories::ancestors(&published, category.parent_id);
    let children = categories::children_of(&published, category.id);

    // Products listed here span this category and all of its descendants, so a
    // parent category is never empty just because its products live in leaves.
    let mut category_ids: Vec<i32> = categories::descendant_ids(&published, category.id)
        .into_iter()
        .collect();
    category_ids.push(category.id);
    let list = products::Entity::find()
        .filter(products::Column::CategoryId.is_in(category_ids))
        .filter(products::Column::Published.eq(true))
        .order_by_desc(products::Column::PublishedAt)
        .all(&ctx.db)
        .await?;

    let user = guard::current_user(&ctx, &jar).await;
    let c = guard::chrome_from(&ctx, user.as_ref());
    format::view(
        &v,
        "shop/category.html",
        json!({
            "category": category,
            "breadcrumbs": breadcrumbs,
            "children": children,
            "products": product_rows(&ctx, user.as_ref(), list).await?,
            "logged_in_admin": c.logged_in_admin,
            "logged_in_customer": c.logged_in_customer,
            "customer_name": c.customer_name,
            "customer_account_type": c.customer_account_type,
            "lang": current_lang(&jar),
        }),
    )
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/shop", get(index))
        .add("/shop/{slug}", get(show))
        .add("/category/{slug}", get(category))
        .add("/partials/categories", get(category_sidebar))
}
