package commerce

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

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

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

type AdminRawPayload map[string]any

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

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

func (s *Service) AdminSaveCloudServer(ctx context.Context, id int64, payload map[string]any) (map[string]any, error) {
	payload = cleanRawPayload(payload)
	if password := strings.TrimSpace(fmt.Sprint(payload["password"])); password != "" && password != "<nil>" {
		encrypted, err := EncryptStoredSecret(password)
		if err != nil {
			return nil, err
		}
		payload["password_encrypted"] = encrypted
	}
	delete(payload, "password")
	if strings.TrimSpace(fmt.Sprint(payload["name"])) == "" || strings.TrimSpace(fmt.Sprint(payload["hostname"])) == "" {
		return nil, BadRequest("cloud server name and hostname are required")
	}
	if strings.TrimSpace(fmt.Sprint(payload["server_type"])) == "" || fmt.Sprint(payload["server_type"]) == "<nil>" {
		payload["server_type"] = "dcim_cloud"
	}
	if id > 0 {
		payload["updated_at"] = time.Now().UTC()
		if err := s.db.WithContext(ctx).Table("cloud_servers").Where("id = ?", id).Updates(payload).Error; err != nil {
			return nil, err
		}
		return s.AdminGetCloudServer(ctx, id)
	}
	payload["created_at"] = time.Now().UTC()
	payload["updated_at"] = time.Now().UTC()
	if err := s.db.WithContext(ctx).Table("cloud_servers").Create(payload).Error; err != nil {
		return nil, err
	}
	if id, ok := int64FromAny(payload["id"]); ok {
		return s.AdminGetCloudServer(ctx, id)
	}
	return payload, nil
}

func (s *Service) AdminDeleteCloudServer(ctx context.Context, id int64) (map[string]any, error) {
	result := s.db.WithContext(ctx).Delete(&CloudServer{}, id)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("cloud server not found")
	}
	return map[string]any{"deleted": true, "id": id}, nil
}

func (s *Service) AdminTestCloudServer(ctx context.Context, id int64, payload map[string]any) (map[string]any, error) {
	var server CloudServer
	if id > 0 {
		if err := s.db.WithContext(ctx).Where("id = ?", id).First(&server).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, NotFound("cloud server not found")
			}
			return nil, err
		}
	} else {
		server = cloudServerFromPayload(payload)
		if password := strings.TrimSpace(fmt.Sprint(payload["password"])); password != "" && password != "<nil>" {
			encrypted, err := EncryptStoredSecret(password)
			if err != nil {
				return nil, err
			}
			server.PasswordEncrypted = encrypted
		}
	}
	if server.ID == 0 {
		server.ID = id
	}
	if strings.TrimSpace(server.Hostname) == "" {
		return nil, BadRequest("cloud server hostname is required")
	}
	if isZJMFServer(server) {
		result, err := CallZJMFAPI(ctx, server, "/host/header", "GET", map[string]any{"host_id": 0}, 8*time.Second)
		if err != nil {
			return map[string]any{"ok": false, "message": err.Error()}, nil
		}
		return map[string]any{"ok": result.StatusCode >= 200 && result.StatusCode < 500, "status_code": result.StatusCode, "message": upstreamMessage(result.Body, result.Text)}, nil
	}
	result, err := CallDCIMPath(ctx, server, "/tasks", "GET", nil, 8*time.Second)
	if err != nil {
		return map[string]any{"ok": false, "message": err.Error()}, nil
	}
	return map[string]any{"ok": result.StatusCode >= 200 && result.StatusCode < 500, "status_code": result.StatusCode, "message": trimText(result.Text, 200)}, nil
}

func (s *Service) AdminListInstances(ctx context.Context, query AdminListQuery, userID *int64) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&Instance{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if userID != nil {
		db = db.Where("user_id = ?", *userID)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("hostname LIKE ? OR main_ip LIKE ? OR alias LIKE ?", like, like, like)
	}
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var items []Instance
	if err := db.Order("id DESC").Limit(query.Limit).Offset(query.Offset).Find(&items).Error; err != nil {
		return nil, err
	}
	mapped, err := s.adminInstanceItems(ctx, items)
	if err != nil {
		return nil, err
	}
	return map[string]any{"items": mapped, "total": total, "limit": query.Limit, "offset": query.Offset}, nil
}

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

func (s *Service) adminUserInstance(ctx context.Context, userID int64, instanceID int64) (Instance, error) {
	var item Instance
	if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", instanceID, userID).First(&item).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return Instance{}, NotFound("instance not found")
		}
		return Instance{}, err
	}
	return item, nil
}

func (s *Service) AdminGetUserInstance(ctx context.Context, userID int64, instanceID int64) (map[string]any, error) {
	instance, err := s.adminUserInstance(ctx, userID, instanceID)
	if err != nil {
		return nil, err
	}
	data := adminInstanceMap(instance)
	var user UserAccount
	if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err == nil {
		data["user"] = adminUserMap(user)
	}
	return data, nil
}

func (s *Service) AdminListUserInstanceNatACL(ctx context.Context, userID int64, instanceID int64, query url.Values) (map[string]any, error) {
	return s.ListInstanceNatACL(ctx, userID, instanceID, query)
}

func (s *Service) AdminCreateUserInstanceNatACL(ctx context.Context, userID int64, instanceID int64, payload map[string]any) (InstanceActionResponse, error) {
	if _, err := s.adminUserInstance(ctx, userID, instanceID); err != nil {
		return InstanceActionResponse{}, err
	}
	return s.CreateInstanceNatACL(ctx, userID, instanceID, payload)
}

func (s *Service) AdminDeleteUserInstanceNatACL(ctx context.Context, userID int64, instanceID int64, natACLID int64) (InstanceActionResponse, error) {
	if _, err := s.adminUserInstance(ctx, userID, instanceID); err != nil {
		return InstanceActionResponse{}, err
	}
	return s.DeleteInstanceNatACL(ctx, userID, instanceID, natACLID)
}

func (s *Service) AdminUpdateInstance(ctx context.Context, instanceID int64, payload map[string]any) (map[string]any, error) {
	payload = cleanRawPayload(payload)
	delete(payload, "id")
	if len(payload) == 0 {
		return s.AdminGetInstance(ctx, instanceID)
	}
	payload["updated_at"] = time.Now().UTC()
	result := s.db.WithContext(ctx).Table("instances").Where("id = ?", instanceID).Updates(payload)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("instance not found")
	}
	return s.AdminGetInstance(ctx, instanceID)
}

func (s *Service) AdminInstancePower(ctx context.Context, instanceID int64, action string) (InstanceActionResponse, error) {
	var instance Instance
	if err := s.db.WithContext(ctx).Where("id = ?", instanceID).First(&instance).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return InstanceActionResponse{}, NotFound("instance not found")
		}
		return InstanceActionResponse{}, err
	}
	return s.RunPowerAction(ctx, instance.UserID, instance.ID, action)
}

func (s *Service) AdminUserInstancePower(ctx context.Context, userID int64, instanceID int64, action string) (InstanceActionResponse, error) {
	if _, err := s.adminUserInstance(ctx, userID, instanceID); err != nil {
		return InstanceActionResponse{}, err
	}
	return s.RunPowerAction(ctx, userID, instanceID, action)
}

func (s *Service) AdminSuspendUserInstance(ctx context.Context, adminID int64, userID int64, instanceID int64, payload map[string]any) (InstanceActionResponse, error) {
	instance, err := s.adminUserInstance(ctx, userID, instanceID)
	if err != nil {
		return InstanceActionResponse{}, err
	}
	if instance.Status != "active" {
		return InstanceActionResponse{}, BadRequest("instance status does not support suspend")
	}
	if payload == nil {
		payload = map[string]any{}
	}
	suspendType := strings.TrimSpace(fmt.Sprint(payload["type"]))
	if suspendType == "" || suspendType == "<nil>" {
		suspendType = "manual"
	}
	payload["type"] = suspendType
	resp, err := s.runAuditedInstancePath(ctx, userID, instanceID, "suspend", "POST", "suspend", payload, fmt.Sprintf("admin %d suspend user instance", adminID))
	if err == nil {
		_ = s.db.WithContext(ctx).Model(&Instance{}).Where("id = ? AND user_id = ?", instanceID, userID).Updates(map[string]any{"status": "suspended", "suspend_type": suspendType}).Error
	}
	return resp, err
}

func (s *Service) AdminUnsuspendUserInstance(ctx context.Context, adminID int64, userID int64, instanceID int64, payload map[string]any) (InstanceActionResponse, error) {
	instance, err := s.adminUserInstance(ctx, userID, instanceID)
	if err != nil {
		return InstanceActionResponse{}, err
	}
	if instance.Status != "suspended" {
		return InstanceActionResponse{}, BadRequest("instance does not need unsuspend")
	}
	resp, err := s.runAuditedInstancePath(ctx, userID, instanceID, "unsuspend", "POST", "unsuspend", payload, fmt.Sprintf("admin %d unsuspend user instance", adminID))
	if err == nil {
		_ = s.db.WithContext(ctx).Model(&Instance{}).Where("id = ? AND user_id = ?", instanceID, userID).Updates(map[string]any{"status": "active", "suspend_type": nil}).Error
	}
	return resp, err
}

func (s *Service) AdminDeleteUserInstance(ctx context.Context, adminID int64, userID int64, instanceID int64) (map[string]any, error) {
	instance, err := s.adminUserInstance(ctx, userID, instanceID)
	if err != nil {
		return nil, err
	}
	if instance.Status == "terminated" {
		return map[string]any{"taskid": nil, "instance_id": instance.ID}, nil
	}
	var action *UpstreamActionLogResponse
	var taskID any
	if instance.UpstreamID != nil {
		log, err := s.submitTerminateUpstreamAction(ctx, instance, nil, nil, map[string]any{"source": "admin", "operator_id": adminID})
		if err != nil {
			return nil, err
		}
		resp := newUpstreamActionLogResponse(log)
		action = &resp
		taskID = log.UpstreamTaskID
	}
	now := time.Now().UTC()
	if err := s.db.WithContext(ctx).Model(&Instance{}).Where("id = ? AND user_id = ?", instanceID, userID).Updates(map[string]any{"status": "terminated", "terminated_at": now}).Error; err != nil {
		return nil, err
	}
	return map[string]any{"instance_id": instanceID, "taskid": taskID, "action": action}, nil
}

func (s *Service) AdminUpdateUserInstanceAutoRenew(ctx context.Context, userID int64, instanceID int64, enabled bool) (map[string]any, error) {
	if _, err := s.adminUserInstance(ctx, userID, instanceID); err != nil {
		return nil, err
	}
	value := 0
	if enabled {
		value = 1
	}
	if err := s.db.WithContext(ctx).Model(&Instance{}).Where("id = ? AND user_id = ? AND status <> ?", instanceID, userID, "terminated").Update("auto_renew", value).Error; err != nil {
		return nil, err
	}
	return map[string]any{"id": instanceID, "auto_renew": value}, nil
}

func (s *Service) AdminUpdateUserInstanceBilling(ctx context.Context, userID int64, instanceID int64, payload map[string]any) (map[string]any, error) {
	if _, err := s.adminUserInstance(ctx, userID, instanceID); err != nil {
		return nil, err
	}
	updates := map[string]any{}
	for _, key := range []string{"cycle", "unit_price", "next_renew_amount", "expires_at", "auto_renew"} {
		if value, ok := payload[key]; ok {
			updates[key] = value
		}
	}
	if len(updates) == 0 {
		return s.AdminGetUserInstance(ctx, userID, instanceID)
	}
	updates["updated_at"] = time.Now().UTC()
	if err := s.db.WithContext(ctx).Table("instances").Where("id = ? AND user_id = ? AND status <> ?", instanceID, userID, "terminated").Updates(updates).Error; err != nil {
		return nil, err
	}
	return s.AdminGetUserInstance(ctx, userID, instanceID)
}

func (s *Service) AdminProvisionUserInstance(ctx context.Context, adminID int64, userID int64, instanceID int64) (map[string]any, error) {
	instance, err := s.adminUserInstance(ctx, userID, instanceID)
	if err != nil {
		return nil, err
	}
	if instance.Status != "provisioning" && instance.Status != "provision_failed" {
		return nil, BadRequest("only provisioning or provision_failed instances can be provisioned")
	}
	log := fmt.Sprintf("admin %d triggered reprovision", adminID)
	if err := s.db.WithContext(ctx).Model(&Instance{}).Where("id = ? AND user_id = ?", instanceID, userID).Updates(map[string]any{"status": "provisioning", "provision_log": log}).Error; err != nil {
		return nil, err
	}
	return map[string]any{"id": instanceID, "status": "provisioning", "provision_log": log}, nil
}

func (s *Service) AdminInstancePluginAction(ctx context.Context, instanceID int64, action string, payload map[string]any) (InstanceActionResponse, error) {
	var instance Instance
	if err := s.db.WithContext(ctx).Where("id = ?", instanceID).First(&instance).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return InstanceActionResponse{}, NotFound("instance not found")
		}
		return InstanceActionResponse{}, err
	}
	return s.RunPluginManageAction(ctx, instance.UserID, instance.ID, action, payload)
}

func (s *Service) AdminSetInstanceStatus(ctx context.Context, instanceID int64, status string, extra map[string]any) (map[string]any, error) {
	updates := map[string]any{"status": status, "updated_at": time.Now().UTC()}
	for key, value := range extra {
		updates[key] = value
	}
	if status == "terminated" {
		now := time.Now().UTC()
		updates["terminated_at"] = now
	}
	result := s.db.WithContext(ctx).Table("instances").Where("id = ?", instanceID).Updates(updates)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("instance not found")
	}
	return s.AdminGetInstance(ctx, instanceID)
}

func (s *Service) AdminBatchInstances(ctx context.Context, action string, ids []int64) (map[string]any, error) {
	results := []map[string]any{}
	for _, id := range ids {
		var data any
		var err error
		switch action {
		case "suspend":
			data, err = s.AdminSetInstanceStatus(ctx, id, "suspended", map[string]any{"suspend_type": "admin"})
		case "unsuspend":
			data, err = s.AdminSetInstanceStatus(ctx, id, "active", map[string]any{"suspend_type": nil})
		case "terminate", "delete":
			data, err = s.AdminSetInstanceStatus(ctx, id, "terminated", nil)
		default:
			resp, actionErr := s.AdminInstancePower(ctx, id, action)
			data, err = resp, actionErr
		}
		row := map[string]any{"id": id, "ok": err == nil, "data": data}
		if err != nil {
			row["error"] = err.Error()
		}
		results = append(results, row)
	}
	return map[string]any{"items": results, "total": len(results)}, nil
}

func (s *Service) AdminListAnnouncements(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Table("announcements")
	if query.Status != "" {
		db = db.Where("level = ? OR target_audience = ?", query.Status, query.Status)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("title LIKE ? OR content LIKE ?", like, like)
	}
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var rows []announcementRow
	if err := db.Order("is_pinned DESC, id DESC").Limit(query.Limit).Offset(query.Offset).Find(&rows).Error; err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0, len(rows))
	for _, item := range rows {
		out = append(out, announcementRowMap(item))
	}
	return map[string]any{"items": out, "total": total, "limit": query.Limit, "offset": query.Offset}, nil
}

func (s *Service) AdminSaveAnnouncement(ctx context.Context, id int64, item Announcement, adminID int64) (map[string]any, error) {
	if strings.TrimSpace(item.Title) == "" {
		return nil, BadRequest("announcement title is required")
	}
	if item.Level == "" {
		item.Level = "info"
	}
	if item.Category == "" {
		item.Category = "announcement"
	}
	if item.TargetType == "" {
		item.TargetType = item.TargetAudience
	}
	if item.TargetType == "" {
		item.TargetType = "all"
	}
	if item.TargetAudience == "" {
		item.TargetAudience = item.TargetType
	}
	if id == 0 {
		item.IsPublished = true
	}
	if item.IsPublished && item.PublishedAt == nil {
		now := time.Now().UTC()
		item.PublishedAt = &now
	}
	if item.CreatedByID == nil && adminID > 0 {
		item.CreatedByID = &adminID
	}
	if item.CreatedBy == nil && adminID > 0 {
		item.CreatedBy = &adminID
	}
	if id > 0 {
		item.ID = id
		if err := s.db.WithContext(ctx).Model(&Announcement{}).Where("id = ?", id).Updates(item).Error; err != nil {
			return nil, err
		}
	} else if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
		return nil, err
	}
	return announcementMap(item), nil
}

func (s *Service) AdminDeleteAnnouncement(ctx context.Context, id int64) (map[string]any, error) {
	return deleteByID(ctx, s.db, "announcements", id)
}

func (s *Service) AdminSendNotification(ctx context.Context, adminID int64, payload map[string]any) (map[string]any, error) {
	title := strings.TrimSpace(fmt.Sprint(payload["title"]))
	content := strings.TrimSpace(fmt.Sprint(payload["content"]))
	if title == "" || content == "" {
		return nil, BadRequest("notification title and content are required")
	}
	level := strings.TrimSpace(fmt.Sprint(payload["level"]))
	if level == "" || level == "<nil>" {
		level = "info"
	}
	category := strings.TrimSpace(fmt.Sprint(payload["category"]))
	if category == "" || category == "<nil>" {
		category = "system"
	}
	var userIDs []int64
	if raw, ok := payload["user_ids"].([]any); ok {
		for _, item := range raw {
			if id, ok := int64FromAny(item); ok {
				userIDs = append(userIDs, id)
			}
		}
	}
	if len(userIDs) == 0 {
		var users []UserAccount
		if err := s.db.WithContext(ctx).Select("id").Find(&users).Error; err != nil {
			return nil, err
		}
		for _, user := range users {
			userIDs = append(userIDs, user.ID)
		}
	}
	senderType := "admin"
	senderName := "Admin"
	created := 0
	for _, userID := range userIDs {
		notice := Notification{
			UserID:     userID,
			Category:   category,
			Level:      level,
			SenderID:   &adminID,
			SenderName: &senderName,
			SenderType: senderType,
			Title:      title,
			Content:    content,
		}
		if link := stringFromMap(payload, "link"); link != "" {
			notice.Link = &link
		}
		if err := s.db.WithContext(ctx).Create(&notice).Error; err != nil {
			return nil, err
		}
		created++
	}
	return map[string]any{"created": created}, nil
}

func (s *Service) AdminListPromoCodes(ctx context.Context, query AdminListQuery, discountLevel string, enabled *bool) (map[string]any, error) {
	db := s.db.WithContext(ctx).Model(&PromoCode{})
	if enabled != nil {
		db = db.Where("is_enabled = ?", *enabled)
	} else if query.Status != "" {
		statusEnabled := query.Status == "enabled" || query.Status == "active"
		db = db.Where("is_enabled = ?", statusEnabled)
	}
	if strings.TrimSpace(discountLevel) != "" && strings.TrimSpace(discountLevel) != "all" {
		db = db.Where("discount_level = ?", strings.TrimSpace(discountLevel))
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("code LIKE ? OR description LIKE ?", like, like)
	}
	return adminPage(db.Order("id DESC"), query, func(items []PromoCode) []map[string]any {
		out := make([]map[string]any, 0, len(items))
		for _, item := range items {
			out = append(out, adminPromoCodeMap(item))
		}
		return out
	})
}

func (s *Service) AdminSavePromoCode(ctx context.Context, id int64, payload map[string]any) (map[string]any, error) {
	payload = cleanRawPayload(payload)
	var item PromoCode
	if id > 0 {
		if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, NotFound("promo code not found")
			}
			return nil, err
		}
	} else {
		item.IsEnabled = true
		item.AppliesToRenewals = true
	}
	applyPromoPayload(&item, payload)
	if strings.TrimSpace(item.Code) == "" {
		return nil, BadRequest("promo code is required")
	}
	item.Code = strings.ToUpper(strings.TrimSpace(item.Code))
	if item.Type == "" {
		item.Type = "fixed"
	}
	if item.Scope == "" {
		item.Scope = "all"
	}
	if item.DiscountLevel == "" {
		item.DiscountLevel = "order"
	}
	if item.ClaimMethod == "" {
		item.ClaimMethod = "code"
	}
	if item.StartsAt == nil {
		now := time.Now().UTC()
		item.StartsAt = &now
	}
	if id > 0 {
		item.ID = id
		if err := s.db.WithContext(ctx).Model(&PromoCode{}).Where("id = ?", id).Updates(adminPromoUpdateMap(item, payload)).Error; err != nil {
			return nil, err
		}
		if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
			return nil, err
		}
	} else if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
		return nil, err
	}
	return adminPromoCodeMap(item), nil
}

func (s *Service) AdminDeletePromoCode(ctx context.Context, id int64) (map[string]any, error) {
	return deleteByID(ctx, s.db, "promo_codes", id)
}

func applyPromoPayload(item *PromoCode, payload map[string]any) {
	if _, ok := payload["code"]; ok {
		item.Code = stringFromMap(payload, "code")
	}
	if _, ok := payload["description"]; ok {
		item.Description = stringPtrFromMap(payload, "description")
	}
	if _, ok := payload["type"]; ok {
		item.Type = stringFromMap(payload, "type")
	}
	if _, ok := payload["value"]; ok {
		item.Value = intValueAny(payload["value"], 0)
	}
	if _, ok := payload["discount_level"]; ok {
		item.DiscountLevel = stringFromMap(payload, "discount_level")
	}
	if _, ok := payload["scope"]; ok {
		item.Scope = stringFromMap(payload, "scope")
	}
	if _, ok := payload["scope_ids"]; ok {
		item.ScopeIDs = jsonStringPtrFromAny(payload["scope_ids"])
	}
	if _, ok := payload["starts_at"]; ok {
		item.StartsAt = timePtrFromMap(payload, "starts_at")
	}
	if _, ok := payload["expires_at"]; ok {
		item.ExpiresAt = timePtrFromMap(payload, "expires_at")
	}
	if _, ok := payload["max_uses"]; ok {
		item.MaxUses = intValueAny(payload["max_uses"], 0)
	}
	if _, ok := payload["max_uses_per_user"]; ok {
		item.MaxUsesPerUser = intValueAny(payload["max_uses_per_user"], 0)
	}
	if _, ok := payload["applicable_cycles"]; ok {
		item.ApplicableCycles = jsonStringPtrFromAny(payload["applicable_cycles"])
	}
	if _, ok := payload["min_order_amount"]; ok {
		item.MinOrderAmount = intValueAny(payload["min_order_amount"], 0)
	}
	if _, ok := payload["min_quantity"]; ok {
		item.MinQuantity = intValueAny(payload["min_quantity"], 0)
	}
	if _, ok := payload["max_discount_amount"]; ok {
		item.MaxDiscountAmount = intValueAny(payload["max_discount_amount"], 0)
	}
	if _, ok := payload["required_product_ids"]; ok {
		item.RequiredProductIDs = jsonStringPtrFromAny(payload["required_product_ids"])
	}
	if _, ok := payload["required_group_ids"]; ok {
		item.RequiredGroupIDs = jsonStringPtrFromAny(payload["required_group_ids"])
	}
	if _, ok := payload["required_category_ids"]; ok {
		item.RequiredCategoryIDs = jsonStringPtrFromAny(payload["required_category_ids"])
	}
	if _, ok := payload["allowed_user_ids"]; ok {
		item.AllowedUserIDs = jsonStringPtrFromAny(payload["allowed_user_ids"])
	}
	if _, ok := payload["new_users_only"]; ok {
		item.NewUsersOnly = boolValueAny(payload["new_users_only"], false)
	}
	if _, ok := payload["applies_to_renewals"]; ok {
		item.AppliesToRenewals = boolValueAny(payload["applies_to_renewals"], false)
	}
	if _, ok := payload["is_stackable"]; ok {
		item.IsStackable = boolValueAny(payload["is_stackable"], false)
	}
	if _, ok := payload["is_enabled"]; ok {
		item.IsEnabled = boolValueAny(payload["is_enabled"], true)
	}
}

func adminPromoUpdateMap(item PromoCode, payload map[string]any) map[string]any {
	updates := map[string]any{"updated_at": time.Now().UTC()}
	add := func(jsonKey, dbKey string, value any) {
		if _, ok := payload[jsonKey]; ok {
			updates[dbKey] = value
		}
	}
	add("code", "code", item.Code)
	add("description", "description", item.Description)
	add("type", "type", item.Type)
	add("value", "value", item.Value)
	add("discount_level", "discount_level", item.DiscountLevel)
	add("scope", "scope", item.Scope)
	add("scope_ids", "scope_ids", item.ScopeIDs)
	add("starts_at", "starts_at", item.StartsAt)
	add("expires_at", "expires_at", item.ExpiresAt)
	add("max_uses", "max_uses", item.MaxUses)
	add("max_uses_per_user", "max_uses_per_user", item.MaxUsesPerUser)
	add("applicable_cycles", "applicable_cycles", item.ApplicableCycles)
	add("min_order_amount", "min_order_amount", item.MinOrderAmount)
	add("min_quantity", "min_quantity", item.MinQuantity)
	add("max_discount_amount", "max_discount_amount", item.MaxDiscountAmount)
	add("required_product_ids", "required_product_ids", item.RequiredProductIDs)
	add("required_group_ids", "required_group_ids", item.RequiredGroupIDs)
	add("required_category_ids", "required_category_ids", item.RequiredCategoryIDs)
	add("allowed_user_ids", "allowed_user_ids", item.AllowedUserIDs)
	add("new_users_only", "new_users_only", item.NewUsersOnly)
	add("applies_to_renewals", "applies_to_renewals", item.AppliesToRenewals)
	add("is_stackable", "is_stackable", item.IsStackable)
	add("is_enabled", "is_enabled", item.IsEnabled)
	return updates
}

func adminPromoCodeMap(item PromoCode) map[string]any {
	return map[string]any{
		"id":                    item.ID,
		"code":                  item.Code,
		"description":           item.Description,
		"type":                  item.Type,
		"value":                 item.Value,
		"discount_level":        item.DiscountLevel,
		"scope":                 item.Scope,
		"scope_ids":             jsonArrayFromString(item.ScopeIDs),
		"starts_at":             item.StartsAt,
		"expires_at":            item.ExpiresAt,
		"max_uses":              item.MaxUses,
		"max_uses_per_user":     item.MaxUsesPerUser,
		"used_count":            item.UsedCount,
		"applicable_cycles":     jsonArrayFromString(item.ApplicableCycles),
		"min_order_amount":      item.MinOrderAmount,
		"min_quantity":          item.MinQuantity,
		"max_discount_amount":   item.MaxDiscountAmount,
		"required_product_ids":  jsonArrayFromString(item.RequiredProductIDs),
		"required_group_ids":    jsonArrayFromString(item.RequiredGroupIDs),
		"required_category_ids": jsonArrayFromString(item.RequiredCategoryIDs),
		"allowed_user_ids":      jsonArrayFromString(item.AllowedUserIDs),
		"new_users_only":        item.NewUsersOnly,
		"applies_to_renewals":   item.AppliesToRenewals,
		"is_stackable":          item.IsStackable,
		"is_enabled":            item.IsEnabled,
		"created_at":            item.CreatedAt,
		"updated_at":            item.UpdatedAt,
	}
}

func (s *Service) AdminListMallItems(ctx context.Context, query AdminListQuery, category string) (map[string]any, error) {
	db := s.db.WithContext(ctx).Table("coupon_mall_items AS m").
		Select("m.*, p.code, p.description AS coupon_description, p.type, p.value, p.min_order_amount, p.max_discount_amount, p.expires_at, p.is_enabled, u.username AS issuer_name").
		Joins("JOIN promo_codes p ON p.id = m.promo_code_id").
		Joins("LEFT JOIN users u ON u.id = m.issuer_user_id")
	if query.Status != "" {
		db = db.Where("m.status = ?", query.Status)
	}
	if strings.TrimSpace(category) != "" {
		db = db.Where("m.category = ?", strings.TrimSpace(category))
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("m.title LIKE ? OR m.subtitle LIKE ? OR m.description LIKE ? OR p.code LIKE ?", like, like, like, like)
	}
	return adminRawPage(ctx, db.Order("m.sort_order ASC, m.id DESC"), query)
}

func (s *Service) AdminCreateMallCoupon(ctx context.Context, adminID int64, payload map[string]any) (map[string]any, error) {
	payload = cleanRawPayload(payload)
	title := stringFromMap(payload, "title")
	if title == "" {
		return nil, BadRequest("mall item title is required")
	}
	couponType := stringFromMap(payload, "type")
	if couponType == "" {
		couponType = "fixed"
	}
	value := intValueAny(payload["value"], 0)
	pointsPrice := intValueAny(payload["points_price"], 0)
	stockTotal := intValueAny(payload["stock_total"], 0)
	if value <= 0 || pointsPrice < 0 || stockTotal <= 0 {
		return nil, BadRequest("coupon value, points_price and stock_total are required")
	}
	if adminID <= 0 {
		adminID = 1
	}
	code := strings.ToUpper(stringFromMap(payload, "code"))
	if code == "" {
		code = s.generateCouponCode(ctx, "AM")
	}
	if len(code) < 4 || len(code) > 16 || !alphaNum(code) {
		return nil, BadRequest("invalid coupon code")
	}
	var exists int64
	if err := s.db.WithContext(ctx).Model(&PromoCode{}).Where("UPPER(code) = ?", code).Count(&exists).Error; err != nil {
		return nil, err
	}
	if exists > 0 {
		return nil, BadRequest("coupon code already exists")
	}
	desc := stringPtrFromMap(payload, "description")
	issuerType := "admin"
	now := time.Now().UTC()
	maxUsesPerUser := intValueAny(payload["max_uses_per_user"], 1)
	if maxUsesPerUser <= 0 {
		maxUsesPerUser = 1
	}
	coupon := PromoCode{
		Code:              code,
		Description:       desc,
		IssuerUserID:      &adminID,
		IssuerType:        &issuerType,
		Type:              couponType,
		Value:             value,
		Scope:             "all",
		DiscountLevel:     "order",
		StartsAt:          &now,
		ExpiresAt:         timePtrFromMap(payload, "expires_at"),
		MaxUses:           stockTotal,
		MaxUsesPerUser:    maxUsesPerUser,
		MinOrderAmount:    intValueAny(payload["threshold_amount"], intValueAny(payload["min_order_amount"], 0)),
		MaxDiscountAmount: intValueAny(payload["max_discount_amount"], 0),
		NewUsersOnly:      boolValueAny(payload["new_users_only"], false),
		AppliesToRenewals: true,
		IsStackable:       false,
		ClaimMethod:       "mall",
		ListOnMall:        true,
		IsEnabled:         true,
	}
	scope := stringFromMap(payload, "scope")
	if scope == "" {
		scope = "public"
	}
	var item CouponMallItem
	if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&coupon).Error; err != nil {
			return err
		}
		item = CouponMallItem{
			PromoCodeID:             coupon.ID,
			IssuerUserID:            adminID,
			Title:                   &title,
			Subtitle:                stringPtrFromMap(payload, "subtitle"),
			CoverImage:              stringPtrFromMap(payload, "cover_image"),
			Description:             desc,
			PointsPrice:             pointsPrice,
			StockTotal:              stockTotal,
			StockRemaining:          stockTotal,
			Status:                  "active",
			Scope:                   scope,
			SortOrder:               intValueAny(payload["sort_order"], 0),
			Category:                stringPtrFromMap(payload, "category"),
			MaxClaimsPerUser:        intValueAny(payload["max_claims_per_user"], 1),
			MaxClaimsPerUserPerDay:  intValueAny(payload["max_claims_per_user_per_day"], 0),
			RequiredMemberLevel:     intValueAny(payload["required_member_level"], 0),
			RequiredMemberLevelName: stringPtrFromMap(payload, "required_member_level_name"),
			IsHot:                   boolValueAny(payload["is_hot"], false),
		}
		if item.MaxClaimsPerUser <= 0 {
			item.MaxClaimsPerUser = 1
		}
		return tx.Create(&item).Error
	}); err != nil {
		return nil, err
	}
	return s.AdminGetMallItem(ctx, item.ID)
}

func (s *Service) AdminGetMallItem(ctx context.Context, itemID int64) (map[string]any, error) {
	rows, err := rawRows(s.db.WithContext(ctx).Table("coupon_mall_items AS m").
		Select("m.*, p.code, p.description AS coupon_description, p.type, p.value, p.min_order_amount, p.max_discount_amount, p.expires_at, p.is_enabled, u.username AS issuer_name").
		Joins("JOIN promo_codes p ON p.id = m.promo_code_id").
		Joins("LEFT JOIN users u ON u.id = m.issuer_user_id").
		Where("m.id = ?", itemID).
		Limit(1))
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, NotFound("mall item not found")
	}
	return rows[0], nil
}

func (s *Service) AdminUpdateMallItem(ctx context.Context, itemID int64, payload map[string]any) (map[string]any, error) {
	var existing CouponMallItem
	if err := s.db.WithContext(ctx).Where("id = ?", itemID).First(&existing).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("mall item not found")
		}
		return nil, err
	}
	updates := map[string]any{"updated_at": time.Now().UTC()}
	for jsonKey, dbKey := range map[string]string{
		"title": "title", "subtitle": "subtitle", "cover_image": "cover_image", "description": "description",
		"category": "category", "status": "status", "scope": "scope", "required_member_level_name": "required_member_level_name",
	} {
		if _, ok := payload[jsonKey]; ok {
			updates[dbKey] = nullableStringFromMap(payload, jsonKey)
		}
	}
	for jsonKey, dbKey := range map[string]string{
		"points_price": "points_price", "sort_order": "sort_order", "max_claims_per_user": "max_claims_per_user",
		"max_claims_per_user_per_day": "max_claims_per_user_per_day", "required_member_level": "required_member_level",
	} {
		if _, ok := payload[jsonKey]; ok {
			updates[dbKey] = intValueAny(payload[jsonKey], 0)
		}
	}
	if _, ok := payload["is_hot"]; ok {
		updates["is_hot"] = boolValueAny(payload["is_hot"], false)
	}
	if _, ok := payload["stock_total"]; ok {
		nextTotal := intValueAny(payload["stock_total"], existing.StockTotal)
		if nextTotal <= 0 {
			return nil, BadRequest("stock_total must be positive")
		}
		claimed := existing.StockTotal - existing.StockRemaining
		if claimed < 0 {
			claimed = 0
		}
		updates["stock_total"] = nextTotal
		updates["stock_remaining"] = maxInt(0, nextTotal-claimed)
	}
	if err := s.db.WithContext(ctx).Table("coupon_mall_items").Where("id = ?", itemID).Updates(updates).Error; err != nil {
		return nil, err
	}
	return s.AdminGetMallItem(ctx, itemID)
}

func (s *Service) AdminDeleteMallItem(ctx context.Context, itemID int64) (map[string]any, error) {
	result := s.db.WithContext(ctx).Table("coupon_mall_items").Where("id = ?", itemID).Updates(map[string]any{"status": "paused", "updated_at": time.Now().UTC()})
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("mall item not found")
	}
	return map[string]any{"id": itemID, "status": "paused", "deleted": true}, nil
}

func (s *Service) AdminListMallClaims(ctx context.Context, query AdminListQuery, mallItemID *int64) (map[string]any, error) {
	db := s.db.WithContext(ctx).Table("coupon_mall_claims AS c").
		Select("c.*, u.username, u.nickname, m.title AS mall_item_title, p.description AS coupon_description, p.type AS coupon_type, p.value AS coupon_value").
		Joins("LEFT JOIN users u ON u.id = c.user_id").
		Joins("LEFT JOIN coupon_mall_items m ON m.id = c.mall_item_id").
		Joins("LEFT JOIN promo_codes p ON p.id = c.promo_code_id")
	if query.Status != "" {
		db = db.Where("c.status = ?", query.Status)
	}
	if mallItemID != nil {
		db = db.Where("c.mall_item_id = ?", *mallItemID)
	}
	return adminRawPage(ctx, db.Order("c.id DESC"), query)
}

func (s *Service) AdminListProvisionInterfaces(ctx context.Context, query AdminListQuery, pluginID string) (map[string]any, error) {
	db := s.db.WithContext(ctx).Table("provision_interfaces")
	if pluginID = strings.TrimSpace(pluginID); pluginID != "" {
		db = db.Where("plugin_id = ?", pluginID)
	}
	if enabled, ok := boolStatus(query.Status); ok {
		db = db.Where("enabled = ?", enabled)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("name LIKE ? OR plugin_id LIKE ? OR remark LIKE ?", like, like, like)
	}
	page, err := adminRawPage(ctx, db.Order("sort_order ASC, id DESC"), query)
	if err != nil {
		return nil, err
	}
	if rows, ok := page["items"].([]map[string]any); ok {
		for _, row := range rows {
			s.redactProvisionInterfaceRow(ctx, row)
		}
	}
	return page, nil
}

func (s *Service) AdminRawList(ctx context.Context, table string, query AdminListQuery) (map[string]any, error) {
	db := applyAdminRawListSpec(s.db.WithContext(ctx).Table(table), table, query)
	return adminRawPage(ctx, db, query)
}

func (s *Service) AdminRawSave(ctx context.Context, table string, id int64, payload map[string]any) (map[string]any, error) {
	if table == "provision_interfaces" {
		prepared, err := s.prepareProvisionInterfacePayload(ctx, id, payload)
		if err != nil {
			return nil, err
		}
		row, err := rawSave(ctx, s.db, table, id, prepared)
		if err != nil {
			return nil, err
		}
		s.redactProvisionInterfaceRow(ctx, row)
		return row, nil
	}
	return rawSave(ctx, s.db, table, id, payload)
}

func (s *Service) AdminRawDelete(ctx context.Context, table string, id int64) (map[string]any, error) {
	return deleteByID(ctx, s.db, table, id)
}

func (s *Service) AdminListMemberLevels(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	return adminPage(s.db.WithContext(ctx).Model(&MemberLevel{}).Order("level ASC, id ASC"), query, func(items []MemberLevel) []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) AdminSaveMemberLevel(ctx context.Context, id int64, item MemberLevel) (map[string]any, error) {
	if strings.TrimSpace(item.Name) == "" {
		return nil, BadRequest("member level name is required")
	}
	if id > 0 {
		item.ID = id
		if err := s.db.WithContext(ctx).Model(&MemberLevel{}).Where("id = ?", id).Updates(item).Error; err != nil {
			return nil, err
		}
	} else if err := s.db.WithContext(ctx).Create(&item).Error; err != nil {
		return nil, err
	}
	return mapFromJSON(item), nil
}

func (s *Service) AdminUpdateSpendEventType(ctx context.Context, code string, payload map[string]any) (map[string]any, error) {
	code = strings.TrimSpace(code)
	if code == "" {
		return nil, BadRequest("code is required")
	}
	if err := s.db.WithContext(ctx).Model(&SpendEventTypeConfig{}).Where("code = ?", code).Updates(cleanRawPayload(payload)).Error; err != nil {
		return nil, err
	}
	var item SpendEventTypeConfig
	if err := s.db.WithContext(ctx).Where("code = ?", code).First(&item).Error; err != nil {
		return nil, err
	}
	return mapFromJSON(item), nil
}

func (s *Service) AdminListTicketDepartments(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	db := s.db.WithContext(ctx).Table("ticket_departments")
	if enabled, ok := boolStatus(query.Status); ok {
		db = db.Where("is_enabled = ?", enabled)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("code LIKE ? OR name LIKE ? OR description LIKE ?", like, like, like)
	}
	return adminRawPage(ctx, db.Order("sort_order ASC, id DESC"), query)
}

func (s *Service) AdminListAgents(ctx context.Context, query AdminListQuery) (map[string]any, error) {
	if query.Limit <= 0 {
		query.Limit = 20
	}
	db := s.db.WithContext(ctx).Model(&UserAgent{})
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Joins("LEFT JOIN users ON users.id = user_agents.user_id").
			Where("user_agents.invite_code LIKE ? OR user_agents.apply_tier LIKE ? OR users.username LIKE ? OR users.email LIKE ? OR users.phone LIKE ? OR users.nickname LIKE ?", like, like, like, like, like, like)
	}
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var agents []UserAgent
	if err := db.Order("user_agents.id DESC").Limit(query.Limit).Offset(query.Offset).Find(&agents).Error; err != nil {
		return nil, err
	}
	items, err := s.adminAgentItems(ctx, agents)
	if err != nil {
		return nil, err
	}
	return map[string]any{"items": items, "total": total, "limit": query.Limit, "offset": query.Offset}, nil
}

func (s *Service) AdminGetAgent(ctx context.Context, agentID int64) (map[string]any, error) {
	var agent UserAgent
	if err := s.db.WithContext(ctx).Where("id = ?", agentID).First(&agent).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("代理记录不存在")
		}
		return nil, err
	}
	items, err := s.adminAgentItems(ctx, []UserAgent{agent})
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, NotFound("代理记录不存在")
	}
	item := items[0]
	item["tickets"] = []map[string]any{}
	item["evaluation_logs"] = []map[string]any{}
	item["status_logs"] = []map[string]any{}
	item["risk_events"] = []map[string]any{}
	return item, nil
}

func (s *Service) AdminReviewAgent(ctx context.Context, agentID int64, payload map[string]any, adminID int64) (map[string]any, error) {
	action := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["action"])))
	if action == "" || action == "<nil>" {
		status := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["status"])))
		if status == "active" || status == "approved" {
			action = "approve"
		} else if status == "rejected" {
			action = "reject"
		}
	}
	switch action {
	case "approve":
		tierID := int64Value(payload["agent_tier_id"])
		if tierID <= 0 {
			return nil, BadRequest("请选择代理档位")
		}
		if err := s.ensureAgentTier(ctx, tierID); err != nil {
			return nil, err
		}
		now := time.Now().UTC()
		updates := map[string]any{
			"status":         "active",
			"agent_tier_id":  tierID,
			"approved_at":    now,
			"approved_by":    adminID,
			"reject_reason":  nil,
			"remark":         adminPayloadStringPtr(payload, "remark"),
			"review_message": adminPayloadStringPtr(payload, "remark"),
			"updated_at":     now,
		}
		if err := s.db.WithContext(ctx).Table("user_agents").Where("id = ?", agentID).Updates(updates).Error; err != nil {
			return nil, err
		}
	case "reject":
		reason := adminPayloadString(payload, "reject_reason")
		if reason == "" {
			reason = adminPayloadString(payload, "remark")
		}
		if reason == "" {
			return nil, BadRequest("请填写拒绝原因")
		}
		now := time.Now().UTC()
		updates := map[string]any{
			"status":         "rejected",
			"reject_reason":  reason,
			"review_message": reason,
			"remark":         adminPayloadStringPtr(payload, "remark"),
			"updated_at":     now,
		}
		if err := s.db.WithContext(ctx).Table("user_agents").Where("id = ?", agentID).Updates(updates).Error; err != nil {
			return nil, err
		}
	default:
		return nil, BadRequest("不支持的代理审核动作")
	}
	return s.AdminGetAgent(ctx, agentID)
}

func (s *Service) AdminAssignAgentTier(ctx context.Context, agentID int64, tierID int64, adminID int64) (map[string]any, error) {
	if tierID <= 0 {
		return nil, BadRequest("请选择代理档位")
	}
	if err := s.ensureAgentTier(ctx, tierID); err != nil {
		return nil, err
	}
	if err := s.db.WithContext(ctx).Table("user_agents").Where("id = ?", agentID).Updates(map[string]any{
		"agent_tier_id": tierID,
		"updated_at":    time.Now().UTC(),
	}).Error; err != nil {
		return nil, err
	}
	return s.AdminGetAgent(ctx, agentID)
}

func (s *Service) AdminListAgentSettlements(ctx context.Context, query AdminAgentSettlementQuery) (map[string]any, error) {
	if query.Limit <= 0 {
		query.Limit = 50
	}
	db := s.db.WithContext(ctx).Model(&AgentSettlement{})
	if strings.TrimSpace(query.Status) != "" {
		db = db.Where("status = ?", strings.TrimSpace(query.Status))
	}
	if strings.TrimSpace(query.SettlementType) != "" {
		db = db.Where("settlement_type = ?", strings.TrimSpace(query.SettlementType))
	}
	if query.UserID != nil {
		db = db.Where("user_id = ?", *query.UserID)
	}
	if query.OrderID != nil {
		db = db.Where("order_id = ?", *query.OrderID)
	}
	return adminPage(db.Order("id DESC"), AdminListQuery{Limit: query.Limit, Offset: query.Offset}, func(items []AgentSettlement) []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) AdminAgentManualAction(ctx context.Context, agentID int64, payload map[string]any, adminID int64) (map[string]any, error) {
	action := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["action"])))
	reason := strings.TrimSpace(fmt.Sprint(payload["reason"]))
	if reason == "" || reason == "<nil>" {
		return nil, BadRequest("请填写调整原因")
	}
	updates := map[string]any{"updated_at": time.Now().UTC(), "remark": reason}
	switch action {
	case "restore":
		updates["status"] = "active"
		updates["risk_locked_at"] = nil
		updates["risk_reason"] = nil
	case "observe":
		updates["status"] = "observing"
		now := time.Now().UTC()
		updates["observe_started_at"] = now
	case "lock":
		updates["status"] = "risk_locked"
		now := time.Now().UTC()
		updates["risk_locked_at"] = now
		updates["risk_reason"] = reason
	case "reject":
		updates["status"] = "rejected"
		updates["reject_reason"] = reason
		updates["review_message"] = reason
	case "assign_tier":
		tierID := int64Value(payload["agent_tier_id"])
		if tierID <= 0 {
			return nil, BadRequest("请选择代理档位")
		}
		if err := s.ensureAgentTier(ctx, tierID); err != nil {
			return nil, err
		}
		updates["agent_tier_id"] = tierID
	default:
		return nil, BadRequest("不支持的代理人工动作")
	}
	if tierID := int64Value(payload["agent_tier_id"]); tierID > 0 && action != "assign_tier" {
		if err := s.ensureAgentTier(ctx, tierID); err != nil {
			return nil, err
		}
		updates["agent_tier_id"] = tierID
	}
	if err := s.db.WithContext(ctx).Table("user_agents").Where("id = ?", agentID).Updates(updates).Error; err != nil {
		return nil, err
	}
	return s.AdminGetAgent(ctx, agentID)
}

func (s *Service) AdminEvaluateAgent(ctx context.Context, agentID int64) (map[string]any, error) {
	agent, err := s.AdminGetAgent(ctx, agentID)
	if err != nil {
		return nil, err
	}
	evaluation := map[string]any{"action": "none", "reason": "当前版本仅完成数据汇总，未触发自动调整"}
	return map[string]any{"agent": agent, "evaluation": evaluation}, nil
}

func (s *Service) AdminAffiliateReport(ctx context.Context) (map[string]any, error) {
	var agents int64
	_ = s.db.WithContext(ctx).Table("user_agents").Count(&agents).Error
	var settlements int64
	_ = s.db.WithContext(ctx).Table("agent_settlements").Count(&settlements).Error
	var totalProfit int64
	_ = s.db.WithContext(ctx).Table("agent_settlements").Where("status = ?", "settled").Select("COALESCE(SUM(delta_amount),0)").Scan(&totalProfit).Error
	return map[string]any{
		"summary": map[string]any{
			"clicks":            0,
			"unique_ips":        0,
			"registrations":     agents,
			"paid_orders":       settlements,
			"revenue":           totalProfit,
			"register_rate_bps": 0,
			"order_rate_bps":    0,
		},
		"channels":      []map[string]any{},
		"recent_clicks": []map[string]any{},
	}, nil
}

func (s *Service) AdminAgentReport(ctx context.Context) (map[string]any, error) {
	return s.AdminAffiliateReport(ctx)
}

func (s *Service) ensureAgentTier(ctx context.Context, tierID int64) error {
	var count int64
	if err := s.db.WithContext(ctx).Model(&AgentTier{}).Where("id = ? AND enabled = ?", tierID, true).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return BadRequest("代理档位不存在或已禁用")
	}
	return nil
}

func (s *Service) adminAgentItems(ctx context.Context, agents []UserAgent) ([]map[string]any, error) {
	userIDs := make([]int64, 0, len(agents))
	tierIDs := make([]int64, 0, len(agents))
	for _, agent := range agents {
		userIDs = append(userIDs, agent.UserID)
		if agent.AgentTierID != nil {
			tierIDs = append(tierIDs, *agent.AgentTierID)
		}
	}
	users := map[int64]UserAccount{}
	if len(userIDs) > 0 {
		var list []UserAccount
		if err := s.db.WithContext(ctx).Where("id IN ?", userIDs).Find(&list).Error; err != nil {
			return nil, err
		}
		for _, user := range list {
			users[user.ID] = user
		}
	}
	tiers := map[int64]AgentTier{}
	if len(tierIDs) > 0 {
		var list []AgentTier
		if err := s.db.WithContext(ctx).Where("id IN ?", tierIDs).Find(&list).Error; err != nil {
			return nil, err
		}
		for _, tier := range list {
			tiers[tier.ID] = tier
		}
	}
	out := make([]map[string]any, 0, len(agents))
	for _, agent := range agents {
		data := mapFromJSON(agent)
		if user, ok := users[agent.UserID]; ok {
			data["user"] = adminAgentUserMap(user)
		} else {
			data["user"] = nil
		}
		if agent.AgentTierID != nil {
			if tier, ok := tiers[*agent.AgentTierID]; ok {
				data["agent_tier"] = mapFromJSON(tier)
			} else {
				data["agent_tier"] = nil
			}
		} else {
			data["agent_tier"] = nil
		}
		out = append(out, data)
	}
	return out, nil
}

func adminAgentUserMap(user UserAccount) map[string]any {
	return map[string]any{
		"id":         user.ID,
		"username":   user.Username,
		"email":      user.Email,
		"nickname":   user.Nickname,
		"phone":      user.Phone,
		"balance":    user.Balance,
		"created_at": user.CreatedAt,
	}
}

func adminPayloadStringPtr(payload map[string]any, key string) *string {
	text := adminPayloadString(payload, key)
	if text == "" {
		return nil
	}
	return &text
}

func adminPayloadString(payload map[string]any, key string) string {
	text := strings.TrimSpace(fmt.Sprint(payload[key]))
	if text == "" || text == "<nil>" {
		return ""
	}
	return text
}

func (s *Service) adminSetUserAgentTier(ctx context.Context, adminID int64, userID int64, tierCode string) error {
	var agent UserAgent
	err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&agent).Error
	if tierCode == "" {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		if err != nil {
			return err
		}
		return s.db.WithContext(ctx).Model(&UserAgent{}).Where("id = ?", agent.ID).Updates(map[string]any{
			"status": "terminated", "agent_tier_id": nil, "updated_at": time.Now().UTC(),
		}).Error
	}
	var tier AgentTier
	if tierErr := s.db.WithContext(ctx).Where("code = ? AND enabled = ?", tierCode, true).First(&tier).Error; tierErr != nil {
		if errors.Is(tierErr, gorm.ErrRecordNotFound) {
			return BadRequest("代理档位不存在或已停用")
		}
		return tierErr
	}
	now := time.Now().UTC()
	if errors.Is(err, gorm.ErrRecordNotFound) {
		agent = UserAgent{UserID: userID, Status: "active", AgentTierID: &tier.ID, ApplyTier: &tier.Code, ApprovedAt: &now, ApprovedBy: &adminID}
		return s.db.WithContext(ctx).Create(&agent).Error
	}
	if err != nil {
		return err
	}
	updates := map[string]any{"status": "active", "agent_tier_id": tier.ID, "apply_tier": tier.Code, "updated_at": now}
	if agent.ApprovedAt == nil {
		updates["approved_at"] = now
		updates["approved_by"] = adminID
	}
	return s.db.WithContext(ctx).Model(&UserAgent{}).Where("id = ?", agent.ID).Updates(updates).Error
}

func (s *Service) AdminAdjustBalance(ctx context.Context, adminID int64, userID int64, delta int, remark string) (map[string]any, error) {
	if delta == 0 {
		return nil, BadRequest("delta must not be zero")
	}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var user UserAccount
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&user).Error; err != nil {
			return err
		}
		next := user.Balance + delta
		if next < 0 {
			return BadRequest("balance cannot be negative")
		}
		if err := tx.Model(&UserAccount{}).Where("id = ?", userID).Update("balance", next).Error; err != nil {
			return err
		}
		changeType := "admin_adjust"
		refNo := fmt.Sprintf("ADMIN%d", time.Now().UnixNano())
		log := BalanceLog{UserID: userID, ChangeType: changeType, Delta: delta, BalanceAfter: next, RefNo: &refNo, OperatorID: &adminID, Remark: &remark}
		return tx.Create(&log).Error
	})
	if err != nil {
		return nil, err
	}
	return s.AdminGetUser(ctx, userID)
}

func (s *Service) AdminResetUserPassword(ctx context.Context, userID int64, password string) (map[string]any, error) {
	password = strings.TrimSpace(password)
	if password == "" {
		generated, err := auth.GeneratePassword(14)
		if err != nil {
			return nil, err
		}
		password = generated
	}
	hashed, err := auth.HashPassword(password)
	if err != nil {
		return nil, err
	}
	result := s.db.WithContext(ctx).Table("users").Where("id = ?", userID).Update("hashed_password", hashed)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("user not found")
	}
	return map[string]any{"id": userID, "password": password}, nil
}

func (s *Service) AdminUserQuotaAction(ctx context.Context, userID int64, table string, payload map[string]any) (map[string]any, error) {
	switch table {
	case "refund_add":
		return s.adjustRefundQuota(ctx, userID, intValueAny(payload["count"], 1), "add", intValueAny(payload["operator_id"], 0), requiredQuotaReason(payload))
	case "refund_reduce":
		return s.adjustRefundQuota(ctx, userID, intValueAny(payload["count"], 1), "reduce", intValueAny(payload["operator_id"], 0), requiredQuotaReason(payload))
	case "refund_reset":
		return s.adjustRefundQuota(ctx, userID, 0, "reset", intValueAny(payload["operator_id"], 0), requiredQuotaReason(payload))
	case "invite_add":
		return s.adjustInviteCodeQuota(ctx, userID, intValueAny(payload["count"], 1), "add", intValueAny(payload["operator_id"], 0), requiredQuotaReason(payload))
	case "invite_reset":
		return s.adjustInviteCodeQuota(ctx, userID, 0, "reset", intValueAny(payload["operator_id"], 0), requiredQuotaReason(payload))
	case "verification_add":
		count := intValueAny(payload["count"], 1)
		if count < 1 {
			count = 1
		}
		return s.adjustVerificationQuota(ctx, userID, count)
	case "verification_reduce":
		count := intValueAny(payload["count"], 1)
		return s.adjustVerificationQuota(ctx, userID, -count)
	default:
		return s.AdminGetUser(ctx, userID)
	}
}

func (s *Service) adjustRefundQuota(ctx context.Context, userID int64, count int, action string, operatorID int, reason string) (map[string]any, error) {
	if reason == "" {
		return nil, BadRequest("reason is required")
	}
	if count < 0 {
		count = 0
	}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var user UserAccount
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&user).Error; err != nil {
			return err
		}
		beforeExtra := maxInt(0, user.FullRefundQuotaExtra)
		beforeResetAt := user.FullRefundQuotaResetAt
		afterExtra := beforeExtra
		afterResetAt := beforeResetAt
		switch action {
		case "add":
			afterExtra = beforeExtra + maxInt(1, count)
		case "reduce":
			afterExtra = maxInt(0, beforeExtra-maxInt(1, count))
		case "reset":
			now := time.Now().UTC()
			afterResetAt = &now
		}
		if err := tx.Model(&UserAccount{}).Where("id = ?", userID).Updates(map[string]any{"full_refund_quota_extra": afterExtra, "full_refund_quota_reset_at": afterResetAt}).Error; err != nil {
			return err
		}
		log := map[string]any{"user_id": userID, "action": action, "count": count, "before_extra": beforeExtra, "after_extra": afterExtra, "before_reset_at": beforeResetAt, "after_reset_at": afterResetAt, "reason": reason, "created_at": time.Now().UTC(), "updated_at": time.Now().UTC()}
		if operatorID > 0 {
			log["operator_id"] = operatorID
		}
		return tx.Table("user_refund_quota_logs").Create(log).Error
	})
	if err != nil {
		return nil, err
	}
	return s.AdminGetUser(ctx, userID)
}

func (s *Service) adjustInviteCodeQuota(ctx context.Context, userID int64, count int, action string, operatorID int, reason string) (map[string]any, error) {
	if reason == "" {
		return nil, BadRequest("reason is required")
	}
	if count < 0 {
		count = 0
	}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var user UserAccount
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&user).Error; err != nil {
			return err
		}
		beforeUsed := user.InviteCodeChangeCount
		beforeExtra := maxInt(0, user.InviteCodeChangeExtra)
		afterUsed := beforeUsed
		afterExtra := beforeExtra
		switch action {
		case "add":
			afterExtra = beforeExtra + maxInt(1, count)
		case "reset":
			afterUsed = 0
		}
		if err := tx.Model(&UserAccount{}).Where("id = ?", userID).Updates(map[string]any{"invite_code_change_count": afterUsed, "invite_code_change_extra": afterExtra}).Error; err != nil {
			return err
		}
		log := map[string]any{"user_id": userID, "action": action, "count": count, "before_used": beforeUsed, "after_used": afterUsed, "before_extra": beforeExtra, "after_extra": afterExtra, "reason": reason, "created_at": time.Now().UTC(), "updated_at": time.Now().UTC()}
		if operatorID > 0 {
			log["operator_id"] = operatorID
		}
		return tx.Table("user_invite_code_logs").Create(log).Error
	})
	if err != nil {
		return nil, err
	}
	return s.AdminGetUser(ctx, userID)
}

func (s *Service) adjustVerificationQuota(ctx context.Context, userID int64, delta int) (map[string]any, error) {
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var count int64
		if err := tx.Table("user_verification_quotas").Where("user_id = ?", userID).Count(&count).Error; err != nil {
			return err
		}
		if count == 0 {
			if err := tx.Table("user_verification_quotas").Create(map[string]any{"user_id": userID, "paid_remaining": 0, "paid_purchased": 0, "paid_used": 0, "free_used": 0, "created_at": time.Now().UTC(), "updated_at": time.Now().UTC()}).Error; err != nil {
				return err
			}
		}
		if delta >= 0 {
			return tx.Exec("UPDATE user_verification_quotas SET paid_remaining = paid_remaining + ?, paid_purchased = paid_purchased + ?, updated_at = ? WHERE user_id = ?", delta, delta, time.Now().UTC(), userID).Error
		}
		return tx.Exec("UPDATE user_verification_quotas SET paid_remaining = GREATEST(paid_remaining - ?, 0), updated_at = ? WHERE user_id = ?", -delta, time.Now().UTC(), userID).Error
	})
	if err != nil {
		return nil, err
	}
	return s.AdminGetUser(ctx, userID)
}

func (s *Service) AdminFinanceRechargeStats(ctx context.Context) (map[string]any, error) {
	var row struct {
		TotalPaidCount int64 `gorm:"column:total_paid_count"`
		TotalPaidCents int64 `gorm:"column:total_paid_cents"`
		TodayPaidCount int64 `gorm:"column:today_paid_count"`
		TodayPaidCents int64 `gorm:"column:today_paid_cents"`
		PendingCount   int64 `gorm:"column:pending_count"`
	}
	if err := s.db.WithContext(ctx).Model(&RechargeOrder{}).Select(`
		COUNT(CASE WHEN status = 'paid' THEN 1 END) AS total_paid_count,
		COALESCE(SUM(CASE WHEN status = 'paid' THEN amount_cents ELSE 0 END), 0) AS total_paid_cents,
		COUNT(CASE WHEN status = 'paid' AND updated_at >= ? THEN 1 END) AS today_paid_count,
		COALESCE(SUM(CASE WHEN status = 'paid' AND updated_at >= ? THEN amount_cents ELSE 0 END), 0) AS today_paid_cents,
		COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending_count
	`, todayStartUTC(), todayStartUTC()).Scan(&row).Error; err != nil {
		return nil, err
	}
	return map[string]any{
		"total_paid_count": row.TotalPaidCount,
		"total_paid_cents": row.TotalPaidCents,
		"today_paid_count": row.TodayPaidCount,
		"today_paid_cents": row.TodayPaidCents,
		"pending_count":    row.PendingCount,
	}, nil
}

func (s *Service) AdminTaskList(ctx context.Context) ([]map[string]any, error) {
	definitions := DefaultTaskDefinitions()
	if s.taskExecutor != nil {
		definitions = s.taskExecutor.Definitions()
	}
	items := make([]map[string]any, 0, len(definitions))
	for _, definition := range definitions {
		item := map[string]any{"key": definition.Key, "name": definition.Name, "group": definition.Group, "description": definition.Description, "manual_enabled": definition.ManualEnabled, "interval_label": definition.IntervalLabel}
		key := definition.Key
		rows, err := rawRows(s.db.WithContext(ctx).Table("task_run_logs").Where("task_key = ?", key).Order("id DESC").Limit(1))
		if err != nil {
			return nil, err
		}
		if len(rows) > 0 {
			item["latest_run"] = rows[0]
			item["running"] = rows[0]["status"] == "running"
		} else {
			item["latest_run"] = nil
			item["running"] = false
		}
		var failed int64
		if err := s.db.WithContext(ctx).Table("task_run_logs").Where("task_key = ? AND status = ?", key, "failed").Count(&failed).Error; err != nil {
			return nil, err
		}
		item["failed_runs"] = failed
		items = append(items, item)
	}
	return items, nil
}

func (s *Service) AdminListTaskRuns(ctx context.Context, query AdminListQuery, taskKey string, triggerType string) (map[string]any, error) {
	db := s.db.WithContext(ctx).Table("task_run_logs")
	if query.Status != "" {
		db = db.Where("status = ?", query.Status)
	}
	if strings.TrimSpace(taskKey) != "" {
		db = db.Where("task_key = ?", strings.TrimSpace(taskKey))
	}
	if strings.TrimSpace(triggerType) != "" {
		db = db.Where("trigger_type = ?", strings.TrimSpace(triggerType))
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("task_key LIKE ? OR task_name LIKE ? OR error_message LIKE ?", like, like, like)
	}
	return adminRawPage(ctx, db.Order("id DESC"), query)
}

func (s *Service) AdminRunTask(ctx context.Context, taskKey string, adminID int64) (map[string]any, error) {
	return s.runRegisteredTask(ctx, taskKey, "manual", &adminID)
}

func (s *Service) RunScheduledTask(ctx context.Context, taskKey string) (map[string]any, error) {
	return s.runRegisteredTask(ctx, taskKey, "schedule", nil)
}

func (s *Service) runRegisteredTask(ctx context.Context, taskKey string, triggerType string, operatorID *int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	taskKey = strings.TrimSpace(taskKey)
	definitions := DefaultTaskDefinitions()
	if s.taskExecutor != nil {
		definitions = s.taskExecutor.Definitions()
	}
	var definition *TaskDefinition
	for i := range definitions {
		if definitions[i].Key == taskKey {
			definition = &definitions[i]
			break
		}
	}
	if definition == nil {
		return nil, BadRequest("unknown task")
	}
	if triggerType == "manual" && !definition.ManualEnabled {
		return nil, BadRequest("task does not support manual execution")
	}
	if s.taskExecutor == nil {
		return nil, ServiceUnavailable("task executor is not configured")
	}
	release, acquired, err := s.acquireTaskExecutionLock(ctx, taskKey)
	if err != nil {
		return nil, err
	}
	if !acquired {
		return map[string]any{"accepted": false, "status": "skipped", "reason": "task is already running", "task_key": taskKey}, nil
	}
	defer release()

	startedAt := time.Now().UTC()
	run := TaskRunLog{
		TaskKey:     taskKey,
		TaskName:    definition.Name,
		TriggerType: triggerType,
		Status:      "running",
		OperatorID:  operatorID,
		StartedAt:   startedAt,
	}
	if err := s.db.WithContext(ctx).Create(&run).Error; err != nil {
		return nil, err
	}
	summary, runErr := executeTaskSafely(ctx, s.taskExecutor, taskKey)
	finishedAt := time.Now().UTC()
	updates := map[string]any{
		"status":      "success",
		"finished_at": finishedAt,
		"duration_ms": maxInt(0, int(finishedAt.Sub(startedAt).Milliseconds())),
		"summary":     jsonRawFromAny(summary),
		"updated_at":  finishedAt,
	}
	if runErr != nil {
		updates["status"] = "failed"
		updates["error_message"] = trimText(runErr.Error(), 4000)
	}
	finalizeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.db.WithContext(finalizeCtx).Table("task_run_logs").Where("id = ?", run.ID).Updates(updates).Error; err != nil {
		return nil, err
	}
	runData, err := rawByID(finalizeCtx, s.db, "task_run_logs", run.ID)
	if err != nil {
		return nil, err
	}
	if runErr != nil {
		return nil, fmt.Errorf("task %s failed: %w", taskKey, runErr)
	}
	return map[string]any{"accepted": true, "status": "success", "task_key": taskKey, "summary": summary, "run": runData}, nil
}

func executeTaskSafely(ctx context.Context, executor TaskExecutor, taskKey string) (summary map[string]any, err error) {
	defer func() {
		if recovered := recover(); recovered != nil {
			err = fmt.Errorf("task handler panic: %v", recovered)
		}
	}()
	return executor.Execute(ctx, taskKey)
}

func (s *Service) acquireTaskExecutionLock(ctx context.Context, taskKey string) (func(), bool, error) {
	sqlDB, err := s.db.DB()
	if err != nil {
		return nil, false, err
	}
	conn, err := sqlDB.Conn(ctx)
	if err != nil {
		return nil, false, err
	}
	lockName := "qycw:task:" + taskKey
	var acquired int
	if err := conn.QueryRowContext(ctx, "SELECT GET_LOCK(?, 0)", lockName).Scan(&acquired); err != nil {
		_ = conn.Close()
		return nil, false, err
	}
	if acquired != 1 {
		_ = conn.Close()
		return func() {}, false, nil
	}
	release := func() {
		_, _ = conn.ExecContext(context.Background(), "SELECT RELEASE_LOCK(?)", lockName)
		_ = conn.Close()
	}
	return release, true, nil
}

func (s *Service) AdminListUpstreamActions(ctx context.Context, query AdminUpstreamActionQuery) (map[string]any, error) {
	if query.Limit <= 0 {
		query.Limit = 100
	}
	db := s.db.WithContext(ctx).Table("upstream_action_logs AS a").
		Select("a.*, u.username, o.out_order_no AS order_no").
		Joins("LEFT JOIN users u ON u.id = a.user_id").
		Joins("LEFT JOIN orders o ON o.id = a.order_id")
	if query.Status != "" {
		db = db.Where("a.status = ?", query.Status)
	}
	if query.Source != "" {
		db = db.Where("a.source = ?", query.Source)
	}
	if query.ActionType != "" {
		db = db.Where("a.action_type = ?", query.ActionType)
	}
	if query.UserID != nil {
		db = db.Where("a.user_id = ?", *query.UserID)
	}
	if query.OrderID != nil {
		db = db.Where("a.order_id = ?", *query.OrderID)
	}
	if query.InstanceID != nil {
		db = db.Where("a.instance_id = ?", *query.InstanceID)
	}
	if query.Search != "" {
		like := "%" + query.Search + "%"
		db = db.Where("a.action_type LIKE ? OR a.request_path LIKE ? OR a.resource_id LIKE ? OR a.error_message LIKE ? OR a.remark LIKE ? OR o.out_order_no LIKE ? OR u.username LIKE ?", like, like, like, like, like, like, like)
	}
	return adminRawPage(ctx, db.Order("a.id DESC"), AdminListQuery{Limit: query.Limit, Offset: query.Offset})
}

func (s *Service) AdminSyncUpstreamAction(ctx context.Context, actionID int64) (map[string]any, error) {
	var action UpstreamActionLog
	if err := s.db.WithContext(ctx).Where("id = ?", actionID).First(&action).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, NotFound("upstream action not found")
		}
		return nil, err
	}
	if _, err := s.syncOneUpstreamAction(ctx, &action); err != nil {
		return nil, err
	}
	return rawByID(ctx, s.db, "upstream_action_logs", actionID)
}

func (s *Service) AdminUpstreamActionUpdate(ctx context.Context, actionID int64, status string, remark string) (map[string]any, error) {
	updates := map[string]any{"status": status, "updated_at": time.Now().UTC(), "last_checked_at": time.Now().UTC()}
	if status == "manual_handled" || status == "ignored" || status == "failed" || status == "success" {
		updates["completed_at"] = time.Now().UTC()
	}
	if status == "submitted" || status == "running" {
		updates["completed_at"] = nil
		updates["error_message"] = nil
	}
	if strings.TrimSpace(remark) != "" {
		updates["remark"] = strings.TrimSpace(remark)
	}
	if err := s.db.WithContext(ctx).Table("upstream_action_logs").Where("id = ?", actionID).Updates(updates).Error; err != nil {
		return nil, err
	}
	return rawByID(ctx, s.db, "upstream_action_logs", actionID)
}

func cloudServerMap(item CloudServer) map[string]any {
	data := mapFromJSON(item)
	delete(data, "PasswordEncrypted")
	delete(data, "password_encrypted")
	data["has_password"] = strings.TrimSpace(item.PasswordEncrypted) != ""
	return data
}

func (s *Service) adminInstanceItems(ctx context.Context, items []Instance) ([]map[string]any, error) {
	productIDs := make([]int64, 0, len(items))
	serverIDs := make([]int64, 0, len(items))
	for _, item := range items {
		productIDs = append(productIDs, item.ProductID)
		if item.CloudServerID != nil {
			serverIDs = append(serverIDs, *item.CloudServerID)
		}
	}
	products := map[int64]Product{}
	if len(productIDs) > 0 {
		var rows []Product
		if err := s.db.WithContext(ctx).Where("id IN ?", productIDs).Find(&rows).Error; err != nil {
			return nil, err
		}
		for _, row := range rows {
			products[row.ID] = row
		}
	}
	groupIDs := make([]int64, 0, len(products))
	for _, product := range products {
		groupIDs = append(groupIDs, product.GroupID)
	}
	groups := map[int64]ProductGroup{}
	if len(groupIDs) > 0 {
		var rows []ProductGroup
		if err := s.db.WithContext(ctx).Where("id IN ?", groupIDs).Find(&rows).Error; err != nil {
			return nil, err
		}
		for _, row := range rows {
			groups[row.ID] = row
		}
	}
	servers := map[int64]CloudServer{}
	if len(serverIDs) > 0 {
		var rows []CloudServer
		if err := s.db.WithContext(ctx).Where("id IN ?", serverIDs).Find(&rows).Error; err != nil {
			return nil, err
		}
		for _, row := range rows {
			servers[row.ID] = row
		}
	}
	out := make([]map[string]any, 0, len(items))
	for _, item := range items {
		data := adminInstanceMap(item)
		if product, ok := products[item.ProductID]; ok {
			data["product_name"] = product.Name
			if group, ok := groups[product.GroupID]; ok {
				data["product_group_name"] = group.Name
			}
		}
		if item.CloudServerID != nil {
			if server, ok := servers[*item.CloudServerID]; ok {
				data["cloud_server_name"] = server.Name
			}
		}
		out = append(out, data)
	}
	return out, nil
}

func adminInstanceMap(item Instance) map[string]any {
	return map[string]any{
		"id": item.ID, "user_id": item.UserID, "order_id": item.OrderID, "product_id": item.ProductID,
		"cloud_server_id": item.CloudServerID, "status": item.Status, "suspend_type": item.SuspendType,
		"cycle": item.Cycle, "unit_price": item.UnitPrice, "next_renew_amount": item.NextRenewAmount,
		"auto_renew": item.AutoRenew, "alias": item.Alias, "hostname": item.Hostname, "main_ip": item.MainIP,
		"ssh_port": item.SSHPort, "upstream_id": item.UpstreamID, "ip_num": item.IPNum, "nat_acl_limit": item.NatACLLimit,
		"expires_at": item.ExpiresAt, "created_at": item.CreatedAt, "terminated_at": item.TerminatedAt,
		"provision_attempts": item.ProvisionAttempts, "last_provision_at": item.LastProvisionAt,
		"provision_log": item.ProvisionLog, "upstream_payload": jsonAnyValue(item.UpstreamPayload), "root_password": nil,
	}
}

func adminRawPage(ctx context.Context, db *gorm.DB, query AdminListQuery) (map[string]any, error) {
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	rows, err := rawRows(db.Limit(query.Limit).Offset(query.Offset))
	if err != nil {
		return nil, err
	}
	return map[string]any{"items": rows, "total": total, "limit": query.Limit, "offset": query.Offset}, nil
}

type adminRawListSpec struct {
	searchColumns []string
	enabledColumn string
	order         string
}

var adminRawListSpecs = map[string]adminRawListSpec{
	"agent_tiers": {
		searchColumns: []string{"name", "code", "description"},
		enabledColumn: "enabled",
		order:         "sort_order ASC, id DESC",
	},
	"spend_event_type_configs": {
		searchColumns: []string{"code", "label", "description"},
		enabledColumn: "enabled",
		order:         "sort_order ASC, id DESC",
	},
}

func applyAdminRawListSpec(db *gorm.DB, table string, query AdminListQuery) *gorm.DB {
	spec, ok := adminRawListSpecs[table]
	if !ok {
		return db.Order("id DESC")
	}
	if enabled, ok := boolStatus(query.Status); ok && spec.enabledColumn != "" {
		db = db.Where(spec.enabledColumn+" = ?", enabled)
	}
	if query.Search != "" && len(spec.searchColumns) > 0 {
		parts := make([]string, 0, len(spec.searchColumns))
		args := make([]any, 0, len(spec.searchColumns))
		like := "%" + query.Search + "%"
		for _, column := range spec.searchColumns {
			parts = append(parts, column+" LIKE ?")
			args = append(args, like)
		}
		db = db.Where(strings.Join(parts, " OR "), args...)
	}
	if spec.order != "" {
		return db.Order(spec.order)
	}
	return db.Order("id DESC")
}

func boolStatus(status string) (bool, bool) {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "enabled", "enable", "active", "true", "1", "yes", "on":
		return true, true
	case "disabled", "disable", "inactive", "false", "0", "no", "off":
		return false, true
	default:
		return false, false
	}
}

func rawRows(db *gorm.DB) ([]map[string]any, error) {
	var rows []map[string]any
	if err := db.Find(&rows).Error; err != nil {
		return nil, err
	}
	for _, row := range rows {
		normalizeRawRow(row)
	}
	return rows, nil
}

func rawByID(ctx context.Context, db *gorm.DB, table string, id int64) (map[string]any, error) {
	rows, err := rawRows(db.WithContext(ctx).Table(table).Where("id = ?", id).Limit(1))
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, NotFound("record not found")
	}
	return rows[0], nil
}

func rawSave(ctx context.Context, db *gorm.DB, table string, id int64, payload map[string]any) (map[string]any, error) {
	payload = cleanRawPayload(payload)
	delete(payload, "id")
	if id > 0 {
		payload["updated_at"] = time.Now().UTC()
		result := db.WithContext(ctx).Table(table).Where("id = ?", id).Updates(payload)
		if result.Error != nil {
			return nil, result.Error
		}
		if result.RowsAffected == 0 {
			return nil, NotFound("record not found")
		}
		return rawByID(ctx, db, table, id)
	}
	payload["created_at"] = time.Now().UTC()
	payload["updated_at"] = time.Now().UTC()
	if err := db.WithContext(ctx).Table(table).Create(payload).Error; err != nil {
		return nil, err
	}
	if id, ok := int64FromAny(payload["id"]); ok {
		return rawByID(ctx, db, table, id)
	}
	return payload, nil
}

func deleteByID(ctx context.Context, db *gorm.DB, table string, id int64) (map[string]any, error) {
	result := db.WithContext(ctx).Table(table).Where("id = ?", id).Delete(map[string]any{})
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, NotFound("record not found")
	}
	return map[string]any{"deleted": true, "id": id}, nil
}

func cleanRawPayload(payload map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range payload {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		out[key] = value
	}
	return out
}

func normalizeRawRow(row map[string]any) {
	for key, value := range row {
		if raw, ok := value.([]byte); ok {
			row[key] = string(raw)
		}
	}
}

func stringFromMap(data map[string]any, key string) string {
	text := strings.TrimSpace(fmt.Sprint(data[key]))
	if text == "<nil>" {
		return ""
	}
	return text
}

func stringPtrFromMap(data map[string]any, key string) *string {
	text := stringFromMap(data, key)
	if text == "" {
		return nil
	}
	return &text
}

func nullableStringFromMap(data map[string]any, key string) any {
	if data[key] == nil {
		return nil
	}
	text := stringFromMap(data, key)
	if text == "" {
		return nil
	}
	return text
}

func timePtrFromMap(data map[string]any, key string) *time.Time {
	text := stringFromMap(data, key)
	if text == "" {
		return nil
	}
	for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02 15:04:05"} {
		if parsed, err := time.Parse(layout, text); err == nil {
			utc := parsed.UTC()
			return &utc
		}
	}
	return nil
}

func boolValueAny(value any, fallback bool) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case string:
		text := strings.ToLower(strings.TrimSpace(typed))
		if text == "true" || text == "1" || text == "yes" || text == "on" {
			return true
		}
		if text == "false" || text == "0" || text == "no" || text == "off" {
			return false
		}
	case float64:
		return typed != 0
	case int:
		return typed != 0
	case int64:
		return typed != 0
	}
	return fallback
}

func jsonStringPtrFromAny(value any) *string {
	if value == nil {
		return nil
	}
	if text, ok := value.(string); ok {
		text = strings.TrimSpace(text)
		if text == "" {
			return nil
		}
		if strings.HasPrefix(text, "[") {
			return &text
		}
		raw, _ := json.Marshal([]string{text})
		out := string(raw)
		return &out
	}
	raw, err := json.Marshal(value)
	if err != nil || string(raw) == "null" {
		return nil
	}
	out := string(raw)
	return &out
}

func jsonArrayFromString(value *string) []any {
	if value == nil || strings.TrimSpace(*value) == "" {
		return []any{}
	}
	var out []any
	if err := json.Unmarshal([]byte(*value), &out); err == nil && out != nil {
		return out
	}
	return []any{}
}

func intValueAny(value any, fallback int) int {
	if parsed, ok := int64FromAny(value); ok {
		return int(parsed)
	}
	return fallback
}

func withOperator(payload map[string]any, operatorID int64) map[string]any {
	if payload == nil {
		payload = map[string]any{}
	}
	if operatorID > 0 {
		payload["operator_id"] = operatorID
	}
	return payload
}

func requiredQuotaReason(payload map[string]any) string {
	for _, key := range []string{"reason", "remark"} {
		text := strings.TrimSpace(fmt.Sprint(payload[key]))
		if text != "" && text != "<nil>" {
			if len(text) > 200 {
				return text[:200]
			}
			return text
		}
	}
	return ""
}

func mapToStruct(in map[string]any, out any) error {
	raw := jsonRawFromAny(in)
	return json.Unmarshal(raw, out)
}

func cloudServerFromPayload(payload map[string]any) CloudServer {
	server := CloudServer{
		Name:        stringFromMap(payload, "name"),
		ServerType:  stringFromMap(payload, "server_type"),
		AccountType: stringFromMap(payload, "account_type"),
		Hostname:    firstString(payload["hostname"], payload["host"], payload["base_url"]),
		Username:    stringFromMap(payload, "username"),
		Status:      stringFromMap(payload, "status"),
	}
	if server.ServerType == "" {
		server.ServerType = "dcim_cloud"
	}
	if server.AccountType == "" {
		server.AccountType = "admin"
	}
	if port, ok := int64FromAny(payload["port"]); ok {
		server.Port = int(port)
	}
	if secure, ok := payload["secure"].(bool); ok {
		server.Secure = secure
	} else {
		server.Secure = truthyAny(payload["secure"])
	}
	if disabled, ok := payload["disabled"].(bool); ok {
		server.Disabled = disabled
	} else {
		server.Disabled = truthyAny(payload["disabled"])
	}
	if encrypted := stringFromMap(payload, "password_encrypted"); encrypted != "" {
		server.PasswordEncrypted = encrypted
	}
	return server
}
