//! 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,
    models::{categories, product_images, products},
    views::shop as view,
};

/// Shape a list of products into card rows, loading each one's primary image.
async fn product_rows(ctx: &AppContext, list: Vec<products::Model>) -> Result<Vec<serde_json::Value>> {
    let mut rows = Vec::with_capacity(list.len());
    for product in list {
        let image = product_images::first_for(ctx, product.id).await?;
        rows.push(view::product_card(&product, 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,
    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, 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 (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
    format::view(
        &v,
        "shop/index.html",
        json!({
            "products": product_rows(&ctx, list).await?,
            "logged_in_admin": logged_in_admin,
            "logged_in_customer": logged_in_customer,
            "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 (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
    format::view(
        &v,
        "shop/show.html",
        json!({
            "product": view::product_card(&product, None, category.as_ref().map(|c| c.name.clone())),
            "images": images.iter().map(|i| i.image_id.clone()).collect::<Vec<_>>(),
            "category": category,
            "logged_in_admin": logged_in_admin,
            "logged_in_customer": logged_in_customer,
            "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 (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
    format::view(
        &v,
        "shop/category.html",
        json!({
            "category": category,
            "breadcrumbs": breadcrumbs,
            "children": children,
            "products": product_rows(&ctx, list).await?,
            "logged_in_admin": logged_in_admin,
            "logged_in_customer": logged_in_customer,
            "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))
}
