package commerce

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"gorm.io/gorm"
)

type AdminListQuery struct {
	Status  string
	Search  string
	Role    string
	GroupID *int64
	Limit   int
	Offset  int
}

type AdminAgentSettlementQuery struct {
	Status         string
	SettlementType string
	UserID         *int64
	OrderID        *int64
	Limit          int
	Offset         int
}

type AdminUpstreamActionQuery struct {
	Status     string
	Search     string
	Source     string
	ActionType string
	UserID     *int64
	OrderID    *int64
	InstanceID *int64
	Limit      int
	Offset     int
}

type AdminServiceOrderQuery struct {
	Status         string
	Search         string
	RefundStatus   string
	RefundType     string
	ResourceStatus string
	UserID         *int64
	InstanceID     *int64
	Limit          int
	Offset         int
}

type AdminUserUpdateRequest struct {
	Role              *string         `json:"role"`
	Status            *string         `json:"status"`
	Nickname          *string         `json:"nickname"`
	Phone             *string         `json:"phone"`
	Email             *string         `json:"email"`
	Avatar            *string         `json:"avatar"`
	RealName          *string         `json:"real_name"`
	IDCardNo          *string         `json:"id_card_no"`
	IDCardVerified    *string         `json:"id_card_verified"`
	MemberLevelID     *int64          `json:"member_level_id"`
	MemberLevelLocked *bool           `json:"member_level_locked"`
	AgentTier         *string         `json:"agent_tier"`
	IsEmailVerified   *bool           `json:"is_email_verified"`
	IsPhoneVerified   *bool           `json:"is_phone_verified"`
	present           map[string]bool `json:"-"`
}

func (r *AdminUserUpdateRequest) UnmarshalJSON(data []byte) error {
	type requestAlias AdminUserUpdateRequest
	var decoded requestAlias
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(data, &fields); err != nil {
		return err
	}
	*r = AdminUserUpdateRequest(decoded)
	r.present = make(map[string]bool, len(fields))
	for key := range fields {
		r.present[key] = true
	}
	return nil
}

func (r AdminUserUpdateRequest) has(key string) bool {
	return r.present[key]
}

type AdminOrderUpdateRequest struct {
	Status *string `json:"status"`
	Remark *string `json:"remark"`
}

type AdminSettingRequest struct {
	Value       any     `json:"value"`
	Label       *string `json:"label"`
	Description *string `json:"description"`
}

type AdminProductPayload struct {
	Category      *ProductCategory           `json:"category"`
	Group         *ProductGroup              `json:"group"`
	Product       *Product                   `json:"product"`
	Pricings      []ProductPricing           `json:"pricings"`
	ProductFields map[string]json.RawMessage `json:"-"`
}

func (s *Service) AdminDashboard(ctx context.Context) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}

	todayStart := todayStartUTC()
	usersTotal, err := s.countModel(ctx, &UserAccount{}, nil)
	if err != nil {
		return nil, err
	}
	productsTotal, err := s.countModel(ctx, &Product{}, nil)
	if err != nil {
		return nil, err
	}
	productsActive, err := s.countModel(ctx, &Product{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status IN ?", []string{"active", "on_sale", "enabled"})
	})
	if err != nil {
		return nil, err
	}
	cloudTotal, err := s.countModel(ctx, &CloudServer{}, nil)
	if err != nil {
		return nil, err
	}
	cloudOnline, err := s.countModel(ctx, &CloudServer{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status IN ?", []string{"online", "active", "success"})
	})
	if err != nil {
		return nil, err
	}
	cloudEnabled, err := s.countModel(ctx, &CloudServer{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("disabled = ?", false)
	})
	if err != nil {
		return nil, err
	}

	var todayIncomeCents int64
	if err := s.db.WithContext(ctx).
		Model(&RechargeOrder{}).
		Where("status = ? AND updated_at >= ?", "paid", todayStart).
		Select("COALESCE(SUM(amount_cents), 0)").
		Scan(&todayIncomeCents).Error; err != nil {
		return nil, err
	}

	ordersTotal, err := s.countModel(ctx, &Order{}, nil)
	if err != nil {
		return nil, err
	}
	pendingOrders, err := s.countModel(ctx, &Order{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status IN ?", []string{"pending", "pending_payment", "processing"})
	})
	if err != nil {
		return nil, err
	}
	doneOrders, err := s.countModel(ctx, &Order{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status IN ?", []string{"paid", "cancelled", "refunded", "completed"})
	})
	if err != nil {
		return nil, err
	}

	rechargeTotal, err := s.countModel(ctx, &RechargeOrder{}, nil)
	if err != nil {
		return nil, err
	}
	pendingRecharges, err := s.countModel(ctx, &RechargeOrder{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status IN ?", []string{"pending", "unpaid", "created"})
	})
	if err != nil {
		return nil, err
	}
	paidRecharges, err := s.countModel(ctx, &RechargeOrder{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status = ?", "paid")
	})
	if err != nil {
		return nil, err
	}
	failedActions, err := s.countModel(ctx, &UpstreamActionLog{}, func(db *gorm.DB) *gorm.DB {
		return db.Where("status IN ?", []string{"failed", "error", "timeout"})
	})
	if err != nil {
		return nil, err
	}
	events, err := s.adminRecentEvents(ctx)
	if err != nil {
		return nil, err
	}

	return map[string]any{
		"stats": map[string]any{
			"users_total":         usersTotal,
			"today_income_cents":  todayIncomeCents,
			"products_total":      productsTotal,
			"products_active":     productsActive,
			"cloud_nodes_total":   cloudTotal,
			"cloud_nodes_online":  cloudOnline,
			"cloud_nodes_enabled": cloudEnabled,
		},
		"overview": map[string]any{
			"order_processing_percent":   percentInt(doneOrders, ordersTotal),
			"cloud_connectivity_percent": percentInt(cloudOnline, cloudEnabled),
			"finance_reconcile_percent":  percentInt(paidRecharges, rechargeTotal),
			"pending_orders":             pendingOrders,
			"pending_recharges":          pendingRecharges,
			"failed_resource_actions":    failedActions,
		},
		"recent_events": events,
	}, nil
}

func todayStartUTC() time.Time {
	loc, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		loc = time.FixedZone("Asia/Shanghai", 8*3600)
	}
	now := time.Now().In(loc)
	return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).UTC()
}

func (s *Service) countModel(ctx context.Context, model any, scope func(*gorm.DB) *gorm.DB) (int64, error) {
	db := s.db.WithContext(ctx).Model(model)
	if scope != nil {
		db = scope(db)
	}
	var count int64
	if err := db.Count(&count).Error; err != nil {
		return 0, err
	}
	return count, nil
}

func percentInt(done, total int64) int {
	if total <= 0 {
		return 0
	}
	value := int((done*100 + total/2) / total)
	if value < 0 {
		return 0
	}
	if value > 100 {
		return 100
	}
	return value
}

func (s *Service) adminRecentEvents(ctx context.Context) ([]map[string]any, error) {
	events := make([]map[string]any, 0, 15)

	var users []UserAccount
	if err := s.db.WithContext(ctx).Order("id DESC").Limit(5).Find(&users).Error; err != nil {
		return nil, err
	}
	for _, user := range users {
		name := strings.TrimSpace(user.Username)
		if user.Nickname != nil && strings.TrimSpace(*user.Nickname) != "" {
			name = strings.TrimSpace(*user.Nickname)
		}
		if name == "" {
			name = fmt.Sprintf("用户 #%d", user.ID)
		}
		events = append(events, map[string]any{
			"key":       fmt.Sprintf("user-%d", user.ID),
			"type":      "用户",
			"action":    fmt.Sprintf("%s 注册", name),
			"status":    user.Status,
			"time":      user.CreatedAt,
			"sort_time": user.CreatedAt,
		})
	}

	var recharges []RechargeOrder
	if err := s.db.WithContext(ctx).Order("id DESC").Limit(5).Find(&recharges).Error; err != nil {
		return nil, err
	}
	for _, item := range recharges {
		ts := item.UpdatedAt
		if ts.IsZero() {
			ts = item.CreatedAt
		}
		events = append(events, map[string]any{
			"key":       fmt.Sprintf("recharge-%d", item.ID),
			"type":      "充值",
			"action":    fmt.Sprintf("充值订单 %s ¥%.2f", item.OutTradeNo, float64(item.AmountCents)/100),
			"status":    item.Status,
			"time":      ts,
			"sort_time": ts,
		})
	}

	var orders []Order
	if err := s.db.WithContext(ctx).Order("id DESC").Limit(5).Find(&orders).Error; err != nil {
		return nil, err
	}
	for _, item := range orders {
		ts := item.UpdatedAt
		if ts.IsZero() {
			ts = item.CreatedAt
		}
		amount := item.TotalAmount - item.DiscountAmount
		if amount < 0 {
			amount = 0
		}
		events = append(events, map[string]any{
			"key":       fmt.Sprintf("order-%d", item.ID),
			"type":      "订单",
			"action":    fmt.Sprintf("服务订单 %s ¥%.2f", item.OutOrderNo, float64(amount)/100),
			"status":    item.Status,
			"time":      ts,
			"sort_time": ts,
		})
	}

	var actions []UpstreamActionLog
	if err := s.db.WithContext(ctx).Order("id DESC").Limit(5).Find(&actions).Error; err != nil {
		return nil, err
	}
	for _, item := range actions {
		ts := item.UpdatedAt
		if ts.IsZero() {
			ts = item.CreatedAt
		}
		events = append(events, map[string]any{
			"key":       fmt.Sprintf("upstream-%d", item.ID),
			"type":      "上游",
			"action":    fmt.Sprintf("%s %s", item.ResourceType, item.ActionType),
			"status":    item.Status,
			"time":      ts,
			"sort_time": ts,
		})
	}

	sort.SliceStable(events, func(i, j int) bool {
		left, _ := events[i]["sort_time"].(time.Time)
		right, _ := events[j]["sort_time"].(time.Time)
		return left.After(right)
	})
	if len(events) > 8 {
		events = events[:8]
	}
	for _, item := range events {
		delete(item, "sort_time")
	}
	return events, nil
}

func (s *Service) AdminListUsers(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&UserAccount{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if query.Role != "" {
		db = db.Where("role = ?", query.Role)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("username LIKE ? OR email LIKE ? OR phone LIKE ?", like, like, like)
	}
	return adminPage(db, query, func(items []UserAccount) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, adminUserMap(item))
		}
		return out
	})
}

func (s *Service) AdminGetUser(ctx context.Context, userID int64) (map[string]any, error) {
	var user UserAccount
	if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("user not found")
		}
		return nil, err
	}
	return s.adminUserDetail(ctx, user)
}

func (s *Service) AdminUpdateUser(ctx context.Context, adminID int64, userID int64, req AdminUserUpdateRequest) (map[string]any, error) {
	updates := map[string]any{}
	if req.Role != nil {
		role := strings.TrimSpace(*req.Role)
		if role != "user" && role != "admin" && role != "super_admin" {
			return nil, BadRequest("invalid role")
		}
		updates["role"] = role
	}
	if req.Status != nil {
		status := strings.TrimSpace(*req.Status)
		if status != "active" && status != "inactive" && status != "banned" {
			return nil, BadRequest("invalid status")
		}
		updates["status"] = status
	}
	if req.Nickname != nil || req.has("nickname") {
		updates["nickname"] = trimPtr(req.Nickname)
	}
	if req.Phone != nil || req.has("phone") {
		updates["phone"] = trimPtr(req.Phone)
	}
	if req.Email != nil || req.has("email") {
		updates["email"] = trimPtr(req.Email)
	}
	if req.Avatar != nil || req.has("avatar") {
		updates["avatar"] = trimPtr(req.Avatar)
	}
	if req.IsEmailVerified != nil {
		updates["is_email_verified"] = *req.IsEmailVerified
	}
	if req.IsPhoneVerified != nil {
		updates["is_phone_verified"] = *req.IsPhoneVerified
	}
	if req.IDCardVerified != nil {
		value := strings.TrimSpace(*req.IDCardVerified)
		if value != "unverified" && value != "pending" && value != "verified" && value != "rejected" {
			return nil, BadRequest("invalid verification status")
		}
		updates["id_card_verified"] = value
		if value == "verified" {
			updates["id_card_verified_at"] = time.Now().UTC()
		} else if value == "unverified" {
			updates["id_card_verified_at"] = nil
			updates["id_card_verify_remark"] = nil
			updates["real_name"] = nil
			updates["id_card_no"] = nil
			updates["id_card_no_hash"] = nil
		}
	}
	if req.RealName != nil || req.has("real_name") {
		if s.identity == nil {
			return nil, ServiceUnavailable("identity service is not configured")
		}
		value := ""
		if req.RealName != nil {
			value = *req.RealName
		}
		encrypted, err := s.identity.EncryptSensitiveOptional(value)
		if err != nil {
			return nil, err
		}
		updates["real_name"] = encrypted
	}
	if req.IDCardNo != nil || req.has("id_card_no") {
		if s.identity == nil {
			return nil, ServiceUnavailable("identity service is not configured")
		}
		value := ""
		if req.IDCardNo != nil {
			value = *req.IDCardNo
		}
		plain := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(value), " ", ""))
		hash := s.identity.SensitiveHash(plain)
		if plain != "" {
			var duplicate int64
			if err := s.db.WithContext(ctx).Model(&UserAccount{}).Where("id <> ? AND (id_card_no_hash = ? OR id_card_no = ?)", userID, hash, plain).Count(&duplicate).Error; err != nil {
				return nil, err
			}
			if duplicate > 0 {
				return nil, BadRequest("身份证号已被其他用户使用")
			}
		}
		encrypted, err := s.identity.EncryptSensitiveOptional(plain)
		if err != nil {
			return nil, err
		}
		updates["id_card_no"] = encrypted
		updates["id_card_no_hash"] = hash
	}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var exists int64
		if err := tx.Model(&UserAccount{}).Where("id = ?", userID).Count(&exists).Error; err != nil {
			return err
		}
		if exists == 0 {
			return NotFound("user not found")
		}
		if len(updates) > 0 {
			if err := tx.Model(&UserAccount{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
				return err
			}
		}
		if req.MemberLevelID != nil || req.MemberLevelLocked != nil || req.has("member_level_id") {
			membership := UserMembership{UserID: userID}
			if err := tx.Where("user_id = ?", userID).FirstOrCreate(&membership).Error; err != nil {
				return err
			}
			membershipUpdates := map[string]any{}
			if req.MemberLevelID != nil || req.has("member_level_id") {
				if req.MemberLevelID != nil && *req.MemberLevelID > 0 {
					var level MemberLevel
					if err := tx.Where("id = ? AND enabled = ?", *req.MemberLevelID, true).First(&level).Error; err != nil {
						if errors.Is(err, gorm.ErrRecordNotFound) {
							return BadRequest("会员等级不存在或已停用")
						}
						return err
					}
					membershipUpdates["member_level_id"] = *req.MemberLevelID
					membershipUpdates["upgraded_at"] = time.Now().UTC()
				} else {
					membershipUpdates["member_level_id"] = nil
					membershipUpdates["upgraded_at"] = nil
				}
			}
			if req.MemberLevelLocked != nil {
				membershipUpdates["member_level_locked"] = *req.MemberLevelLocked
			}
			if len(membershipUpdates) > 0 {
				if err := tx.Model(&UserMembership{}).Where("user_id = ?", userID).Updates(membershipUpdates).Error; err != nil {
					return err
				}
			}
		}
		if req.AgentTier != nil || req.has("agent_tier") {
			tierCode := ""
			if req.AgentTier != nil {
				tierCode = strings.TrimSpace(*req.AgentTier)
			}
			var membership UserMembership
			if err := tx.Where("user_id = ?", userID).FirstOrCreate(&membership, UserMembership{UserID: userID}).Error; err != nil {
				return err
			}
			var storedTier any
			if tierCode != "" {
				storedTier = tierCode
			}
			if err := tx.Model(&UserMembership{}).Where("user_id = ?", userID).Update("agent_tier", storedTier).Error; err != nil {
				return err
			}
			return s.withDB(tx).adminSetUserAgentTier(ctx, adminID, userID, tierCode)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return s.AdminGetUser(ctx, userID)
}

func (s *Service) AdminListOrders(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&Order{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("out_order_no LIKE ? OR remark LIKE ?", like, like)
	}
	return adminPage(db.Order("id DESC"), query, func(items []Order) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, adminOrderMap(item))
		}
		return out
	})
}

func (s *Service) AdminGetOrder(ctx context.Context, orderID int64) (map[string]any, error) {
	var order Order
	if err := s.db.WithContext(ctx).Where("id = ?", orderID).First(&order).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("order not found")
		}
		return nil, err
	}
	data := adminOrderMap(order)
	var instances []Instance
	_ = s.db.WithContext(ctx).Where("order_id = ?", order.ID).Order("id ASC").Find(&instances).Error
	list := make([]map[string]any, 0, len(instances))
	for _, instance := range instances {
		list = append(list, map[string]any{"id": instance.ID, "status": instance.Status, "main_ip": instance.MainIP, "hostname": instance.Hostname, "expires_at": instance.ExpiresAt})
	}
	data["instances"] = list
	return data, nil
}

func (s *Service) AdminUpdateOrder(ctx context.Context, orderID int64, req AdminOrderUpdateRequest) (map[string]any, error) {
	updates := map[string]any{}
	if req.Status != nil {
		updates["status"] = strings.TrimSpace(*req.Status)
	}
	if req.Remark != nil {
		updates["remark"] = trimPtr(req.Remark)
	}
	if len(updates) == 0 {
		return s.AdminGetOrder(ctx, orderID)
	}
	result := s.db.WithContext(ctx).Model(&Order{}).Where("id = ?", orderID).Updates(updates)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("order not found")
	}
	return s.AdminGetOrder(ctx, orderID)
}

func (s *Service) AdminListServiceOrders(ctx context.Context, query AdminServiceOrderQuery) ([]map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	db := s.db.WithContext(ctx).Model(&Order{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if query.UserID != nil {
		db = db.Where("user_id = ?", *query.UserID)
	}
	if query.InstanceID != nil {
		db = db.Where("id IN (?)", s.db.Table("instances").Select("order_id").Where("id = ?", *query.InstanceID))
	}
	if query.RefundStatus != "" {
		db = db.Where("id IN (?)", s.db.Table("order_refunds").Select("order_id").Where("status = ?", query.RefundStatus))
	}
	if query.RefundType != "" {
		db = db.Where("id IN (?)", s.db.Table("order_refunds").Select("order_id").Where("refund_type = ?", query.RefundType))
	}
	if query.ResourceStatus != "" {
		db = scopeServiceOrderResourceStatus(db, s.db, query.ResourceStatus)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		userIDs := s.db.Table("users").Select("id").Where("username LIKE ? OR email LIKE ? OR phone LIKE ? OR nickname LIKE ?", like, like, like, like)
		instanceOrderIDs := s.db.Table("instances").Select("order_id").Where("hostname LIKE ? OR main_ip LIKE ?", like, like)
		db = db.Where("out_order_no LIKE ? OR remark LIKE ? OR user_id IN (?) OR id IN (?)", like, like, userIDs, instanceOrderIDs)
	}
	limit := query.Limit
	if limit <= 0 {
		limit = 100
	}
	var orders []Order
	if err := db.Order("id DESC").Limit(limit).Offset(query.Offset).Find(&orders).Error; err != nil {
		return nil, err
	}
	users, err := s.adminUsersByOrder(ctx, orders)
	if err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0, len(orders))
	for _, order := range orders {
		item, err := s.adminServiceOrderMap(ctx, order, users[order.UserID])
		if err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	return out, nil
}

func (s *Service) AdminGetServiceOrder(ctx context.Context, orderID int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var order Order
	if err := s.db.WithContext(ctx).Where("id = ?", orderID).First(&order).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("order not found")
		}
		return nil, err
	}
	var user UserAccount
	if err := s.db.WithContext(ctx).Where("id = ?", order.UserID).First(&user).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	return s.adminServiceOrderMap(ctx, order, &user)
}

func (s *Service) AdminUnsubscribeServiceOrder(ctx context.Context, orderID int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var order Order
	if err := s.db.WithContext(ctx).Where("id = ?", orderID).First(&order).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("order not found")
		}
		return nil, err
	}
	return s.UnsubscribeOrder(ctx, order.UserID, order.ID)
}

func scopeServiceOrderResourceStatus(db *gorm.DB, root *gorm.DB, status string) *gorm.DB {
	switch status {
	case "none":
		refunds := root.Table("order_refunds").Select("order_id").Where("resource_status <> '' AND resource_status <> ?", "none")
		actions := root.Table("upstream_action_logs").Select("order_id").Where("order_id IS NOT NULL")
		return db.Where("id NOT IN (?) AND id NOT IN (?)", refunds, actions)
	case "failed":
		refunds := root.Table("order_refunds").Select("order_id").Where("resource_status = ?", "failed")
		actions := root.Table("upstream_action_logs").Select("order_id").Where("status = ?", "failed")
		return db.Where("id IN (?) OR id IN (?)", refunds, actions)
	case "running":
		refunds := root.Table("order_refunds").Select("order_id").Where("resource_status = ?", "running")
		actions := root.Table("upstream_action_logs").Select("order_id").Where("status IN ?", []string{"submitted", "running"})
		return db.Where("id IN (?) OR id IN (?)", refunds, actions)
	case "finished":
		refunds := root.Table("order_refunds").Select("order_id").Where("resource_status = ?", "finished")
		actions := root.Table("upstream_action_logs").Select("order_id").Where("status IN ?", []string{"success", "no_task"})
		return db.Where("id IN (?) OR id IN (?)", refunds, actions)
	case "manual_handled", "ignored":
		refunds := root.Table("order_refunds").Select("order_id").Where("resource_status = ?", status)
		actions := root.Table("upstream_action_logs").Select("order_id").Where("status = ?", status)
		return db.Where("id IN (?) OR id IN (?)", refunds, actions)
	default:
		return db
	}
}

func (s *Service) adminUsersByOrder(ctx context.Context, orders []Order) (map[int64]*UserAccount, error) {
	ids := make([]int64, 0, len(orders))
	seen := map[int64]struct{}{}
	for _, order := range orders {
		if _, ok := seen[order.UserID]; ok {
			continue
		}
		seen[order.UserID] = struct{}{}
		ids = append(ids, order.UserID)
	}
	out := map[int64]*UserAccount{}
	if len(ids) == 0 {
		return out, nil
	}
	var users []UserAccount
	if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&users).Error; err != nil {
		return nil, err
	}
	for i := range users {
		out[users[i].ID] = &users[i]
	}
	return out, nil
}

func (s *Service) adminServiceOrderMap(ctx context.Context, order Order, user *UserAccount) (map[string]any, error) {
	instances, err := s.adminOrderInstances(ctx, order)
	if err != nil {
		return nil, err
	}
	refund, err := s.latestRefund(ctx, order.ID)
	if err != nil {
		return nil, err
	}
	data := s.newServiceOrderMap(ctx, order, instances, refund)
	data["user"] = adminServiceOrderUserMap(order.UserID, user)
	data["resource_summary"] = serviceOrderResourceSummary(data["upstream_actions"], refund)
	return data, nil
}

func (s *Service) adminOrderInstances(ctx context.Context, order Order) ([]Instance, error) {
	instanceIDs := instanceIDsFromOrderItems(order.ItemsSnapshot)
	db := s.db.WithContext(ctx).Where("user_id = ?", order.UserID)
	if len(instanceIDs) > 0 {
		db = db.Where("order_id = ? OR id IN ?", order.ID, instanceIDs)
	} else {
		db = db.Where("order_id = ?", order.ID)
	}
	var instances []Instance
	if err := db.Order("id ASC").Find(&instances).Error; err != nil {
		return nil, err
	}
	return instances, nil
}

func instanceIDsFromOrderItems(raw JSONRaw) []int64 {
	items := jsonList(raw)
	seen := map[int64]struct{}{}
	out := make([]int64, 0)
	for _, item := range items {
		row, ok := item.(map[string]any)
		if !ok {
			continue
		}
		id, ok := int64FromAny(row["instance_id"])
		if !ok || id <= 0 {
			continue
		}
		if _, exists := seen[id]; exists {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out
}

func adminServiceOrderUserMap(userID int64, user *UserAccount) map[string]any {
	if user == nil || user.ID == 0 {
		return map[string]any{"id": userID, "username": nil, "email": nil, "phone": nil, "nickname": nil}
	}
	return map[string]any{
		"id":       user.ID,
		"username": user.Username,
		"email":    user.Email,
		"phone":    user.Phone,
		"nickname": user.Nickname,
	}
}

func serviceOrderResourceSummary(rawActions any, refund *OrderRefund) map[string]any {
	actions, _ := rawActions.([]map[string]any)
	counts := map[string]int{"failed": 0, "running": 0, "finished": 0, "manual_handled": 0, "ignored": 0}
	logs := make([]UpstreamActionLog, 0, len(actions))
	for _, action := range actions {
		status := strings.TrimSpace(fmt.Sprint(action["status"]))
		switch status {
		case "failed":
			counts["failed"]++
		case "submitted", "running":
			counts["running"]++
		case "success", "no_task":
			counts["finished"]++
		case "manual_handled":
			counts["manual_handled"]++
		case "ignored":
			counts["ignored"]++
		}
		logs = append(logs, UpstreamActionLog{Status: status})
	}
	status := summarizeResourceStatus(logs)
	if refund != nil && strings.TrimSpace(refund.ResourceStatus) != "" {
		status = refund.ResourceStatus
	}
	return map[string]any{
		"status":        status,
		"label":         resourceStatusLabel(status),
		"failed_count":  counts["failed"],
		"running_count": counts["running"],
	}
}

func resourceStatusLabel(status string) string {
	switch status {
	case "none":
		return "无资源动作"
	case "running":
		return "处理中"
	case "failed":
		return "处理失败"
	case "finished":
		return "已完成"
	case "manual_handled":
		return "人工已处理"
	case "ignored":
		return "已忽略"
	default:
		return status
	}
}

func (s *Service) AdminListCategories(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&ProductCategory{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("name LIKE ? OR slug LIKE ?", like, like)
	}
	return adminPage(db.Order("sort_order ASC, id DESC"), query, func(items []ProductCategory) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, mapFromJSON(item))
		}
		return out
	})
}

func (s *Service) AdminSaveCategory(ctx context.Context, item ProductCategory) (map[string]any, error) {
	if item.ID > 0 {
		var existing ProductCategory
		if err := s.db.WithContext(ctx).Where("id = ?", item.ID).First(&existing).Error; err != nil {
			return nil, err
		}
		if strings.TrimSpace(item.Name) == "" {
			item.Name = existing.Name
		}
		if strings.TrimSpace(item.Slug) == "" {
			item.Slug = existing.Slug
		}
		if item.Status == "" {
			item.Status = existing.Status
		}
		if item.FeaturedLabel == "" {
			item.FeaturedLabel = "推荐"
		}
		if strings.TrimSpace(item.Name) == "" || strings.TrimSpace(item.Slug) == "" {
			return nil, BadRequest("category name and slug are required")
		}
		updates := map[string]any{
			"slug":           strings.TrimSpace(item.Slug),
			"name":           strings.TrimSpace(item.Name),
			"description":    item.Description,
			"icon":           item.Icon,
			"sort_order":     item.SortOrder,
			"status":         item.Status,
			"is_featured":    item.IsFeatured,
			"featured_label": item.FeaturedLabel,
		}
		if err := s.db.WithContext(ctx).Model(&ProductCategory{}).Where("id = ?", item.ID).Updates(updates).Error; err != nil {
			return nil, err
		}
		if err := s.db.WithContext(ctx).Where("id = ?", item.ID).First(&item).Error; err != nil {
			return nil, err
		}
	} else {
		item.Name = strings.TrimSpace(item.Name)
		item.Slug = strings.TrimSpace(item.Slug)
		if strings.TrimSpace(item.Name) == "" || strings.TrimSpace(item.Slug) == "" {
			return nil, BadRequest("category name and slug are required")
		}
		if item.Status == "" {
			item.Status = "active"
		}
		if item.FeaturedLabel == "" {
			item.FeaturedLabel = "推荐"
		}
		if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
			return nil, err
		}
	}
	return mapFromJSON(item), nil
}

func (s *Service) AdminListGroups(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&ProductGroup{})
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("name LIKE ? OR slug LIKE ?", like, like)
	}
	return adminPage(db.Order("sort_order ASC, id DESC"), query, func(items []ProductGroup) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			row := mapFromJSON(item)
			row["os_options"] = jsonList(item.OSOptions)
			out = append(out, row)
		}
		return out
	})
}

func (s *Service) AdminSaveGroup(ctx context.Context, item ProductGroup) (map[string]any, error) {
	if item.ID > 0 {
		var existing ProductGroup
		if err := s.db.WithContext(ctx).Where("id = ?", item.ID).First(&existing).Error; err != nil {
			return nil, err
		}
		if item.CategoryID <= 0 {
			item.CategoryID = existing.CategoryID
		}
		if strings.TrimSpace(item.Name) == "" {
			item.Name = existing.Name
		}
		if strings.TrimSpace(item.Slug) == "" {
			item.Slug = existing.Slug
		}
		if item.CategoryID <= 0 || strings.TrimSpace(item.Name) == "" || strings.TrimSpace(item.Slug) == "" {
			return nil, BadRequest("group category_id, name and slug are required")
		}
		updates := map[string]any{
			"category_id":        item.CategoryID,
			"slug":               strings.TrimSpace(item.Slug),
			"name":               strings.TrimSpace(item.Name),
			"headline":           item.Headline,
			"description":        item.Description,
			"icon":               item.Icon,
			"network_type":       item.NetworkType,
			"sort_order":         item.SortOrder,
			"requires_real_name": item.RequiresRealName,
			"area":               item.Area,
			"node":               item.Node,
		}
		if item.OSOptions != nil {
			updates["os_options"] = item.OSOptions
		}
		if err := s.db.WithContext(ctx).Model(&ProductGroup{}).Where("id = ?", item.ID).Updates(updates).Error; err != nil {
			return nil, err
		}
		if err := s.db.WithContext(ctx).Where("id = ?", item.ID).First(&item).Error; err != nil {
			return nil, err
		}
	} else {
		item.Name = strings.TrimSpace(item.Name)
		item.Slug = strings.TrimSpace(item.Slug)
		if item.CategoryID <= 0 || strings.TrimSpace(item.Name) == "" || strings.TrimSpace(item.Slug) == "" {
			return nil, BadRequest("group category_id, name and slug are required")
		}
		if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
			return nil, err
		}
	}
	row := mapFromJSON(item)
	row["os_options"] = jsonList(item.OSOptions)
	return row, nil
}

func (s *Service) AdminSaveGroupVerificationRule(ctx context.Context, groupID int64, requiresRealName bool, applyToProducts bool) (map[string]any, error) {
	var group ProductGroup
	var updatedProducts int64
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Where("id = ?", groupID).First(&group).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return NotFound("product group not found")
			}
			return err
		}
		if err := tx.Model(&ProductGroup{}).Where("id = ?", groupID).Update("requires_real_name", requiresRealName).Error; err != nil {
			return err
		}
		if applyToProducts {
			result := tx.Model(&Product{}).Where("group_id = ?", groupID).Update("requires_real_name", requiresRealName)
			if result.Error != nil {
				return result.Error
			}
			updatedProducts = result.RowsAffected
		}
		if err := tx.Where("id = ?", groupID).First(&group).Error; err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	row := mapFromJSON(group)
	row["os_options"] = jsonList(group.OSOptions)
	return map[string]any{"group": row, "updated_products": updatedProducts}, nil
}

func (s *Service) AdminListProducts(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&Product{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if query.GroupID != nil {
		db = db.Where("group_id = ?", *query.GroupID)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("name LIKE ? OR sku LIKE ?", like, like)
	}
	return adminPage(db.Order("sort_order ASC, id DESC"), query, func(items []Product) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, s.adminProductMap(ctx, item, false))
		}
		return out
	})
}

func (s *Service) AdminGetProduct(ctx context.Context, productID int64) (map[string]any, error) {
	var product Product
	if err := s.db.WithContext(ctx).Where("id = ?", productID).First(&product).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("product not found")
		}
		return nil, err
	}
	return s.adminProductMap(ctx, product, true), nil
}

func (s *Service) AdminSaveProduct(ctx context.Context, product Product, pricings []ProductPricing, fields map[string]json.RawMessage) (map[string]any, error) {
	if product.ID > 0 {
		var existing Product
		if err := s.db.WithContext(ctx).Where("id = ?", product.ID).First(&existing).Error; err != nil {
			return nil, err
		}
		product = mergeProductForUpdate(existing, product, fields)
	}
	if product.GroupID <= 0 || strings.TrimSpace(product.Name) == "" || strings.TrimSpace(product.SKU) == "" {
		return nil, BadRequest("product group_id, sku and name are required")
	}
	if product.Status == "" {
		product.Status = "active"
	}
	if product.Type == "" {
		product.Type = "server"
	}
	if product.StockPolicy == "" {
		product.StockPolicy = "finite"
	}
	if err := s.validateProductForSave(ctx, product, pricings); err != nil {
		return nil, err
	}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if product.ID > 0 {
			updates := productUpdateMap(product, fields)
			if len(updates) > 0 {
				if err := tx.Model(&Product{}).Where("id = ?", product.ID).Updates(updates).Error; err != nil {
					return err
				}
			}
			if err := tx.Where("id = ?", product.ID).First(&product).Error; err != nil {
				return err
			}
		} else if err := tx.Create(&product).Error; err != nil {
			return err
		}
		if pricings != nil {
			if err := tx.Where("product_id = ?", product.ID).Delete(&ProductPricing{}).Error; err != nil {
				return err
			}
			for i := range pricings {
				pricings[i].ID = 0
				pricings[i].ProductID = product.ID
				if pricings[i].Cycle == "" {
					continue
				}
				if err := tx.Create(&pricings[i]).Error; err != nil {
					return err
				}
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return s.AdminGetProduct(ctx, product.ID)
}

func (s *Service) validateProductForSave(ctx context.Context, product Product, pricings []ProductPricing) error {
	sku := strings.TrimSpace(product.SKU)
	if sku == "" {
		return BadRequest("产品 SKU 不能为空")
	}
	var duplicateCount int64
	query := s.db.WithContext(ctx).Model(&Product{}).Where("sku = ?", sku)
	if product.ID > 0 {
		query = query.Where("id <> ?", product.ID)
	}
	if err := query.Count(&duplicateCount).Error; err != nil {
		return err
	}
	if duplicateCount > 0 {
		return BadRequest("产品 SKU 已存在")
	}

	if pricings != nil {
		seenCycles := map[string]struct{}{}
		for _, pricing := range pricings {
			cycle := strings.TrimSpace(pricing.Cycle)
			if cycle == "" {
				continue
			}
			if _, exists := seenCycles[cycle]; exists {
				return BadRequest("价格周期不能重复：" + cycle)
			}
			seenCycles[cycle] = struct{}{}
			if pricing.Price < 0 || pricing.SetupFee < 0 {
				return BadRequest("价格和初装费不能为负数")
			}
		}
	}

	if strings.TrimSpace(product.Status) != "active" || !product.AutoProvision {
		return nil
	}
	apiType := strings.TrimSpace(product.APIType)
	switch apiType {
	case "", "none":
		return BadRequest("产品开启了自动开通，但没有配置开通方式")
	case "plugin":
		if product.APIIdentifier == nil || strings.TrimSpace(*product.APIIdentifier) == "" {
			return BadRequest("插件开通产品必须绑定开通插件")
		}
	case "dcim_cloud_image", "dcim_cloud_rid", "zjmf_finance":
		if product.CloudServerID == nil {
			return BadRequest("自动开通产品必须绑定上游接口")
		}
		var server CloudServer
		if err := s.db.WithContext(ctx).Where("id = ?", *product.CloudServerID).First(&server).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return BadRequest("绑定的上游接口不存在")
			}
			return err
		}
		if server.Disabled {
			return BadRequest("绑定的上游接口已禁用")
		}
		if apiType == "zjmf_finance" && !isZJMFServer(server) {
			return BadRequest("ZJMF 财务产品必须绑定 ZJMF 财务类型上游")
		}
		if strings.HasPrefix(apiType, "dcim_cloud") && !isDCIMServer(server) {
			return BadRequest("DCIM 产品必须绑定 DCIM 云服务器类型上游")
		}
		if apiType == "dcim_cloud_image" && (product.APIIdentifier == nil || strings.TrimSpace(*product.APIIdentifier) == "") {
			var group ProductGroup
			if err := s.db.WithContext(ctx).Where("id = ?", product.GroupID).First(&group).Error; err != nil {
				return err
			}
			if readinessOSCount(group.OSOptions) == 0 {
				return BadRequest("DCIM 镜像产品未填写默认镜像 ID 时，产品分组必须配置可选系统镜像")
			}
		}
		if apiType != "dcim_cloud_image" && (product.APIIdentifier == nil || strings.TrimSpace(*product.APIIdentifier) == "") {
			return BadRequest("自动开通产品必须填写上游产品/镜像标识")
		}
	default:
		return BadRequest("未知开通方式，不能直接开启自动开通")
	}
	return nil
}

func mergeProductForUpdate(existing Product, next Product, fields map[string]json.RawMessage) Product {
	next.ID = existing.ID
	if !hasJSONField(fields, "group_id") {
		next.GroupID = existing.GroupID
	}
	if !hasJSONField(fields, "sku") {
		next.SKU = existing.SKU
	}
	if !hasJSONField(fields, "name") {
		next.Name = existing.Name
	}
	if !hasJSONField(fields, "type") {
		next.Type = existing.Type
	}
	if !hasJSONField(fields, "status") {
		next.Status = existing.Status
	}
	if !hasJSONField(fields, "stock_policy") {
		next.StockPolicy = existing.StockPolicy
	}
	return next
}

func productUpdateMap(product Product, fields map[string]json.RawMessage) map[string]any {
	if len(fields) == 0 {
		return mapFromJSON(product)
	}
	out := map[string]any{}
	add := func(jsonKey string, dbKey string, value any) {
		if hasJSONField(fields, jsonKey) {
			out[dbKey] = value
		}
	}
	add("group_id", "group_id", product.GroupID)
	add("sku", "sku", strings.TrimSpace(product.SKU))
	add("name", "name", strings.TrimSpace(product.Name))
	add("type", "type", product.Type)
	add("status", "status", product.Status)
	add("vcpu", "vcpu", product.VCPU)
	add("cpu_model", "cpu_model", product.CPUModel)
	add("cpu_base_mhz", "cpu_base_mhz", product.CPUBaseMHz)
	add("cpu_turbo_mhz", "cpu_turbo_mhz", product.CPUTurboMHz)
	add("memory", "memory", product.Memory)
	add("disk", "disk", product.Disk)
	add("bandwidth", "bandwidth", product.Bandwidth)
	add("features", "features", product.Features)
	add("badges", "badges", product.Badges)
	add("base_setup_fee", "base_setup_fee", product.BaseSetupFee)
	add("base_price", "base_price", product.BasePrice)
	add("stock_policy", "stock_policy", product.StockPolicy)
	add("stock", "stock", product.Stock)
	add("min_quantity", "min_quantity", product.MinQuantity)
	add("max_quantity", "max_quantity", product.MaxQuantity)
	add("auto_provision", "auto_provision", product.AutoProvision)
	add("trial_enabled", "trial_enabled", product.TrialEnabled)
	add("trial_days", "trial_days", product.TrialDays)
	add("sort_order", "sort_order", product.SortOrder)
	add("is_featured", "is_featured", product.IsFeatured)
	add("requires_real_name", "requires_real_name", product.RequiresRealName)
	add("cloud_server_id", "cloud_server_id", product.CloudServerID)
	add("api_type", "api_type", product.APIType)
	add("api_identifier", "api_identifier", product.APIIdentifier)
	add("provision_interface_id", "provision_interface_id", product.ProvisionInterfaceID)
	add("provision_config", "provision_config", product.ProvisionConfig)
	return out
}

func hasJSONField(fields map[string]json.RawMessage, key string) bool {
	if fields == nil {
		return false
	}
	_, ok := fields[key]
	return ok
}

func (s *Service) AdminListSettings(ctx context.Context, prefix string) ([]map[string]any, error) {
	prefix = strings.TrimSpace(prefix)
	if isInternalLicenseSetting(prefix) {
		return []map[string]any{}, nil
	}
	db := s.db.WithContext(ctx).Model(&SystemSetting{}).
		Where("`key` NOT LIKE ?", "platform.license_%")
	if prefix != "" {
		db = db.Where("`key` LIKE ?", prefix+"%")
	}
	var items []SystemSetting
	if err := db.Order("`key` ASC").Find(&items).Error; err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0, len(items))
	for _, item := range items {
		out = append(out, systemSettingMap(item))
	}
	return out, nil
}

func (s *Service) AdminSetSetting(ctx context.Context, key string, req AdminSettingRequest) (map[string]any, error) {
	key = strings.TrimSpace(key)
	if key == "" {
		return nil, BadRequest("setting key is required")
	}
	if isInternalLicenseSetting(key) {
		return nil, NotFound("setting not found")
	}
	raw := jsonRawFromAny(req.Value)
	updates := map[string]any{"value": raw}
	if req.Label != nil {
		updates["label"] = strings.TrimSpace(*req.Label)
	}
	if req.Description != nil {
		updates["description"] = trimPtr(req.Description)
	}
	var item SystemSetting
	err := s.db.WithContext(ctx).Where("`key` = ?", key).First(&item).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		item = SystemSetting{Key: key, Value: raw}
		if req.Label != nil {
			item.Label = strings.TrimSpace(*req.Label)
		}
		if req.Description != nil {
			item.Description = trimPtr(req.Description)
		}
		if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
			return nil, err
		}
		return systemSettingMap(item), nil
	}
	if err != nil {
		return nil, err
	}
	if err := s.db.WithContext(ctx).Model(&SystemSetting{}).Where("id = ?", item.ID).Updates(updates).Error; err != nil {
		return nil, err
	}
	return s.AdminGetSetting(ctx, key)
}

func (s *Service) AdminGetSetting(ctx context.Context, key string) (map[string]any, error) {
	key = strings.TrimSpace(key)
	if isInternalLicenseSetting(key) {
		return nil, NotFound("setting not found")
	}
	var item SystemSetting
	if err := s.db.WithContext(ctx).Where("`key` = ?", key).First(&item).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("setting not found")
		}
		return nil, err
	}
	return systemSettingMap(item), nil
}

func isInternalLicenseSetting(key string) bool {
	return strings.HasPrefix(strings.ToLower(strings.TrimSpace(key)), "platform.license_")
}

func (s *Service) AdminListRechargeOrders(ctx context.Context, query AdminListQuery, channel string) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&RechargeOrder{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if strings.TrimSpace(channel) != "" {
		db = db.Where("channel = ?", strings.TrimSpace(channel))
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("out_trade_no LIKE ? OR trade_no LIKE ?", like, like)
	}
	return adminFastPage(db.Order("id DESC"), query, func(items []RechargeOrder) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, rechargeOrderItemMap(item))
		}
		return out
	})
}

func (s *Service) AdminListBalanceLogs(ctx context.Context, query AdminListQuery, userID *int64, changeType string) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&BalanceLog{})
	if userID != nil {
		db = db.Where("user_id = ?", *userID)
	}
	if strings.TrimSpace(changeType) != "" {
		db = db.Where("change_type = ?", strings.TrimSpace(changeType))
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("ref_no LIKE ? OR remark LIKE ?", like, like)
	}
	return adminFastPage(db.Order("id DESC"), query, func(items []BalanceLog) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, balanceLogItemMap(item))
		}
		return out
	})
}

func (s *Service) adminProductMap(ctx context.Context, product Product, detail bool) map[string]any {
	data := mapFromJSON(product)
	data["provision_config"] = jsonAnyValue(product.ProvisionConfig)
	if detail {
		var pricings []ProductPricing
		_ = s.db.WithContext(ctx).Where("product_id = ?", product.ID).Order("sort_order ASC, id ASC").Find(&pricings).Error
		data["pricings"] = pricings
	}
	return data
}

func adminUserMap(user UserAccount) map[string]any {
	return map[string]any{
		"id":                user.ID,
		"email":             user.Email,
		"username":          user.Username,
		"phone":             user.Phone,
		"balance":           user.Balance,
		"role":              user.Role,
		"status":            user.Status,
		"nickname":          user.Nickname,
		"avatar":            user.Avatar,
		"is_email_verified": user.IsEmailVerified,
		"is_phone_verified": user.IsPhoneVerified,
		"last_login_at":     user.LastLoginAt,
		"invite_code":       user.InviteCode,
		"bound_agent_id":    user.BoundAgentID,
		"created_at":        user.CreatedAt,
		"updated_at":        user.UpdatedAt,
	}
}

func adminOrderMap(order Order) map[string]any {
	return map[string]any{
		"id":                order.ID,
		"user_id":           order.UserID,
		"out_order_no":      order.OutOrderNo,
		"status":            order.Status,
		"total_amount":      order.TotalAmount,
		"discount_amount":   order.DiscountAmount,
		"promo_code_id":     order.PromoCodeID,
		"items_snapshot":    jsonAnyValue(order.ItemsSnapshot),
		"remark":            order.Remark,
		"settlement_status": order.SettlementStatus,
		"points_claimed":    order.PointsClaimed,
		"points_claimed_at": order.PointsClaimedAt,
		"created_at":        order.CreatedAt,
		"updated_at":        order.UpdatedAt,
	}
}

func systemSettingMap(item SystemSetting) map[string]any {
	out := map[string]any{
		"id":          item.ID,
		"key":         item.Key,
		"value":       jsonAnyValue(item.Value),
		"label":       item.Label,
		"description": item.Description,
		"created_at":  item.CreatedAt,
		"updated_at":  item.UpdatedAt,
		"value_type":  "text",
		"min_value":   nil,
		"max_value":   nil,
		"unit":        "",
		"group":       "platform",
	}
	if spec, ok := businessSettingSpecs[item.Key]; ok {
		if strings.TrimSpace(item.Label) == "" {
			out["label"] = spec.Label
		}
		out["value_type"] = spec.ValueType
		out["min_value"] = spec.MinValue
		out["max_value"] = spec.MaxValue
		out["unit"] = spec.Unit
		out["group"] = spec.Group
	}
	return out
}

func balanceLogItemMap(item BalanceLog) map[string]any {
	return map[string]any{
		"id":            item.ID,
		"user_id":       item.UserID,
		"change_type":   item.ChangeType,
		"delta":         item.Delta,
		"balance_after": item.BalanceAfter,
		"ref_no":        item.RefNo,
		"operator_id":   item.OperatorID,
		"remark":        item.Remark,
		"created_at":    item.CreatedAt,
	}
}

func mapFromJSON(value any) map[string]any {
	raw, _ := json.Marshal(value)
	out := map[string]any{}
	_ = json.Unmarshal(raw, &out)
	return out
}

func adminPage[T any](db *gorm.DB, query AdminListQuery, mapItems func([]T) []map[string]any) (map[string]any, error) {
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var items []T
	if err := db.Limit(query.Limit).Offset(query.Offset).Find(&items).Error; err != nil {
		return nil, err
	}
	return map[string]any{"items": mapItems(items), "total": total, "limit": query.Limit, "offset": query.Offset}, nil
}

func adminFastPage[T any](db *gorm.DB, query AdminListQuery, mapItems func([]T) []map[string]any) (map[string]any, error) {
	var items []T
	if err := db.Limit(query.Limit).Offset(query.Offset).Find(&items).Error; err != nil {
		return nil, err
	}
	mapped := mapItems(items)
	return map[string]any{
		"items":  mapped,
		"total":  query.Offset + len(mapped),
		"limit":  query.Limit,
		"offset": query.Offset,
	}, nil
}

func trimPtr(value *string) *string {
	if value == nil {
		return nil
	}
	text := strings.TrimSpace(*value)
	if text == "" {
		return nil
	}
	return &text
}

func nowUTC() time.Time {
	return time.Now().UTC()
}
