// handlers/payment.rs — 支付相关处理器（接入支付宝）
use actix_web::{get, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use rsa::pkcs1v15::{Pkcs1v15Sign, SigningKey, VerifyingKey};
use rsa::pkcs8::DecodePrivateKey;
use rsa::signature::{SignatureEncoding, Signer, Verifier};
use rsa::RsaPrivateKey;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use sqlx::postgres::PgPool;
use std::collections::BTreeMap;
use tracing::info;
use uuid::Uuid;

use crate::db;
use crate::error::AppError;
use crate::models::{Claims, CreateOrderRequest, MockConfirmRequest};

// ===== 套餐定义 =====

struct PackageInfo {
    amount: i32,
    display_amount: &'static str,
    display_name: &'static str,
    days: Option<i64>,
}

fn get_package_info(package_type: &str) -> Option<PackageInfo> {
    match package_type {
        "monthly" => Some(PackageInfo {
            amount: 990,
            display_amount: "9.9",
            display_name: "包月会员",
            days: Some(30),
        }),
        "yearly" => Some(PackageInfo {
            amount: 5900,
            display_amount: "59",
            display_name: "包年会员",
            days: Some(365),
        }),
        "permanent" => Some(PackageInfo {
            amount: 19900,
            display_amount: "199",
            display_name: "永久会员",
            days: None,
        }),
        _ => None,
    }
}

// ===== 支付宝配置 =====

struct AlipayConfig {
    app_id: String,
    private_key: String,
    alipay_public_key: String,
    gateway: String,
}

impl AlipayConfig {
    fn from_env() -> Option<Self> {
        Some(Self {
            app_id: std::env::var("ALIPAY_APP_ID").ok()?,
            private_key: std::env::var("ALIPAY_PRIVATE_KEY").ok()?,
            alipay_public_key: std::env::var("ALIPAY_ALIPAY_PUBLIC_KEY").ok()?,
            gateway: std::env::var("ALIPAY_GATEWAY")
                .unwrap_or_else(|_| "https://openapi.alipay.com/gateway.do".to_string()),
        })
    }
}

/// URL 编码（RFC 3986）
fn urlencoding(s: &str) -> String {
    let mut result = String::new();
    for c in s.chars() {
        match c {
            'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
            _ => {
                for b in c.to_string().as_bytes() {
                    result.push_str(&format!("%{:02X}", b));
                }
            }
        }
    }
    result
}

/// RSA2 (SHA256) 签名
fn rsa2_sign(content: &str, private_key_pem: &str) -> Result<String, String> {
    let private_key = match RsaPrivateKey::from_pkcs8_pem(private_key_pem) {
        Ok(key) => key,
        Err(_) => {
            let decoded = base64::Engine::decode(
                &base64::engine::general_purpose::STANDARD,
                private_key_pem,
            )
            .map_err(|e| format!("Base64 解码失败: {}", e))?;
            let pem_str = String::from_utf8(decoded)
                .map_err(|e| format!("PEM 字符串格式错误: {}", e))?;
            RsaPrivateKey::from_pkcs8_pem(&pem_str)
                .map_err(|e| format!("私钥解析失败: {}", e))?
        }
    };
    let signing_key = SigningKey::<Sha256>::new(private_key);
    let signature = signing_key.sign(content.as_bytes());
    Ok(base64::Engine::encode(
        &base64::engine::general_purpose::STANDARD,
        signature.to_bytes().as_ref(),
    ))
}

/// 验证 RSA2 签名
#[allow(dead_code)]
fn rsa2_verify(content: &str, sign: &str, public_key_pem: &str) -> Result<bool, String> {
    use rsa::pkcs8::DecodePublicKey;
    use rsa::RsaPublicKey;
    use sha2::Digest;
    let public_key =
        RsaPublicKey::from_public_key_pem(public_key_pem)
            .map_err(|e| format!("支付宝公钥解析失败: {}", e))?;
    let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, sign)
        .map_err(|e| format!("签名 Base64 解码失败: {}", e))?;
    let hashed = Sha256::digest(content.as_bytes());
    public_key
        .verify(rsa::Pkcs1v15Sign::new::<Sha256>(), &hashed, &sig_bytes)
        .map_err(|e| format!("签名验证失败: {}", e))?;
    Ok(true)
}

// ===== Helper: 生成支付宝支付表单 HTML =====

fn build_alipay_form_html(
    config: &AlipayConfig,
    out_trade_no: &str,
    total_amount: &str,
    subject: &str,
    notify_url: &str,
    return_url: &str,
) -> Result<String, String> {
    let biz_content = serde_json::json!({
        "out_trade_no": out_trade_no,
        "total_amount": total_amount,
        "subject": subject,
        "product_code": "FAST_INSTANT_TRADE_PAY",
    });

    let biz_content_str =
        serde_json::to_string(&biz_content).map_err(|e| format!("biz_content 序列化失败: {}", e))?;

    let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
    let mut params: BTreeMap<&str, &str> = BTreeMap::new();
    params.insert("app_id", &config.app_id);
    params.insert("method", "alipay.trade.page.pay");
    params.insert("format", "JSON");
    params.insert("charset", "utf-8");
    params.insert("sign_type", "RSA2");
    params.insert("timestamp", &timestamp);
    params.insert("version", "1.0");
    params.insert("biz_content", &biz_content_str);
    params.insert("notify_url", notify_url);
    params.insert("return_url", return_url);

    // 构造待签名串（注意：签名源使用原始值，不做 URL 编码）
    let sign_source: String = params
        .iter()
        .map(|(k, v)| format!("{}={}", k, *v))
        .collect::<Vec<_>>()
        .join("&");

    let sign = rsa2_sign(&sign_source, &config.private_key)?;

    // 生成自动提交的表单 HTML（HTML 转义防止特殊字符破坏属性）
    fn h(s: &str) -> String {
        s.replace('&', "&amp;")
            .replace('"', "&quot;")
            .replace('<', "&lt;")
            .replace('>', "&gt;")
    }
    let mut form_fields = String::new();
    for (k, v) in &params {
        form_fields.push_str(&format!(
            r#"<input type="hidden" name="{}" value="{}" />"#,
            h(k), h(v)
        ));
    }
    form_fields.push_str(&format!(
        r#"<input type="hidden" name="sign" value="{}" />"#,
        h(&sign)
    ));

    let html = format!(
        r##"<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>正在跳转支付宝...</title></head>
<body>
    <form id="alipay" action="{}" method="get">
        {}
    </form>
    <script>document.getElementById('alipay').submit();</script>
</body>
</html>"##,
        config.gateway, form_fields
    );

    Ok(html)
}

// ===== 提取 JWT token =====

fn extract_token(req: &HttpRequest) -> Option<String> {
    req.headers()
        .get("Authorization")?
        .to_str()
        .ok()?
        .strip_prefix("Bearer ")
        .map(|s| s.to_string())
}

fn get_jwt_secret() -> String {
    std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string())
}

// ===== Handler: GET /payment — 套餐选择页（网页端微信扫码登录） =====

#[get("/payment")]
pub async fn payment_index() -> Result<HttpResponse, AppError> {
    let html = r##"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开通会员 - 大气稳定度判定</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', sans-serif; background: #f0f2f5; min-height: 100vh; }
  .header { text-align: center; padding: 60px 0 40px; }
  .header h1 { font-size: 28px; color: #333; margin-bottom: 8px; }
  .header p { font-size: 14px; color: #999; }

  /* ===== 登录区域 ===== */
  .login-section { max-width: 400px; margin: 0 auto; padding: 0 24px; }
  .login-card { background: #fff; border-radius: 20px; padding: 48px 32px; text-align: center; box-shadow: 0 4px 24px rgba(0,0,0,0.06); }
  .login-icon { width: 80px; height: 80px; margin: 0 auto 24px; background: #07c160; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
  .login-icon svg { width: 48px; height: 48px; }
  .login-title { font-size: 20px; font-weight: 600; color: #333; margin-bottom: 8px; }
  .login-desc { font-size: 14px; color: #999; margin-bottom: 32px; line-height: 1.6; }

  /* 登录码展示 */
  .code-display { background: #f7f8fa; border-radius: 12px; padding: 24px; margin-bottom: 24px; }
  .code-label { font-size: 13px; color: #999; margin-bottom: 12px; }
  .code-value { font-size: 32px; font-weight: bold; color: #07c160; letter-spacing: 4px; font-family: 'SF Mono', monospace; }
  .code-hint { font-size: 12px; color: #bbb; margin-top: 8px; }

  /* 扫码状态 */
  .scan-status { padding: 16px; border-radius: 12px; margin-bottom: 24px; font-size: 14px; }
  .scan-status.waiting { background: #fff7e6; color: #ad6800; }
  .scan-status.confirmed { background: #f6ffed; color: #52c41a; }

  .btn-login { display: block; width: 100%; background: #07c160; color: #fff; border: none; border-radius: 12px; padding: 16px; font-size: 17px; font-weight: 600; cursor: pointer; margin-bottom: 16px; }
  .btn-login:disabled { background: #d9d9d9; cursor: not-allowed; }
  .btn-refresh { background: #fff; color: #666; border: 1px solid #d9d9d9; }
  .login-note { font-size: 12px; color: #bbb; margin-top: 12px; }

  /* ===== 会员区域 ===== */
  .paid-section { display: none; }
  .paid-banner { background: linear-gradient(135deg, #07c160, #06ad56); color: #fff; padding: 48px 24px; text-align: center; }
  .paid-banner h2 { font-size: 24px; margin-bottom: 8px; }
  .paid-banner p { font-size: 14px; opacity: 0.9; }
  .paid-info { background: #fff; margin: -20px 16px 16px; border-radius: 16px; padding: 24px; box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
  .paid-info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f0f0f0; font-size: 15px; }
  .paid-info-row:last-child { border-bottom: none; }
  .paid-info-label { color: #999; }
  .paid-info-value { color: #333; font-weight: 500; }
  .paid-badge { display: inline-block; background: #07c160; color: #fff; font-size: 12px; padding: 2px 8px; border-radius: 4px; }

  /* ===== 套餐区域 ===== */
  .packages { display: none; max-width: 480px; margin: 0 auto; padding: 0 16px 80px; }
  .packages-title { text-align: center; padding: 40px 0 24px; font-size: 20px; color: #333; }
  .pkg-card { background: #fff; border-radius: 16px; padding: 24px; margin-bottom: 16px; cursor: pointer; transition: all 0.2s; border: 2px solid transparent; position: relative; }
  .pkg-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
  .pkg-card.selected { border-color: #07c160; background: #f0f7ff; }
  .pkg-tag { position: absolute; top: -1px; right: 16px; background: #07c160; color: #fff; font-size: 12px; padding: 4px 10px; border-radius: 0 0 8px 8px; }
  .pkg-name { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 8px; }
  .pkg-price { font-size: 32px; font-weight: 700; color: #07c160; margin-bottom: 4px; }
  .pkg-price .unit { font-size: 14px; font-weight: 400; }
  .pkg-desc { font-size: 13px; color: #999; }
  .pkg-features { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
  .pkg-feature { font-size: 13px; color: #666; margin-bottom: 6px; }
  .btn-pay { display: block; width: 100%; max-width: 480px; margin: 0 auto 16px; background: #07c160; color: #fff; border: none; border-radius: 12px; padding: 16px; font-size: 17px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
  .btn-pay:hover { background: #06ad56; }
  .btn-pay:disabled { background: #d9d9d9; cursor: not-allowed; }
  .notice { text-align: center; font-size: 12px; color: #bbb; }
  .btn-logout { display: block; width: 100%; max-width: 480px; margin: 0 auto; background: #fff; color: #999; border: 1px solid #d9d9d9; border-radius: 12px; padding: 12px; font-size: 14px; cursor: pointer; }
  .btn-logout:hover { color: #666; border-color: #999; }

  /* 错误提示 */
  .error-msg { background: #fff2f0; color: #cf1322; border: 1px solid #ffccc7; border-radius: 8px; padding: 12px; margin-bottom: 16px; font-size: 14px; display: none; }
</style>
</head>
<body>

<!-- 登录区（等待授权） -->
<div class="login-section" id="loginSection">
  <div class="login-card">
    <div class="login-icon">
      <svg viewBox="0 0 24 24" fill="#fff">
        <path d="M8.68 10.74a.5.5 0 01-.01.85l-3.6 2.88a.5.5 0 01-.74-.38V7.13a.5.5 0 01.74-.38l3.6 2.88a.5.5 0 01.01.85l-1.6 1.28 1.6 1.28z"/>
        <path d="M12.02 5.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zm0 10.5a4 4 0 110-8 4 4 0 010 8z"/>
        <path d="M15.32 8.68a.5.5 0 01.01.85l-1.6 1.28 1.6 1.28a.5.5 0 01-.74.38l-3.6-2.88a.5.5 0 01.01-.85l3.6-2.88a.5.5 0 01.72.38v3.22z"/>
      </svg>
    </div>
    <div class="login-title">等待授权</div>
    <div class="login-desc" id="loginDesc">请在微信小程序中<br>点击「去授权」完成登录</div>

    <div class="error-msg" id="errorMsg"></div>

    <div class="code-display" id="codeDisplay" style="display:none">
      <div class="code-label">登录码</div>
      <div class="code-value" id="codeValue">--</div>
    </div>

    <div class="scan-status waiting" id="scanStatus" style="display:none">
      等待小程序授权确认...
    </div>
  </div>
</div>

<!-- 会员信息区（已登录） -->
<div class="paid-section" id="paidSection">
  <div class="paid-banner">
    <h2 id="paidTitle">会员专享福利</h2>
    <p id="paidSubtitle">解锁无限检测额度</p>
  </div>
  <div class="paid-info">
    <div class="paid-info-row">
      <span class="paid-info-label">会员状态</span>
      <span class="paid-info-value"><span class="paid-badge" id="paidBadge">付费会员</span></span>
    </div>
    <div class="paid-info-row" id="expiresRow">
      <span class="paid-info-label">到期时间</span>
      <span class="paid-info-value" id="paidExpires">--</span>
    </div>
    <div class="paid-info-row">
      <span class="paid-info-label">检测额度</span>
      <span class="paid-info-value">无限次</span>
    </div>
  </div>
</div>

<!-- 套餐区（登录后显示） -->
<div class="packages" id="packages">
  <div class="packages-title">选择会员套餐</div>

  <div class="pkg-card" data-package="monthly" onclick="selectPackage('monthly')">
    <div class="pkg-name">包月会员</div>
    <div class="pkg-price">¥9.9<span class="unit">/月</span></div>
    <div class="pkg-desc">适合短期使用需求</div>
    <div class="pkg-features">
      <div class="pkg-feature">每月 500 次检测额度</div>
      <div class="pkg-feature">查看完整历史记录</div>
    </div>
  </div>

  <div class="pkg-card" data-package="yearly" onclick="selectPackage('yearly')">
    <div class="pkg-tag">推荐</div>
    <div class="pkg-name">包年会员</div>
    <div class="pkg-price">¥59<span class="unit">/年</span></div>
    <div class="pkg-desc">相当于每月 ¥4.9，性价比最高</div>
    <div class="pkg-features">
      <div class="pkg-feature">每年 5000 次检测额度</div>
      <div class="pkg-feature">查看完整历史记录</div>
    </div>
  </div>

  <button class="btn-pay" id="payBtn" onclick="goPay()" disabled>请先选择套餐</button>
  <button class="btn-logout" onclick="logout()">退出登录</button>
  <div class="notice" style="margin-top:16px">支付成功后额度将自动到账</div>
</div>

<script>
  const API_BASE = '';  // 同源
  let currentShortCode = '';
  let pollTimer = null;
  let jwt = '';
  let isPaidActive = false;
  let paidExpiresAt = null;

  // ---- 页面初始化：检查 URL 中的 code 或 jwt 参数 ----
  (function initFromUrl() {
    const params = new URLSearchParams(window.location.search);
    const urlJwt = params.get('jwt');
    if (urlJwt) {
      jwt = urlJwt;
      checkPaidStatus(urlJwt);
      return;
    }
    const urlCode = params.get('code');
    if (urlCode) {
      currentShortCode = urlCode;
      document.getElementById('codeValue').textContent = currentShortCode;
      document.getElementById('codeDisplay').style.display = 'block';
      document.getElementById('scanStatus').style.display = 'block';
      document.getElementById('loginDesc').style.display = 'none';
      // 启动轮询
      if (pollTimer) clearInterval(pollTimer);
      pollTimer = setInterval(pollLoginStatus, 2000);
    }
})();

  async function checkPaidStatus(token) {
    try {
      const resp = await fetch(API_BASE + '/api/user/profile', {
        headers: { 'Authorization': 'Bearer ' + token }
      });
      if (resp.ok) {
        const data = await resp.json();
        if (data.success) {
          isPaidActive = data.data.is_paid_active || false;
          paidExpiresAt = data.data.paid_expires_at || null;
        }
      }
    } catch(e) {}
    showLoggedIn();
  }

  async function pollLoginStatus() {
    if (!currentShortCode) return;
    try {
      const resp = await fetch(API_BASE + '/payment/login-status?code=' + encodeURIComponent(currentShortCode));
      const data = await resp.json();
      if (data.success && data.token) {
        jwt = data.token;
        isPaidActive = data.is_paid_active || false;
        paidExpiresAt = data.paid_expires_at;
        clearInterval(pollTimer);
        showLoggedIn();
      }
    } catch(e) {
      // 继续轮询
    }
  }

  function showLoggedIn() {
    document.getElementById('loginSection').style.display = 'none';
    if (isPaidActive) {
      // 已付费用户，显示会员信息
      document.getElementById('paidSection').style.display = 'block';
      document.getElementById('paidBadge').textContent = '付费会员';
      if (paidExpiresAt) {
        document.getElementById('paidExpires').textContent = new Date(paidExpiresAt).toLocaleString('zh-CN');
        document.getElementById('expiresRow').style.display = 'flex';
      } else {
        document.getElementById('paidExpires').textContent = '永久有效';
        document.getElementById('paidBadge').textContent = '永久会员';
      }
      document.getElementById('packages').style.display = 'none';
    } else {
      // 未付费用户，显示套餐
      document.getElementById('paidSection').style.display = 'none';
      document.getElementById('packages').style.display = 'block';
    }
  }

  function logout() {
    jwt = '';
    currentShortCode = '';
    isPaidActive = false;
    paidExpiresAt = null;
    document.getElementById('loginSection').style.display = 'block';
    document.getElementById('paidSection').style.display = 'none';
    document.getElementById('packages').style.display = 'none';
    document.getElementById('codeDisplay').style.display = 'none';
    document.getElementById('scanStatus').style.display = 'none';
    document.getElementById('loginDesc').style.display = 'block';
  }

  function showError(msg) {
    const el = document.getElementById('errorMsg');
    el.textContent = msg;
    el.style.display = 'block';
  }

  function hideError() {
    document.getElementById('errorMsg').style.display = 'none';
  }

  // ---- 套餐选择 ----
  let selected = null;
  function selectPackage(pkg) {
    selected = pkg;
    document.querySelectorAll('.pkg-card').forEach(c => c.classList.remove('selected'));
    document.querySelector('[data-package="' + pkg + '"]').classList.add('selected');
    var btn = document.getElementById('payBtn');
    var labels = { monthly: '立即开通 - ¥9.9/月', yearly: '立即开通 - ¥59/年' };
    btn.textContent = labels[pkg];
    btn.disabled = false;
    btn.className = 'btn-pay';
  }

  function goPay() {
    if (!selected || !jwt) {
      showError('请先登录');
      return;
    }
    window.location.href = '/payment/page?package=' + selected + '&jwt=' + encodeURIComponent(jwt);
  }
</script>
</body>
</html>"##;

    Ok(HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html))
}

// ===== Handler: GET /payment/page — 创建订单并跳转支付宝 =====

#[derive(Debug, Deserialize)]
pub struct PaymentPageQuery {
    #[serde(rename = "package")]
    pub package_: String,
    /// JWT: 从 URL 参数传入（外部浏览器无 Cookie 时使用）
    #[serde(default)]
    pub jwt: Option<String>,
}

#[get("/payment/page")]
pub async fn payment_page(
    req: HttpRequest,
    pool: web::Data<PgPool>,
    query: web::Query<PaymentPageQuery>,
) -> Result<HttpResponse, AppError> {
    let token = extract_token(&req)
        .or_else(|| query.jwt.clone())
        .ok_or_else(|| AppError::Unauthorized("未登录".to_string()))?;

    let claims = crate::auth::verify_token(&token, &get_jwt_secret())
        .map_err(|_| AppError::Unauthorized("Token 无效".to_string()))?;

    let pkg = get_package_info(&query.package_)
        .ok_or_else(|| AppError::BadRequest("无效的套餐类型".to_string()))?;

    let order_no = Uuid::new_v4().to_string();
    let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d));

    db::create_payment_order(
        pool.get_ref(),
        claims.user_id,
        &order_no,
        &query.package_,
        pkg.amount,
        expires_at,
    )
    .await?;

    let base_url = std::env::var("APP_BASE_URL")
        .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string());
    let notify_url = format!("{}/payment/notify", base_url);
    let return_url = format!("{}/payment/success?order_no={}", base_url, order_no);

    let Some(config) = AlipayConfig::from_env() else {
        let jwt_for_mock = token.clone();
        let mock_html = format!(r#"<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>模拟支付</title></head>
<body style="font-family:-apple-system;padding:40px;text-align:center">
<h2 style="color:#52c41a">模拟支付环境</h2>
<p style="color:#666">当前为沙箱模拟环境，无需真实支付</p>
<p style="color:#999;font-size:14px">订单号: {}</p>
<p style="color:#999;font-size:14px">套餐: {}</p>
<button id="confirmBtn" onclick="confirmMockPay()" style="padding:12px 32px;font-size:16px;background:#1677ff;color:#fff;border:none;border-radius:4px;cursor:pointer">确认模拟支付</button>
<p id="result"></p>
<p><a href="/payment" style="color:#1677ff">返回重试</a></p>
<script>
function confirmMockPay() {{
    var btn = document.getElementById('confirmBtn');
    btn.disabled = true;
    btn.textContent = '处理中...';
    fetch('/api/payment/mock-confirm', {{
        method: 'POST',
        headers: {{
            'Content-Type': 'application/json',
            'Authorization': 'Bearer {}'
        }},
        body: JSON.stringify({{order_id: '{}'}})
    }})
    .then(r => r.json())
    .then(data => {{
        if (data.success) {{
            document.getElementById('result').innerHTML = '<span style="color:#52c41a">支付成功！</span>';
            setTimeout(() => window.location.href = '/payment?mock=1', 1500);
        }} else {{
            document.getElementById('result').innerHTML = '<span style="color:#f5222d">失败: ' + (data.error || '未知错误') + '</span>';
            btn.disabled = false;
            btn.textContent = '重试';
        }}
    }})
    .catch(e => {{
        document.getElementById('result').innerHTML = '<span style="color:#f5222d">网络错误</span>';
        btn.disabled = false;
        btn.textContent = '重试';
    }});
}}
</script>
</body>
</html>
"#, order_no, pkg.display_name, jwt_for_mock, order_no);
        return Ok(HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(mock_html));
    };

    let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0);

    match build_alipay_form_html(
        &config,
        &order_no,
        &total_amount_str,
        pkg.display_name,
        &notify_url,
        &return_url,
    ) {
        Ok(form_html) => Ok(HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(form_html)),
        Err(e) => {
            tracing::error!("支付宝下单失败: {}", e);
            Ok(HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(format_error_html(&e, &order_no)))
        }
    }
}

// ===== Handler: GET /payment/pay — 直接支付接口 =====

#[derive(Debug, Deserialize)]
pub struct AlipayPayQuery {
    pub order_no: String,
    pub package_type: String,
}

#[get("/payment/pay")]
pub async fn alipay_pay_page(
    req: HttpRequest,
    query: web::Query<AlipayPayQuery>,
) -> Result<HttpResponse, AppError> {
    let token = extract_token(&req).ok_or_else(|| AppError::Unauthorized("未登录".to_string()))?;
    crate::auth::verify_token(&token, &get_jwt_secret())
        .map_err(|_| AppError::Unauthorized("Token 无效".to_string()))?;

    let pkg = get_package_info(&query.package_type)
        .ok_or_else(|| AppError::BadRequest("无效的套餐类型".to_string()))?;

    let base_url = std::env::var("APP_BASE_URL")
        .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string());
    let notify_url = format!("{}/payment/notify", base_url);
    let return_url = format!("{}/payment/success?order_no={}", base_url, query.order_no);

    let Some(config) = AlipayConfig::from_env() else {
        let jwt_for_mock = token.clone();
        let mock_html = format!(r#"<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>模拟支付</title></head>
<body style="font-family:-apple-system;padding:40px;text-align:center">
<h2 style="color:#52c41a">模拟支付环境</h2>
<p style="color:#666">当前为沙箱模拟环境，无需真实支付</p>
<p style="color:#999;font-size:14px">订单号: {}</p>
<p style="color:#999;font-size:14px">套餐: {}</p>
<button id="confirmBtn" onclick="confirmMockPay()" style="padding:12px 32px;font-size:16px;background:#1677ff;color:#fff;border:none;border-radius:4px;cursor:pointer">确认模拟支付</button>
<p id="result"></p>
<p><a href="/payment" style="color:#1677ff">返回重试</a></p>
<script>
function confirmMockPay() {{
    var btn = document.getElementById('confirmBtn');
    btn.disabled = true;
    btn.textContent = '处理中...';
    fetch('/api/payment/mock-confirm', {{
        method: 'POST',
        headers: {{
            'Content-Type': 'application/json',
            'Authorization': 'Bearer {}'
        }},
        body: JSON.stringify({{order_id: '{}'}})
    }})
    .then(r => r.json())
    .then(data => {{
        if (data.success) {{
            document.getElementById('result').innerHTML = '<span style="color:#52c41a">支付成功！</span>';
            setTimeout(() => window.location.href = '/payment?mock=1', 1500);
        }} else {{
            document.getElementById('result').innerHTML = '<span style="color:#f5222d">失败: ' + (data.error || '未知错误') + '</span>';
            btn.disabled = false;
            btn.textContent = '重试';
        }}
    }})
    .catch(e => {{
        document.getElementById('result').innerHTML = '<span style="color:#f5222d">网络错误</span>';
        btn.disabled = false;
        btn.textContent = '重试';
    }});
}}
</script>
</body>
</html>
"#, query.order_no, pkg.display_name, jwt_for_mock, query.order_no);
        return Ok(HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(mock_html));
    };

    let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0);

    match build_alipay_form_html(
        &config,
        &query.order_no,
        &total_amount_str,
        pkg.display_name,
        &notify_url,
        &return_url,
    ) {
        Ok(form_html) => Ok(HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(form_html)),
        Err(e) => {
            tracing::error!("支付宝下单失败: {}", e);
            Ok(HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(format_error_html(&e, &query.order_no)))
        }
    }
}

// ===== Handler: POST /payment/notify — 支付宝异步回调 =====

#[derive(Debug, Deserialize)]
pub struct AlipayNotify {
    pub out_trade_no: String,
    pub trade_no: String,
    pub trade_status: String,
    pub total_amount: Option<String>,
    pub app_id: Option<String>,
    pub sign: Option<String>,
}

#[post("/payment/notify")]
pub async fn alipay_notify(
    pool: web::Data<PgPool>,
    body: web::Form<AlipayNotify>,
) -> HttpResponse {
    let body = body.into_inner();

    tracing::info!(
        "收到支付宝回调: out_trade_no={}, trade_status={}",
        body.out_trade_no,
        body.trade_status
    );

    // 1. 检查交易状态
    if body.trade_status != "TRADE_SUCCESS" && body.trade_status != "TRADE_FINISHED" {
        return HttpResponse::Ok().body("success");
    }

    // 2. 确认订单（通过 order_no，不校验 user_id）
    match db::confirm_payment_order_by_orderno(pool.get_ref(), &body.out_trade_no).await {
        Ok(_) => {
            tracing::info!("订单 {} 支付确认成功", body.out_trade_no);
            HttpResponse::Ok().body("success")
        }
        Err(e) => {
            tracing::error!("订单 {} 确认失败: {}", body.out_trade_no, e);
            HttpResponse::Ok().body("fail")
        }
    }
}

// ===== Handler: GET /payment/success — 支付成功页面 =====

#[derive(Debug, Deserialize)]
pub struct AlipaySuccessQuery {
    pub order_no: Option<String>,
}

fn build_success_html(order_no: &str) -> String {
    let green = "#52c41a";
    let white = "white";
    let html = format!(
        r##"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支付成功</title>
<style>
  body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f0f2f5; margin: 0; padding: 40px; text-align: center; }}
  .card {{ background: #fff; border-radius: 16px; padding: 48px 32px; max-width: 400px; margin: 0 auto; box-shadow: 0 2px 12px rgba(0,0,0,0.1); }}
  .icon {{ width: 64px; height: 64px; margin-bottom: 16px; }}
  h2 {{ color: {0}; font-size: 22px; margin-bottom: 8px; }}
  p {{ color: #666; font-size: 14px; margin-bottom: 24px; }}
  .tip {{ background: #f0f7ff; border-radius: 8px; padding: 16px; font-size: 13px; color: #1677ff; margin-top: 16px; }}
  .order-no {{ font-size: 12px; color: #bbb; margin-top: 12px; }}
</style>
</head>
<body>
<div class="card">
  <svg class="icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
    <circle cx="32" cy="32" r="32" fill="{0}"/>
    <path d="M20 32l8 8 16-16" stroke="{1}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
  </svg>
  <h2>支付成功！</h2>
  <p>恭喜您已成为会员，额度已自动到账</p>
  <div class="tip">请返回微信小程序查看您的会员状态</div>
  <div class="order-no">订单号: {2}</div>
</div>
</body>
</html>"##,
        green, white, order_no
    );
    html
}

fn format_error_html(msg: &str, order_no: &str) -> String {
    format!(
        r##"<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>支付失败</title></head>
<body style="font-family:-apple-system;padding:40px;text-align:center">
<h2 style="color:#ff4d4f">支付页面生成失败</h2>
<p style="color:#666">{}</p>
<p style="color:#999;font-size:13px">订单号: {}</p>
<p><a href="/payment" style="color:#1677ff">返回重试</a></p>
</body>
</html>"##,
        msg, order_no
    )
}

#[get("/payment/success")]
pub async fn payment_success(
    pool: web::Data<PgPool>,
    query: web::Query<AlipaySuccessQuery>,
) -> HttpResponse {
    let order_no = query.order_no.as_deref().unwrap_or("");

    // 同步确认订单（幂等：已确认的订单会跳过）
    if !order_no.is_empty() {
        if let Err(e) = db::confirm_payment_order_by_orderno(pool.get_ref(), order_no).await {
            tracing::warn!("支付成功页同步确认失败（可能是异步回调已处理）: {}", e);
        }
    }

    let html = build_success_html(order_no);
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(html)
}

// ===== 旧的 API Handler（保持兼容）=====

/// POST /api/payment/create-order
#[post("/api/payment/create-order")]
pub async fn create_order(
    pool: web::Data<PgPool>,
    claims: web::ReqData<Claims>,
    body: web::Json<CreateOrderRequest>,
) -> Result<HttpResponse, AppError> {
    let user_id = claims.user_id;

    let pkg = match get_package_info(&body.package_type) {
        Some(p) => p,
        None => {
            return Err(AppError::BadRequest("无效的套餐类型".to_string()));
        }
    };

    let order_no = Uuid::new_v4().to_string();
    let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d));

    db::create_payment_order(
        pool.get_ref(),
        user_id,
        &order_no,
        &body.package_type,
        pkg.amount,
        expires_at,
    )
    .await?;

    Ok(HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "data": {
            "order_id": order_no,
            "package_type": body.package_type,
            "amount": pkg.amount,
            "display_amount": pkg.display_amount,
            "display_name": pkg.display_name,
            "expires_at": expires_at,
        }
    })))
}

/// POST /api/payment/mock-confirm
#[post("/api/payment/mock-confirm")]
pub async fn mock_confirm(
    pool: web::Data<PgPool>,
    claims: web::ReqData<Claims>,
    body: web::Json<MockConfirmRequest>,
) -> Result<HttpResponse, AppError> {
    let user_id = claims.user_id;

    let expires_at =
        db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await?;

    Ok(HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "data": {
            "is_paid_active": true,
            "paid_expires_at": expires_at,
        }
    })))
}

/// POST /api/payment/sync-order — 根据订单号强制同步会员状态
///
/// 用户支付完成后，若前端会员状态未刷新，可调用此接口同步确认订单并获取最新状态。
#[post("/api/payment/sync-order")]
pub async fn sync_order(
    pool: web::Data<PgPool>,
    claims: web::ReqData<Claims>,
    body: web::Json<MockConfirmRequest>,
) -> Result<HttpResponse, AppError> {
    let user_id = claims.user_id;

    // 尝试确认订单（幂等）
    let _ = db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await;

    // 查询最新状态
    let user = sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
        r#"SELECT is_paid, paid_expires_at FROM users WHERE id = $1"#,
    )
    .bind(user_id)
    .fetch_optional(pool.get_ref())
    .await
    .map_err(|e| AppError::Database(format!("查询用户失败: {}", e)))?
    .ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?;

    let (is_paid, paid_expires_at) = user;
    let is_paid_active = is_paid && (paid_expires_at.is_none() || paid_expires_at.unwrap() > Utc::now());

    Ok(HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "data": {
            "is_paid": is_paid,
            "is_paid_active": is_paid_active,
            "paid_expires_at": paid_expires_at,
        }
    })))
}

/// GET /api/payment/orders — 获取当前用户的订单记录
#[get("/api/payment/orders")]
pub async fn get_user_orders(
    pool: web::Data<PgPool>,
    claims: web::ReqData<Claims>,
) -> Result<HttpResponse, AppError> {
    let orders = db::get_user_orders(pool.get_ref(), claims.user_id).await?;

    Ok(HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "data": orders.iter().map(|o| serde_json::json!({
            "order_no": o.order_no,
            "package_type": o.package_type,
            "amount": o.amount,
            "status": o.status,
            "paid_at": o.paid_at,
            "expires_at": o.expires_at,
            "created_at": o.created_at,
        })).collect::<Vec<_>>()
    })))
}

/// GET /api/user/quota
#[get("/api/user/quota")]
pub async fn get_user_quota(
    pool: web::Data<PgPool>,
    claims: web::ReqData<Claims>,
) -> Result<HttpResponse, AppError> {
    let user_id = claims.user_id;

    let (used, is_paid_active, paid_expires_at) =
        db::get_user_quota(pool.get_ref(), user_id).await?;
    let limit: i64 = std::env::var("FREE_USER_DATA_LIMIT")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(20);

    Ok(HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "data": {
            "used": used,
            "limit": limit,
            "unlimited": is_paid_active,
            "is_paid_active": is_paid_active,
            "paid_expires_at": paid_expires_at,
        }
    })))
}

/// GET /payment/login-status?code=ASD-XXXXX
/// 网页端轮询：查询登录码是否已被小程序确认
#[derive(Debug, Deserialize)]
pub struct LoginStatusQuery {
    pub code: String,
}

#[derive(Debug, serde::Serialize)]
pub struct LoginStatusResponse {
    pub success: bool,
    pub confirmed: bool,
    pub token: Option<String>,
    pub is_paid_active: bool,
    pub paid_expires_at: Option<String>,
}

// ===== Handler: GET /payment/generate-code — 网页端生成登录码 =====

#[derive(Debug, serde::Serialize)]
pub struct GenerateCodeResponse {
    pub code: String,
    pub expires_in: i64, // 秒
}

/// 生成随机登录码（网页端专用，无需认证）
#[get("/payment/generate-code")]
pub async fn generate_code(
    pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
    use rand::Rng;

    let mut rng = rand::thread_rng();
    let suffix: String = (0..6)
        .map(|_| {
            let idx = rng.gen_range(0..36);
            if idx < 10 { (b'0' + idx) as char } else { (b'A' + idx - 10) as char }
        })
        .collect();
    let code = format!("ASD-{}", suffix);

    let expires_at = Utc::now() + chrono::Duration::minutes(10);

    sqlx::query("INSERT INTO web_login_codes (code, expires_at) VALUES ($1, $2)")
        .bind(&code)
        .bind(expires_at)
        .execute(pool.get_ref())
        .await
        .map_err(|e| AppError::Internal(format!("建码失败: {}", e)))?;

    Ok(HttpResponse::Ok().json(GenerateCodeResponse {
        code,
        expires_in: 600,
    }))
}

#[get("/payment/login-status")]
pub async fn payment_login_status(
    pool: web::Data<PgPool>,
    query: web::Query<LoginStatusQuery>,
    app_state: web::Data<crate::models::AppState>,
) -> Result<HttpResponse, AppError> {
    let code = query.code.trim();

    // 查询登录码记录（包含 token 字段用于判断是否已确认）
    let record: Option<(String, chrono::DateTime<chrono::Utc>, Option<String>, Option<i32>)> =
        sqlx::query_as(
            "SELECT code, expires_at, token, user_id FROM web_login_codes WHERE code = $1",
        )
        .bind(code)
        .fetch_optional(pool.get_ref())
        .await
        .map_err(|e| AppError::Internal(format!("数据库查询失败: {}", e)))?;

    let (db_code, expires_at, token, user_id) = match record {
        Some(r) => r,
        None => {
            return Ok(HttpResponse::Ok().json(LoginStatusResponse {
                success: false,
                confirmed: false,
                token: None,
                is_paid_active: false,
                paid_expires_at: None,
            }));
        }
    };

    // 检查是否过期
    if Utc::now() > expires_at {
        let _ = sqlx::query("DELETE FROM web_login_codes WHERE code = $1")
            .bind(&db_code)
            .execute(pool.get_ref())
            .await;
        return Ok(HttpResponse::Ok().json(LoginStatusResponse {
            success: false,
            confirmed: false,
            token: None,
            is_paid_active: false,
            paid_expires_at: None,
        }));
    }

    // 登录码存在但还没被小程序确认（token 为空 = 刚生成，还没点确认）
    // 使用 token 字段判断是否已确认（而非 user_id，因为 web_generate_login_code 会设置 user_id）
    if token.is_none() {
        return Ok(HttpResponse::Ok().json(LoginStatusResponse {
            success: true,
            confirmed: false,
            token: None,
            is_paid_active: false,
            paid_expires_at: None,
        }));
    }

    // 已确认 → 使用已生成的 token
    let token = token.unwrap();
    let user_id = user_id.unwrap();

    let (is_paid_active, paid_expires_at): (bool, Option<String>) =
        match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>("SELECT is_paid_active($1)")
            .bind(user_id)
            .fetch_optional(pool.get_ref())
            .await
        {
            Ok(Some((active, expires))) => (active, expires.map(|e| e.to_rfc3339())),
            _ => (false, None),
        };

    // 清理已使用的登录码
    let _ = sqlx::query("DELETE FROM web_login_codes WHERE code = $1")
        .bind(&db_code)
        .execute(pool.get_ref())
        .await;

    info!("[LOGIN STATUS] user_id={} confirmed=true", user_id);
    Ok(HttpResponse::Ok().json(LoginStatusResponse {
        success: true,
        confirmed: true,
        token: Some(token),
        is_paid_active,
        paid_expires_at,
    }))
}
