//! Admin category CRUD, including parent/child hierarchy management.

use std::collections::HashSet;

use axum::extract::{DefaultBodyLimit, Multipart};
use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter, 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,
        slug::{slugify, unique_slug},
    },
    models::{categories, products},
};

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

/// Fields parsed from a category form.
struct CategoryFields {
    name: String,
    slug: String,
    description: Option<String>,
    position: i32,
    published: bool,
    parent_id: Option<i32>,
}

async fn parse_category_fields(
    ctx: &AppContext,
    form: &MultipartForm,
    current_id: Option<i32>,
) -> Result<CategoryFields> {
    let name = form
        .text("name")
        .ok_or_else(|| Error::BadRequest("category name is required".to_string()))?;
    let description = form.text("description");
    let published = form.checked("published");

    // Resolve the chosen parent, rejecting cycles: a category may not be its
    // own parent nor be re-parented under one of its descendants.
    let parent_id = match form.text("parent_id").and_then(|s| s.parse::<i32>().ok()) {
        Some(parent_id) => {
            categories::Entity::find_by_id(parent_id)
                .one(&ctx.db)
                .await?
                .ok_or_else(|| Error::BadRequest("parent category not found".to_string()))?;
            if let Some(id) = current_id {
                if parent_id == id {
                    return Err(Error::BadRequest(
                        "a category cannot be its own parent".to_string(),
                    ));
                }
                if categories::descendant_ids(&categories::all(ctx).await?, id).contains(&parent_id)
                {
                    return Err(Error::BadRequest(
                        "a category cannot be moved under its own descendant".to_string(),
                    ));
                }
            }
            Some(parent_id)
        }
        None => None,
    };

    // Position is optional: an explicit value sorts the category among its
    // siblings, but a blank field appends it to the end of its parent's group
    // (one past the current max), so new categories land last instead of first.
    let position = match form.text("position").and_then(|s| s.parse::<i32>().ok()) {
        Some(explicit) => explicit,
        None => {
            let mut query = categories::Entity::find();
            query = match parent_id {
                Some(pid) => query.filter(categories::Column::ParentId.eq(pid)),
                None => query.filter(categories::Column::ParentId.is_null()),
            };
            query
                .all(&ctx.db)
                .await?
                .iter()
                .filter(|c| Some(c.id) != current_id)
                .map(|c| c.position)
                .max()
                .map_or(0, |max| max + 1)
        }
    };

    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 =
                categories::Entity::find().filter(categories::Column::Slug.eq(candidate));
            if let Some(id) = current_id {
                query = query.filter(categories::Column::Id.ne(id));
            }
            Ok(query.count(&ctx.db).await? > 0)
        }
    })
    .await?;

    Ok(CategoryFields {
        name,
        slug,
        description,
        position,
        published,
        parent_id,
    })
}

/// Build the parent-category dropdown options for the category form, as a
/// depth-ordered list of `{ id, name, depth }`. When editing, the category
/// itself and all of its descendants are excluded to keep the tree acyclic.
async fn form_context(
    ctx: &AppContext,
    jar: &CookieJar,
    editing: Option<i32>,
) -> Result<serde_json::Value> {
    let all = categories::all(ctx).await?;
    let blocked: HashSet<i32> = match editing {
        Some(id) => {
            let mut set = categories::descendant_ids(&all, id);
            set.insert(id);
            set
        }
        None => HashSet::new(),
    };
    let parents: Vec<serde_json::Value> = categories::tree(&all)
        .into_iter()
        .filter(|(category, _)| !blocked.contains(&category.id))
        .map(|(category, depth)| json!({ "id": category.id, "name": category.name, "depth": depth }))
        .collect();
    Ok(json!({ "parents": parents, "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 = categories::all(&ctx).await?;
    let mut rows = Vec::new();
    for (category, depth) in categories::tree(&list) {
        let product_count = products::Entity::find()
            .filter(products::Column::CategoryId.eq(category.id))
            .count(&ctx.db)
            .await?;
        rows.push(json!({ "category": category, "depth": depth, "product_count": product_count }));
    }
    format::view(
        &v,
        "admin/catalog/categories.html",
        json!({ "categories": 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, None).await?;
    context["category"] = serde_json::Value::Null;
    format::view(&v, "admin/catalog/category_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_category_fields(&ctx, &form, None).await?;
    let image_id = match form.single_image() {
        Some(data) => Some(store_image(&ctx, data).await?),
        None => None,
    };

    categories::ActiveModel {
        name: Set(fields.name),
        slug: Set(fields.slug),
        description: Set(fields.description),
        image_id: Set(image_id),
        position: Set(fields.position),
        published: Set(fields.published),
        parent_id: Set(fields.parent_id),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

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

#[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 mut context = form_context(&ctx, &jar, Some(id)).await?;
    context["category"] = json!(category_by_id(&ctx, id).await?);
    format::view(&v, "admin/catalog/category_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 = category_by_id(&ctx, id).await?;
    let form = read_multipart_form(multipart).await?;
    let fields = parse_category_fields(&ctx, &form, Some(id)).await?;

    let mut category = existing.into_active_model();
    category.name = Set(fields.name);
    category.slug = Set(fields.slug);
    category.description = Set(fields.description);
    category.position = Set(fields.position);
    category.published = Set(fields.published);
    category.parent_id = Set(fields.parent_id);
    if let Some(data) = form.single_image() {
        category.image_id = Set(Some(store_image(&ctx, data).await?));
    }
    category.update(&ctx.db).await?;

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

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

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