//! Admin order list, detail, status updates, and manual carrier dispatch.

use std::collections::HashMap;

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

use crate::{
    integrations::{self, ShipmentRequest},
    models::{order_items, orders, shipping_methods},
    views::checkout as view,
    controllers::i18n::current_lang,
    shared::{guard, settings},
};

pub(crate) const ORDER_STATUSES: [&str; 5] =
    ["pending", "paid", "shipped", "delivered", "cancelled"];

/// Fallback parcel weight when products carry no weight of their own.
const DEFAULT_PARCEL_WEIGHT_GRAMS: i32 = 1000;

#[derive(Debug, Deserialize)]
struct StatusForm {
    status: String,
}

#[debug_handler]
async fn index(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Query(params): Query<HashMap<String, String>>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    // Optional search over order number / customer / email / etc., otherwise the
    // full list newest first.
    let query = params.get("q").map(String::as_str).unwrap_or("").trim().to_string();
    let list = if query.is_empty() {
        orders::Entity::find()
            .order_by_desc(orders::Column::CreatedAt)
            .all(&ctx.db)
            .await?
    } else {
        orders::Entity::search(&ctx.db, &query, 500).await?
    };
    let rows: Vec<serde_json::Value> = list.iter().map(view::summary).collect();
    format::view(
        &v,
        "admin/orders/index.html",
        json!({
            "orders": rows,
            "query": query,
            "total": list.len(),
            "lang": current_lang(&jar),
        }),
    )
}

/// Resolve the carrier code (`none`/`packeta`/`dpd`/`dhl`) for an order from its
/// chosen shipping method, defaulting to `none` when unknown.
async fn order_carrier(ctx: &AppContext, order: &orders::Model) -> Result<String> {
    let Some(code) = order.carrier_code.as_deref() else {
        return Ok("none".to_string());
    };
    Ok(shipping_methods::Entity::find()
        .filter(shipping_methods::Column::Code.eq(code))
        .one(&ctx.db)
        .await?
        .map(|m| m.carrier)
        .unwrap_or_else(|| "none".to_string()))
}

/// Render the order detail page, optionally with a dispatch error banner.
async fn render_show(
    jar: &CookieJar,
    v: &TeraView,
    ctx: &AppContext,
    id: i32,
    error: Option<String>,
) -> Result<Response> {
    let order = orders::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;
    let items = order_items::Entity::find()
        .filter(order_items::Column::OrderId.eq(order.id))
        .all(&ctx.db)
        .await?;

    let carrier = order_carrier(ctx, &order).await?;
    let (bank_iban, bank_account_name) = settings::bank_details(ctx).await?;
    // The order can be sent only if it maps to a real carrier and hasn't been
    // dispatched yet.
    let can_ship = carrier != "none" && order.tracking_number.is_none();

    format::view(
        v,
        "admin/orders/show.html",
        json!({
            "order": view::detail(
                &order,
                &bank_iban,
                &bank_account_name,
            ),
            "items": view::items(&items),
            "statuses": ORDER_STATUSES,
            "carrier": carrier,
            "can_ship": can_ship,
            "ship_error": error,
            "lang": current_lang(jar),
        }),
    )
}

#[debug_handler]
async fn show(
    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?;
    render_show(&jar, &v, &ctx, id, None).await
}

#[debug_handler]
async fn update_status(
    auth: auth::JWT,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    Form(form): Form<StatusForm>,
) -> Result<Response> {
    guard::current_admin(auth, &ctx).await?;
    if !ORDER_STATUSES.contains(&form.status.as_str()) {
        return Err(Error::BadRequest("invalid status".to_string()));
    }
    let order = orders::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;
    let mut active = order.into_active_model();
    active.status = Set(form.status);
    active.update(&ctx.db).await?;

    format::redirect(&format!("/admin/orders/{id}"))
}

/// Manually dispatch an order to its carrier. This is the *only* place that
/// calls a carrier API, and it is triggered exclusively by an admin clicking
/// "Send to carrier" after the goods are verified and ready.
#[debug_handler]
async fn ship(
    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 order = orders::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;

    // Idempotency: never create a second shipment for an already-dispatched order.
    if order.tracking_number.is_some() {
        return render_show(
            &jar,
            &v,
            &ctx,
            id,
            Some("This order has already been sent to the carrier.".to_string()),
        )
        .await;
    }

    let carrier = order_carrier(&ctx, &order).await?;
    let goods_value = (order.total_cents - order.shipping_cents).max(0);
    let cod_cents = match order.payment_method.as_deref() {
        Some("cod") => order.total_cents,
        _ => 0,
    };
    let recipient = order
        .customer_name
        .as_deref()
        .filter(|s| !s.is_empty())
        .unwrap_or(&order.email);

    let req = ShipmentRequest {
        order_number: &order.order_number,
        recipient_name: recipient,
        email: &order.email,
        phone: order.phone.as_deref(),
        address: order.address.as_deref(),
        city: order.city.as_deref(),
        zip: order.zip.as_deref(),
        country: order.country.as_deref(),
        pickup_point_id: order.pickup_point_id.as_deref(),
        cod_cents,
        value_cents: goods_value,
        weight_grams: DEFAULT_PARCEL_WEIGHT_GRAMS,
    };

    match integrations::create_shipment(&ctx, &carrier, req).await {
        Ok(result) => {
            let mut active = order.into_active_model();
            active.tracking_number = Set(Some(result.tracking_number));
            active.shipment_id = Set(Some(result.shipment_id));
            active.label_url = Set(result.label_url);
            active.status = Set("shipped".to_string());
            active.update(&ctx.db).await?;
            format::redirect(&format!("/admin/orders/{id}"))
        }
        // Show the carrier's error in-page rather than a generic error screen,
        // so the admin can fix the cause and retry.
        Err(err) => render_show(&jar, &v, &ctx, id, Some(err.to_string())).await,
    }
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/admin/orders", get(index))
        .add("/admin/orders/{id}", get(show))
        .add("/admin/orders/{id}/status", post(update_status))
        .add("/admin/orders/{id}/ship", post(ship))
}
