//! Admin management of business (company) accounts and their pricing.
//!
//! Per company the admin can: assign reusable discount profiles (the automated
//! layer), resolve per-product collisions when two assigned profiles cover the
//! same product, and set a manually negotiated price per product. The effective
//! price the business pays is always resolved by [`crate::shared::pricing`]
//! (lowest of public / automated / negotiated), shown here for reference.

use std::collections::{HashMap, HashSet};

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

use crate::{
    controllers::i18n::current_lang,
    models::{
        account_discount_profiles, account_product_prices, account_product_resolutions,
        categories, discount_profiles, product_variants, products, _entities::users,
    },
    shared::{
        guard,
        money::{format_bp, format_price, parse_price_to_cents},
        pricing,
    },
    views::shop as view,
};

const COMPANY: &str = "company";
const BUSINESS_AUDIENCE: &str = "business";

#[derive(Debug, Deserialize)]
struct PriceForm {
    price: String,
}

#[derive(Debug, Deserialize)]
struct ResolutionForm {
    profile_id: i32,
}

async fn company_by_id(ctx: &AppContext, id: i32) -> Result<users::Model> {
    let user = users::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;
    if user.account_type != COMPANY {
        return Err(Error::NotFound);
    }
    Ok(user)
}

async fn assigned_profile_ids(ctx: &AppContext, user_id: i32) -> Result<HashSet<i32>> {
    Ok(account_discount_profiles::Entity::find()
        .filter(account_discount_profiles::Column::UserId.eq(user_id))
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(|a| a.discount_profile_id)
        .collect())
}

#[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 companies = users::Entity::find()
        .filter(users::Column::AccountType.eq(COMPANY))
        .order_by_asc(users::Column::Name)
        .all(&ctx.db)
        .await?;

    let mut rows = Vec::with_capacity(companies.len());
    for company in &companies {
        let negotiated = account_product_prices::Entity::find()
            .filter(account_product_prices::Column::UserId.eq(company.id))
            .count(&ctx.db)
            .await?;
        rows.push(json!({
            "id": company.id,
            "name": company.name,
            "email": company.email,
            "negotiated_count": negotiated,
        }));
    }

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

#[debug_handler]
async fn show(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path(id): Path<i32>,
    Query(params): Query<HashMap<String, String>>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let company = company_by_id(&ctx, id).await?;

    // All profiles (for the assignment section + name lookup) and which are
    // assigned to this company.
    let all_profiles = discount_profiles::Entity::find()
        .order_by_asc(discount_profiles::Column::Name)
        .all(&ctx.db)
        .await?;
    let assigned = assigned_profile_ids(&ctx, company.id).await?;
    let profiles_json: Vec<serde_json::Value> = all_profiles
        .iter()
        .map(|p| {
            json!({
                "id": p.id,
                "name": p.name,
                "percent": format_bp(p.percent_bp),
                "scope_type": p.scope_type,
                "assigned": assigned.contains(&p.id),
            })
        })
        .collect();

    let all_categories = categories::Entity::find()
        .order_by_asc(categories::Column::Position)
        .order_by_asc(categories::Column::Name)
        .all(&ctx.db)
        .await?;

    // Optional text search (drafts included), otherwise the whole catalog by
    // name. Reuses the storefront's hybrid full-text + fuzzy product search.
    let query = params.get("q").map(String::as_str).unwrap_or("").trim().to_string();
    let list = if query.is_empty() {
        products::Entity::find()
            .order_by_asc(products::Column::Name)
            .all(&ctx.db)
            .await?
    } else {
        products::Entity::search(&ctx.db, &query, 1000, false).await?
    };

    // Category sidebar tree (counts over the full, unfiltered product list) plus
    // the active `?category=` filter applied to the rows.
    let category_ids: Vec<Option<i32>> = list.iter().map(|p| p.category_id).collect();
    let category_groups = view::admin_category_groups(&all_categories, &category_ids);
    let selected_category = params
        .get("category")
        .map(String::as_str)
        .unwrap_or("all")
        .to_string();
    let filter = view::category_filter_ids(&all_categories, &selected_category);

    // Pricing is per variant. Flatten the (filtered) products into their variants
    // in product-name then variant-position order, carrying each variant's
    // product for the row's display name.
    let product_ids: Vec<i32> = list.iter().map(|p| p.id).collect();
    let grouped = product_variants::Entity::grouped_for_products(&ctx.db, &product_ids).await?;
    let mut variant_rows: Vec<(&products::Model, product_variants::Model)> = Vec::new();
    for product in &list {
        if !view::category_filter_keep(&filter, product.category_id) {
            continue;
        }
        if let Some(variants) = grouped.get(&product.id) {
            for variant in variants {
                variant_rows.push((product, variant.clone()));
            }
        }
    }

    // Two prices per variant:
    // - the generic business price a freshly-registered company sees (business
    //   baseline + business-audience profiles, no per-company deals), and
    // - this company's effective price (its negotiated price + assigned profiles).
    // The effective price is highlighted only when it differs from the generic one.
    let variants_only: Vec<product_variants::Model> =
        variant_rows.iter().map(|(_, v)| v.clone()).collect();
    let business = pricing::audience_price_variants(&ctx, &variants_only, BUSINESS_AUDIENCE).await?;
    let details = pricing::detail_variants(&ctx, &variants_only, Some(&company)).await?;

    let rows: Vec<serde_json::Value> = variant_rows
        .iter()
        .zip(business.iter())
        .zip(details.iter())
        .map(|(((product, variant), b), d)| {
            json!({
                "product_id": product.id,
                "variant_id": variant.id,
                "name": product.name,
                "variant_label": variant.label,
                "regular_price": format_price(d.regular_cents),
                "business_price": format_price(b.price_cents),
                "business_reduced": b.price_cents < d.regular_cents,
                "has_negotiated": d.manual_cents.is_some(),
                "collision": d.collision,
                "effective_price": format_price(d.price_cents),
                "effective_differs": d.price_cents != b.price_cents,
            })
        })
        .collect();

    format::view(
        &v,
        "admin/customers/show.html",
        json!({
            "customer": { "id": company.id, "name": company.name, "email": company.email },
            "profiles": profiles_json,
            "products": rows,
            "category_groups": category_groups,
            "selected_category": selected_category,
            "query": query,
            "total_count": list.len(),
            "uncategorized_count": category_ids.iter().filter(|c| c.is_none()).count(),
            "error": params.get("error"),
            "lang": current_lang(&jar),
        }),
    )
}

/// Dedicated per-product page for the negotiated price (and, when two assigned
/// profiles collide, the resolution selector). Mirrors the catalog "Set discount"
/// page but for a single company.
#[debug_handler]
async fn price_edit(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path((id, variant_id)): Path<(i32, i32)>,
    Query(params): Query<HashMap<String, String>>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let company = company_by_id(&ctx, id).await?;
    let variant = product_variants::Entity::find_by_id(variant_id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;
    let product = products::Entity::find_by_id(variant.product_id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;

    let business =
        pricing::audience_price_variants(&ctx, std::slice::from_ref(&variant), BUSINESS_AUDIENCE)
            .await?;
    let business_cents = business[0].price_cents;
    let detail =
        pricing::detail_variants(&ctx, std::slice::from_ref(&variant), Some(&company)).await?;
    let d = &detail[0];

    // Names for the covering profiles, used by the collision resolution selector.
    let covering: Vec<serde_json::Value> = if d.covering_profile_ids.is_empty() {
        Vec::new()
    } else {
        let profiles = discount_profiles::Entity::find()
            .filter(discount_profiles::Column::Id.is_in(d.covering_profile_ids.clone()))
            .all(&ctx.db)
            .await?;
        let name: HashMap<i32, String> =
            profiles.iter().map(|p| (p.id, p.name.clone())).collect();
        d.covering_profile_ids
            .iter()
            .map(|pid| json!({ "id": pid, "name": name.get(pid) }))
            .collect()
    };

    format::view(
        &v,
        "admin/customers/price_form.html",
        json!({
            "customer": { "id": company.id, "name": company.name },
            "product": {
                "id": product.id,
                "variant_id": variant.id,
                "name": product.name,
                "variant_label": variant.label,
                "regular_price": format_price(d.regular_cents),
                "regular_cents": d.regular_cents,
                "business_price": format_price(business_cents),
                "business_reduced": business_cents < d.regular_cents,
                "effective_price": format_price(d.price_cents),
                "effective_differs": d.price_cents != business_cents,
            },
            "negotiated": d.manual_cents.map(format_price).unwrap_or_default(),
            "has_negotiated": d.manual_cents.is_some(),
            "collision": d.collision,
            "covering": covering,
            "auto_profile_id": d.auto_profile_id,
            "error": params.get("error"),
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn set_price(
    auth: auth::JWT,
    Path((id, variant_id)): Path<(i32, i32)>,
    State(ctx): State<AppContext>,
    Form(form): Form<PriceForm>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let company = company_by_id(&ctx, id).await?;

    let entered = form.price.trim().to_string();
    if entered.is_empty() {
        account_product_prices::Model::clear(&ctx.db, company.id, variant_id).await?;
        return format::redirect(&format!("/admin/customers/{id}"));
    }

    let cents = match parse_price_to_cents(&entered) {
        Ok(cents) if cents > 0 => cents,
        _ => {
            return format::redirect(&format!(
                "/admin/customers/{id}/prices/{variant_id}/edit?error=discount-must-be-positive"
            ))
        }
    };
    account_product_prices::Model::upsert(&ctx.db, company.id, variant_id, cents).await?;
    format::redirect(&format!("/admin/customers/{id}"))
}

#[debug_handler]
async fn remove_price(
    auth: auth::JWT,
    Path((id, variant_id)): Path<(i32, i32)>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let company = company_by_id(&ctx, id).await?;
    account_product_prices::Model::clear(&ctx.db, company.id, variant_id).await?;
    format::redirect(&format!("/admin/customers/{id}"))
}

/// Replace the company's assigned profiles with the submitted set of checkboxes
/// (`profile_ids`, a repeated field axum `Form` can't collect, parsed directly).
#[debug_handler]
async fn sync_profiles(
    auth: auth::JWT,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    body: String,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let company = company_by_id(&ctx, id).await?;

    let profile_ids: Vec<i32> = form_urlencoded::parse(body.as_bytes())
        .filter(|(k, _)| k == "profile_ids")
        .filter_map(|(_, v)| v.parse::<i32>().ok())
        .collect();

    let txn = ctx.db.begin().await?;
    account_discount_profiles::Entity::delete_many()
        .filter(account_discount_profiles::Column::UserId.eq(company.id))
        .exec(&txn)
        .await?;
    for profile_id in profile_ids {
        account_discount_profiles::ActiveModel {
            user_id: Set(company.id),
            discount_profile_id: Set(profile_id),
            ..Default::default()
        }
        .insert(&txn)
        .await?;
    }
    txn.commit().await?;
    format::redirect(&format!("/admin/customers/{id}"))
}

/// Record the admin's chosen winning profile for a colliding product.
#[debug_handler]
async fn set_resolution(
    auth: auth::JWT,
    Path((id, variant_id)): Path<(i32, i32)>,
    State(ctx): State<AppContext>,
    Form(form): Form<ResolutionForm>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    let company = company_by_id(&ctx, id).await?;

    let existing = account_product_resolutions::Entity::find()
        .filter(account_product_resolutions::Column::UserId.eq(company.id))
        .filter(account_product_resolutions::Column::VariantId.eq(variant_id))
        .one(&ctx.db)
        .await?;
    let mut active = match existing {
        Some(row) => row.into_active_model(),
        None => account_product_resolutions::ActiveModel {
            user_id: Set(company.id),
            variant_id: Set(variant_id),
            ..Default::default()
        },
    };
    active.discount_profile_id = Set(form.profile_id);
    active.save(&ctx.db).await?;
    format::redirect(&format!("/admin/customers/{id}"))
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/admin/customers", get(index))
        .add("/admin/customers/{id}", get(show))
        .add("/admin/customers/{id}/profiles", post(sync_profiles))
        .add(
            "/admin/customers/{id}/prices/{variant_id}/edit",
            get(price_edit),
        )
        .add("/admin/customers/{id}/prices/{variant_id}", post(set_price))
        .add(
            "/admin/customers/{id}/prices/{variant_id}/remove",
            post(remove_price),
        )
        .add(
            "/admin/customers/{id}/resolutions/{variant_id}",
            post(set_resolution),
        )
}
