package identity

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"qycw/go-server/internal/plugins"
)

const verificationDailyLimit = 5

var idCardPattern = regexp.MustCompile(`^[0-9Xx]{15,18}$`)

type VerificationIdentityRequest struct {
	RealName string `json:"real_name"`
	IDCardNo string `json:"id_card_no"`
}

type VerificationThreeFactorRequest struct {
	RealName string `json:"real_name"`
	IDCardNo string `json:"id_card_no"`
	Phone    string `json:"phone"`
}

type VerificationQuotaPurchaseRequest struct {
	Count int `json:"count"`
}

type VerificationRedirectRequest struct {
	RealName  string  `json:"real_name"`
	IDCardNo  string  `json:"id_card_no"`
	ReturnURL *string `json:"return_url"`
	NotifyURL *string `json:"notify_url"`
}

type VerificationRedirectResultRequest struct {
	ExternalID *string `json:"external_id"`
	RecordID   *int64  `json:"record_id"`
}

type AdminManualVerifyRequest struct {
	Verified bool    `json:"verified"`
	Reason   *string `json:"reason"`
}

type verificationConfig struct {
	Enabled               bool
	Provider              string
	DefaultMode           string
	FreeQuotaPerUser      int
	QuotaPurchaseEnabled  bool
	QuotaPriceCents       int
	AliyunAppCode         string
	AliyunEndpoint        string
	BaiduAccessKey        string
	BaiduSecretKey        string
	BaiduScoreThreshold   float64
	TencentSecretID       string
	TencentSecretKey      string
	TencentWechatRuleID   string
	TencentFloatRuleID    string
	TencentEndpoint       string
	TencentInfoType       string
	TencentScoreThreshold float64
	LeafSMAppID           string
	LeafSMAppSecret       string
	LeafSMBaseURL         string
	LeafSMH5BaseURL       string
	LeafSMTimeout         int
	RetentionDays         int
}

func (s *Service) GetVerificationStatus(ctx context.Context, user User) (map[string]any, error) {
	quota, err := s.verificationQuotaSummary(ctx, user)
	if err != nil {
		return nil, err
	}
	plainName := DecryptOptional(user.RealName, s.cfg.JWTSecretKey)
	plainID := DecryptOptional(user.IDCardNo, s.cfg.JWTSecretKey)
	var latest UserVerificationRecord
	latestPayload := map[string]any{}
	if err := s.db.WithContext(ctx).Where("user_id = ?", user.ID).Order("id DESC").First(&latest).Error; err == nil {
		latestPayload = verificationRecordPublic(latest, s.cfg.JWTSecretKey)
	}
	return map[string]any{
		"id_card_verified":    defaultVerificationString(user.IDCardVerified, "unverified"),
		"id_card_verified_at": user.IDCardVerifiedAt,
		"real_name":           MaskName(plainName),
		"id_card_no":          MaskIDCard(plainID),
		"remark":              user.IDCardVerifyRemark,
		"quota":               quota,
		"latest_record":       latestPayload,
	}, nil
}

func (s *Service) GetVerificationOptions(ctx context.Context) (map[string]any, error) {
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	modes := []string{"two_factor"}
	if !strings.Contains(cfg.Provider, "aliyun_idcert") {
		modes = append(modes, "three_factor", "face_redirect")
	}
	return map[string]any{
		"enabled":      cfg.Enabled,
		"provider":     cfg.Provider,
		"default_mode": cfg.DefaultMode,
		"modes":        modes,
		"quota": map[string]any{
			"free_quota_per_user": cfg.FreeQuotaPerUser,
			"purchase_enabled":    cfg.QuotaPurchaseEnabled,
			"price_cents":         cfg.QuotaPriceCents,
		},
	}, nil
}

func (s *Service) SubmitTwoFactorVerification(ctx context.Context, user User, req VerificationIdentityRequest) (map[string]any, error) {
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	if !cfg.Enabled {
		return nil, BadRequest("实名认证服务未启用")
	}
	realName, idCardNo, err := normalizeVerificationIdentity(req.RealName, req.IDCardNo)
	if err != nil {
		return nil, err
	}
	if err := s.ensureVerificationAttemptAllowed(ctx, user, idCardNo); err != nil {
		return nil, err
	}
	quota, err := s.consumeVerificationQuota(ctx, user, cfg)
	if err != nil {
		return nil, err
	}
	if pluginID := plugins.PluginIDFromProvider(cfg.Provider); pluginID != "" {
		return s.submitPluginVerification(ctx, user, cfg, pluginID, "two_factor", realName, idCardNo, "", "", quota)
	}
	result, err := s.verifyAliyunIDCert(ctx, cfg, realName, idCardNo)
	if err != nil {
		return nil, err
	}
	record, err := s.createVerificationRecord(ctx, user.ID, realName, idCardNo, "aliyun_idcert", "two_factor", result, quota)
	if err != nil {
		return nil, err
	}
	if result["passed"] == true {
		if err := s.markUserVerified(ctx, user.ID, realName, idCardNo, "aliyun_idcert two_factor verified"); err != nil {
			return nil, err
		}
	} else {
		reason := firstVerificationString(result["reason"], "实名认证未通过")
		now := time.Now().UTC()
		_ = s.db.WithContext(ctx).Model(&record).Updates(map[string]any{"rejected_at": now, "reject_reason": reason}).Error
	}
	return verificationRecordPublic(record, s.cfg.JWTSecretKey), nil
}

func (s *Service) SubmitThreeFactorVerification(ctx context.Context, user User, req VerificationThreeFactorRequest) (map[string]any, error) {
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	if !cfg.Enabled {
		return nil, BadRequest("实名认证服务未启用")
	}
	pluginID := plugins.PluginIDFromProvider(cfg.Provider)
	if pluginID == "" {
		return nil, BadRequest("当前实名 provider 不支持三要素，请配置 plugin:<插件ID>")
	}
	realName, idCardNo, err := normalizeVerificationIdentity(req.RealName, req.IDCardNo)
	if err != nil {
		return nil, err
	}
	if err := s.ensureVerificationAttemptAllowed(ctx, user, idCardNo); err != nil {
		return nil, err
	}
	quota, err := s.consumeVerificationQuota(ctx, user, cfg)
	if err != nil {
		return nil, err
	}
	return s.submitPluginVerification(ctx, user, cfg, pluginID, "three_factor", realName, idCardNo, req.Phone, "", quota)
}

func (s *Service) SubmitFaceAPIVerification(ctx context.Context, user User, req VerificationIdentityRequest, imageBase64 string) (map[string]any, error) {
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	if !cfg.Enabled {
		return nil, BadRequest("实名认证服务未启用")
	}
	if plugins.PluginIDFromProvider(cfg.Provider) == "" && !strings.Contains(cfg.Provider, "baidu_face_v3") {
		return nil, BadRequest("当前 Go 原生 Face API 仅支持 baidu_face_v3")
	}
	realName, idCardNo, err := normalizeVerificationIdentity(req.RealName, req.IDCardNo)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(imageBase64) == "" {
		return nil, BadRequest("请上传人脸图片")
	}
	if err := s.ensureVerificationAttemptAllowed(ctx, user, idCardNo); err != nil {
		return nil, err
	}
	quota, err := s.consumeVerificationQuota(ctx, user, cfg)
	if err != nil {
		return nil, err
	}
	if pluginID := plugins.PluginIDFromProvider(cfg.Provider); pluginID != "" {
		return s.submitPluginVerification(ctx, user, cfg, pluginID, "face_api", realName, idCardNo, "", imageBase64, quota)
	}
	result, err := s.verifyBaiduFaceAPI(ctx, cfg, realName, idCardNo, imageBase64)
	if err != nil {
		return nil, err
	}
	record, err := s.createVerificationRecord(ctx, user.ID, realName, idCardNo, "baidu_face_v3", "face_api", result, quota)
	if err != nil {
		return nil, err
	}
	if result["passed"] == true {
		if err := s.markUserVerified(ctx, user.ID, realName, idCardNo, "baidu_face_v3 face_api verified"); err != nil {
			return nil, err
		}
	} else {
		reason := firstVerificationString(result["reason"], "实名认证未通过")
		now := time.Now().UTC()
		_ = s.db.WithContext(ctx).Model(&record).Updates(map[string]any{"rejected_at": now, "reject_reason": reason}).Error
	}
	return verificationRecordPublic(record, s.cfg.JWTSecretKey), nil
}

func (s *Service) CreateRedirectVerificationSession(ctx context.Context, user User, req VerificationRedirectRequest) (map[string]any, error) {
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	if !cfg.Enabled {
		return nil, BadRequest("实名认证服务未启用")
	}
	pluginID := plugins.PluginIDFromProvider(cfg.Provider)
	provider := redirectProviderName(cfg.Provider)
	if provider == "" && pluginID != "" {
		provider = pluginID
	}
	if provider == "" {
		return nil, BadRequest("当前实名认证插件不支持 H5 跳转核身")
	}
	realName, idCardNo, err := normalizeVerificationIdentity(req.RealName, req.IDCardNo)
	if err != nil {
		return nil, err
	}
	if err := s.ensureVerificationAttemptAllowed(ctx, user, idCardNo); err != nil {
		return nil, err
	}
	quota, err := s.consumeVerificationQuota(ctx, user, cfg)
	if err != nil {
		return nil, err
	}
	returnURL := s.safeVerificationReturnURL(ctx, req.ReturnURL)
	notifyURL := s.safeVerificationNotifyURL(ctx, req.NotifyURL, provider)
	expiresAt := time.Now().UTC().AddDate(0, 0, maxInt(1, cfg.RetentionDays))
	creating := map[string]any{"mode": "face_redirect", "provider": provider, "status": "creating", "return_url": returnURL, "notify_url": notifyURL, "quota": quota}
	record, err := s.createPendingVerificationRecord(ctx, user.ID, realName, idCardNo, provider, "face_redirect", creating, &expiresAt)
	if err != nil {
		return nil, err
	}
	if pluginID != "" {
		return s.createPluginRedirectSession(ctx, user, cfg, pluginID, &record, realName, idCardNo, returnURL, notifyURL, quota)
	}
	session, err := s.createRedirectProviderSession(ctx, cfg, provider, realName, idCardNo, returnURL, notifyURL)
	if err != nil {
		reason := err.Error()
		now := time.Now().UTC()
		_ = s.db.WithContext(ctx).Model(&record).Updates(map[string]any{"status": "rejected", "rejected_at": now, "reject_reason": reason}).Error
		return nil, err
	}
	response := map[string]any{
		"mode":       "face_redirect",
		"provider":   provider,
		"status":     "processing",
		"return_url": returnURL,
		"notify_url": notifyURL,
		"session": map[string]any{
			"external_id": session.ExternalID,
			"expires_in":  session.ExpiresIn,
			"task_id":     session.Raw["task_id"],
			"task_no":     session.Raw["task_no"],
			"request_id":  session.Raw["RequestId"],
		},
		"quota": quota,
	}
	raw, _ := json.Marshal(response)
	record.ProviderTaskID = &session.ExternalID
	record.ProviderResponse = SettingValue(raw)
	if err := s.db.WithContext(ctx).Save(&record).Error; err != nil {
		return nil, err
	}
	return map[string]any{"record_id": record.ID, "url": session.URL, "qr_payload": session.URL, "external_id": session.ExternalID, "expires_in": session.ExpiresIn, "quota": quota}, nil
}

func (s *Service) QueryRedirectVerificationResult(ctx context.Context, user User, req VerificationRedirectResultRequest) (map[string]any, error) {
	record, err := s.findRedirectRecord(ctx, user.ID, req.ExternalID, req.RecordID)
	if err != nil {
		return nil, err
	}
	if record.Status == "verified" || record.Status == "rejected" {
		return recordResultSummary(record), nil
	}
	if record.ProviderTaskID == nil || strings.TrimSpace(*record.ProviderTaskID) == "" {
		return nil, BadRequest("该实名认证记录缺少第三方会话编号")
	}
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	plainName := DecryptOptional(record.RealName, s.cfg.JWTSecretKey)
	plainID := DecryptOptional(record.IDCardNo, s.cfg.JWTSecretKey)
	if pluginID := plugins.PluginIDFromProvider(cfg.Provider); pluginID != "" || (plugins.SafeID(record.Provider) && redirectProviderName(record.Provider) == "") {
		if pluginID == "" {
			pluginID = record.Provider
		}
		result, err := s.queryPluginVerificationResult(ctx, cfg, pluginID, record, derefPtr(plainName), derefPtr(plainID))
		if err != nil {
			return nil, err
		}
		if err := s.applyVerificationResult(ctx, user.ID, &record, result); err != nil {
			return nil, err
		}
		return recordResultSummary(record), nil
	}
	result, err := s.queryRedirectProviderResult(ctx, cfg, record.Provider, *record.ProviderTaskID, derefPtr(plainName), derefPtr(plainID))
	if err != nil {
		return nil, err
	}
	if err := s.applyVerificationResult(ctx, user.ID, &record, result); err != nil {
		return nil, err
	}
	return recordResultSummary(record), nil
}

func (s *Service) HandleRedirectVerificationNotify(ctx context.Context, providerName string, payload map[string]any) (map[string]any, error) {
	externalID := extractNotifyExternalID(payload)
	if externalID == "" {
		return map[string]any{"accepted": true, "updated": false, "reason": "external_id_missing"}, nil
	}
	record, err := s.findRedirectRecordByProvider(ctx, providerName, externalID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return map[string]any{"accepted": true, "updated": false, "reason": "record_not_found"}, nil
		}
		return nil, err
	}
	if record.Status == "verified" || record.Status == "rejected" {
		return map[string]any{"accepted": true, "updated": false, "status": record.Status}, nil
	}
	user, err := s.findUserByID(ctx, record.UserID)
	if err != nil {
		return map[string]any{"accepted": true, "updated": false, "reason": "user_not_found"}, nil
	}
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	plainName := DecryptOptional(record.RealName, s.cfg.JWTSecretKey)
	plainID := DecryptOptional(record.IDCardNo, s.cfg.JWTSecretKey)
	if pluginID := plugins.PluginIDFromProvider(cfg.Provider); pluginID != "" || (plugins.SafeID(record.Provider) && redirectProviderName(record.Provider) == "") {
		if pluginID == "" {
			pluginID = record.Provider
		}
		result, err := s.notifyPluginVerification(ctx, cfg, pluginID, record, payload, derefPtr(plainName), derefPtr(plainID))
		if err != nil {
			return map[string]any{"accepted": true, "updated": false, "reason": err.Error()}, nil
		}
		if err := s.applyVerificationResult(ctx, user.ID, &record, result); err != nil {
			return nil, err
		}
		return map[string]any{"accepted": true, "updated": true, "result": recordResultSummary(record)}, nil
	}
	result, err := s.queryRedirectProviderResult(ctx, cfg, record.Provider, derefPtr(record.ProviderTaskID), derefPtr(plainName), derefPtr(plainID))
	if err != nil {
		return map[string]any{"accepted": true, "updated": false, "reason": err.Error()}, nil
	}
	if err := s.applyVerificationResult(ctx, user.ID, &record, result); err != nil {
		return nil, err
	}
	return map[string]any{"accepted": true, "updated": true, "result": recordResultSummary(record)}, nil
}

func (s *Service) PurchaseVerificationQuota(ctx context.Context, user User, count int) (map[string]any, error) {
	if count < 1 || count > 100 {
		return nil, BadRequest("购买次数必须在 1-100 之间")
	}
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	if !cfg.QuotaPurchaseEnabled {
		return nil, BadRequest("实名认证次数购买未启用")
	}
	total := count * cfg.QuotaPriceCents
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var locked User
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", user.ID).First(&locked).Error; err != nil {
			return err
		}
		if int(locked.Balance) < total {
			return BadRequest("余额不足")
		}
		quota, err := getOrCreateVerificationQuota(ctx, tx, user.ID, true)
		if err != nil {
			return err
		}
		if err := tx.Model(&User{}).Where("id = ?", user.ID).Update("balance", gorm.Expr("balance - ?", total)).Error; err != nil {
			return err
		}
		return tx.Model(&quota).Updates(map[string]any{
			"paid_remaining": gorm.Expr("paid_remaining + ?", count),
			"paid_purchased": gorm.Expr("paid_purchased + ?", count),
		}).Error
	})
	if err != nil {
		return nil, err
	}
	fresh, _ := s.findUserByID(ctx, user.ID)
	return s.verificationQuotaSummary(ctx, fresh)
}

func (s *Service) ListVerificationRecords(ctx context.Context, status string, userID int64, page int, perPage int) (map[string]any, error) {
	if page < 1 {
		page = 1
	}
	if perPage < 1 || perPage > 100 {
		perPage = 20
	}
	query := s.db.WithContext(ctx).Model(&UserVerificationRecord{})
	if strings.TrimSpace(status) != "" {
		query = query.Where("status = ?", strings.TrimSpace(status))
	}
	if userID > 0 {
		query = query.Where("user_id = ?", userID)
	}
	var total int64
	if err := query.Count(&total).Error; err != nil {
		return nil, err
	}
	var records []UserVerificationRecord
	if err := query.Order("id DESC").Offset((page - 1) * perPage).Limit(perPage).Find(&records).Error; err != nil {
		return nil, err
	}
	list := make([]map[string]any, 0, len(records))
	for _, record := range records {
		list = append(list, verificationRecordPublic(record, s.cfg.JWTSecretKey))
	}
	return map[string]any{"list": list, "meta": map[string]any{"total": total, "page": page, "per_page": perPage}}, nil
}

func (s *Service) ManualVerifyUser(ctx context.Context, userID int64, req AdminManualVerifyRequest) (map[string]any, error) {
	var user User
	if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	status := "rejected"
	if req.Verified {
		status = "verified"
	}
	updates := map[string]any{"id_card_verified": status, "id_card_verify_remark": req.Reason}
	if req.Verified {
		updates["id_card_verified_at"] = now
	}
	if err := s.db.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
		return nil, err
	}
	return map[string]any{"user_id": userID, "id_card_verified": status}, nil
}

func (s *Service) loadVerificationConfig(ctx context.Context) (verificationConfig, error) {
	cfg := verificationConfig{
		Provider:              "mode.verification.aliyun_idcert.provider:create_provider",
		DefaultMode:           "two_factor",
		FreeQuotaPerUser:      1,
		QuotaPurchaseEnabled:  true,
		QuotaPriceCents:       350,
		AliyunEndpoint:        "https://idcert.market.alicloudapi.com/idcard",
		BaiduScoreThreshold:   80,
		TencentEndpoint:       "faceid.tencentcloudapi.com",
		TencentInfoType:       "1",
		TencentScoreThreshold: 80,
		LeafSMBaseURL:         "https://server.face.ly-y.cn/",
		LeafSMH5BaseURL:       "https://face.ly-y.cn/",
		LeafSMTimeout:         20,
		RetentionDays:         3,
	}
	var settings []SystemSetting
	if err := s.db.WithContext(ctx).Where("`key` LIKE ?", "verification.%").Find(&settings).Error; err != nil {
		return cfg, err
	}
	for _, item := range settings {
		switch item.Key {
		case "verification.enabled":
			cfg.Enabled = item.Value.Bool(false)
		case "verification.provider":
			cfg.Provider = settingString(item.Value, cfg.Provider)
		case "verification.default_mode":
			cfg.DefaultMode = settingString(item.Value, cfg.DefaultMode)
		case "verification.free_quota_per_user":
			cfg.FreeQuotaPerUser = settingInt(item.Value, cfg.FreeQuotaPerUser)
		case "verification.quota_purchase_enabled":
			cfg.QuotaPurchaseEnabled = item.Value.Bool(true)
		case "verification.quota_price_cents":
			cfg.QuotaPriceCents = settingInt(item.Value, cfg.QuotaPriceCents)
		case "verification.aliyun_idcert.appcode":
			cfg.AliyunAppCode = settingString(item.Value, "")
		case "verification.aliyun_idcert.endpoint":
			cfg.AliyunEndpoint = settingString(item.Value, cfg.AliyunEndpoint)
		case "verification.baidu_face_v3.access_key":
			cfg.BaiduAccessKey = settingString(item.Value, "")
		case "verification.baidu_face_v3.secret_key":
			cfg.BaiduSecretKey = settingString(item.Value, "")
		case "verification.baidu_face_v3.score_threshold":
			cfg.BaiduScoreThreshold = float64(settingInt(item.Value, int(cfg.BaiduScoreThreshold)))
		case "verification.tencent.secret_id":
			cfg.TencentSecretID = settingString(item.Value, "")
		case "verification.tencent.secret_key":
			cfg.TencentSecretKey = settingString(item.Value, "")
		case "verification.tencent.wechat_h5.rule_id":
			cfg.TencentWechatRuleID = settingString(item.Value, "")
		case "verification.tencent.wechat_float_h5.rule_id":
			cfg.TencentFloatRuleID = settingString(item.Value, "")
		case "verification.tencent.endpoint":
			cfg.TencentEndpoint = settingString(item.Value, cfg.TencentEndpoint)
		case "verification.tencent.info_type":
			cfg.TencentInfoType = settingString(item.Value, cfg.TencentInfoType)
		case "verification.tencent.score_threshold":
			cfg.TencentScoreThreshold = float64(settingInt(item.Value, int(cfg.TencentScoreThreshold)))
		case "verification.leafsm_h5_face.app_id":
			cfg.LeafSMAppID = settingString(item.Value, "")
		case "verification.leafsm_h5_face.app_secret":
			cfg.LeafSMAppSecret = settingString(item.Value, "")
		case "verification.leafsm_h5_face.base_url":
			cfg.LeafSMBaseURL = settingString(item.Value, cfg.LeafSMBaseURL)
		case "verification.leafsm_h5_face.h5_base_url":
			cfg.LeafSMH5BaseURL = settingString(item.Value, cfg.LeafSMH5BaseURL)
		case "verification.leafsm_h5_face.timeout":
			cfg.LeafSMTimeout = settingInt(item.Value, cfg.LeafSMTimeout)
		case "verification.retention_days":
			cfg.RetentionDays = settingInt(item.Value, cfg.RetentionDays)
		}
	}
	return cfg, nil
}

func verificationPluginConfig(cfg verificationConfig, pluginID string) map[string]any {
	switch pluginID {
	case "aliyun_idcert":
		return map[string]any{
			"appcode":  cfg.AliyunAppCode,
			"endpoint": cfg.AliyunEndpoint,
		}
	case "baidu_face_v3":
		return map[string]any{
			"access_key":      cfg.BaiduAccessKey,
			"secret_key":      cfg.BaiduSecretKey,
			"score_threshold": cfg.BaiduScoreThreshold,
		}
	case "leafsm_h5_face":
		return map[string]any{
			"app_id":      cfg.LeafSMAppID,
			"app_secret":  cfg.LeafSMAppSecret,
			"base_url":    cfg.LeafSMBaseURL,
			"h5_base_url": cfg.LeafSMH5BaseURL,
			"timeout":     cfg.LeafSMTimeout,
		}
	case "tencent_wechat_h5":
		return map[string]any{
			"secret_id":         cfg.TencentSecretID,
			"secret_key":        cfg.TencentSecretKey,
			"wechat_h5_rule_id": cfg.TencentWechatRuleID,
			"endpoint":          cfg.TencentEndpoint,
			"info_type":         cfg.TencentInfoType,
			"score_threshold":   cfg.TencentScoreThreshold,
		}
	case "tencent_wechat_float_h5":
		return map[string]any{
			"secret_id":               cfg.TencentSecretID,
			"secret_key":              cfg.TencentSecretKey,
			"wechat_float_h5_rule_id": cfg.TencentFloatRuleID,
			"endpoint":                cfg.TencentEndpoint,
			"info_type":               cfg.TencentInfoType,
			"score_threshold":         cfg.TencentScoreThreshold,
		}
	default:
		return map[string]any{}
	}
}

func (s *Service) verificationQuotaSummary(ctx context.Context, user User) (map[string]any, error) {
	cfg, err := s.loadVerificationConfig(ctx)
	if err != nil {
		return nil, err
	}
	quota, err := getOrCreateVerificationQuota(ctx, s.db, user.ID, false)
	if err != nil {
		return nil, err
	}
	freeRemaining := cfg.FreeQuotaPerUser - quota.FreeUsed
	if freeRemaining < 0 {
		freeRemaining = 0
	}
	return map[string]any{
		"free_quota_per_user": cfg.FreeQuotaPerUser,
		"purchase_enabled":    cfg.QuotaPurchaseEnabled,
		"price_cents":         cfg.QuotaPriceCents,
		"free_used":           quota.FreeUsed,
		"free_remaining":      freeRemaining,
		"paid_remaining":      quota.PaidRemaining,
		"paid_purchased":      quota.PaidPurchased,
		"paid_used":           quota.PaidUsed,
		"remaining_total":     freeRemaining + quota.PaidRemaining,
		"balance_cents":       user.Balance,
		"can_submit":          freeRemaining+quota.PaidRemaining > 0,
		"can_purchase":        cfg.QuotaPurchaseEnabled,
	}, nil
}

func (s *Service) ensureVerificationAttemptAllowed(ctx context.Context, user User, idCardNo string) error {
	if user.IDCardVerified == "verified" {
		return BadRequest("您已完成实名认证")
	}
	hash := DeterministicHash(idCardNo, s.cfg.JWTSecretKey)
	var count int64
	if err := s.db.WithContext(ctx).Model(&User{}).Where("(id_card_no_hash = ? OR id_card_no = ?) AND id <> ?", hash, idCardNo, user.ID).Count(&count).Error; err != nil {
		return err
	}
	if count > 0 {
		return BadRequest("该身份证号已绑定其他账号")
	}
	today := time.Now().UTC().Truncate(24 * time.Hour)
	if err := s.db.WithContext(ctx).Model(&UserVerificationRecord{}).Where("user_id = ? AND created_at >= ?", user.ID, today).Count(&count).Error; err != nil {
		return err
	}
	if count >= verificationDailyLimit {
		return TooManyRequests("每天最多提交 5 次实名认证")
	}
	return nil
}

func (s *Service) consumeVerificationQuota(ctx context.Context, user User, cfg verificationConfig) (map[string]any, error) {
	var summary map[string]any
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		quota, err := getOrCreateVerificationQuota(ctx, tx, user.ID, true)
		if err != nil {
			return err
		}
		if quota.FreeUsed < cfg.FreeQuotaPerUser {
			quota.FreeUsed++
		} else if quota.PaidRemaining > 0 {
			quota.PaidRemaining--
			quota.PaidUsed++
		} else {
			return HTTPError{Status: http.StatusPaymentRequired, Detail: map[string]any{"code": "VERIFICATION_QUOTA_REQUIRED", "message": "实名认证免费次数已用完，请先购买实名认证次数"}}
		}
		if err := tx.Save(&quota).Error; err != nil {
			return err
		}
		freeRemaining := cfg.FreeQuotaPerUser - quota.FreeUsed
		if freeRemaining < 0 {
			freeRemaining = 0
		}
		summary = map[string]any{"free_used": quota.FreeUsed, "free_remaining": freeRemaining, "paid_remaining": quota.PaidRemaining}
		return nil
	})
	return summary, err
}

func getOrCreateVerificationQuota(ctx context.Context, db *gorm.DB, userID int64, lock bool) (UserVerificationQuota, error) {
	var quota UserVerificationQuota
	query := db.WithContext(ctx).Where("user_id = ?", userID)
	if lock {
		query = query.Clauses(clause.Locking{Strength: "UPDATE"})
	}
	err := query.First(&quota).Error
	if err == nil {
		return quota, nil
	}
	if err != gorm.ErrRecordNotFound {
		return quota, err
	}
	quota = UserVerificationQuota{UserID: userID}
	err = db.WithContext(ctx).Create(&quota).Error
	return quota, err
}

func (s *Service) verifyAliyunIDCert(ctx context.Context, cfg verificationConfig, realName string, idCardNo string) (map[string]any, error) {
	if !strings.Contains(cfg.Provider, "aliyun_idcert") {
		return nil, BadRequest("当前 Go 原生实名认证仅支持 aliyun_idcert 二要素插件")
	}
	if cfg.AliyunAppCode == "" {
		return nil, BadRequest("阿里云市场二要素插件未配置 AppCode")
	}
	u, _ := url.Parse(cfg.AliyunEndpoint)
	q := u.Query()
	q.Set("name", realName)
	q.Set("idCard", idCardNo)
	u.RawQuery = q.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "APPCODE "+cfg.AliyunAppCode)
	req.Header.Set("Accept", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, ServiceUnavailable("请求阿里云市场二要素接口失败")
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	var body map[string]any
	_ = json.Unmarshal(raw, &body)
	if resp.StatusCode != http.StatusOK {
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: fmt.Sprintf("阿里云市场二要素接口错误：HTTP %d", resp.StatusCode)}
	}
	status := firstVerificationString(body["status"])
	message := firstVerificationString(body["msg"], "实名认证未通过")
	passed := subtle.ConstantTimeCompare([]byte(status), []byte("01")) == 1
	if status == "9999" {
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: "阿里云市场二要素接口系统维护"}
	}
	resultStatus := "rejected"
	if passed {
		resultStatus = "verified"
		message = ""
	}
	return map[string]any{
		"passed":      passed,
		"provider":    "aliyun_idcert",
		"mode":        "two_factor",
		"status":      resultStatus,
		"reason":      message,
		"external_id": firstVerificationString(body["traceId"]),
		"raw":         body,
	}, nil
}

func (s *Service) verifyBaiduFaceAPI(ctx context.Context, cfg verificationConfig, realName string, idCardNo string, imageBase64 string) (map[string]any, error) {
	if cfg.BaiduAccessKey == "" || cfg.BaiduSecretKey == "" {
		return nil, BadRequest("百度人脸实名认证插件未配置 API Key / Secret Key")
	}
	token, err := s.baiduAccessToken(ctx, cfg)
	if err != nil {
		return nil, err
	}
	payload := map[string]any{
		"image":            strings.TrimSpace(stripDataURLBase64(imageBase64)),
		"image_type":       "BASE64",
		"id_card_number":   idCardNo,
		"name":             realName,
		"quality_control":  "LOW",
		"liveness_control": "NORMAL",
		"spoofing_control": "NORMAL",
	}
	endpoint := "https://aip.baidubce.com/rest/2.0/face/v3/person/verify?access_token=" + url.QueryEscape(token)
	body, err := postJSON(ctx, endpoint, payload, nil)
	if err != nil {
		return nil, err
	}
	if intValue(body["error_code"]) != 0 {
		reason := firstVerificationString(body["error_msg"], "百度人脸实名认证未通过")
		return verificationResult(false, "baidu_face_v3", "face_api", "rejected", reason, firstVerificationString(body["log_id"]), nil, body), nil
	}
	result, _ := body["result"].(map[string]any)
	score := floatPtr(result["score"])
	passed := score != nil && *score >= cfg.BaiduScoreThreshold
	reason := ""
	if !passed {
		if score == nil {
			reason = "百度人脸实名认证未返回分数"
		} else {
			reason = fmt.Sprintf("人脸比对分数 %.2f 低于阈值 %.2f", *score, cfg.BaiduScoreThreshold)
		}
	}
	return verificationResult(passed, "baidu_face_v3", "face_api", statusFromPassed(passed), reason, firstVerificationString(body["log_id"]), score, body), nil
}

func (s *Service) baiduAccessToken(ctx context.Context, cfg verificationConfig) (string, error) {
	params := url.Values{}
	params.Set("grant_type", "client_credentials")
	params.Set("client_id", cfg.BaiduAccessKey)
	params.Set("client_secret", cfg.BaiduSecretKey)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://aip.baidubce.com/oauth/2.0/token", strings.NewReader(params.Encode()))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", ServiceUnavailable("获取百度 AccessToken 失败")
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	var body map[string]any
	_ = json.Unmarshal(raw, &body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 || firstVerificationString(body["access_token"]) == "" {
		return "", HTTPError{Status: http.StatusBadGateway, Detail: firstVerificationString(body["error_description"], body["error"], "获取百度 AccessToken 失败")}
	}
	return firstVerificationString(body["access_token"]), nil
}

func (s *Service) tencentFaceIDRequest(ctx context.Context, cfg verificationConfig, action string, payload map[string]any, allowError bool) (map[string]any, error) {
	host, endpoint := parseTencentEndpoint(cfg.TencentEndpoint)
	payloadRaw, _ := json.Marshal(payload)
	payloadJSON := string(payloadRaw)
	timestamp := time.Now().Unix()
	date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
	contentType := "application/json; charset=utf-8"
	canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\n", contentType, host)
	signedHeaders := "content-type;host"
	canonicalRequest := strings.Join([]string{"POST", "/", "", canonicalHeaders, signedHeaders, sha256Hex([]byte(payloadJSON))}, "\n")
	scope := date + "/faceid/tc3_request"
	stringToSign := strings.Join([]string{"TC3-HMAC-SHA256", fmt.Sprint(timestamp), scope, sha256Hex([]byte(canonicalRequest))}, "\n")
	secretDate := hmacSHA256([]byte("TC3"+cfg.TencentSecretKey), date)
	secretService := hmacSHA256(secretDate, "faceid")
	secretSigning := hmacSHA256(secretService, "tc3_request")
	signature := hex.EncodeToString(hmacSHA256(secretSigning, stringToSign))
	authorization := fmt.Sprintf("TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", cfg.TencentSecretID, scope, signedHeaders, signature)
	headers := map[string]string{
		"Authorization":  authorization,
		"Content-Type":   contentType,
		"Host":           host,
		"X-TC-Action":    action,
		"X-TC-Timestamp": fmt.Sprint(timestamp),
		"X-TC-Version":   "2018-03-01",
	}
	body, err := postJSONRaw(ctx, endpoint, []byte(payloadJSON), headers)
	if err != nil {
		return nil, err
	}
	response, _ := body["Response"].(map[string]any)
	if len(response) == 0 {
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: "腾讯云实名核身接口响应格式异常"}
	}
	if _, exists := response["Error"]; exists && !allowError {
		errMap, _ := response["Error"].(map[string]any)
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: firstVerificationString(errMap["Message"], errMap["Code"], "腾讯云实名核身接口错误")}
	}
	return response, nil
}

func (s *Service) leafSMRequest(ctx context.Context, cfg verificationConfig, method string, path string, payload map[string]any, query map[string]string) (map[string]any, error) {
	base := strings.TrimRight(defaultVerificationString(cfg.LeafSMBaseURL, "https://server.face.ly-y.cn/"), "/")
	if strings.Contains(base, "face.ly-y.cn") && !strings.Contains(base, "server.face.ly-y.cn") {
		base = "https://server.face.ly-y.cn"
	}
	var bodyRaw []byte
	if payload != nil {
		bodyRaw, _ = json.Marshal(payload)
	} else {
		bodyRaw = []byte{}
	}
	timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z")
	nonceBytes := make([]byte, 18)
	_, _ = randRead(nonceBytes)
	nonce := base64.RawURLEncoding.EncodeToString(nonceBytes)
	bodyHash := sha256Hex(bodyRaw)
	signText := strings.Join([]string{strings.ToUpper(method), path, timestamp, nonce, bodyHash}, "\n")
	signature := hex.EncodeToString(hmacSHA256([]byte(cfg.LeafSMAppSecret), signText))
	u, _ := url.Parse(base + path)
	q := u.Query()
	for key, value := range query {
		q.Set(key, value)
	}
	u.RawQuery = q.Encode()
	req, err := http.NewRequestWithContext(ctx, method, u.String(), bytes.NewReader(bodyRaw))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-App-Id", cfg.LeafSMAppID)
	req.Header.Set("X-Timestamp", timestamp)
	req.Header.Set("X-Nonce", nonce)
	req.Header.Set("X-Body-Sha256", bodyHash)
	req.Header.Set("X-Signature", signature)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, ServiceUnavailable("请求 LeafSM H5 人脸核身接口失败")
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	var body map[string]any
	_ = json.Unmarshal(raw, &body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, HTTPError{Status: mapLeafSMStatus(resp.StatusCode), Detail: firstVerificationString(body["message"], body["msg"], body["detail"], fmt.Sprintf("LeafSM H5 人脸核身接口错误：HTTP %d", resp.StatusCode))}
	}
	return body, nil
}

func (s *Service) submitPluginVerification(ctx context.Context, user User, cfg verificationConfig, pluginID string, mode string, realName string, idCardNo string, phone string, imageBase64 string, quota map[string]any) (map[string]any, error) {
	if s.plugins == nil {
		return nil, ServiceUnavailable("实名插件运行时未启用")
	}
	resp, err := s.plugins.VerifyIdentity(ctx, pluginID, plugins.VerificationRequest{
		Mode:        mode,
		UserID:      user.ID,
		RealName:    realName,
		IDCardNo:    idCardNo,
		Phone:       phone,
		ImageBase64: imageBase64,
		Config:      s.plugins.MergeConfig(ctx, pluginID, verificationPluginConfig(cfg, pluginID)),
	})
	if err != nil {
		return nil, ServiceUnavailable("实名插件调用失败：" + err.Error())
	}
	result := pluginVerificationResult(pluginID, mode, resp)
	record, err := s.createVerificationRecord(ctx, user.ID, realName, idCardNo, pluginID, mode, result, quota)
	if err != nil {
		return nil, err
	}
	if result["passed"] == true {
		if err := s.markUserVerified(ctx, user.ID, realName, idCardNo, pluginID+" "+mode+" verified"); err != nil {
			return nil, err
		}
	} else {
		reason := firstVerificationString(result["reason"], "实名认证未通过")
		now := time.Now().UTC()
		_ = s.db.WithContext(ctx).Model(&record).Updates(map[string]any{"rejected_at": now, "reject_reason": reason}).Error
	}
	return verificationRecordPublic(record, s.cfg.JWTSecretKey), nil
}

func (s *Service) createPluginRedirectSession(ctx context.Context, user User, cfg verificationConfig, pluginID string, record *UserVerificationRecord, realName string, idCardNo string, returnURL string, notifyURL string, quota map[string]any) (map[string]any, error) {
	if s.plugins == nil {
		return nil, ServiceUnavailable("实名插件运行时未启用")
	}
	resp, err := s.plugins.StartVerificationSession(ctx, pluginID, plugins.VerificationRequest{
		Mode:      "face_redirect",
		UserID:    user.ID,
		RealName:  realName,
		IDCardNo:  idCardNo,
		ReturnURL: returnURL,
		NotifyURL: notifyURL,
		Config:    s.plugins.MergeConfig(ctx, pluginID, verificationPluginConfig(cfg, pluginID)),
	})
	if err != nil {
		reason := err.Error()
		now := time.Now().UTC()
		_ = s.db.WithContext(ctx).Model(record).Updates(map[string]any{"status": "rejected", "rejected_at": now, "reject_reason": reason}).Error
		return nil, ServiceUnavailable("实名插件创建会话失败：" + err.Error())
	}
	result := pluginVerificationResult(pluginID, "face_redirect", resp)
	response := map[string]any{
		"mode":       "face_redirect",
		"provider":   pluginID,
		"status":     firstVerificationString(result["status"], "processing"),
		"return_url": returnURL,
		"notify_url": notifyURL,
		"session": map[string]any{
			"external_id": resp.ExternalID,
			"expires_in":  resp.ExpiresIn,
			"url":         resp.URL,
		},
		"quota": quota,
		"raw":   resp.Raw,
	}
	raw, _ := json.Marshal(response)
	if resp.ExternalID != "" {
		record.ProviderTaskID = &resp.ExternalID
	}
	record.ProviderResponse = SettingValue(raw)
	if status := firstVerificationString(result["status"]); status != "" {
		record.Status = status
	}
	if record.Status == "" || record.Status == "pending" {
		record.Status = "processing"
	}
	if err := s.db.WithContext(ctx).Save(record).Error; err != nil {
		return nil, err
	}
	qrPayload := resp.QRPayload
	if qrPayload == "" {
		qrPayload = resp.URL
	}
	return map[string]any{"record_id": record.ID, "url": resp.URL, "qr_payload": qrPayload, "external_id": resp.ExternalID, "expires_in": resp.ExpiresIn, "quota": quota}, nil
}

func (s *Service) queryPluginVerificationResult(ctx context.Context, cfg verificationConfig, pluginID string, record UserVerificationRecord, realName string, idCardNo string) (map[string]any, error) {
	if s.plugins == nil {
		return nil, ServiceUnavailable("实名插件运行时未启用")
	}
	resp, err := s.plugins.QueryVerificationResult(ctx, pluginID, plugins.VerificationRequest{
		Mode:       record.Mode,
		UserID:     record.UserID,
		RealName:   realName,
		IDCardNo:   idCardNo,
		ExternalID: derefPtr(record.ProviderTaskID),
		RecordID:   record.ID,
		Config:     s.plugins.MergeConfig(ctx, pluginID, verificationPluginConfig(cfg, pluginID)),
	})
	if err != nil {
		return nil, err
	}
	return pluginVerificationResult(pluginID, record.Mode, resp), nil
}

func (s *Service) notifyPluginVerification(ctx context.Context, cfg verificationConfig, pluginID string, record UserVerificationRecord, payload map[string]any, realName string, idCardNo string) (map[string]any, error) {
	if s.plugins == nil {
		return nil, ServiceUnavailable("实名插件运行时未启用")
	}
	resp, err := s.plugins.HandleVerificationNotify(ctx, pluginID, plugins.VerificationRequest{
		Mode:       record.Mode,
		UserID:     record.UserID,
		RealName:   realName,
		IDCardNo:   idCardNo,
		ExternalID: derefPtr(record.ProviderTaskID),
		RecordID:   record.ID,
		Payload:    payload,
		Config:     s.plugins.MergeConfig(ctx, pluginID, verificationPluginConfig(cfg, pluginID)),
	})
	if err != nil {
		return nil, err
	}
	return pluginVerificationResult(pluginID, record.Mode, resp), nil
}

func pluginVerificationResult(pluginID string, mode string, resp plugins.VerificationResponse) map[string]any {
	status := strings.TrimSpace(resp.Status)
	if status == "" {
		if resp.Passed {
			status = "verified"
		} else {
			status = "rejected"
		}
	}
	provider := strings.TrimSpace(resp.Provider)
	if provider == "" {
		provider = pluginID
	}
	resultMode := strings.TrimSpace(resp.Mode)
	if resultMode == "" {
		resultMode = mode
	}
	raw := resp.Raw
	if raw == nil {
		raw = map[string]any{}
	}
	out := map[string]any{
		"passed":      resp.Passed || status == "verified",
		"provider":    provider,
		"mode":        resultMode,
		"status":      status,
		"reason":      resp.Reason,
		"external_id": resp.ExternalID,
		"raw":         raw,
	}
	if resp.Score != nil {
		out["score"] = *resp.Score
	}
	if resp.URL != "" {
		out["url"] = resp.URL
	}
	if resp.QRPayload != "" {
		out["qr_payload"] = resp.QRPayload
	}
	if resp.ExpiresIn > 0 {
		out["expires_in"] = resp.ExpiresIn
	}
	return out
}

func (s *Service) createVerificationRecord(ctx context.Context, userID int64, realName string, idCardNo string, provider string, mode string, result map[string]any, quota map[string]any) (UserVerificationRecord, error) {
	encryptedName, err := EncryptOptional(realName, s.cfg.JWTSecretKey)
	if err != nil {
		return UserVerificationRecord{}, err
	}
	encryptedID, err := EncryptOptional(idCardNo, s.cfg.JWTSecretKey)
	if err != nil {
		return UserVerificationRecord{}, err
	}
	hash := DeterministicHash(idCardNo, s.cfg.JWTSecretKey)
	response := map[string]any{}
	for key, value := range result {
		response[key] = value
	}
	response["quota"] = quota
	raw, _ := json.Marshal(response)
	now := time.Now().UTC()
	record := UserVerificationRecord{
		UserID:           userID,
		RealName:         encryptedName,
		IDCardNo:         encryptedID,
		IDCardNoHash:     hash,
		Provider:         provider,
		Mode:             mode,
		ProviderTaskID:   stringPtr(firstVerificationString(result["external_id"])),
		Status:           firstVerificationString(result["status"], "pending"),
		ProviderResponse: SettingValue(raw),
	}
	if result["passed"] == true {
		record.VerifiedAt = &now
	}
	if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
		return UserVerificationRecord{}, err
	}
	return record, nil
}

func (s *Service) createPendingVerificationRecord(ctx context.Context, userID int64, realName string, idCardNo string, provider string, mode string, response map[string]any, expiresAt *time.Time) (UserVerificationRecord, error) {
	encryptedName, err := EncryptOptional(realName, s.cfg.JWTSecretKey)
	if err != nil {
		return UserVerificationRecord{}, err
	}
	encryptedID, err := EncryptOptional(idCardNo, s.cfg.JWTSecretKey)
	if err != nil {
		return UserVerificationRecord{}, err
	}
	hash := DeterministicHash(idCardNo, s.cfg.JWTSecretKey)
	raw, _ := json.Marshal(response)
	record := UserVerificationRecord{
		UserID:            userID,
		RealName:          encryptedName,
		IDCardNo:          encryptedID,
		IDCardNoHash:      hash,
		Provider:          provider,
		Mode:              mode,
		Status:            "processing",
		ProviderResponse:  SettingValue(raw),
		MaterialExpiresAt: expiresAt,
	}
	if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
		return UserVerificationRecord{}, err
	}
	return record, nil
}

type redirectSession struct {
	URL        string
	ExternalID string
	ExpiresIn  int
	Raw        map[string]any
}

func (s *Service) createRedirectProviderSession(ctx context.Context, cfg verificationConfig, provider string, realName string, idCardNo string, returnURL string, notifyURL string) (redirectSession, error) {
	switch provider {
	case "tencent_wechat_h5", "tencent_wechat_float_h5":
		return s.createTencentRedirectSession(ctx, cfg, provider, realName, idCardNo, returnURL)
	case "leafsm_h5_face":
		return s.createLeafSMRedirectSession(ctx, cfg, realName, idCardNo, returnURL, notifyURL)
	default:
		return redirectSession{}, BadRequest("当前实名认证插件不支持 H5 跳转核身")
	}
}

func (s *Service) queryRedirectProviderResult(ctx context.Context, cfg verificationConfig, provider string, externalID string, realName string, idCardNo string) (map[string]any, error) {
	switch provider {
	case "tencent_wechat_h5", "tencent_wechat_float_h5":
		return s.queryTencentRedirectResult(ctx, cfg, provider, externalID, realName, idCardNo)
	case "leafsm_h5_face":
		return s.queryLeafSMRedirectResult(ctx, cfg, externalID)
	default:
		return nil, BadRequest("当前实名认证插件不支持查询跳转核身结果")
	}
}

func (s *Service) createTencentRedirectSession(ctx context.Context, cfg verificationConfig, provider string, realName string, idCardNo string, returnURL string) (redirectSession, error) {
	ruleID := cfg.TencentWechatRuleID
	if provider == "tencent_wechat_float_h5" {
		ruleID = cfg.TencentFloatRuleID
	}
	if cfg.TencentSecretID == "" || cfg.TencentSecretKey == "" || ruleID == "" {
		return redirectSession{}, BadRequest("腾讯云实名核身插件未配置 SecretId / SecretKey / RuleId")
	}
	body, err := s.tencentFaceIDRequest(ctx, cfg, "DetectAuth", map[string]any{
		"RuleId":      ruleID,
		"Name":        realName,
		"IdCard":      idCardNo,
		"RedirectUrl": returnURL,
	}, false)
	if err != nil {
		return redirectSession{}, err
	}
	verifyURL := firstVerificationString(body["Url"])
	bizToken := firstVerificationString(body["BizToken"])
	if verifyURL == "" || bizToken == "" {
		return redirectSession{}, HTTPError{Status: http.StatusBadGateway, Detail: "腾讯云 DetectAuth 未返回 Url 或 BizToken"}
	}
	return redirectSession{URL: verifyURL, ExternalID: bizToken, ExpiresIn: 7200, Raw: sanitizeTencentResponse(body)}, nil
}

func (s *Service) queryTencentRedirectResult(ctx context.Context, cfg verificationConfig, provider string, externalID string, realName string, idCardNo string) (map[string]any, error) {
	ruleID := cfg.TencentWechatRuleID
	if provider == "tencent_wechat_float_h5" {
		ruleID = cfg.TencentFloatRuleID
	}
	body, err := s.tencentFaceIDRequest(ctx, cfg, "GetDetectInfoEnhanced", map[string]any{
		"BizToken": externalID,
		"RuleId":   ruleID,
		"InfoType": cfg.TencentInfoType,
	}, true)
	if err != nil {
		return nil, err
	}
	if errValue, ok := body["Error"]; ok {
		errMap, _ := errValue.(map[string]any)
		message := firstVerificationString(errMap["Message"], errMap["Code"])
		if looksProcessing(message) {
			return verificationResult(false, provider, "face_redirect", "processing", "核身尚未完成", externalID, nil, sanitizeTencentResponse(body)), nil
		}
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: fmt.Sprintf("腾讯云实名核身结果查询失败：%s", message)}
	}
	text := parseTextPayload(body["Text"])
	if len(text) == 0 {
		return verificationResult(false, provider, "face_redirect", "processing", "核身尚未完成", externalID, nil, sanitizeTencentResponse(body)), nil
	}
	errCode := intValue(firstNonNil(text["ErrCode"], text["errCode"]))
	compareStatus := intValue(firstNonNil(text["CompareStatus"], text["Comparestatus"]))
	liveStatus := intValue(text["LiveStatus"])
	score := floatPtr(firstNonNil(text["Sim"], text["sim"]))
	reason := firstVerificationString(text["ErrMsg"], text["CompareMsg"], text["Comparemsg"], text["LiveMsg"], "腾讯云实名核身未通过")
	if errCode != 0 || compareStatus != 0 || liveStatus != 0 {
		status := "rejected"
		if looksProcessing(reason) {
			status = "processing"
			reason = "核身尚未完成"
		}
		return verificationResult(false, provider, "face_redirect", status, reason, externalID, score, sanitizeTencentResponse(body)), nil
	}
	if returned := firstVerificationString(text["Name"]); returned != "" && returned != strings.TrimSpace(realName) {
		return verificationResult(false, provider, "face_redirect", "rejected", "腾讯云返回姓名与提交信息不一致", externalID, score, sanitizeTencentResponse(body)), nil
	}
	if returned := strings.ToUpper(firstVerificationString(text["IdCard"], text["IdCardNo"])); returned != "" && returned != strings.ToUpper(strings.TrimSpace(idCardNo)) {
		return verificationResult(false, provider, "face_redirect", "rejected", "腾讯云返回身份证号与提交信息不一致", externalID, score, sanitizeTencentResponse(body)), nil
	}
	if score != nil && *score < cfg.TencentScoreThreshold {
		return verificationResult(false, provider, "face_redirect", "rejected", fmt.Sprintf("人脸比对分数 %.2f 低于阈值 %.2f", *score, cfg.TencentScoreThreshold), externalID, score, sanitizeTencentResponse(body)), nil
	}
	return verificationResult(true, provider, "face_redirect", "verified", "", externalID, score, sanitizeTencentResponse(body)), nil
}

func (s *Service) createLeafSMRedirectSession(ctx context.Context, cfg verificationConfig, realName string, idCardNo string, returnURL string, notifyURL string) (redirectSession, error) {
	if cfg.LeafSMAppID == "" || cfg.LeafSMAppSecret == "" {
		return redirectSession{}, BadRequest("LeafSM H5 人脸核身插件未配置 AppId 或 AppSecret")
	}
	payload := map[string]any{"type": "h5_face", "real_name": realName, "card_no": idCardNo, "return_url": returnURL}
	if notifyURL != "" {
		payload["notify_url"] = notifyURL
	}
	body, err := s.leafSMRequest(ctx, cfg, http.MethodPost, "/api/merchant/verify/tasks", payload, nil)
	if err != nil {
		return redirectSession{}, err
	}
	task, _ := body["task"].(map[string]any)
	taskNo := firstVerificationString(body["task_no"], task["task_no"])
	taskID := firstVerificationString(body["task_id"], body["id"], task["id"])
	verifyURL := firstVerificationString(body["verify_url"])
	if taskNo == "" || verifyURL == "" {
		return redirectSession{}, HTTPError{Status: http.StatusBadGateway, Detail: "LeafSM H5 人脸核身未返回 task_no 或 verify_url"}
	}
	if strings.HasPrefix(verifyURL, "/") {
		verifyURL = strings.TrimRight(defaultVerificationString(cfg.LeafSMH5BaseURL, "https://face.ly-y.cn/"), "/") + verifyURL
	}
	expiresIn := intValue(firstNonNil(body["expires_in"], 7200))
	externalID := taskID
	if externalID == "" {
		externalID = taskNo
	}
	return redirectSession{URL: verifyURL, ExternalID: externalID, ExpiresIn: expiresIn, Raw: map[string]any{"task_no": taskNo, "task_id": taskID, "expires_in": expiresIn}}, nil
}

func (s *Service) queryLeafSMRedirectResult(ctx context.Context, cfg verificationConfig, externalID string) (map[string]any, error) {
	body, err := s.leafSMRequest(ctx, cfg, http.MethodGet, "/api/merchant/verify/tasks/lookup", nil, map[string]string{"task_id": externalID})
	if err != nil {
		return nil, err
	}
	task, _ := body["task"].(map[string]any)
	order, _ := body["order"].(map[string]any)
	liveness, _ := body["liveness_result"].(map[string]any)
	taskStatus := strings.ToLower(firstVerificationString(task["status"]))
	orderStatus := strings.ToLower(firstVerificationString(order["status"]))
	score := floatPtr(firstNonNil(liveness["hack_score"], order["score"]))
	raw := sanitizeLeafSMResponse(body)
	if taskStatus == "completed" {
		matched := truthyAny(order["matched"]) || orderStatus == "completed"
		if matched {
			return verificationResult(true, "leafsm_h5_face", "face_redirect", "verified", "", externalID, score, raw), nil
		}
		return verificationResult(false, "leafsm_h5_face", "face_redirect", "rejected", "人脸核身未通过", externalID, score, raw), nil
	}
	if taskStatus == "failed" || taskStatus == "canceled" || taskStatus == "cancelled" || taskStatus == "expired" {
		reason := firstVerificationString(task["reason"], order["reason"], body["message"], body["msg"], "人脸核身未通过")
		return verificationResult(false, "leafsm_h5_face", "face_redirect", "rejected", reason, externalID, score, raw), nil
	}
	return verificationResult(false, "leafsm_h5_face", "face_redirect", "processing", "用户尚未完成 H5 人脸核身", externalID, score, raw), nil
}

func (s *Service) markUserVerified(ctx context.Context, userID int64, realName string, idCardNo string, remark string) error {
	encryptedName, err := EncryptOptional(realName, s.cfg.JWTSecretKey)
	if err != nil {
		return err
	}
	encryptedID, err := EncryptOptional(idCardNo, s.cfg.JWTSecretKey)
	if err != nil {
		return err
	}
	hash := DeterministicHash(idCardNo, s.cfg.JWTSecretKey)
	now := time.Now().UTC()
	return s.db.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Updates(map[string]any{
		"real_name":             encryptedName,
		"id_card_no":            encryptedID,
		"id_card_no_hash":       hash,
		"id_card_verified":      "verified",
		"id_card_verified_at":   now,
		"id_card_verify_remark": remark,
	}).Error
}

func normalizeVerificationIdentity(realName string, idCardNo string) (string, string, error) {
	name := strings.TrimSpace(realName)
	card := strings.ToUpper(strings.TrimSpace(idCardNo))
	if len([]rune(name)) < 2 || len([]rune(name)) > 50 {
		return "", "", BadRequest("真实姓名长度不正确")
	}
	if !idCardPattern.MatchString(card) {
		return "", "", BadRequest("身份证号格式不正确")
	}
	return name, card, nil
}

func verificationRecordPublic(record UserVerificationRecord, secret string) map[string]any {
	name := DecryptOptional(record.RealName, secret)
	idCard := DecryptOptional(record.IDCardNo, secret)
	return map[string]any{
		"id":                record.ID,
		"user_id":           record.UserID,
		"real_name":         MaskName(name),
		"id_card_no":        MaskIDCard(idCard),
		"provider":          record.Provider,
		"mode":              record.Mode,
		"provider_task_id":  record.ProviderTaskID,
		"status":            record.Status,
		"verified_at":       record.VerifiedAt,
		"rejected_at":       record.RejectedAt,
		"reject_reason":     record.RejectReason,
		"provider_response": jsonAnyValue(record.ProviderResponse),
		"created_at":        record.CreatedAt,
	}
}

func (s *Service) findRedirectRecord(ctx context.Context, userID int64, externalID *string, recordID *int64) (UserVerificationRecord, error) {
	var record UserVerificationRecord
	query := s.db.WithContext(ctx).Where("user_id = ? AND mode = ?", userID, "face_redirect")
	if externalID != nil && strings.TrimSpace(*externalID) != "" {
		query = query.Where("provider_task_id = ?", strings.TrimSpace(*externalID))
	} else if recordID != nil && *recordID > 0 {
		query = query.Where("id = ?", *recordID)
	} else {
		query = query.Order("id DESC")
	}
	err := query.First(&record).Error
	if err == nil {
		return record, nil
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return record, HTTPError{Status: http.StatusNotFound, Detail: "未找到待查询的跳转实名认证记录"}
	}
	return record, err
}

func (s *Service) findRedirectRecordByProvider(ctx context.Context, provider string, externalID string) (UserVerificationRecord, error) {
	var record UserVerificationRecord
	err := s.db.WithContext(ctx).Where("provider = ? AND provider_task_id = ? AND mode = ?", provider, externalID, "face_redirect").First(&record).Error
	if err == nil {
		return record, nil
	}
	var records []UserVerificationRecord
	if e := s.db.WithContext(ctx).Where("provider = ? AND mode = ? AND status = ?", provider, "face_redirect", "processing").Order("id DESC").Limit(50).Find(&records).Error; e != nil {
		return record, e
	}
	for _, item := range records {
		session := recordSession(item)
		if externalID == firstVerificationString(session["task_id"]) || externalID == firstVerificationString(session["task_no"]) || externalID == firstVerificationString(session["external_id"]) {
			return item, nil
		}
	}
	return record, err
}

func (s *Service) applyVerificationResult(ctx context.Context, userID int64, record *UserVerificationRecord, result map[string]any) error {
	status := firstVerificationString(result["status"], "processing")
	record.Status = status
	raw, _ := json.Marshal(result)
	record.ProviderResponse = SettingValue(raw)
	now := time.Now().UTC()
	if status == "verified" || result["passed"] == true {
		record.Status = "verified"
		record.VerifiedAt = &now
		plainName := DecryptOptional(record.RealName, s.cfg.JWTSecretKey)
		plainID := DecryptOptional(record.IDCardNo, s.cfg.JWTSecretKey)
		if err := s.markUserVerified(ctx, userID, derefPtr(plainName), derefPtr(plainID), record.Provider+" "+record.Mode+" verified"); err != nil {
			return err
		}
	} else if status == "rejected" {
		record.RejectedAt = &now
		reason := firstVerificationString(result["reason"], "实名认证未通过")
		record.RejectReason = &reason
	}
	return s.db.WithContext(ctx).Save(record).Error
}

func recordResultSummary(record UserVerificationRecord) map[string]any {
	response, _ := jsonAnyValue(record.ProviderResponse).(map[string]any)
	return map[string]any{
		"record_id":     record.ID,
		"passed":        record.Status == "verified",
		"status":        record.Status,
		"mode":          record.Mode,
		"provider":      record.Provider,
		"score":         response["score"],
		"verified_at":   record.VerifiedAt,
		"reject_reason": record.RejectReason,
		"reason":        response["reason"],
	}
}

func recordSession(record UserVerificationRecord) map[string]any {
	response, _ := jsonAnyValue(record.ProviderResponse).(map[string]any)
	session, _ := response["session"].(map[string]any)
	return session
}

func verificationResult(passed bool, provider string, mode string, status string, reason string, externalID string, score *float64, raw map[string]any) map[string]any {
	out := map[string]any{"passed": passed, "provider": provider, "mode": mode, "status": status, "reason": reason, "external_id": externalID, "raw": raw}
	if score != nil {
		out["score"] = *score
	}
	return out
}

func redirectProviderName(providerPath string) string {
	switch {
	case strings.Contains(providerPath, "tencent_wechat_float_h5"):
		return "tencent_wechat_float_h5"
	case strings.Contains(providerPath, "tencent_wechat_h5"):
		return "tencent_wechat_h5"
	case strings.Contains(providerPath, "leafsm_h5_face"):
		return "leafsm_h5_face"
	default:
		return ""
	}
}

func (s *Service) safeVerificationReturnURL(ctx context.Context, raw *string) string {
	if raw != nil && strings.HasPrefix(strings.TrimSpace(*raw), "http") {
		return strings.TrimSpace(*raw)
	}
	base := s.settingString(ctx, "platform.frontend_url", "http://localhost:5173")
	return strings.TrimRight(base, "/") + "/settings?tab=verification"
}

func (s *Service) safeVerificationNotifyURL(ctx context.Context, raw *string, provider string) string {
	if raw != nil && strings.HasPrefix(strings.TrimSpace(*raw), "http") {
		return strings.TrimSpace(*raw)
	}
	base := s.settingString(ctx, "platform.backend_url", "")
	if base == "" {
		return ""
	}
	return strings.TrimRight(base, "/") + "/api/v1/verification/notify/" + provider
}

func (s *Service) settingString(ctx context.Context, key string, fallback string) string {
	var item SystemSetting
	if err := s.db.WithContext(ctx).Where("`key` = ?", key).First(&item).Error; err != nil {
		return fallback
	}
	return settingString(item.Value, fallback)
}

func extractNotifyExternalID(payload map[string]any) string {
	flat := map[string]any{}
	flattenPayload(payload, flat)
	for _, key := range []string{"external_id", "biz_token", "BizToken", "bizToken", "token", "Token", "task_no", "TaskNo", "taskNo", "task_id", "TaskId", "taskId", "order_no", "OrderNo"} {
		if value := firstVerificationString(flat[key]); value != "" {
			return value
		}
	}
	return ""
}

func flattenPayload(input map[string]any, out map[string]any) {
	for key, value := range input {
		if nested, ok := value.(map[string]any); ok {
			flattenPayload(nested, out)
			continue
		}
		out[key] = value
	}
}

func postJSON(ctx context.Context, endpoint string, payload map[string]any, headers map[string]string) (map[string]any, error) {
	raw, _ := json.Marshal(payload)
	return postJSONRaw(ctx, endpoint, raw, headers)
}

func postJSONRaw(ctx context.Context, endpoint string, raw []byte, headers map[string]string) (map[string]any, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, ServiceUnavailable("第三方实名认证接口请求失败")
	}
	defer resp.Body.Close()
	bodyRaw, _ := io.ReadAll(resp.Body)
	var body map[string]any
	_ = json.Unmarshal(bodyRaw, &body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: fmt.Sprintf("第三方实名认证接口异常：HTTP %d", resp.StatusCode)}
	}
	return body, nil
}

func parseTencentEndpoint(value string) (string, string) {
	text := defaultVerificationString(value, "faceid.tencentcloudapi.com")
	if strings.HasPrefix(text, "http://") || strings.HasPrefix(text, "https://") {
		parsed, _ := url.Parse(text)
		return parsed.Host, parsed.Scheme + "://" + parsed.Host
	}
	return text, "https://" + text
}

func hmacSHA256(key []byte, value string) []byte {
	mac := hmac.New(sha256.New, key)
	_, _ = mac.Write([]byte(value))
	return mac.Sum(nil)
}

func sha256Hex(value []byte) string {
	sum := sha256.Sum256(value)
	return hex.EncodeToString(sum[:])
}

func sanitizeTencentResponse(response map[string]any) map[string]any {
	return sanitizeLargeFields(response)
}

func sanitizeLeafSMResponse(response map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range response {
		if key == "card_no" || key == "real_name" {
			continue
		}
		out[key] = value
	}
	return out
}

func sanitizeLargeFields(response map[string]any) map[string]any {
	large := map[string]bool{"BestFrame": true, "BestFrames": true, "BestFrameBase64": true, "VideoData": true, "VideoBase64": true, "IdCardData": true, "IdCardImage": true, "ImageBase64": true}
	out := map[string]any{}
	for key, value := range response {
		if large[key] {
			continue
		}
		out[key] = value
	}
	return out
}

func parseTextPayload(value any) map[string]any {
	switch typed := value.(type) {
	case map[string]any:
		return typed
	case string:
		var out map[string]any
		if err := json.Unmarshal([]byte(typed), &out); err == nil {
			return out
		}
	}
	return map[string]any{}
}

func jsonAnyValue(raw SettingValue) any {
	if len(raw) == 0 {
		return nil
	}
	var out any
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil
	}
	return out
}

func firstVerificationString(values ...any) string {
	for _, value := range values {
		text := strings.TrimSpace(fmt.Sprint(value))
		if text != "" && text != "<nil>" {
			return text
		}
	}
	return ""
}

func defaultVerificationString(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func firstNonNil(values ...any) any {
	for _, value := range values {
		if value != nil {
			return value
		}
	}
	return nil
}

func intValue(value any) int {
	switch typed := value.(type) {
	case int:
		return typed
	case int64:
		return int(typed)
	case float64:
		return int(typed)
	case string:
		parsed, _ := strconv.Atoi(strings.TrimSpace(typed))
		return parsed
	default:
		return 0
	}
}

func floatPtr(value any) *float64 {
	switch typed := value.(type) {
	case float64:
		return &typed
	case int:
		out := float64(typed)
		return &out
	case int64:
		out := float64(typed)
		return &out
	case string:
		parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
		if err == nil {
			return &parsed
		}
	}
	return nil
}

func statusFromPassed(passed bool) string {
	if passed {
		return "verified"
	}
	return "rejected"
}

func derefPtr(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func truthyAny(value any) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case string:
		return strings.EqualFold(strings.TrimSpace(typed), "true") || strings.TrimSpace(typed) == "1"
	case float64:
		return typed != 0
	default:
		return false
	}
}

func looksProcessing(values ...any) bool {
	text := strings.ToLower(firstVerificationString(values...))
	for _, marker := range []string{"not finished", "not complete", "unfinished", "processing", "in progress", "未完成", "处理中", "等待"} {
		if strings.Contains(text, marker) {
			return true
		}
	}
	return false
}

func stripDataURLBase64(value string) string {
	text := strings.TrimSpace(value)
	if idx := strings.Index(text, ","); strings.HasPrefix(text, "data:") && idx >= 0 {
		return text[idx+1:]
	}
	return text
}

func maxInt(a int, b int) int {
	if a > b {
		return a
	}
	return b
}

func mapLeafSMStatus(status int) int {
	switch status {
	case http.StatusUnauthorized, http.StatusBadRequest, http.StatusUnprocessableEntity:
		return http.StatusBadRequest
	case http.StatusTooManyRequests:
		return http.StatusTooManyRequests
	case http.StatusNotFound:
		return http.StatusNotFound
	default:
		return http.StatusBadGateway
	}
}

func randRead(out []byte) (int, error) {
	return rand.Read(out)
}

func stringPtr(value string) *string {
	if value == "" {
		return nil
	}
	return &value
}
