//! Admin CRUD for reusable discount profiles (a named percentage over a product
//! scope). Profiles are assigned to business accounts on the customer page; here
//! the admin only defines them.

use std::collections::HashSet;

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

use crate::{
    controllers::i18n::current_lang,
    models::{discount_profile_products, discount_profiles, products},
    shared::{
        guard,
        money::{format_bp, parse_percent, percent_to_bp},
    },
};

/// Scalar + repeated fields parsed from the profile form. `product_ids` is a
/// repeated checkbox field, which `serde_urlencoded` (axum `Form`) can't collect,
/// so the body is parsed directly.
struct ProfileInput {
    name: String,
    percent: String,
    scope_type: String,
    product_ids: Vec<i32>,
}

fn parse_profile_form(body: &str) -> ProfileInput {
    let mut name = String::new();
    let mut percent = String::new();
    let mut scope_type = discount_profiles::SCOPE_INCLUDE.to_string();
    let mut product_ids = Vec::new();
    for (key, value) in form_urlencoded::parse(body.as_bytes()) {
        match key.as_ref() {
            "name" => name = value.into_owned(),
            "percent" => percent = value.into_owned(),
            "scope_type" => scope_type = value.into_owned(),
            "product_ids" => {
                if let Ok(id) = value.parse::<i32>() {
                    product_ids.push(id);
                }
            }
            _ => {}
        }
    }
    ProfileInput {
        name,
        percent,
        scope_type,
        product_ids,
    }
}

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

#[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 profiles = discount_profiles::Entity::find()
        .order_by_asc(discount_profiles::Column::Name)
        .all(&ctx.db)
        .await?;

    let mut rows = Vec::with_capacity(profiles.len());
    for profile in &profiles {
        let count = discount_profile_products::Entity::find()
            .filter(discount_profile_products::Column::DiscountProfileId.eq(profile.id))
            .count(&ctx.db)
            .await?;
        rows.push(json!({
            "id": profile.id,
            "name": profile.name,
            "percent": format_bp(profile.percent_bp),
            "scope_type": profile.scope_type,
            "product_count": count,
        }));
    }

    format::view(
        &v,
        "admin/catalog/discount_profiles.html",
        json!({ "profiles": rows, "lang": current_lang(&jar) }),
    )
}

/// Render the create/edit form. `profile` is null on create.
async fn render_form(
    ctx: &AppContext,
    v: &TeraView,
    jar: &CookieJar,
    profile: Option<&discount_profiles::Model>,
    selected: &HashSet<i32>,
    error: Option<&str>,
) -> Result<Response> {
    let all_products = products::Entity::find()
        .order_by_asc(products::Column::Name)
        .all(&ctx.db)
        .await?;
    let product_rows: Vec<serde_json::Value> = all_products
        .iter()
        .map(|p| json!({ "id": p.id, "name": p.name, "selected": selected.contains(&p.id) }))
        .collect();

    let profile_json = match profile {
        Some(p) => json!({
            "id": p.id,
            "name": p.name,
            "percent": format_bp(p.percent_bp),
            "scope_type": p.scope_type,
        }),
        None => serde_json::Value::Null,
    };

    format::view(
        v,
        "admin/catalog/discount_profile_form.html",
        json!({
            "profile": profile_json,
            "products": product_rows,
            "error": error,
            "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?;
    render_form(&ctx, &v, &jar, None, &HashSet::new(), None).await
}

#[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 profile = profile_by_id(&ctx, id).await?;
    let selected = member_ids(&ctx, id).await?;
    render_form(&ctx, &v, &jar, Some(&profile), &selected, None).await
}

async fn member_ids(ctx: &AppContext, profile_id: i32) -> Result<HashSet<i32>> {
    Ok(discount_profile_products::Entity::find()
        .filter(discount_profile_products::Column::DiscountProfileId.eq(profile_id))
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(|r| r.product_id)
        .collect())
}

/// Validate the parsed form into `(name, percent_bp, scope_type)`, or an error key.
fn validate(input: &ProfileInput) -> std::result::Result<(String, i32, String), &'static str> {
    let name = input.name.trim().to_string();
    if name.is_empty() {
        return Err("profile-name-required");
    }
    let pct = parse_percent(&input.percent).ok_or("discount-invalid")?;
    if pct <= 0.0 || pct >= 100.0 {
        return Err("discount-percent-range");
    }
    let scope = if input.scope_type == discount_profiles::SCOPE_ALL_EXCEPT {
        discount_profiles::SCOPE_ALL_EXCEPT
    } else {
        discount_profiles::SCOPE_INCLUDE
    };
    Ok((name, percent_to_bp(pct), scope.to_string()))
}

/// Replace a profile's product membership with `product_ids`.
async fn sync_membership(
    ctx: &AppContext,
    profile_id: i32,
    product_ids: &[i32],
) -> Result<()> {
    let txn = ctx.db.begin().await?;
    discount_profile_products::Entity::delete_many()
        .filter(discount_profile_products::Column::DiscountProfileId.eq(profile_id))
        .exec(&txn)
        .await?;
    for product_id in product_ids {
        discount_profile_products::ActiveModel {
            discount_profile_id: Set(profile_id),
            product_id: Set(*product_id),
            ..Default::default()
        }
        .insert(&txn)
        .await?;
    }
    txn.commit().await?;
    Ok(())
}

#[debug_handler]
async fn create(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    body: String,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let input = parse_profile_form(&body);
    let (name, percent_bp, scope_type) = match validate(&input) {
        Ok(values) => values,
        Err(key) => {
            let selected: HashSet<i32> = input.product_ids.iter().copied().collect();
            return render_form(&ctx, &v, &jar, None, &selected, Some(key)).await;
        }
    };

    let profile = discount_profiles::ActiveModel {
        name: Set(name),
        percent_bp: Set(percent_bp),
        scope_type: Set(scope_type),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;
    sync_membership(&ctx, profile.id, &input.product_ids).await?;
    format::redirect("/admin/catalog/discount-profiles")
}

#[debug_handler]
async fn update(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    body: String,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let profile = profile_by_id(&ctx, id).await?;
    let input = parse_profile_form(&body);
    let (name, percent_bp, scope_type) = match validate(&input) {
        Ok(values) => values,
        Err(key) => {
            let selected: HashSet<i32> = input.product_ids.iter().copied().collect();
            return render_form(&ctx, &v, &jar, Some(&profile), &selected, Some(key)).await;
        }
    };

    let mut active = profile.into_active_model();
    active.name = Set(name);
    active.percent_bp = Set(percent_bp);
    active.scope_type = Set(scope_type);
    active.update(&ctx.db).await?;
    sync_membership(&ctx, id, &input.product_ids).await?;
    format::redirect("/admin/catalog/discount-profiles")
}

#[debug_handler]
async fn delete(
    auth: auth::JWT,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    // FK cascades remove membership, assignments and resolutions.
    profile_by_id(&ctx, id).await?.delete(&ctx.db).await?;
    format::redirect("/admin/catalog/discount-profiles")
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/admin/catalog/discount-profiles", get(index))
        .add("/admin/catalog/discount-profiles/new", get(new))
        .add("/admin/catalog/discount-profiles", post(create))
        .add("/admin/catalog/discount-profiles/{id}/edit", get(edit))
        .add("/admin/catalog/discount-profiles/{id}", post(update))
        .add("/admin/catalog/discount-profiles/{id}/delete", post(delete))
}
