//! HTTP surface for the Ask Ash AI credits wallet (Phase 1).
//!
//! `/api/ai-credits/wallet` is real and safe to use from the frontend now.
//! `/api/ai-credits/charge` is a Phase-1 verification endpoint: it proves
//! the reserve -> work -> capture/release flow end-to-end over a real HTTP
//! request, but the "work" it charges for is a stub, not a real LiteLLM
//! call -- there is no LiteLLM client in this service yet (see Section 8 of
//! docs/ASK_ASH_BILLING_ARCHITECTURE.md in nxtgauge-ai-assistant). Wiring
//! real AI features to `charge_ai_feature` is Phase 2 work.

use crate::ai_credits::{charge_ai_feature, ChargeError};
use crate::AppState;
use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use contracts::auth_middleware::{AuthUser, require_admin};
use db::models::ai_credits::{AiCreditsError, AiCreditsRepository};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/wallet", get(get_wallet))
        .route("/charge", post(charge_feature))
}

/// Admin router for AI credits management
pub fn admin_router() -> Router<AppState> {
    Router::new()
        .route("/balance", get(admin_balance))
        .route("/ledger", get(admin_ledger))
        .route("/adjust", post(admin_adjust))
        .route("/reconcile", get(admin_reconcile))
        .route("/refunds", post(admin_create_refund).get(admin_list_refunds))
}

#[derive(Serialize)]
struct WalletDto {
    available_credits: i32,
    monthly_credits_total: i32,
    monthly_credits_used: i32,
    purchased_credits_total: i32,
    purchased_credits_used: i32,
    bonus_credits_total: i32,
    bonus_credits_used: i32,
    reserved_credits: i32,
    locked_credits: i32,
    daily_actions_used: i32,
    daily_credits_used: i32,
}

fn credits_error_response(err: &AiCreditsError) -> (StatusCode, Json<serde_json::Value>) {
    let (status, code) = match err {
        AiCreditsError::InsufficientCredits => (StatusCode::PAYMENT_REQUIRED, "INSUFFICIENT_AI_CREDITS"),
        AiCreditsError::DailyActionLimitReached => (StatusCode::TOO_MANY_REQUESTS, "AI_DAILY_ACTION_LIMIT"),
        AiCreditsError::DailyCreditLimitReached => (StatusCode::TOO_MANY_REQUESTS, "AI_DAILY_CREDIT_LIMIT"),
        AiCreditsError::UnknownFeature(_) => (StatusCode::BAD_REQUEST, "UNKNOWN_AI_FEATURE"),
        AiCreditsError::ReservationNotFound => (StatusCode::NOT_FOUND, "RESERVATION_NOT_FOUND"),
        AiCreditsError::InvalidAmount(_) => (StatusCode::BAD_REQUEST, "INVALID_AMOUNT"),
        AiCreditsError::Db(e) => {
            tracing::error!("ai credits db error: {e}");
            (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
        }
    };
    (status, Json(serde_json::json!({ "error": err.to_string(), "code": code })))
}

async fn get_wallet(auth: AuthUser, State(state): State<AppState>) -> impl IntoResponse {
    match AiCreditsRepository::ensure_wallet(&state.pool, auth.user_id).await {
        Ok(wallet) => {
            let dto = WalletDto {
                available_credits: wallet.available_credits(),
                monthly_credits_total: wallet.monthly_credits_total,
                monthly_credits_used: wallet.monthly_credits_used,
                purchased_credits_total: wallet.purchased_credits_total,
                purchased_credits_used: wallet.purchased_credits_used,
                bonus_credits_total: wallet.bonus_credits_total,
                bonus_credits_used: wallet.bonus_credits_used,
                reserved_credits: wallet.reserved_credits,
                locked_credits: wallet.locked_credits,
                daily_actions_used: wallet.daily_actions_used,
                daily_credits_used: wallet.daily_credits_used,
            };
            (StatusCode::OK, Json(serde_json::to_value(dto).unwrap())).into_response()
        }
        Err(e) => credits_error_response(&AiCreditsError::Db(e)).into_response(),
    }
}

#[derive(Deserialize)]
struct ChargeRequest {
    feature_code: String,
    request_id: Option<String>,
    idempotency_key: Option<String>,
}

#[derive(Serialize)]
struct ChargeResponse {
    feature_code: String,
    credits_charged: i32,
    available_credits: i32,
}

async fn charge_feature(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(body): Json<ChargeRequest>,
) -> impl IntoResponse {
    let outcome = charge_ai_feature::<_, std::convert::Infallible, _, _>(
        &state.pool,
        auth.user_id,
        &body.feature_code,
        body.request_id.as_deref(),
        body.idempotency_key.as_deref(),
        |_feature| async move { Ok(()) },
    )
    .await;

    match outcome {
        Ok(outcome) => {
            let wallet = AiCreditsRepository::get_wallet(&state.pool, auth.user_id)
                .await
                .ok()
                .flatten();
            let response = ChargeResponse {
                feature_code: outcome.feature.feature_code,
                credits_charged: outcome.credits_charged,
                available_credits: wallet.map(|w| w.available_credits()).unwrap_or_default(),
            };
            (StatusCode::OK, Json(serde_json::to_value(response).unwrap())).into_response()
        }
        Err(ChargeError::Credits(e)) => credits_error_response(&e).into_response(),
        Err(ChargeError::UnknownFeature(f)) => credits_error_response(&AiCreditsError::UnknownFeature(f)).into_response(),
        Err(ChargeError::Work(never)) => match never {},
    }
}

#[derive(Serialize)]
struct PlanDetailsDto {
    remaining_credits: i32,
    remaining_daily_actions: i32,
    daily_action_limit: i32,
    plan_code: String,
    plan_name: String,
    monthly_credits_total: i32,
    monthly_credits_used: i32,
    purchased_credits_total: i32,
    purchased_credits_used: i32,
}

/// GET /api/ai/usage/summary -- powers CreditsPage.tsx's "AI Usage" tab
/// overview cards (`AiUsageSummary` type in that file).
pub async fn ai_usage_summary(auth: AuthUser, State(state): State<AppState>) -> impl IntoResponse {
    let wallet = match AiCreditsRepository::ensure_wallet(&state.pool, auth.user_id).await {
        Ok(w) => w,
        Err(e) => return credits_error_response(&AiCreditsError::Db(e)).into_response(),
    };
    let plan = match AiCreditsRepository::get_plan(&state.pool, wallet.plan_id).await {
        Ok(Some(p)) => p,
        Ok(None) => return credits_error_response(&AiCreditsError::Db(sqlx::Error::RowNotFound)).into_response(),
        Err(e) => return credits_error_response(&AiCreditsError::Db(e)).into_response(),
    };

    let dto = PlanDetailsDto {
        remaining_credits: wallet.available_credits(),
        remaining_daily_actions: (plan.daily_action_limit - wallet.daily_actions_used).max(0),
        daily_action_limit: plan.daily_action_limit,
        plan_code: plan.code,
        plan_name: plan.name,
        monthly_credits_total: wallet.monthly_credits_total,
        monthly_credits_used: wallet.monthly_credits_used,
        purchased_credits_total: wallet.purchased_credits_total,
        purchased_credits_used: wallet.purchased_credits_used,
    };
    (StatusCode::OK, Json(serde_json::json!({ "plan_details": dto }))).into_response()
}

#[derive(Serialize, sqlx::FromRow)]
struct UsageLogDto {
    id: uuid::Uuid,
    feature_code: String,
    model_alias: String,
    credits_charged: i32,
    status: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

/// GET /api/ai/usage/logs -- powers CreditsPage.tsx's "AI Usage" tab
/// history list (`AiUsageLog` type in that file).
pub async fn ai_usage_logs_handler(auth: AuthUser, State(state): State<AppState>) -> impl IntoResponse {
    let logs = sqlx::query_as::<_, UsageLogDto>(
        r#"
        SELECT id, feature_code, model_alias, credits_charged, status, created_at
        FROM ai_usage_logs
        WHERE user_id = $1
        ORDER BY created_at DESC
        LIMIT 100
        "#,
    )
    .bind(auth.user_id)
    .fetch_all(&state.pool)
    .await;

    match logs {
        Ok(logs) => (StatusCode::OK, Json(serde_json::json!({ "logs": logs }))).into_response(),
        Err(e) => credits_error_response(&AiCreditsError::Db(e)).into_response(),
    }
}

// ============================================================================
// ADMIN ENDPOINTS
// ============================================================================



/// GET /api/admin/ai-credits/balance?userId=<uuid>
/// Get wallet balance for a specific user (admin only)
async fn admin_balance(
    auth: AuthUser,
    State(state): State<AppState>,
    Query(params): Query<AdminBalanceQuery>,
) -> impl IntoResponse {
    if let Err(e) = require_admin(&auth) {
        return e.into_response();
    }

    let user_id = match params.user_id {
        Some(id) => id,
        None => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": "userId parameter required", "code": "MISSING_USER_ID" }))
            ).into_response();
        }
    };

    match AiCreditsRepository::ensure_wallet(&state.pool, user_id).await {
        Ok(wallet) => {
            let dto = WalletDto {
                available_credits: wallet.available_credits(),
                monthly_credits_total: wallet.monthly_credits_total,
                monthly_credits_used: wallet.monthly_credits_used,
                purchased_credits_total: wallet.purchased_credits_total,
                purchased_credits_used: wallet.purchased_credits_used,
                bonus_credits_total: wallet.bonus_credits_total,
                bonus_credits_used: wallet.bonus_credits_used,
                reserved_credits: wallet.reserved_credits,
                locked_credits: wallet.locked_credits,
                daily_actions_used: wallet.daily_actions_used,
                daily_credits_used: wallet.daily_credits_used,
            };
            (StatusCode::OK, Json(serde_json::to_value(dto).unwrap())).into_response()
        }
        Err(e) => credits_error_response(&AiCreditsError::Db(e)).into_response(),
    }
}

#[derive(Deserialize)]
struct AdminBalanceQuery {
    user_id: Option<Uuid>,
}

/// GET /api/admin/ai-credits/ledger?userId=<uuid>&page=&limit=
/// Get paginated ledger entries for a user (admin only)
async fn admin_ledger(
    auth: AuthUser,
    State(state): State<AppState>,
    Query(params): Query<AdminLedgerQuery>,
) -> impl IntoResponse {
    if let Err(e) = require_admin(&auth) {
        return e.into_response();
    }

    let user_id = match params.user_id {
        Some(id) => id,
        None => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": "userId parameter required", "code": "MISSING_USER_ID" }))
            ).into_response();
        }
    };

    let page = params.page.unwrap_or(1).max(1);
    let limit = params.limit.unwrap_or(20).clamp(1, 100);
    let offset = (page - 1) * limit;

    // Get total count
    let total: i64 = match sqlx::query_scalar(
        r#"
        SELECT COUNT(*) 
        FROM ai_credit_ledger l
        JOIN user_ai_subscriptions w ON l.wallet_id = w.id
        WHERE w.user_id = $1
        "#
    )
    .bind(user_id)
    .fetch_one(&state.pool)
    .await
    {
        Ok(count) => count,
        Err(e) => return credits_error_response(&AiCreditsError::Db(e)).into_response(),
    };

    // Get ledger entries
    let entries: Vec<LedgerEntryDto> = match sqlx::query_as(
        r#"
        SELECT 
            l.id,
            l.entry_type,
            l.credits,
            l.balance_after,
            l.description,
            l.idempotency_key,
            l.created_at
        FROM ai_credit_ledger l
        JOIN user_ai_subscriptions w ON l.wallet_id = w.id
        WHERE w.user_id = $1
        ORDER BY l.created_at DESC
        LIMIT $2 OFFSET $3
        "#
    )
    .bind(user_id)
    .bind(limit)
    .bind(offset)
    .fetch_all(&state.pool)
    .await
    {
        Ok(entries) => entries,
        Err(e) => return credits_error_response(&AiCreditsError::Db(e)).into_response(),
    };

    (
        StatusCode::OK,
        Json(serde_json::json!({
            "data": entries,
            "total": total,
            "page": page,
            "limit": limit
        }))
    ).into_response()
}

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

#[derive(Serialize, sqlx::FromRow)]
struct LedgerEntryDto {
    id: uuid::Uuid,
    entry_type: String,
    credits: i32,
    balance_after: i32,
    description: Option<String>,
    idempotency_key: Option<String>,
    created_at: chrono::DateTime<chrono::Utc>,
}

/// POST /api/admin/ai-credits/adjust
/// Adjust credits for a user (ADD or DEDUCT)
async fn admin_adjust(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(body): Json<AdminAdjustRequest>,
) -> impl IntoResponse {
    if let Err(e) = require_admin(&auth) {
        return e.into_response();
    }

    // Validate reason is not empty
    if body.reason.trim().is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({ 
                "error": "reason is required and cannot be empty", 
                "code": "INVALID_REASON" 
            }))
        ).into_response();
    }

    let is_add = body.type_ == "ADD";
    let idempotency_key = body.idempotency_key
        .unwrap_or_else(|| format!("admin-adjust:{}:{}", auth.user_id, uuid::Uuid::new_v4()));

    match AiCreditsRepository::admin_adjust_credits(
        &state.pool,
        body.user_id,
        body.amount,
        is_add,
        &body.reason,
        auth.user_id,
        Some(&idempotency_key),
    ).await
    {
        Ok(wallet) => {
            let dto = WalletDto {
                available_credits: wallet.available_credits(),
                monthly_credits_total: wallet.monthly_credits_total,
                monthly_credits_used: wallet.monthly_credits_used,
                purchased_credits_total: wallet.purchased_credits_total,
                purchased_credits_used: wallet.purchased_credits_used,
                bonus_credits_total: wallet.bonus_credits_total,
                bonus_credits_used: wallet.bonus_credits_used,
                reserved_credits: wallet.reserved_credits,
                locked_credits: wallet.locked_credits,
                daily_actions_used: wallet.daily_actions_used,
                daily_credits_used: wallet.daily_credits_used,
            };
            (StatusCode::OK, Json(serde_json::json!({
                "success": true,
                "wallet": dto,
                "operation": if is_add { "ADD" } else { "DEDUCT" },
                "amount": body.amount
            }))).into_response()
        }
        Err(AiCreditsError::InsufficientCredits) => (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({ 
                "error": "Insufficient credits for deduction", 
                "code": "INSUFFICIENT_CREDITS" 
            }))
        ).into_response(),
        Err(e) => credits_error_response(&e).into_response(),
    }
}

#[derive(Deserialize)]
struct AdminAdjustRequest {
    user_id: Uuid,
    amount: i32,
    #[serde(rename = "type")]
    type_: String,
    reason: String,
    idempotency_key: Option<String>,
}

/// GET /api/admin/ai-credits/reconcile?from=<date>&to=<date>
/// Reconcile ledger entries within date range
async fn admin_reconcile(
    auth: AuthUser,
    State(state): State<AppState>,
    Query(params): Query<AdminReconcileQuery>,
) -> impl IntoResponse {
    if let Err(e) = require_admin(&auth) {
        return e.into_response();
    }

    let from_date = match params.from {
        Some(d) => d,
        None => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": "from date parameter required", "code": "MISSING_FROM_DATE" }))
            ).into_response();
        }
    };

    let to_date = match params.to {
        Some(d) => d,
        None => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": "to date parameter required", "code": "MISSING_TO_DATE" }))
            ).into_response();
        }
    };

    // Sum credits by entry type within date range
    let summary: Vec<ReconcileSummaryDto> = match sqlx::query_as(
        r#"
        SELECT 
            entry_type,
            COUNT(*) as count,
            SUM(credits) as total_credits
        FROM ai_credit_ledger
        WHERE created_at >= $1 AND created_at <= $2
        GROUP BY entry_type
        ORDER BY entry_type
        "#
    )
    .bind(from_date)
    .bind(to_date)
    .fetch_all(&state.pool)
    .await
    {
        Ok(rows) => rows,
        Err(e) => return credits_error_response(&AiCreditsError::Db(e)).into_response(),
    };

    // Check for drift in sample of wallets
    let drift_check: Vec<WalletDriftDto> = match sqlx::query_as(
        r#"
        SELECT 
            w.user_id,
            u.email as user_email,
            w.id as wallet_id,
            COALESCE(SUM(l.credits), 0) as ledger_sum,
            (
                w.bonus_credits_total - w.bonus_credits_used +
                w.monthly_credits_total - w.monthly_credits_used +
                w.purchased_credits_total - w.purchased_credits_used -
                w.reserved_credits - w.locked_credits
            ) as computed_available
        FROM user_ai_subscriptions w
        JOIN users u ON w.user_id = u.id
        LEFT JOIN ai_credit_ledger l ON l.wallet_id = w.id
        WHERE w.updated_at >= $1 AND w.updated_at <= $2
        GROUP BY w.id, w.user_id, u.email, w.bonus_credits_total, w.bonus_credits_used,
                 w.monthly_credits_total, w.monthly_credits_used,
                 w.purchased_credits_total, w.purchased_credits_used,
                 w.reserved_credits, w.locked_credits
        LIMIT 100
        "#
    )
    .bind(from_date)
    .bind(to_date)
    .fetch_all(&state.pool)
    .await
    {
        Ok(rows) => rows,
        Err(e) => return credits_error_response(&AiCreditsError::Db(e)).into_response(),
    };

    let drift_issues: Vec<_> = drift_check
        .into_iter()
        .filter(|w| w.ledger_sum != w.computed_available)
        .map(|w| serde_json::json!({
            "user_id": w.user_id,
            "user_email": w.user_email,
            "ledger_sum": w.ledger_sum,
            "computed_available": w.computed_available,
            "drift": w.ledger_sum - w.computed_available
        }))
        .collect();

    (
        StatusCode::OK,
        Json(serde_json::json!({
            "from": from_date,
            "to": to_date,
            "summary": summary,
            "drift_check": {
                "wallets_checked": drift_issues.len(),
                "issues_found": drift_issues.len(),
                "issues": drift_issues
            }
        }))
    ).into_response()
}

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

#[derive(Serialize, sqlx::FromRow)]
struct ReconcileSummaryDto {
    entry_type: String,
    count: i64,
    total_credits: i64,
}

#[derive(Serialize, sqlx::FromRow)]
struct WalletDriftDto {
    user_id: Uuid,
    user_email: String,
    #[sqlx(rename = "wallet_id")]
    _wallet_id: Uuid,
    ledger_sum: i32,
    computed_available: i32,
}

// ============================================================================
// REFUND ENDPOINTS (Task 5)
// ============================================================================

/// POST /api/admin/ai-credits/refunds
/// Process a manual refund for a completed charge
async fn admin_create_refund(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(body): Json<CreateRefundRequest>,
) -> impl IntoResponse {
    if let Err(e) = require_admin(&auth) {
        return e.into_response();
    }

    // Validate reason
    let valid_reasons = ["provider_failure", "timeout", "validation_failure", "manual", "user_dispute"];
    if !valid_reasons.contains(&body.reason.as_str()) {
        return (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({ 
                "error": "Invalid reason", 
                "code": "INVALID_REASON",
                "valid_reasons": valid_reasons
            }))
        ).into_response();
    }

    // Generate idempotency key if not provided
    let idempotency_key = body.idempotency_key
        .unwrap_or_else(|| format!("admin-refund:{}: {}", auth.user_id, uuid::Uuid::new_v4()));

    match AiCreditsRepository::refund_credits(
        &state.pool,
        body.user_id,
        body.ledger_debit_entry_id,
        body.credits_to_refund,
        &body.reason,
        "admin",
        Some(auth.user_id),
        Some(&idempotency_key),
        body.notes.as_deref(),
    ).await {
        Ok((refund_id, wallet)) => {
            let dto = WalletDto {
                available_credits: wallet.available_credits(),
                monthly_credits_total: wallet.monthly_credits_total,
                monthly_credits_used: wallet.monthly_credits_used,
                purchased_credits_total: wallet.purchased_credits_total,
                purchased_credits_used: wallet.purchased_credits_used,
                bonus_credits_total: wallet.bonus_credits_total,
                bonus_credits_used: wallet.bonus_credits_used,
                reserved_credits: wallet.reserved_credits,
                locked_credits: wallet.locked_credits,
                daily_actions_used: wallet.daily_actions_used,
                daily_credits_used: wallet.daily_credits_used,
            };
            (
                StatusCode::OK,
                Json(serde_json::json!({
                    "success": true,
                    "refund_id": refund_id,
                    "wallet": dto,
                    "credits_refunded": body.credits_to_refund,
                    "reason": body.reason
                }))
            ).into_response()
        }
        Err(AiCreditsError::InvalidAmount(msg)) => (
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({ "error": msg, "code": "INVALID_AMOUNT" }))
        ).into_response(),
        Err(AiCreditsError::ReservationNotFound) => (
            StatusCode::NOT_FOUND,
            Json(serde_json::json!({ 
                "error": "Original ledger entry not found", 
                "code": "LEDGER_ENTRY_NOT_FOUND" 
            }))
        ).into_response(),
        Err(e) => credits_error_response(&e).into_response(),
    }
}

#[derive(Deserialize)]
struct CreateRefundRequest {
    user_id: Uuid,
    ledger_debit_entry_id: Uuid,
    credits_to_refund: i32,
    reason: String,
    notes: Option<String>,
    idempotency_key: Option<String>,
}

/// GET /api/admin/ai-credits/refunds?userId=<uuid>&page=&limit=
/// List refunds for a user (admin view)
async fn admin_list_refunds(
    auth: AuthUser,
    State(state): State<AppState>,
    Query(params): Query<ListRefundsQuery>,
) -> impl IntoResponse {
    if let Err(e) = require_admin(&auth) {
        return e.into_response();
    }

    let user_id = match params.user_id {
        Some(id) => id,
        None => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": "userId parameter required", "code": "MISSING_USER_ID" }))
            ).into_response();
        }
    };

    let page = params.page.unwrap_or(1).max(1);
    let limit = params.limit.unwrap_or(20).clamp(1, 100);

    match AiCreditsRepository::list_refunds(&state.pool, user_id, page, limit).await {
        Ok(refunds) => {
            (
                StatusCode::OK,
                Json(serde_json::json!({
                    "data": refunds,
                    "page": page,
                    "limit": limit
                }))
            ).into_response()
        }
        Err(e) => credits_error_response(&AiCreditsError::Db(e)).into_response(),
    }
}

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