//! AI credit package purchase flow via PayU, matching the already-shipped
//! frontend contract in
//! nxtgauge-frontend-solid/src/components/dashboard/CreditsPage.tsx
//! (GET /api/ai-credits, POST /api/ai-credits/order, POST /api/ai-credits/verify).
//!
//! Deliberately separate from the TraceCoins purchase flow in main.rs --
//! its own table (`ai_credit_orders`), its own wallet
//! (`db::models::ai_credits::AiCreditsRepository`), same PayU integration
//! (crate::payu) reused for the hash logic.

use crate::payu;
use crate::AppState;
use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{get, patch, post},
    Json, Router,
};
use contracts::auth_middleware::{require_admin, AuthUser};
use db::models::ai_credits::AiCreditsRepository;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/", get(list_packages))
        .route("/order", post(create_order))
        .route("/verify", post(verify_order))
}

/// Admin CRUD for the AI credit package catalog -- mounted at
/// /api/admin/ai-credits/packages by the gateway (see apps/gateway's
/// resolve_upstream). Mirrors the shape of the existing TraceCoins
/// package admin form in nxtgauge-admin-solid's pricing.tsx, but against
/// `ai_credit_packages`, not `pricing_packages`.
pub fn admin_router() -> Router<AppState> {
    Router::new()
        .route("/", get(admin_list_packages).post(admin_create_package))
        .route("/{id}", patch(admin_update_package))
}

#[derive(Debug, FromRow, Serialize)]
struct AiCreditPackageRow {
    id: Uuid,
    name: String,
    description: Option<String>,
    credits: i32,
    price_inr: i32,
}

#[derive(Serialize)]
struct ListPackagesResponse {
    packages: Vec<AiCreditPackageRow>,
}

async fn list_packages(State(state): State<AppState>) -> Result<Json<ListPackagesResponse>, (StatusCode, String)> {
    let packages = sqlx::query_as::<_, AiCreditPackageRow>(
        "SELECT id, name, description, credits, price_inr FROM ai_credit_packages WHERE is_active = TRUE ORDER BY price_inr",
    )
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok(Json(ListPackagesResponse { packages }))
}

#[derive(Debug, Deserialize)]
struct CreateAiOrderRequest {
    package_id: String,
    coupon_code: Option<String>,
}

#[derive(Debug, Serialize)]
struct CreateAiOrderResponse {
    key: String,
    txnid: String,
    amount: String,
    productinfo: String,
    firstname: String,
    email: String,
    phone: String,
    surl: String,
    furl: String,
    hash: String,
    payu_base_url: String,
    udf1: String,
    udf2: String,
    order_id: String,
    currency: String,
}

#[derive(Debug, FromRow)]
struct AiCreditPackagePriceRow {
    name: String,
    credits: i32,
    price_inr: i32,
}

#[derive(Debug, FromRow)]
struct UserContactRow {
    email: String,
    full_name: Option<String>,
    phone: Option<String>,
}

/// Validate and apply a coupon code
async fn validate_coupon(
    pool: &PgPool,
    user_id: Uuid,
    coupon_code: &str,
    package_id: Uuid,
    original_price: i32,
) -> Result<Option<(i32, String, i32)>, String> {
    // Get coupon details
    let coupon: Option<(Uuid, String, i32, Option<i32>, i32, i32, Vec<Uuid>)> = sqlx::query_as(
        r#"
        SELECT 
            id, discount_type, 
            CAST(discount_value * 100 AS INTEGER) as discount_value,
            CAST(max_discount_amount * 100 AS INTEGER) as max_discount_amount,
            max_redemptions_per_user, redemptions_used, applicable_package_ids
        FROM ai_coupons
        WHERE code = $1 
          AND is_active = TRUE 
          AND valid_from <= NOW() 
          AND (valid_until IS NULL OR valid_until > NOW())
        "#
    )
    .bind(coupon_code)
    .fetch_optional(pool)
    .await
    .map_err(|e| format!("DB error: {e}"))?;

    let Some((coupon_id, discount_type, discount_value, max_discount_amount, max_per_user, total_redemptions, applicable_packages)) = coupon else {
        return Ok(None); // Coupon not found or invalid
    };

    // Check if coupon has remaining redemptions
    if total_redemptions >= max_per_user {
        return Err("Coupon redemption limit reached".to_string());
    }

    // Check if user already redeemed this coupon
    let user_redemptions: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM ai_coupon_redemptions WHERE coupon_id = $1 AND user_id = $2"
    )
    .bind(coupon_id)
    .bind(user_id)
    .fetch_one(pool)
    .await
    .map_err(|e| format!("DB error: {e}"))?;

    if user_redemptions >= max_per_user as i64 {
        return Err("You have already used this coupon".to_string());
    }

    // Check if package is applicable
    if !applicable_packages.is_empty() && !applicable_packages.contains(&package_id) {
        return Err("Coupon not applicable to this package".to_string());
    }

    // Calculate discount in paise
    let discount_paise = if discount_type == "percentage" {
        // discount_value is already in paise (stored * 100 in DB query)
        // For percentage: original * (discount_percent / 100)
        (original_price as i64 * discount_value as i64 / 10000) as i32
    } else {
        // Fixed amount in paise
        discount_value
    };
    
    // Apply max discount limit (in paise)
    let discount_paise = if let Some(max) = max_discount_amount {
        discount_paise.min(max)
    } else {
        discount_paise
    };
    
    let discounted_price = original_price - discount_paise;

    Ok(Some((discount_paise, coupon_id.to_string(), discounted_price)))
}

async fn create_order(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(payload): Json<CreateAiOrderRequest>,
) -> Result<Json<CreateAiOrderResponse>, (StatusCode, String)> {
    let package_id = Uuid::parse_str(&payload.package_id)
        .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid package id".to_string()))?;

    let package = sqlx::query_as::<_, AiCreditPackagePriceRow>(
        "SELECT name, credits, price_inr FROM ai_credit_packages WHERE id = $1 AND is_active = TRUE",
    )
    .bind(package_id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    .ok_or((StatusCode::BAD_REQUEST, "Invalid or inactive AI credit package".to_string()))?;

    let contact = sqlx::query_as::<_, UserContactRow>("SELECT email, full_name, phone FROM users WHERE id = $1")
        .bind(auth.user_id)
        .fetch_optional(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
        .ok_or((StatusCode::NOT_FOUND, "User not found".to_string()))?;

    let firstname = contact
        .full_name
        .as_deref()
        .and_then(|n| n.split_whitespace().next())
        .unwrap_or("Customer")
        .to_string();
    let phone = contact.phone.unwrap_or_default();

    // Validate coupon if provided
    let (final_price, discount_applied, coupon_id) = if let Some(code) = &payload.coupon_code {
        match validate_coupon(&state.pool, auth.user_id, code, package_id, package.price_inr).await {
            Ok(Some((discount, coupon_id_str, discounted_price))) => {
                (discounted_price, discount, Some(coupon_id_str))
            }
            Ok(None) => return Err((StatusCode::BAD_REQUEST, "Invalid coupon code".to_string())),
            Err(e) => return Err((StatusCode::BAD_REQUEST, e)),
        }
    } else {
        (package.price_inr, 0, None)
    };

    let txnid = payu::generate_txnid();
    let amount_str = payu::paise_to_rupee_string(final_price);
    let productinfo = package.name.clone();
    let udf1 = package_id.to_string();
    let udf2 = coupon_id.unwrap_or_default(); // Store coupon ID in UDF2

    let hash = payu::request_hash(
        &state.payu,
        &txnid,
        &amount_str,
        &productinfo,
        &firstname,
        &contact.email,
        &udf1,
        &udf2,
    );

    sqlx::query(
        r#"
        INSERT INTO ai_credit_orders (user_id, package_id, txnid, amount_inr, credits, status, coupon_code, discount_applied)
        VALUES ($1, $2, $3, $4, $5, 'PENDING', $6, $7)
        "#,
    )
    .bind(auth.user_id)
    .bind(package_id)
    .bind(&txnid)
    .bind(final_price)
    .bind(package.credits)
    .bind(payload.coupon_code.as_ref())
    .bind(discount_applied)
    .execute(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok(Json(CreateAiOrderResponse {
        key: state.payu.merchant_key.clone(),
        txnid: txnid.clone(),
        amount: amount_str,
        productinfo,
        firstname,
        email: contact.email,
        phone,
        surl: state.payu.surl.clone(),
        furl: state.payu.furl.clone(),
        hash,
        payu_base_url: state.payu.base_url.clone(),
        udf1,
        udf2,
        order_id: txnid,
        currency: "INR".to_string(),
    }))
}

#[derive(Debug, Deserialize)]
struct VerifyAiOrderRequest {
    txnid: String,
    mihpayid: String,
    status: String,
    hash: String,
    amount: String,
    productinfo: String,
    firstname: String,
    email: String,
    #[serde(default)]
    udf1: Option<String>,
    #[serde(default)]
    udf2: Option<String>,
}

#[derive(Debug, Serialize)]
struct VerifyAiOrderResponse {
    verified: bool,
    credits_added: i32,
}

#[derive(Debug, FromRow)]
struct AiCreditOrderRow {
    id: Uuid,
    user_id: Uuid,
    credits: i32,
    coupon_code: Option<String>,
    discount_applied: Option<i32>,
    amount_inr: i32,
    package_id: Uuid,
}

async fn verify_order(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(payload): Json<VerifyAiOrderRequest>,
) -> Result<Json<VerifyAiOrderResponse>, (StatusCode, String)> {
    if !payu::verify_response_hash(
        &state.payu,
        &payload.status,
        &payload.txnid,
        &payload.amount,
        &payload.productinfo,
        &payload.firstname,
        &payload.email,
        payload.udf1.as_deref().unwrap_or(""),
        payload.udf2.as_deref().unwrap_or(""),
        &payload.hash,
    ) {
        return Err((StatusCode::BAD_REQUEST, "Payment hash verification failed".to_string()));
    }

    if !payload.status.eq_ignore_ascii_case("success") {
        return Err((StatusCode::BAD_REQUEST, "Payment was not successful".to_string()));
    }

    let order = sqlx::query_as::<_, AiCreditOrderRow>(
        "SELECT id, user_id, credits, coupon_code, discount_applied, amount_inr, package_id FROM ai_credit_orders WHERE txnid = $1 AND status = 'PENDING'",
    )
    .bind(&payload.txnid)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    .ok_or((StatusCode::NOT_FOUND, "Order not found or already processed".to_string()))?;

    if order.user_id != auth.user_id {
        return Err((StatusCode::FORBIDDEN, "Order does not belong to user".to_string()));
    }

    // Idempotency key ties this credit grant to the specific order --
    // AiCreditsRepository::add_purchased_credits no-ops on a retried call
    // with the same key rather than double-crediting.
    let idempotency_key = format!("ai-credit-order:{}", order.id);
    AiCreditsRepository::ensure_wallet(&state.pool, order.user_id)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
    AiCreditsRepository::add_purchased_credits(
        &state.pool,
        order.user_id,
        order.credits,
        &idempotency_key,
        "ai_credit_order",
        Some(order.id),
    )
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    // Track coupon redemption if coupon was used
    if let Some(coupon_code) = order.coupon_code {
        let coupon_id: Option<Uuid> = sqlx::query_scalar("SELECT id FROM ai_coupons WHERE code = $1")
            .bind(&coupon_code)
            .fetch_optional(&state.pool)
            .await
            .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

        if let Some(coupon_id) = coupon_id {
            // Record redemption
            sqlx::query(
                r#"
                INSERT INTO ai_coupon_redemptions 
                    (coupon_id, user_id, order_id, credits_purchased, discount_applied, final_amount_paid)
                VALUES ($1, $2, $3, $4, $5, $6)
                ON CONFLICT (coupon_id, user_id) DO UPDATE SET
                    order_id = EXCLUDED.order_id,
                    credits_purchased = EXCLUDED.credits_purchased,
                    discount_applied = EXCLUDED.discount_applied,
                    final_amount_paid = EXCLUDED.final_amount_paid,
                    redeemed_at = NOW()
                "#
            )
            .bind(coupon_id)
            .bind(order.user_id)
            .bind(order.id)
            .bind(order.credits)
            .bind(order.discount_applied.unwrap_or(0))
            .bind(order.amount_inr)
            .execute(&state.pool)
            .await
            .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

            // Update coupon redemption count
            sqlx::query("UPDATE ai_coupons SET redemptions_used = redemptions_used + 1 WHERE id = $1")
                .bind(coupon_id)
                .execute(&state.pool)
                .await
                .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
        }
    }

    sqlx::query("UPDATE ai_credit_orders SET status = 'SUCCESS', payu_payment_id = $1, verified_at = NOW() WHERE id = $2")
        .bind(&payload.mihpayid)
        .bind(order.id)
        .execute(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok(Json(VerifyAiOrderResponse {
        verified: true,
        credits_added: order.credits,
    }))
}

// ── Admin package management ────────────────────────────────────────────

#[derive(Debug, FromRow, Serialize)]
struct AdminAiCreditPackageRow {
    id: Uuid,
    name: String,
    description: Option<String>,
    credits: i32,
    price_inr: i32,
    is_active: bool,
    created_at: chrono::DateTime<chrono::Utc>,
    updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Serialize)]
struct AdminListPackagesResponse {
    packages: Vec<AdminAiCreditPackageRow>,
}

async fn admin_list_packages(
    auth: AuthUser,
    State(state): State<AppState>,
) -> Result<Json<AdminListPackagesResponse>, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Admin access required".to_string()))?;

    let packages = sqlx::query_as::<_, AdminAiCreditPackageRow>(
        "SELECT id, name, description, credits, price_inr, is_active, created_at, updated_at FROM ai_credit_packages ORDER BY price_inr",
    )
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok(Json(AdminListPackagesResponse { packages }))
}

#[derive(Debug, Deserialize)]
struct CreatePackageRequest {
    name: String,
    description: Option<String>,
    credits: i32,
    price_inr: i32,
}

async fn admin_create_package(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(body): Json<CreatePackageRequest>,
) -> Result<Json<AdminAiCreditPackageRow>, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Admin access required".to_string()))?;

    if body.credits <= 0 || body.price_inr <= 0 {
        return Err((StatusCode::BAD_REQUEST, "credits and price_inr must be positive".to_string()));
    }

    let package = sqlx::query_as::<_, AdminAiCreditPackageRow>(
        r#"
        INSERT INTO ai_credit_packages (name, description, credits, price_inr)
        VALUES ($1, $2, $3, $4)
        RETURNING id, name, description, credits, price_inr, is_active, created_at, updated_at
        "#,
    )
    .bind(&body.name)
    .bind(&body.description)
    .bind(body.credits)
    .bind(body.price_inr)
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok(Json(package))
}

#[derive(Debug, Deserialize)]
struct UpdatePackageRequest {
    name: Option<String>,
    description: Option<String>,
    credits: Option<i32>,
    price_inr: Option<i32>,
    is_active: Option<bool>,
}

async fn admin_update_package(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(body): Json<UpdatePackageRequest>,
) -> Result<Json<AdminAiCreditPackageRow>, (StatusCode, String)> {
    require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Admin access required".to_string()))?;

    if body.credits.is_some_and(|c| c <= 0) || body.price_inr.is_some_and(|p| p <= 0) {
        return Err((StatusCode::BAD_REQUEST, "credits and price_inr must be positive".to_string()));
    }

    let package = sqlx::query_as::<_, AdminAiCreditPackageRow>(
        r#"
        UPDATE ai_credit_packages
        SET name = COALESCE($1, name),
            description = COALESCE($2, description),
            credits = COALESCE($3, credits),
            price_inr = COALESCE($4, price_inr),
            is_active = COALESCE($5, is_active),
            updated_at = NOW()
        WHERE id = $6
        RETURNING id, name, description, credits, price_inr, is_active, created_at, updated_at
        "#,
    )
    .bind(&body.name)
    .bind(&body.description)
    .bind(body.credits)
    .bind(body.price_inr)
    .bind(body.is_active)
    .bind(id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    .ok_or((StatusCode::NOT_FOUND, "Package not found".to_string()))?;

    Ok(Json(package))
}
