use crate::AppState;
use axum::{
    extract::State,
    http::StatusCode,
    response::IntoResponse,
    routing::{get, patch, post, put},
    Json, Router,
};
use contracts::auth_middleware::{require_admin, AuthUser};
use serde::{Deserialize, Serialize};

const PAYU_GATEWAY_KEY: &str = "PAYU";
const MASK_CHAR: char = '\u{2022}'; // •

/// Masks a secret so only its last 4 characters are ever exposed to the browser,
/// e.g. "••••••••3f2a". Never returns the real value.
fn mask_secret(secret: &str) -> String {
    if secret.is_empty() {
        return String::new();
    }
    let chars: Vec<char> = secret.chars().collect();
    if chars.len() <= 4 {
        return MASK_CHAR.to_string().repeat(chars.len().max(4));
    }
    let last4: String = chars[chars.len() - 4..].iter().collect();
    format!("{}{}", MASK_CHAR.to_string().repeat(8), last4)
}

/// True if the value looks like a masked placeholder (contains the mask char),
/// meaning the admin did not actually type a new secret and the field should be
/// left untouched server-side rather than overwritten with the placeholder.
fn is_masked_placeholder(value: &str) -> bool {
    value.chars().any(|c| c == MASK_CHAR) || value.trim().is_empty()
}

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/", get(get_payment_gateway_config))
        .route("/", post(upsert_payment_gateway_config))
        .route("/", put(upsert_payment_gateway_config))
        .route("/", patch(upsert_payment_gateway_config))
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct PaymentGatewayConfigPayload {
    provider: String,
    mode: String,
    enabled: bool,
    base_url: String,
    callback_url: String,
    webhook_url: String,
    merchant_id: String,
    api_key: String,
    secret_key: String,
}

#[derive(Debug, sqlx::FromRow)]
struct PaymentGatewayRow {
    display_name: Option<String>,
    config_json: Option<serde_json::Value>,
    is_active: bool,
}

#[derive(Debug, sqlx::FromRow)]
struct PaymentGatewayRowWithId {
    id: uuid::Uuid,
    display_name: Option<String>,
    config_json: Option<serde_json::Value>,
    is_active: bool,
}

impl Default for PaymentGatewayConfigPayload {
    fn default() -> Self {
        Self {
            provider: "PayU".to_string(),
            mode: "sandbox".to_string(),
            enabled: true,
            base_url: "https://test.payu.in".to_string(),
            callback_url: "".to_string(),
            webhook_url: "".to_string(),
            merchant_id: "".to_string(),
            api_key: "".to_string(),
            secret_key: "".to_string(),
        }
    }
}

fn normalize_payload(payload: PaymentGatewayConfigPayload) -> PaymentGatewayConfigPayload {
    PaymentGatewayConfigPayload {
        provider: if payload.provider.trim().is_empty() {
            "PayU".to_string()
        } else {
            payload.provider.trim().to_string()
        },
        mode: if payload.mode.eq_ignore_ascii_case("live") {
            "live".to_string()
        } else {
            "sandbox".to_string()
        },
        enabled: payload.enabled,
        base_url: if payload.base_url.trim().is_empty() {
            "https://test.payu.in".to_string()
        } else {
            payload.base_url.trim().to_string()
        },
        callback_url: payload.callback_url.trim().to_string(),
        webhook_url: payload.webhook_url.trim().to_string(),
        merchant_id: payload.merchant_id.trim().to_string(),
        api_key: payload.api_key.trim().to_string(),
        secret_key: payload.secret_key.trim().to_string(),
    }
}

fn row_to_payload(row: PaymentGatewayRow) -> PaymentGatewayConfigPayload {
    let mut payload = PaymentGatewayConfigPayload::default();
    payload.provider = row
        .display_name
        .filter(|v| !v.trim().is_empty())
        .unwrap_or_else(|| "PayU".to_string());
    payload.enabled = row.is_active;

    if let Some(config) = row.config_json {
        payload.mode = config
            .get("mode")
            .and_then(|v| v.as_str())
            .unwrap_or("sandbox")
            .to_string();
        payload.base_url = config
            .get("base_url")
            .and_then(|v| v.as_str())
            .unwrap_or("https://test.payu.in")
            .to_string();
        payload.callback_url = config
            .get("callback_url")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        payload.webhook_url = config
            .get("webhook_url")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        payload.merchant_id = config
            .get("merchant_id")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        payload.api_key = config
            .get("api_key")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        payload.secret_key = config
            .get("secret_key")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
    }

    payload
}

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

    let row = sqlx::query_as::<_, PaymentGatewayRow>(
        r#"
        SELECT display_name, config_json, is_active
        FROM payment_gateway_configs
        WHERE gateway_key = $1
        ORDER BY is_active DESC, created_at DESC
        LIMIT 1
        "#,
    )
    .bind(PAYU_GATEWAY_KEY)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let mut payload = match row {
        Some(row) => row_to_payload(row),
        None => {
            let mut defaults = PaymentGatewayConfigPayload::default();
            defaults.api_key = std::env::var("PAYU_MERCHANT_KEY").unwrap_or_default();
            defaults.secret_key = std::env::var("PAYU_SALT").unwrap_or_default();
            defaults
        }
    };

    // Never send the real merchant salt to the browser — mask it down to its
    // last 4 characters. The full value stays server-side only.
    payload.secret_key = mask_secret(&payload.secret_key);

    Ok((StatusCode::OK, Json(serde_json::json!({ "config": payload }))))
}

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

    let mut payload = normalize_payload(payload);

    // Look up the currently stored row first, both to know whether to INSERT vs
    // UPDATE and to recover the real secret if the admin submitted the masked
    // placeholder back unchanged (the browser never has the real value to send).
    let existing_row = sqlx::query_as::<_, PaymentGatewayRowWithId>(
        r#"
        SELECT id, display_name, config_json, is_active
        FROM payment_gateway_configs
        WHERE gateway_key = $1
        ORDER BY created_at DESC
        LIMIT 1
        "#,
    )
    .bind(PAYU_GATEWAY_KEY)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    if is_masked_placeholder(&payload.secret_key) {
        // Admin didn't actually change the salt — keep the real stored value
        // instead of overwriting it with the masked placeholder string.
        let existing_secret_key = existing_row
            .as_ref()
            .and_then(|r| r.config_json.as_ref())
            .and_then(|c| c.get("secret_key"))
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        payload.secret_key = existing_secret_key;
    }

    let config_json = serde_json::json!({
        "mode": payload.mode,
        "base_url": payload.base_url,
        "callback_url": payload.callback_url,
        "webhook_url": payload.webhook_url,
        "merchant_id": payload.merchant_id,
        "api_key": payload.api_key,
        "secret_key": payload.secret_key,
    });

    let existing_id = existing_row.map(|r| r.id);

    let row = if let Some(id) = existing_id {
        sqlx::query_as::<_, PaymentGatewayRow>(
            r#"
            UPDATE payment_gateway_configs
            SET display_name = $2,
                config_json = $3,
                is_active = $4
            WHERE id = $1
            RETURNING display_name, config_json, is_active
            "#,
        )
        .bind(id)
        .bind(&payload.provider)
        .bind(config_json)
        .bind(payload.enabled)
        .fetch_one(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    } else {
        sqlx::query_as::<_, PaymentGatewayRow>(
            r#"
            INSERT INTO payment_gateway_configs (gateway_key, display_name, config_json, is_active)
            VALUES ($1, $2, $3, $4)
            RETURNING display_name, config_json, is_active
            "#,
        )
        .bind(PAYU_GATEWAY_KEY)
        .bind(&payload.provider)
        .bind(config_json)
        .bind(payload.enabled)
        .fetch_one(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    };

    let mut response_payload = row_to_payload(row);
    // Never echo the real salt back in the save response either — mask it
    // just like the GET endpoint does.
    response_payload.secret_key = mask_secret(&response_payload.secret_key);

    Ok((
        StatusCode::OK,
        Json(serde_json::json!({
            "message": "Payment gateway configuration saved successfully.",
            "config": response_payload,
        })),
    ))
}
