//! Buyer-selectable display currency.
//!
//! EUR is the base/transaction currency: every price is stored and reasoned
//! about in EUR minor units (cents). A buyer may switch their *display* currency
//! (cookie [`COOKIE`]); non-base currencies live in the `currencies` table with
//! an admin-set exchange `rate_e4` (units per 1 EUR, scaled ×10000). The
//! [`Currency`] resolved per request converts EUR cents into the chosen currency
//! for display only — the cart logic, orders and admin stay in EUR.

use std::sync::RwLock;

use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};

use crate::models::currencies;
use crate::shared::money::format_price;

/// Cookie holding the buyer's chosen display-currency code.
pub const COOKIE: &str = "currency";
/// The base/transaction currency code.
pub const BASE_CODE: &str = "EUR";
/// The base currency symbol.
pub const BASE_SYMBOL: &str = "€";
/// Fixed-point scale for exchange rates (`rate_e4` = rate × 10000).
pub const SCALE: i64 = 10_000;

/// A resolved display currency: how to label prices and how to convert them
/// from the EUR base.
#[derive(Debug, Clone)]
pub struct Currency {
    pub code: String,
    pub symbol: String,
    /// Units of this currency per 1 EUR, scaled ×10000. `SCALE` for the base.
    pub rate_e4: i64,
}

impl Currency {
    /// The base currency (EUR), the identity conversion.
    #[must_use]
    pub fn eur() -> Self {
        Self {
            code: BASE_CODE.to_string(),
            symbol: BASE_SYMBOL.to_string(),
            rate_e4: SCALE,
        }
    }

    #[must_use]
    pub fn is_base(&self) -> bool {
        self.code == BASE_CODE
    }

    /// Convert EUR minor units into this currency's minor units (half-up).
    #[must_use]
    pub fn convert_cents(&self, eur_cents: i64) -> i64 {
        if self.is_base() {
            return eur_cents;
        }
        let scale = i128::from(SCALE);
        ((i128::from(eur_cents) * i128::from(self.rate_e4) + scale / 2) / scale) as i64
    }

    /// Inverse of [`convert_cents`]: this currency's minor units back to EUR
    /// minor units (half-up). Used to interpret price-filter bounds typed in the
    /// display currency.
    #[must_use]
    pub fn to_eur_cents(&self, cents: i64) -> i64 {
        if self.is_base() || self.rate_e4 == 0 {
            return cents;
        }
        let rate = i128::from(self.rate_e4);
        ((i128::from(cents) * i128::from(SCALE) + rate / 2) / rate) as i64
    }

    /// Render EUR minor units as a plain decimal string in this currency (no
    /// symbol). The symbol is appended by templates via `currency_symbol`.
    #[must_use]
    pub fn format(&self, eur_cents: i64) -> String {
        format_price(self.convert_cents(eur_cents))
    }
}

/// Resolve the buyer's display currency from the `currency` cookie, falling back
/// to EUR when the cookie is absent, names the base, or names a currency that is
/// missing or disabled.
pub async fn resolve(ctx: &AppContext, jar: &CookieJar) -> Currency {
    let code = jar
        .get(COOKIE)
        .map(|c| c.value().to_string())
        .unwrap_or_default();
    if code.is_empty() || code.eq_ignore_ascii_case(BASE_CODE) {
        return Currency::eur();
    }
    match currencies::Entity::find_enabled_by_code(&ctx.db, &code).await {
        Ok(Some(m)) => Currency {
            code: m.code,
            symbol: m.symbol,
            rate_e4: m.rate_e4,
        },
        _ => Currency::eur(),
    }
}

/// One enabled, buyer-selectable alternative currency in the process-wide
/// snapshot below.
#[derive(Clone)]
struct Selectable {
    code: String,
    symbol: String,
    rate_e4: i64,
}

/// Process-wide snapshot of the enabled alternative currencies, so the global
/// chrome (the settings-menu switcher and the navbar rate) can be rendered via a
/// Tera function without a per-request DB hit. Loaded at boot by
/// `initializers::currency_seeder` and refreshed by the admin on every edit (see
/// [`refresh_snapshot`]). EUR (the base) is implicit and never listed here.
static ENABLED: RwLock<Vec<Selectable>> = RwLock::new(Vec::new());

/// Reload the [`ENABLED`] snapshot from the database. Call at boot and after any
/// admin change to a currency's rate/enabled state.
pub async fn refresh_snapshot<C: sea_orm::ConnectionTrait>(db: &C) -> Result<()> {
    let rows = currencies::Entity::find()
        .filter(currencies::Column::Enabled.eq(true))
        .order_by_asc(currencies::Column::Code)
        .all(db)
        .await?;
    let list = rows
        .into_iter()
        .map(|m| Selectable {
            code: m.code,
            symbol: m.symbol,
            rate_e4: m.rate_e4,
        })
        .collect();
    *ENABLED.write().unwrap() = list;
    Ok(())
}

/// The selectable currencies for templates (the Tera `currencies()` function):
/// the EUR base plus every enabled alternative, each with a human rate string.
/// `alts` is empty when the store is effectively EUR-only.
#[must_use]
pub fn selectable_json() -> serde_json::Value {
    let alts: Vec<serde_json::Value> = ENABLED
        .read()
        .unwrap()
        .iter()
        .map(|s| {
            serde_json::json!({
                "code": s.code,
                "symbol": s.symbol,
                "rate": format_rate(s.rate_e4),
            })
        })
        .collect();
    serde_json::json!({
        "base": { "code": BASE_CODE, "symbol": BASE_SYMBOL },
        "alts": alts,
    })
}

/// Parse an exchange rate typed in major units ("25", "25.3", "25,30",
/// "25.3045") into `rate_e4` (×10000). Rejects negatives and >4 decimals.
pub fn parse_rate(value: &str) -> Result<i64> {
    let value = value.trim().replace(',', ".");
    let invalid = || Error::BadRequest("invalid exchange rate".to_string());
    let (whole, frac) = match value.split_once('.') {
        Some((w, f)) => (w, f),
        None => (value.as_str(), ""),
    };
    if frac.len() > 4 || whole.is_empty() || !whole.chars().all(|c| c.is_ascii_digit()) {
        return Err(invalid());
    }
    if !frac.chars().all(|c| c.is_ascii_digit()) {
        return Err(invalid());
    }
    let whole: i64 = whole.parse().map_err(|_| invalid())?;
    // Right-pad the fractional part to exactly 4 digits.
    let padded = format!("{frac:0<4}");
    let frac: i64 = if padded.is_empty() {
        0
    } else {
        padded.parse().map_err(|_| invalid())?
    };
    let rate = whole * SCALE + frac;
    if rate <= 0 {
        return Err(invalid());
    }
    Ok(rate)
}

/// Render `rate_e4` as a human string, trimming trailing zeros (253000 → "25.3",
/// 250000 → "25").
#[must_use]
pub fn format_rate(rate_e4: i64) -> String {
    let whole = rate_e4 / SCALE;
    let frac = (rate_e4 % SCALE).abs();
    if frac == 0 {
        return whole.to_string();
    }
    let frac = format!("{frac:04}");
    let trimmed = frac.trim_end_matches('0');
    format!("{whole}.{trimmed}")
}
