use crate::AppState;
use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{delete, get, post},
    Json, Router,
};
use contracts::auth_middleware::{require_admin, AuthUser};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;

// ──────────────────────────────────────────────────────────────────────────
// Tax rules CRUD
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, FromRow)]
struct TaxRuleRow {
    id: Uuid,
    name: String,
    tax_type: String,
    tax_rate: f64,
    applies_to: Option<String>,
    is_active: bool,
    created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Deserialize)]
struct CreateTaxRuleRequest {
    name: String,
    tax_type: String,
    tax_rate: f64,
    applies_to: Option<String>,
    is_active: Option<bool>,
}

async fn list_tax_rules(
    _auth: AuthUser,
    State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let rows: Vec<TaxRuleRow> = sqlx::query_as::<_, TaxRuleRow>(
        r#"
        SELECT id, name, tax_type,
               tax_rate::FLOAT8 AS tax_rate,
               applies_to, is_active, created_at
        FROM tax_rules
        ORDER BY created_at DESC
        "#,
    )
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok((StatusCode::OK, Json(serde_json::json!({ "taxes": rows }))))
}

async fn create_tax_rule(
    _auth: AuthUser,
    State(state): State<AppState>,
    Json(payload): Json<CreateTaxRuleRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    if payload.name.trim().is_empty() {
        return Err((StatusCode::BAD_REQUEST, "name is required".to_string()));
    }
    if payload.tax_type.trim().is_empty() {
        return Err((StatusCode::BAD_REQUEST, "tax_type is required".to_string()));
    }
    if payload.tax_rate < 0.0 || payload.tax_rate > 100.0 {
        return Err((StatusCode::BAD_REQUEST, "tax_rate must be between 0 and 100".to_string()));
    }

    let id = Uuid::new_v4();
    let is_active = payload.is_active.unwrap_or(true);

    sqlx::query(
        r#"
        INSERT INTO tax_rules (id, name, tax_type, tax_rate, applies_to, is_active, created_at)
        VALUES ($1, $2, $3, $4::numeric, $5, $6, NOW())
        "#,
    )
    .bind(id)
    .bind(payload.name.trim())
    .bind(payload.tax_type.trim().to_uppercase())
    .bind(payload.tax_rate)
    .bind(payload.applies_to.as_deref().map(|s| s.trim().to_uppercase()))
    .bind(is_active)
    .execute(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let row_map: TaxRuleRow = sqlx::query_as::<_, TaxRuleRow>(
        r#"
        SELECT id, name, tax_type,
               tax_rate::FLOAT8 AS tax_rate,
               applies_to, is_active, created_at
        FROM tax_rules WHERE id = $1
        "#,
    )
    .bind(id)
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok((
        StatusCode::CREATED,
        Json(serde_json::json!({ "message": "Tax rule created.", "tax": row_map })),
    ))
}

async fn delete_tax_rule(
    _auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let result = sqlx::query("DELETE FROM tax_rules WHERE id = $1")
        .bind(id)
        .execute(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    if result.rows_affected() == 0 {
        return Err((StatusCode::NOT_FOUND, "Tax rule not found".to_string()));
    }

    Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Tax rule deleted." }))))
}

// ──────────────────────────────────────────────────────────────────────────
// Tracecoin ledger
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, FromRow)]
struct LedgerEntryRow {
    id: Uuid,
    wallet_id: Uuid,
    user_id: Uuid,
    user_email: Option<String>,
    entry_type: String,
    amount: i32,
    balance_after: Option<i32>,
    reason: Option<String>,
    reference_id: Option<Uuid>,
    actor_user_id: Option<Uuid>,
    metadata: Option<serde_json::Value>,
    created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Deserialize)]
struct LedgerQuery {
    user_id: Option<Uuid>,
    #[serde(default)]
    page: Option<i64>,
    #[serde(default)]
    limit: Option<i64>,
}

async fn list_ledger(
    _auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<LedgerQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let limit = q.limit.unwrap_or(50).clamp(1, 500);
    let page = q.page.unwrap_or(1).max(1);
    let offset = (page - 1) * limit;

    let rows = if let Some(user_id) = q.user_id {
        sqlx::query_as::<_, LedgerEntryRow>(
            r#"
            SELECT
                tl.id, tl.wallet_id, tw.user_id, u.email AS user_email,
                tl.type AS entry_type, tl.amount, tl.balance_after, tl.reason,
                tl.reference_id, tl.actor_user_id, tl.metadata, tl.created_at
            FROM tracecoin_ledger tl
            JOIN tracecoin_wallets tw ON tw.id = tl.wallet_id
            LEFT JOIN users u ON u.id = tw.user_id
            WHERE tw.user_id = $1
            ORDER BY tl.created_at DESC
            LIMIT $2 OFFSET $3
            "#,
        )
        .bind(user_id)
        .bind(limit)
        .bind(offset)
        .fetch_all(&state.pool)
        .await
    } else {
        sqlx::query_as::<_, LedgerEntryRow>(
            r#"
            SELECT
                tl.id, tl.wallet_id, tw.user_id, u.email AS user_email,
                tl.type AS entry_type, tl.amount, tl.balance_after, tl.reason,
                tl.reference_id, tl.actor_user_id, tl.metadata, tl.created_at
            FROM tracecoin_ledger tl
            JOIN tracecoin_wallets tw ON tw.id = tl.wallet_id
            LEFT JOIN users u ON u.id = tw.user_id
            ORDER BY tl.created_at DESC
            LIMIT $1 OFFSET $2
            "#,
        )
        .bind(limit)
        .bind(offset)
        .fetch_all(&state.pool)
        .await
    }
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "entries": rows,
            "page": page,
            "limit": limit,
        })),
    ))
}

// ──────────────────────────────────────────────────────────────────────────
// Orders / payments
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, FromRow)]
struct OrderRow {
    id: Uuid,
    reference_number: String,
    user_id: Uuid,
    user_email: Option<String>,
    user_name: Option<String>,
    package_id: Option<Uuid>,
    package_name: Option<String>,
    payu_txnid: Option<String>,
    payu_mihpayid: Option<String>,
    amount_inr: i32,
    tracecoins_credited: i32,
    status: String,
    verified_at: Option<chrono::DateTime<chrono::Utc>>,
    created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Deserialize)]
struct OrdersQuery {
    status: Option<String>,
    page: Option<i64>,
    limit: Option<i64>,
}

async fn list_orders(
    _auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<OrdersQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let limit = q.limit.unwrap_or(50).clamp(1, 500);
    let page = q.page.unwrap_or(1).max(1);
    let offset = (page - 1) * limit;

    let rows = if let Some(status) = q.status.as_deref() {
        sqlx::query_as::<_, OrderRow>(
            r#"
            SELECT
                p.id, p.reference_number, p.user_id, u.email AS user_email,
                NULL::text AS user_name,
                p.package_id, NULL::text AS package_name,
                p.payu_txnid, p.payu_mihpayid,
                p.amount_inr, p.tracecoins_credited,
                p.status, p.verified_at, p.created_at
            FROM payments p
            LEFT JOIN users u ON u.id = p.user_id
            WHERE UPPER(p.status) = UPPER($1)
            ORDER BY p.created_at DESC
            LIMIT $2 OFFSET $3
            "#,
        )
        .bind(status)
        .bind(limit)
        .bind(offset)
        .fetch_all(&state.pool)
        .await
    } else {
        sqlx::query_as::<_, OrderRow>(
            r#"
            SELECT
                p.id, p.reference_number, p.user_id, u.email AS user_email,
                NULL::text AS user_name,
                p.package_id, NULL::text AS package_name,
                p.payu_txnid, p.payu_mihpayid,
                p.amount_inr, p.tracecoins_credited,
                p.status, p.verified_at, p.created_at
            FROM payments p
            LEFT JOIN users u ON u.id = p.user_id
            ORDER BY p.created_at DESC
            LIMIT $1 OFFSET $2
            "#,
        )
        .bind(limit)
        .bind(offset)
        .fetch_all(&state.pool)
        .await
    }
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let mapped: Vec<serde_json::Value> = rows
        .into_iter()
        .map(|o| {
            serde_json::json!({
                "id": o.id,
                "reference_number": o.reference_number,
                "order_number": o.payu_txnid.clone().unwrap_or_else(|| o.id.to_string()),
                "user_id": o.user_id,
                "user_email": o.user_email,
                "user_name": o.user_name,
                "package_id": o.package_id,
                "package_name": o.package_name,
                "payu_txnid": o.payu_txnid,
                "payu_mihpayid": o.payu_mihpayid,
                "amount_inr": o.amount_inr,
                "total": o.amount_inr,
                "amount": o.amount_inr,
                "tracecoin_amount": o.tracecoins_credited,
                "tracecoins_credited": o.tracecoins_credited,
                "status": o.status,
                "verified_at": o.verified_at,
                "created_at": o.created_at,
            })
        })
        .collect();

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "orders": mapped,
            "page": page,
            "limit": limit,
        })),
    ))
}

// ──────────────────────────────────────────────────────────────────────────
// Credits admin
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
#[allow(non_snake_case)]
struct BalanceQuery {
    user_id: Option<Uuid>,
    userId: Option<Uuid>,
}

async fn get_credit_balance(
    _auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<BalanceQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let user_id = q.user_id.or(q.userId);
    let user_id = user_id.ok_or_else(|| {
        (StatusCode::BAD_REQUEST, "user_id is required".to_string())
    })?;

    let wallet = wallet::get_or_create(&state.pool, user_id)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "user_id": wallet.user_id,
            "balance": wallet.balance,
            "reserved": wallet.reserved,
            "available": wallet.available,
            "updated_at": wallet.updated_at,
        })),
    ))
}

async fn get_credit_ledger(
    _auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<LedgerQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let user_id = q.user_id.ok_or_else(|| {
        (StatusCode::BAD_REQUEST, "user_id is required".to_string())
    })?;

    let limit = q.limit.unwrap_or(50).clamp(1, 500);
    let page = q.page.unwrap_or(1).max(1);
    let offset = (page - 1) * limit;

    let rows = sqlx::query_as::<_, LedgerEntryRow>(
        r#"
        SELECT
            tl.id, tl.wallet_id, tw.user_id, u.email AS user_email,
            tl.type AS entry_type, tl.amount, tl.balance_after, tl.reason,
            tl.reference_id, tl.actor_user_id, tl.metadata, tl.created_at
        FROM tracecoin_ledger tl
        JOIN tracecoin_wallets tw ON tw.id = tl.wallet_id
        LEFT JOIN users u ON u.id = tw.user_id
        WHERE tw.user_id = $1
        ORDER BY tl.created_at DESC
        LIMIT $2 OFFSET $3
        "#,
    )
    .bind(user_id)
    .bind(limit)
    .bind(offset)
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "entries": rows,
            "page": page,
            "limit": limit,
        })),
    ))
}

#[derive(Debug, Deserialize)]
struct AdjustCreditRequest {
    user_id: Uuid,
    amount: i32,
    #[serde(default)]
    r#type: Option<String>,
    reason: String,
    #[serde(default)]
    reference_id: Option<Uuid>,
}

async fn adjust_credits(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(payload): Json<AdjustCreditRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    if payload.reason.trim().is_empty() {
        return Err((StatusCode::BAD_REQUEST, "reason is required".to_string()));
    }

    let adj_type = payload
        .r#type
        .as_deref()
        .unwrap_or(if payload.amount >= 0 { "ADD" } else { "DEDUCT" })
        .to_uppercase();
    let signed_amount = match adj_type.as_str() {
        "ADD" | "CREDIT" => payload.amount.unsigned_abs() as i32,
        "DEDUCT" | "DEBIT" => -(payload.amount.unsigned_abs() as i32),
        _ => return Err((StatusCode::BAD_REQUEST, "type must be ADD or DEDUCT".to_string())),
    };

    // Map free-text reason to one of the known enum variants so the
    // immutable ledger stays self-describing.
    let ledger_reason = match adj_type.as_str() {
        "DEDUCT" | "DEBIT" => wallet::LedgerReason::AdminDebuct,
        _ => wallet::LedgerReason::AdminCredit,
    };

    let metadata = serde_json::json!({
        "admin_reason": payload.reason.trim(),
        "admin_user_id": auth.user_id,
    });

    let result = wallet::admin_adjust(
        &state.pool,
        payload.user_id,
        signed_amount,
        ledger_reason,
        auth.user_id,
        Some(metadata),
    )
    .await;

    let result = match result {
        Ok((wallet, entry)) => {
            // Best-effort audit log entry.
            let _ = wallet::audit(
                &state.pool,
                auth.user_id,
                payload.user_id,
                signed_amount,
                &payload.reason,
                entry.id,
            )
            .await;

            // Best-effort user notification so the customer knows their
            // balance changed (e.g. when an admin corrects an issue).
            let direction = if signed_amount >= 0 { "credited" } else { "debited" };
            let abs_amount = signed_amount.unsigned_abs() as i32;
            let notif_title = if signed_amount >= 0 {
                "Tracecoins Credited to Your Wallet"
            } else {
                "Tracecoins Deducted From Your Wallet"
            };
            let notif_body = format!(
                "An administrator has {} {} Tracecoins to your wallet. Reason: {}. New balance: {} (available: {}).",
                direction, abs_amount, payload.reason, wallet.balance, wallet.available,
            );
            let _ = sqlx::query(
                r#"
                INSERT INTO notifications (user_id, title, body, type, reference_id)
                VALUES ($1, $2, $3, 'WALLET_ADJUST', $4)
                "#,
            )
            .bind(payload.user_id)
            .bind(notif_title)
            .bind(&notif_body)
            .bind("ADMIN")
            .bind(entry.id)
            .execute(&state.pool)
            .await;

            (wallet, entry)
        }
        Err(e) => {
            return match e {
                wallet::WalletError::InsufficientFunds { available, requested } => Err((
                    StatusCode::BAD_REQUEST,
                    format!("Insufficient balance: available={available}, requested={requested}"),
                )),
                wallet::WalletError::InvalidAmount(a) => Err((
                    StatusCode::BAD_REQUEST,
                    format!("Invalid amount: {a}"),
                )),
                _ => Err((
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("DB error: {e}"),
                )),
            };
        }
    };

    let (wallet, entry) = result;
    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "message": "Credits adjusted successfully.",
            "user_id": payload.user_id,
            "amount": signed_amount,
            "direction": if signed_amount >= 0 { "credit" } else { "debit" },
            "balance": wallet.balance,
            "reserved": wallet.reserved,
            "available": wallet.available,
            "reason": payload.reason,
            "ledger_entry_id": entry.id,
            "audit_logged": true,
            "user_notified": true,
        })),
    ))
}

#[derive(Debug, Deserialize)]
struct ReconcileQuery {
    from: Option<chrono::DateTime<chrono::Utc>>,
    to: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Serialize)]
struct ReconcileRow {
    user_id: Uuid,
    user_email: Option<String>,
    expected_balance: i64,
    actual_balance: i32,
    discrepancy: i64,
}

async fn reconcile_credits(
    _auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<ReconcileQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&_auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let from = q
        .from
        .unwrap_or_else(|| chrono::Utc::now() - chrono::Duration::days(30));
    let to = q.to.unwrap_or_else(chrono::Utc::now);

    let rows: Vec<(Uuid, Option<String>, i32, i64)> = sqlx::query_as(
        r#"
        SELECT
            tw.user_id,
            u.email AS user_email,
            tw.balance AS actual_balance,
            COALESCE(SUM(CASE WHEN tl.created_at BETWEEN $1 AND $2 THEN tl.amount ELSE 0 END), 0)::BIGINT AS expected_balance
        FROM tracecoin_wallets tw
        LEFT JOIN tracecoin_ledger tl ON tl.wallet_id = tw.id
        LEFT JOIN users u ON u.id = tw.user_id
        GROUP BY tw.user_id, u.email, tw.balance
        "#,
    )
    .bind(from)
    .bind(to)
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let result: Vec<ReconcileRow> = rows
        .into_iter()
        .map(|(user_id, user_email, actual_balance, expected_balance)| {
            let actual = actual_balance as i64;
            ReconcileRow {
                user_id,
                user_email,
                expected_balance,
                actual_balance,
                discrepancy: expected_balance - actual,
            }
        })
        .collect();

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "results": result,
            "from": from,
            "to": to,
        })),
    ))
}

// ──────────────────────────────────────────────────────────────────────────
// Invoices — admin
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize)]
struct AdminInvoiceSummary {
    id: Uuid,
    invoice_number: String,
    payment_id: Uuid,
    user_id: Uuid,
    user_email: Option<String>,
    status: String,
    currency: String,
    invoice_type: String,
    subtotal: i64,
    discount_amount: i64,
    cgst_amount: i64,
    sgst_amount: i64,
    igst_amount: i64,
    total: i64,
    customer_name: Option<String>,
    customer_gstin: Option<String>,
    pdf_object_key: Option<String>,
    issued_at: chrono::DateTime<chrono::Utc>,
    paid_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Deserialize)]
struct ListInvoicesQuery {
    status: Option<String>,
    page: Option<i64>,
    limit: Option<i64>,
}

async fn list_invoices(
    auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<ListInvoicesQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let limit = q.limit.unwrap_or(50).clamp(1, 200);
    let page = q.page.unwrap_or(1).max(1);

    let invoices = invoice::service::InvoiceService::list_all(
        &state.pool,
        q.status.as_deref(),
        page,
        limit,
    )
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let mut summaries = Vec::with_capacity(invoices.len());
    for inv in invoices {
        let user_email: Option<String> = sqlx::query_scalar(
            "SELECT email FROM users WHERE id = $1",
        )
        .bind(inv.user_id)
        .fetch_optional(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
        summaries.push(AdminInvoiceSummary {
            id: inv.id,
            invoice_number: inv.invoice_number,
            payment_id: inv.payment_id,
            user_id: inv.user_id,
            user_email,
            status: inv.status,
            currency: inv.currency,
            invoice_type: inv.invoice_type,
            subtotal: inv.subtotal,
            discount_amount: inv.discount_amount,
            cgst_amount: inv.cgst_amount,
            sgst_amount: inv.sgst_amount,
            igst_amount: inv.igst_amount,
            total: inv.total,
            customer_name: inv.customer_name,
            customer_gstin: inv.customer_gstin,
            pdf_object_key: inv.pdf_object_key,
            issued_at: inv.issued_at,
            paid_at: inv.paid_at,
        });
    }

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "invoices": summaries,
            "page": page,
            "limit": limit,
        })),
    ))
}

async fn get_invoice(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let inv = invoice::service::InvoiceService::get(&state.pool, id)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
        .ok_or_else(|| (StatusCode::NOT_FOUND, "Invoice not found".to_string()))?;
    let lines = invoice::service::InvoiceService::list_line_items(&state.pool, id)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
    let totals = invoice::service::InvoiceService::totals(&inv, &lines);
    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "invoice": inv,
            "lines": lines,
            "totals": totals,
        })),
    ))
}

async fn get_invoice_html(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    let inv = invoice::service::InvoiceService::get(&state.pool, id)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
        .ok_or_else(|| (StatusCode::NOT_FOUND, "Invoice not found".to_string()))?;
    let lines = invoice::service::InvoiceService::list_line_items(&state.pool, id)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
    let totals = invoice::service::InvoiceService::totals(&inv, &lines);
    let html = invoice::html::render_html(&inv, &lines, &totals);
    let content_type: axum::http::HeaderValue =
        "text/html; charset=utf-8".parse().unwrap();
    Ok((
        StatusCode::OK,
        [(axum::http::header::CONTENT_TYPE, content_type)],
        html,
    ))
}

#[derive(Debug, Deserialize)]
struct VoidInvoicePayload {
    reason: String,
}

async fn void_invoice(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(payload): Json<VoidInvoicePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    invoice::service::InvoiceService::void(&state.pool, id, auth.user_id, &payload.reason)
        .await
        .map_err(|e| match e {
            invoice::InvoiceError::IllegalState(s) => {
                (StatusCode::CONFLICT, format!("Cannot void: {s}"))
            }
            invoice::InvoiceError::InvalidInput(s) => (StatusCode::BAD_REQUEST, s),
            other => (StatusCode::INTERNAL_SERVER_ERROR, other.to_string()),
        })?;

    Ok((StatusCode::OK, Json(serde_json::json!({
        "message": "Invoice voided.",
    }))))
}

async fn mark_invoice_paid(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Forbidden".to_string()))?;

    invoice::service::InvoiceService::mark_paid(&state.pool, id)
        .await
        .map_err(|e| match e {
            invoice::InvoiceError::IllegalState(s) => {
                (StatusCode::CONFLICT, format!("Cannot mark paid: {s}"))
            }
            other => (StatusCode::INTERNAL_SERVER_ERROR, other.to_string()),
        })?;

    Ok((StatusCode::OK, Json(serde_json::json!({
        "message": "Invoice marked as paid.",
    }))))
}

pub fn admin_router() -> Router<AppState> {
    Router::new()
        // Tax
        .route("/api/admin/tax", get(list_tax_rules))
        .route("/api/admin/tax", post(create_tax_rule))
        .route("/api/admin/tax/:id", delete(delete_tax_rule))
        // Ledger
        .route("/api/admin/ledger", get(list_ledger))
        // Orders
        .route("/api/admin/orders", get(list_orders))
        // Credits
        .route("/api/admin/credits/balance", get(get_credit_balance))
        .route("/api/admin/credits/ledger", get(get_credit_ledger))
        .route("/api/admin/credits/adjust", post(adjust_credits))
        .route("/api/admin/credits/reconcile", get(reconcile_credits))
        .route("/api/admin/credits/reconcile-report", get(reconcile_credits))
        .route("/api/admin/credits/reconcile/:order_id", post(reconcile_credits))
        // Invoices
        .route("/api/admin/invoices", get(list_invoices))
        .route("/api/admin/invoices/:id", get(get_invoice))
        .route("/api/admin/invoices/:id/html", get(get_invoice_html))
        .route("/api/admin/invoices/:id/void", axum::routing::post(void_invoice))
        .route("/api/admin/invoices/:id/mark-paid", axum::routing::post(mark_invoice_paid))
}
