//! Customer account area. Currently just the shipping/contact profile, whose
//! fields prefill the checkout form. Gated to authenticated non-admin users:
//! anonymous visitors are bounced to `/login`. Admins have their own area and
//! are sent to the dashboard.
//!
//! The account *type* (personal vs company) is fixed at registration and lives
//! on the user — it is shown here read-only and can never be changed. The
//! profile only edits the type-specific details (company identity + address).

use axum::extract::{DefaultBodyLimit, Multipart};
use axum_extra::extract::cookie::CookieJar;
use loco_rs::prelude::*;
use sea_orm::QueryOrder;
use serde::Deserialize;
use serde_json::json;

use crate::{
    controllers::{
        admin_form::{read_multipart_form, store_image},
        i18n::current_lang,
        media::IMAGE_MAX_BYTES,
    },
    models::{
        customer_profiles::{self, ProfileFields},
        order_items, orders, users,
    },
    shared::{guard, settings},
    views::checkout as order_view,
};

/// Active (still-being-fulfilled) order statuses. Anything else
/// (`delivered`, `cancelled`) is considered closed/past.
const ACTIVE_STATUSES: [&str; 3] = ["pending", "paid", "shipped"];

#[derive(Debug, Deserialize)]
struct ProfileForm {
    first_name: Option<String>,
    last_name: Option<String>,
    company_name: Option<String>,
    company_id: Option<String>,
    tax_id: Option<String>,
    vat_id: Option<String>,
    phone_prefix: Option<String>,
    phone: Option<String>,
    address: Option<String>,
    city: Option<String>,
    zip: Option<String>,
    country: Option<String>,
}

fn trimmed(value: Option<&str>) -> Option<String> {
    value.map(str::trim).filter(|v| !v.is_empty()).map(String::from)
}

/// Split a stored full name into (first name, surname). The surname is
/// everything after the first whitespace, so multi-word surnames round-trip.
fn split_name(name: &str) -> (String, String) {
    match name.trim().split_once(char::is_whitespace) {
        Some((first, rest)) => (first.to_string(), rest.trim().to_string()),
        None => (name.trim().to_string(), String::new()),
    }
}

/// Recombine the two name fields into the single stored `name`. Returns `None`
/// when the result is too short to be a valid name (the user can't blank it out).
fn full_name_from_form(form: &ProfileForm) -> Option<String> {
    let first = form.first_name.as_deref().unwrap_or("").trim();
    let last = form.last_name.as_deref().unwrap_or("").trim();
    let full = format!("{first} {last}").trim().to_string();
    (full.chars().count() >= 2).then_some(full)
}

/// Build the persisted fields from the submitted form. Company identifiers are
/// only kept for company accounts (a personal account can never carry them).
fn fields_from_form(form: &ProfileForm, is_company: bool) -> ProfileFields {
    let company = |v: Option<&str>| if is_company { trimmed(v) } else { None };
    ProfileFields {
        company_name: company(form.company_name.as_deref()),
        company_id: company(form.company_id.as_deref()),
        tax_id: company(form.tax_id.as_deref()),
        vat_id: company(form.vat_id.as_deref()),
        phone_prefix: trimmed(form.phone_prefix.as_deref()),
        phone: trimmed(form.phone.as_deref()),
        address: trimmed(form.address.as_deref()),
        city: trimmed(form.city.as_deref()),
        zip: trimmed(form.zip.as_deref()),
        country: trimmed(form.country.as_deref()),
    }
}

/// The profile fields held by a saved profile, for re-prefilling the form.
fn fields_of(profile: Option<&customer_profiles::Model>) -> ProfileFields {
    match profile {
        Some(p) => ProfileFields {
            company_name: p.company_name.clone(),
            company_id: p.company_id.clone(),
            tax_id: p.tax_id.clone(),
            vat_id: p.vat_id.clone(),
            phone_prefix: p.phone_prefix.clone(),
            phone: p.phone.clone(),
            address: p.address.clone(),
            city: p.city.clone(),
            zip: p.zip.clone(),
            country: p.country.clone(),
        },
        None => ProfileFields::default(),
    }
}

/// A company account must carry its invoicing identity (company name + IČO +
/// DIČ; IČ DPH stays optional). Personal accounts have no such requirement.
fn company_fields_missing(fields: &ProfileFields) -> bool {
    fields.company_name.is_none() || fields.company_id.is_none() || fields.tax_id.is_none()
}

/// Render the profile form for `user`, prefilled from `fields`. `saved` shows
/// the success banner; `error` shows the company-required validation message.
fn profile_view(
    v: &TeraView,
    jar: &CookieJar,
    user: &users::Model,
    fields: &ProfileFields,
    saved: bool,
    error: bool,
) -> Result<Response> {
    let (first_name, last_name) = split_name(&user.name);
    format::view(
        v,
        "account/profile.html",
        json!({
            "logged_in_admin": false,
            "logged_in_customer": true,
            "account_nav": true,
            "customer_name": user.name,
            "customer_account_type": user.account_type,
            "customer_avatar": user.avatar_id,
            "avatar_id": user.avatar_id,
            "saved": saved,
            "error": error,
            "name": user.name,
            "first_name": first_name,
            "last_name": last_name,
            "email": user.email,
            "account_type": user.account_type,
            "company_name": fields.company_name,
            "company_id": fields.company_id,
            "tax_id": fields.tax_id,
            "vat_id": fields.vat_id,
            "phone_prefix": fields.phone_prefix,
            "phone": fields.phone,
            "address": fields.address,
            "city": fields.city,
            "zip": fields.zip,
            "country": fields.country,
            "lang": current_lang(jar),
        }),
    )
}

#[debug_handler]
async fn profile_page(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?;
    profile_view(&v, &jar, &user, &fields_of(profile.as_ref()), false, false)
}

#[debug_handler]
async fn save_profile(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Form(form): Form<ProfileForm>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    // Apply the edited name to a working copy so it's reflected on both the
    // success and re-rendered-error views. A blank/too-short name is ignored —
    // the field can't be cleared.
    let mut user = user;
    let new_name = full_name_from_form(&form).filter(|n| *n != user.name);
    if let Some(name) = new_name.clone() {
        user.name = name;
    }
    let fields = fields_from_form(&form, user.is_company());
    // A company account's profile is rejected (and re-shown with the entered
    // values) until it carries its required identifiers.
    if user.is_company() && company_fields_missing(&fields) {
        return profile_view(&v, &jar, &user, &fields, false, true);
    }
    if let Some(name) = new_name {
        let mut active = user.clone().into_active_model();
        active.name = ActiveValue::set(name);
        active.update(&ctx.db).await?;
    }
    customer_profiles::Model::upsert(&ctx.db, user.id, fields.clone()).await?;
    profile_view(&v, &jar, &user, &fields, true, false)
}

/// Persist `avatar_id` (a stored image filename, or `None` to clear) on the
/// signed-in customer and re-render the profile page with the success banner.
async fn set_avatar(
    v: &TeraView,
    jar: &CookieJar,
    ctx: &AppContext,
    user: users::Model,
    avatar_id: Option<String>,
) -> Result<Response> {
    let mut active = user.clone().into_active_model();
    active.avatar_id = ActiveValue::set(avatar_id.clone());
    let user = active.update(&ctx.db).await?;
    let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?;
    profile_view(v, jar, &user, &fields_of(profile.as_ref()), true, false)
}

/// Upload (or replace) the signed-in customer's avatar picture. The single
/// `image` file part is validated and stored through the shared image storage,
/// then its generated filename is saved as the user's `avatar_id`.
#[debug_handler]
async fn upload_avatar(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    multipart: Multipart,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    let form = read_multipart_form(multipart).await?;
    let Some(image) = form.single_image() else {
        // No file chosen — nothing to do, just re-show the profile.
        let profile = customer_profiles::Model::find_for_user(&ctx.db, user.id).await?;
        return profile_view(&v, &jar, &user, &fields_of(profile.as_ref()), false, false);
    };
    let filename = store_image(&ctx, image).await?;
    set_avatar(&v, &jar, &ctx, user, Some(filename)).await
}

/// Remove the signed-in customer's avatar, reverting to the initials fallback.
#[debug_handler]
async fn remove_avatar(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    set_avatar(&v, &jar, &ctx, user, None).await
}

/// Lists the signed-in customer's orders, split into still-active and past.
#[debug_handler]
async fn orders_page(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    let rows = orders::Entity::find()
        .filter(orders::Column::UserId.eq(user.id))
        .order_by_desc(orders::Column::CreatedAt)
        .all(&ctx.db)
        .await?;
    let (active, past): (Vec<_>, Vec<_>) = rows
        .iter()
        .partition(|o| ACTIVE_STATUSES.contains(&o.status.as_str()));
    let shape = |list: Vec<&orders::Model>| -> Vec<_> {
        list.into_iter().map(order_view::summary).collect()
    };

    format::view(
        &v,
        "account/orders.html",
        json!({
            "logged_in_admin": false,
            "logged_in_customer": true,
            "account_nav": true,
            "customer_name": user.name,
            "customer_account_type": user.account_type,
            "customer_avatar": user.avatar_id,
            "active_orders": shape(active),
            "past_orders": shape(past),
            "lang": current_lang(&jar),
        }),
    )
}

/// Shows a single order belonging to the signed-in customer. Orders owned by
/// someone else (or guest orders) are not found here.
#[debug_handler]
async fn order_detail_page(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Path(order_number): Path<String>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    let order = orders::Entity::find()
        .filter(orders::Column::OrderNumber.eq(order_number))
        .one(&ctx.db)
        .await?
        .filter(|o| o.user_id == Some(user.id))
        .ok_or_else(|| Error::NotFound)?;
    let items = order_items::Entity::find()
        .filter(order_items::Column::OrderId.eq(order.id))
        .all(&ctx.db)
        .await?;

    let (bank_iban, bank_account_name) = settings::bank_details(&ctx).await?;
    format::view(
        &v,
        "account/order_detail.html",
        json!({
            "logged_in_admin": false,
            "logged_in_customer": true,
            "account_nav": true,
            "customer_name": user.name,
            "customer_account_type": user.account_type,
            "customer_avatar": user.avatar_id,
            "order": order_view::detail(
                &order,
                &bank_iban,
                &bank_account_name,
            ),
            "items": order_view::items(&items),
            "lang": current_lang(&jar),
        }),
    )
}

#[derive(Debug, Deserialize)]
struct ChangePasswordForm {
    current_password: String,
    password: String,
    password_confirm: String,
}

fn password_view(
    v: &TeraView,
    jar: &CookieJar,
    user: &users::Model,
    changed: bool,
    error: Option<&str>,
) -> Result<Response> {
    format::view(
        v,
        "account/password.html",
        json!({
            "logged_in_admin": false,
            "logged_in_customer": true,
            "account_nav": true,
            "customer_name": user.name,
            "customer_account_type": user.account_type,
            "customer_avatar": user.avatar_id,
            "changed": changed,
            "error": error,
            "lang": current_lang(jar),
        }),
    )
}

#[debug_handler]
async fn change_password_page(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    password_view(&v, &jar, &user, false, None)
}

#[debug_handler]
async fn change_password(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Form(form): Form<ChangePasswordForm>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    if !user.verify_password(&form.current_password) {
        return password_view(&v, &jar, &user, false, Some("current"));
    }
    if form.password != form.password_confirm {
        return password_view(&v, &jar, &user, false, Some("mismatch"));
    }
    if form.password.len() < 8 {
        return password_view(&v, &jar, &user, false, Some("weak"));
    }
    let user = user
        .into_active_model()
        .reset_password(&ctx.db, &form.password)
        .await?;
    password_view(&v, &jar, &user, true, None)
}

// ---- Two-factor authentication (TOTP / Google Authenticator) -------------
//
// Entirely opt-in. The security page has three shapes, all rendered from
// `security.html`:
//   * disabled  -> an "enable" button,
//   * enrolling -> the QR + a confirm-code field (secret staged, not yet on),
//   * enabled   -> status, remaining backup codes, disable/regenerate forms.
// Both turning 2FA off and regenerating backup codes require re-entering the
// account password, so a walk-up attacker on an open session can't weaken it.

#[derive(Debug, Deserialize)]
struct ConfirmTotpForm {
    code: String,
}

#[derive(Debug, Deserialize)]
struct PasswordConfirmForm {
    current_password: String,
}

/// Render the security page. Exactly one of (`enrolling`, plain status) applies;
/// `backup_codes` is non-empty only on the one render right after enabling or
/// regenerating, where the plaintext codes are shown once.
#[allow(clippy::too_many_arguments)]
fn security_view(
    v: &TeraView,
    jar: &CookieJar,
    user: &users::Model,
    enrolling: bool,
    qr: Option<&str>,
    secret: Option<&str>,
    backup_codes: &[String],
    error: Option<&str>,
) -> Result<Response> {
    format::view(
        v,
        "account/security.html",
        json!({
            "logged_in_admin": false,
            "logged_in_customer": true,
            "account_nav": true,
            "customer_name": user.name,
            "customer_account_type": user.account_type,
            "customer_avatar": user.avatar_id,
            "totp_enabled": user.totp_enabled(),
            "enrolling": enrolling,
            "qr": qr,
            "secret": secret,
            "backup_codes": backup_codes,
            "backup_remaining": user.backup_codes_remaining(),
            "error": error,
            "lang": current_lang(jar),
        }),
    )
}

/// Common guard for every security handler: a signed-in, non-admin customer.
async fn require_customer(ctx: &AppContext, jar: &CookieJar) -> Result<users::Model> {
    match guard::current_user(ctx, jar).await {
        Some(user) if guard::is_admin(ctx, &user) => Err(Error::string("admin")),
        Some(user) => Ok(user),
        None => Err(Error::Unauthorized("login required".into())),
    }
}

#[debug_handler]
async fn security_page(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let Some(user) = guard::current_user(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if guard::is_admin(&ctx, &user) {
        return format::redirect("/admin/dashboard");
    }
    security_view(&v, &jar, &user, false, None, None, &[], None)
}

/// Stage a fresh secret and show the QR + confirm-code field.
#[debug_handler]
async fn enable_totp(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let Ok(user) = require_customer(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    // Already on — nothing to enroll.
    if user.totp_enabled() {
        return security_view(&v, &jar, &user, false, None, None, &[], None);
    }
    let user = user.into_active_model().begin_totp_enrollment(&ctx.db).await?;
    let Some((qr, secret)) = user.totp_provisioning() else {
        return security_view(&v, &jar, &user, false, None, None, &[], Some("enroll"));
    };
    security_view(&v, &jar, &user, true, Some(&qr), Some(&secret), &[], None)
}

/// Verify the first code against the staged secret; on success flip 2FA on and
/// show the one-time backup codes. On a wrong code, re-show the QR to retry.
#[debug_handler]
async fn confirm_totp(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Form(form): Form<ConfirmTotpForm>,
) -> Result<Response> {
    let Ok(user) = require_customer(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if user.totp_enabled() {
        return security_view(&v, &jar, &user, false, None, None, &[], None);
    }
    if !user.verify_totp_code(&form.code) {
        let qr = user.totp_provisioning();
        let (qr, secret) = match &qr {
            Some((q, s)) => (Some(q.as_str()), Some(s.as_str())),
            None => (None, None),
        };
        return security_view(&v, &jar, &user, true, qr, secret, &[], Some("code"));
    }
    let (user, backup_codes) = user.into_active_model().enable_totp(&ctx.db).await?;
    security_view(&v, &jar, &user, false, None, None, &backup_codes, None)
}

/// Turn 2FA off — requires the account password as confirmation.
#[debug_handler]
async fn disable_totp(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Form(form): Form<PasswordConfirmForm>,
) -> Result<Response> {
    let Ok(user) = require_customer(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if !user.totp_enabled() {
        return security_view(&v, &jar, &user, false, None, None, &[], None);
    }
    if !user.verify_password(&form.current_password) {
        return security_view(&v, &jar, &user, false, None, None, &[], Some("password"));
    }
    let user = user.into_active_model().disable_totp(&ctx.db).await?;
    security_view(&v, &jar, &user, false, None, None, &[], None)
}

/// Issue a fresh set of backup codes (invalidating the old ones) — also gated by
/// the account password.
#[debug_handler]
async fn regenerate_backup_codes(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Form(form): Form<PasswordConfirmForm>,
) -> Result<Response> {
    let Ok(user) = require_customer(&ctx, &jar).await else {
        return format::redirect("/login");
    };
    if !user.totp_enabled() {
        return security_view(&v, &jar, &user, false, None, None, &[], None);
    }
    if !user.verify_password(&form.current_password) {
        return security_view(&v, &jar, &user, false, None, None, &[], Some("password"));
    }
    let (user, backup_codes) =
        user.into_active_model().regenerate_backup_codes(&ctx.db).await?;
    security_view(&v, &jar, &user, false, None, None, &backup_codes, None)
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/account/profile", get(profile_page))
        .add("/account/profile", post(save_profile))
        .add(
            "/account/profile/avatar",
            post(upload_avatar).layer(DefaultBodyLimit::max(IMAGE_MAX_BYTES + 1024 * 1024)),
        )
        .add("/account/profile/avatar/remove", post(remove_avatar))
        .add("/account/orders", get(orders_page))
        .add("/account/orders/{order_number}", get(order_detail_page))
        .add("/account/password", get(change_password_page))
        .add("/account/password", post(change_password))
        .add("/account/security", get(security_page))
        .add("/account/security/enable", post(enable_totp))
        .add("/account/security/confirm", post(confirm_totp))
        .add("/account/security/disable", post(disable_totp))
        .add("/account/security/backup-codes", post(regenerate_backup_codes))
}
