use crate::{controllers::i18n::current_lang, shared::{guard, money::format_price}, models::products};
use axum::{
    http::{HeaderMap, StatusCode},
    response::Redirect,
};
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use serde::Deserialize;
use serde_json::json;
use time::Duration as TimeDuration;

pub(crate) const CART_COOKIE: &str = "cart";
const CART_MAX_AGE_DAYS: i64 = 30;

#[derive(Debug, Deserialize)]
struct AddForm {
    product_id: i32,
    quantity: Option<i32>,
}

#[derive(Debug, Deserialize)]
struct UpdateForm {
    product_id: i32,
    quantity: i32,
}

#[derive(Debug, Deserialize)]
struct RemoveForm {
    product_id: i32,
}

/// Parse the `cart` cookie ("id:qty,id:qty") into `(product_id, quantity)`
/// pairs, silently dropping malformed or non-positive entries.
pub(crate) fn parse_cart(jar: &CookieJar) -> Vec<(i32, i32)> {
    let Some(cookie) = jar.get(CART_COOKIE) else {
        return Vec::new();
    };
    cookie
        .value()
        .split(',')
        .filter_map(|entry| {
            let (id, qty) = entry.split_once(':')?;
            let id = id.trim().parse::<i32>().ok()?;
            let qty = qty.trim().parse::<i32>().ok()?;
            (qty > 0).then_some((id, qty))
        })
        .collect()
}

fn serialize_cart(items: &[(i32, i32)]) -> String {
    items
        .iter()
        .map(|(id, qty)| format!("{id}:{qty}"))
        .collect::<Vec<_>>()
        .join(",")
}

fn cart_cookie(value: String) -> Cookie<'static> {
    Cookie::build((CART_COOKIE, value))
        .path("/")
        .same_site(SameSite::Lax)
        .max_age(TimeDuration::days(CART_MAX_AGE_DAYS))
        .build()
}

/// Look up a published product, returning its current stock cap.
async fn published_product(ctx: &AppContext, id: i32) -> Result<Option<products::Model>> {
    Ok(products::Entity::find_by_id(id)
        .filter(products::Column::Published.eq(true))
        .one(&ctx.db)
        .await?)
}

#[debug_handler]
async fn add(
    jar: CookieJar,
    State(ctx): State<AppContext>,
    headers: HeaderMap,
    Form(form): Form<AddForm>,
) -> Result<Response> {
    let Some(product) = published_product(&ctx, form.product_id).await? else {
        return Err(Error::NotFound);
    };

    let mut items = parse_cart(&jar);
    let add_qty = form.quantity.unwrap_or(1).max(1);
    if let Some(entry) = items.iter_mut().find(|(id, _)| *id == product.id) {
        entry.1 = (entry.1 + add_qty).min(product.stock);
    } else {
        items.push((product.id, add_qty.min(product.stock)));
    }
    items.retain(|(_, qty)| *qty > 0);

    let jar = jar.add(cart_cookie(serialize_cart(&items)));

    // Adding to the cart should never navigate away: htmx requests get an empty
    // 204 (the header cart badge updates client-side), and a no-JS submit goes
    // back to the page the customer was on rather than to the basket.
    if headers.contains_key("HX-Request") {
        Ok((jar, StatusCode::NO_CONTENT).into_response())
    } else {
        let back = headers
            .get("referer")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("/shop")
            .to_string();
        Ok((jar, Redirect::to(&back)).into_response())
    }
}

#[debug_handler]
async fn update(
    jar: CookieJar,
    State(ctx): State<AppContext>,
    ViewEngine(v): ViewEngine<TeraView>,
    headers: HeaderMap,
    Form(form): Form<UpdateForm>,
) -> Result<Response> {
    let stock = published_product(&ctx, form.product_id)
        .await?
        .map(|p| p.stock)
        .unwrap_or(0);

    let mut items = parse_cart(&jar);
    let clamped = form.quantity.clamp(0, stock);
    if let Some(entry) = items.iter_mut().find(|(id, _)| *id == form.product_id) {
        entry.1 = clamped;
    }
    items.retain(|(_, qty)| *qty > 0);

    let jar = jar.add(cart_cookie(serialize_cart(&items)));
    cart_response(&ctx, &v, jar, &headers).await
}

#[debug_handler]
async fn remove(
    jar: CookieJar,
    State(ctx): State<AppContext>,
    ViewEngine(v): ViewEngine<TeraView>,
    headers: HeaderMap,
    Form(form): Form<RemoveForm>,
) -> Result<Response> {
    let mut items = parse_cart(&jar);
    items.retain(|(id, _)| *id != form.product_id);

    let jar = jar.add(cart_cookie(serialize_cart(&items)));
    cart_response(&ctx, &v, jar, &headers).await
}

/// Response after a cart mutation: for an htmx request, just the `#cart-body`
/// fragment (so the page never fully reloads); otherwise a redirect back to
/// `/cart` for no-JS fallback. `jar` must already hold the updated cart cookie.
async fn cart_response(
    ctx: &AppContext,
    v: &TeraView,
    jar: CookieJar,
    headers: &HeaderMap,
) -> Result<Response> {
    if !headers.contains_key("HX-Request") {
        return Ok((jar, Redirect::to("/cart")).into_response());
    }

    let (lines, valid, total) = resolve_cart(ctx, &jar).await?;
    let currency = lines
        .first()
        .and_then(|line| line["currency"].as_str())
        .unwrap_or("EUR")
        .to_string();
    // Persist the re-validated cookie (drops now-invalid lines).
    let jar = jar.add(cart_cookie(serialize_cart(&valid)));
    let response = format::view(
        v,
        "shop/_cart_body.html",
        json!({
            "items": lines,
            "total": format_price(total),
            "currency": currency,
            "lang": current_lang(&jar),
        }),
    )?;
    Ok((jar, response).into_response())
}

/// Resolve the cart cookie into priced line items, dropping anything that is no
/// longer purchasable and clamping quantities to current stock. Returns the
/// (re-validated) lines, the rebuilt cookie value, and the total in cents.
pub(crate) async fn resolve_cart(
    ctx: &AppContext,
    jar: &CookieJar,
) -> Result<(Vec<serde_json::Value>, Vec<(i32, i32)>, i64)> {
    let mut lines = Vec::new();
    let mut valid = Vec::new();
    let mut total: i64 = 0;

    for (id, qty) in parse_cart(jar) {
        let Some(product) = published_product(ctx, id).await? else {
            continue;
        };
        let qty = qty.clamp(0, product.stock);
        if qty == 0 {
            continue;
        }
        let line_total = product.price_cents * i64::from(qty);
        total += line_total;
        valid.push((product.id, qty));
        lines.push(json!({
            "id": product.id,
            "name": product.name,
            "slug": product.slug,
            "price": format_price(product.price_cents),
            "currency": product.currency,
            "quantity": qty,
            "stock": product.stock,
            "line_total": format_price(line_total),
        }));
    }

    Ok((lines, valid, total))
}

#[debug_handler]
async fn show(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let (lines, valid, total) = resolve_cart(&ctx, &jar).await?;
    let currency = lines
        .first()
        .and_then(|line| line["currency"].as_str())
        .unwrap_or("EUR")
        .to_string();

    // Drop any now-invalid lines from the cookie so the badge stays accurate.
    let rebuilt = serialize_cart(&valid);
    let (logged_in_admin, logged_in_customer) = guard::chrome(&ctx, &jar).await;
    let response = format::view(
        &v,
        "shop/cart.html",
        json!({
            "items": lines,
            "total": format_price(total),
            "currency": currency,
            "logged_in_admin": logged_in_admin,
            "logged_in_customer": logged_in_customer,
            "lang": current_lang(&jar),
        }),
    )?;

    Ok((jar.add(cart_cookie(rebuilt)), response).into_response())
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/cart", get(show))
        .add("/cart/add", post(add))
        .add("/cart/update", post(update))
        .add("/cart/remove", post(remove))
}
