// Package asset 提供统一的资产查询与操作服务
// 资产包含两种类型：IoT卡(card)和设备(device)
// 支持资产解析、实时状态查询、网关刷新、套餐查询等功能
package asset

import (
	"context"
	stderrors "errors"
	"sort"
	"strconv"
	"time"

	"github.com/break/junhong_cmp_fiber/internal/gateway"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/model/dto"
	packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
	assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
	"github.com/break/junhong_cmp_fiber/internal/store/postgres"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/errors"
	"github.com/break/junhong_cmp_fiber/pkg/logger"
	"github.com/break/junhong_cmp_fiber/pkg/middleware"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// IotCardRefresher 用于调用 RefreshCardDataFromGateway，避免循环依赖
type IotCardRefresher interface {
	RefreshCardDataFromGateway(ctx context.Context, iccid string) error
}

// PackageExpiryResolver 查询资产的套餐最终到期推算结果。
type PackageExpiryResolver interface {
	Resolve(ctx context.Context, assetType string, assetID uint) (dto.PackageExpiryEstimate, error)
}

// Service 资产查询与操作服务
type Service struct {
	db                    *gorm.DB
	deviceStore           *postgres.DeviceStore
	iotCardStore          *postgres.IotCardStore
	packageUsageStore     *postgres.PackageUsageStore
	packageStore          *postgres.PackageStore
	packageSeriesStore    *postgres.PackageSeriesStore
	deviceSimBindingStore *postgres.DeviceSimBindingStore
	shopStore             *postgres.ShopStore
	orderStore            *postgres.OrderStore
	orderItemStore        *postgres.OrderItemStore
	exchangeOrderStore    *postgres.ExchangeOrderStore
	redis                 *redis.Client
	iotCardService        IotCardRefresher
	gatewayClient         *gateway.Client
	assetIdentifierStore  *postgres.AssetIdentifierStore
	assetAuditService     assetAuditSvc.OperationLogger
	packageExpiryQuery    PackageExpiryResolver
}

// SetPackageExpiryQuery 注入套餐最终到期查询，供资产详情统一投影使用。
func (s *Service) SetPackageExpiryQuery(query *packageexpiry.Query) {
	s.packageExpiryQuery = query
}

// New 创建资产服务实例
func New(
	db *gorm.DB,
	deviceStore *postgres.DeviceStore,
	iotCardStore *postgres.IotCardStore,
	packageUsageStore *postgres.PackageUsageStore,
	packageStore *postgres.PackageStore,
	packageSeriesStore *postgres.PackageSeriesStore,
	deviceSimBindingStore *postgres.DeviceSimBindingStore,
	shopStore *postgres.ShopStore,
	redisClient *redis.Client,
	iotCardService IotCardRefresher,
	gatewayClient *gateway.Client,
	assetIdentifierStore *postgres.AssetIdentifierStore,
	orderStore *postgres.OrderStore,
	orderItemStore *postgres.OrderItemStore,
	exchangeOrderStore *postgres.ExchangeOrderStore,
	assetAuditService assetAuditSvc.OperationLogger,
) *Service {
	return &Service{
		db:                    db,
		deviceStore:           deviceStore,
		iotCardStore:          iotCardStore,
		packageUsageStore:     packageUsageStore,
		packageStore:          packageStore,
		packageSeriesStore:    packageSeriesStore,
		deviceSimBindingStore: deviceSimBindingStore,
		shopStore:             shopStore,
		orderStore:            orderStore,
		orderItemStore:        orderItemStore,
		exchangeOrderStore:    exchangeOrderStore,
		redis:                 redisClient,
		iotCardService:        iotCardService,
		gatewayClient:         gatewayClient,
		assetIdentifierStore:  assetIdentifierStore,
		assetAuditService:     assetAuditService,
		packageExpiryQuery:    packageexpiry.NewQuery(db),
	}
}

// Resolve 通过任意标识符解析资产
// 主路径：查注册表（精确匹配 ICCID 或 VirtualNo）
// Fallback：原有跨表 OR 查询（处理 IMEI/SN/MSISDN 等非注册标识符）
// includeUsageSummary 为 true 时额外填充当前世代流量汇总字段
func (s *Service) Resolve(ctx context.Context, identifier string, includeUsageSummary bool) (*dto.AssetResolveResponse, error) {
	if s.assetIdentifierStore != nil {
		regRecord, regErr := s.assetIdentifierStore.FindByIdentifier(ctx, identifier)
		if regErr == nil && regRecord != nil {
			switch regRecord.AssetType {
			case model.AssetTypeDevice:
				device, devErr := s.deviceStore.GetByID(ctx, regRecord.AssetID)
				if devErr == nil && device != nil {
					resp, buildErr := s.buildDeviceResolveResponse(ctx, device)
					if buildErr == nil {
						resp.Identifier = identifier
						if includeUsageSummary {
							s.fillUsageSummary(ctx, resp, "device", device.ID)
						}
					}
					return resp, buildErr
				}
			case model.AssetTypeIotCard:
				card, cardErr := s.iotCardStore.GetByID(ctx, regRecord.AssetID)
				if cardErr == nil && card != nil {
					resp, buildErr := s.buildCardResolveResponse(ctx, card)
					if buildErr == nil {
						resp.Identifier = identifier
						if includeUsageSummary {
							s.fillUsageSummary(ctx, resp, "iot_card", card.ID)
						}
					}
					return resp, buildErr
				}
			}
		}
	}

	device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
	if err == nil && device != nil {
		resp, buildErr := s.buildDeviceResolveResponse(ctx, device)
		if buildErr == nil {
			resp.Identifier = identifier
			if includeUsageSummary {
				s.fillUsageSummary(ctx, resp, "device", device.ID)
			}
		}
		return resp, buildErr
	}

	var card model.IotCard
	query := s.db.WithContext(ctx).
		Where("virtual_no = ? OR iccid = ? OR msisdn = ?", identifier, identifier, identifier)
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.First(&card).Error; err == nil {
		resp, buildErr := s.buildCardResolveResponse(ctx, &card)
		if buildErr == nil {
			resp.Identifier = identifier
			if includeUsageSummary {
				s.fillUsageSummary(ctx, resp, "iot_card", card.ID)
			}
		}
		return resp, buildErr
	}

	return nil, errors.New(errors.CodeNotFound, "未找到匹配的资产")
}

// buildDeviceResolveResponse 构建设备类型的资产解析响应
func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.Device) (*dto.AssetResolveResponse, error) {
	resp := &dto.AssetResolveResponse{
		AssetType:         "device",
		AssetID:           device.ID,
		VirtualNo:         device.VirtualNo,
		Status:            device.Status,
		StatusName:        constants.GetDeviceStatusName(device.Status),
		BatchNo:           device.BatchNo,
		ShopID:            device.ShopID,
		SeriesID:          device.SeriesID,
		ActivatedAt:       device.ActivatedAt,
		CreatedAt:         device.CreatedAt,
		UpdatedAt:         device.UpdatedAt,
		DeviceName:        device.DeviceName,
		IMEI:              device.IMEI,
		SN:                device.SN,
		DeviceModel:       device.DeviceModel,
		DeviceType:        device.DeviceType,
		MaxSimSlots:       device.MaxSimSlots,
		Manufacturer:      device.Manufacturer,
		OnlineStatus:      device.OnlineStatus,
		LastOnlineTime:    device.LastOnlineTime,
		SoftwareVersion:   device.SoftwareVersion,
		SwitchMode:        device.SwitchMode,
		LastGatewaySyncAt: device.LastGatewaySyncAt,
		EnablePolling:     device.EnablePolling,
		RealnamePolicy:    device.RealnamePolicy,
	}

	// 查绑定卡
	bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, device.ID)
	if err == nil && len(bindings) > 0 {
		resp.BoundCardCount = len(bindings)
		cardIDs := make([]uint, 0, len(bindings))
		slotMap := make(map[uint]int, len(bindings))
		isCurrentMap := make(map[uint]bool, len(bindings))
		for _, b := range bindings {
			cardIDs = append(cardIDs, b.IotCardID)
			slotMap[b.IotCardID] = b.SlotPosition
			isCurrentMap[b.IotCardID] = b.IsCurrent
		}
		cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
		if err != nil {
			logger.GetAppLogger().Warn("查询设备绑定卡信息失败，结果可能不完整",
				zap.Uints("card_ids", cardIDs),
				zap.Error(err))
		}
		for _, c := range cards {
			resp.Cards = append(resp.Cards, dto.BoundCardInfo{
				CardID:                c.ID,
				ICCID:                 c.ICCID,
				MSISDN:                c.MSISDN,
				NetworkStatus:         c.NetworkStatus,
				RealNameStatus:        c.RealNameStatus,
				RealNameAt:            c.FirstRealnameAt,
				SlotPosition:          slotMap[c.ID],
				IsCurrent:             isCurrentMap[c.ID],
				CarrierID:             c.CarrierID,
				CarrierType:           c.CarrierType,
				CarrierName:           c.CarrierName,
				RealnamePolicy:        c.RealnamePolicy,
				GatewayExtend:         c.GatewayExtend,
				LastSyncTime:          c.LastSyncTime,
				LastDataCheckAt:       c.LastDataCheckAt,
				LastRealNameCheckAt:   c.LastRealNameCheckAt,
				LastCardStatusCheckAt: c.LastCardStatusCheckAt,
			})
		}

		// RealNameStatus 以当前使用卡为准；无当前卡则视为未实名
		for _, c := range cards {
			if isCurrentMap[c.ID] {
				resp.RealNameStatus = c.RealNameStatus
				break
			}
		}
		// RealNameAt 取所有绑定卡中最早的实名时间
		for _, c := range cards {
			if c.FirstRealnameAt == nil {
				continue
			}
			if resp.RealNameAt == nil || c.FirstRealnameAt.Before(*resp.RealNameAt) {
				t := *c.FirstRealnameAt
				resp.RealNameAt = &t
			}
		}
	}

	// 查当前主套餐
	s.fillPackageInfo(ctx, resp, "device", device.ID)
	if err := s.fillPackageExpiryEstimate(ctx, resp, constants.AssetTypeDevice, device.ID); err != nil {
		return nil, err
	}

	activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
	if err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备激活状态失败")
	}
	resp.ActivationStatus = activationStatuses[device.ID]
	resp.ActivationStatusName = constants.GetActivationStatusName(resp.ActivationStatus)

	// 查 shop 名称
	s.fillShopName(ctx, resp)

	// 查套餐系列名称
	s.fillSeriesName(ctx, resp)

	// 查 Redis 保护期
	resp.DeviceProtectStatus = s.getDeviceProtectStatus(ctx, device.ID)

	return resp, nil
}

// buildCardResolveResponse 构建卡类型的资产解析响应
func (s *Service) buildCardResolveResponse(ctx context.Context, card *model.IotCard) (*dto.AssetResolveResponse, error) {
	resp := &dto.AssetResolveResponse{
		AssetType:            "card",
		AssetID:              card.ID,
		VirtualNo:            card.VirtualNo,
		Status:               card.Status,
		StatusName:           constants.GetIotCardStatusName(card.Status),
		BatchNo:              card.BatchNo,
		ShopID:               card.ShopID,
		SeriesID:             card.SeriesID,
		ActivatedAt:          card.ActivatedAt,
		CreatedAt:            card.CreatedAt,
		UpdatedAt:            card.UpdatedAt,
		RealNameStatus:       card.RealNameStatus,
		RealNameAt:           card.FirstRealnameAt,
		RealnamePolicy:       card.RealnamePolicy,
		NetworkStatus:        card.NetworkStatus,
		GatewayExtend:        card.GatewayExtend,
		GatewayCardIMEI:      card.GatewayCardIMEI,
		ICCID:                card.ICCID,
		CarrierID:            card.CarrierID,
		CarrierType:          card.CarrierType,
		CarrierName:          card.CarrierName,
		MSISDN:               card.MSISDN,
		IMSI:                 card.IMSI,
		CardCategory:         card.CardCategory,
		Supplier:             card.Supplier,
		ActivationStatus:     card.ActivationStatus,
		ActivationStatusName: constants.GetActivationStatusName(card.ActivationStatus),
		EnablePolling:        card.EnablePolling,
	}

	// 查绑定设备
	binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
	if err == nil && binding != nil {
		resp.BoundDeviceID = &binding.DeviceID
		device, devErr := s.deviceStore.GetByID(ctx, binding.DeviceID)
		if devErr == nil && device != nil {
			resp.BoundDeviceNo = device.VirtualNo
			resp.BoundDeviceName = device.DeviceName
		}
	}

	// 查当前主套餐
	s.fillPackageInfo(ctx, resp, "iot_card", card.ID)
	if err := s.fillPackageExpiryEstimate(ctx, resp, constants.AssetTypeIotCard, card.ID); err != nil {
		return nil, err
	}

	// 查 shop 名称
	s.fillShopName(ctx, resp)

	// 查套餐系列名称
	s.fillSeriesName(ctx, resp)

	return resp, nil
}

// fillPackageInfo 填充当前主套餐信息到响应中
func (s *Service) fillPackageInfo(ctx context.Context, resp *dto.AssetResolveResponse, carrierType string, carrierID uint) {
	usage, err := s.packageUsageStore.GetActiveMainPackage(ctx, carrierType, carrierID)
	if err != nil || usage == nil {
		return
	}

	resp.CurrentPackageUsageID = &usage.ID
	resp.CurrentPackage = usage.PackageName
	resp.CurrentPackageActivatedAt = usage.ActivatedAt
	resp.CurrentPackageExpiresAt = usage.ExpiresAt
	if resp.CurrentPackage == "" {
		pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
		if pkgErr == nil && pkg != nil {
			resp.CurrentPackage = pkg.PackageName
		}
	}

	metrics := usage.BuildTrafficMetrics()
	resp.RealTotalMB = metrics.RealTotalMB
	resp.RealUsedMB = metrics.RealUsedMB
	resp.VirtualTotalMB = metrics.VirtualTotalMB
	resp.VirtualUsedMB = metrics.VirtualUsedMB
	resp.ReductionPct = metrics.ReductionPct
	resp.EnableVirtualData = usage.EnableVirtualDataSnapshot
}

// fillPackageExpiryEstimate 使用统一 Query 投影资产的最终套餐到期，不允许查询失败降级为无套餐。
func (s *Service) fillPackageExpiryEstimate(ctx context.Context, resp *dto.AssetResolveResponse, assetType string, assetID uint) error {
	if s.packageExpiryQuery == nil {
		return errors.New(errors.CodeInternalError, "套餐最终到期查询未初始化")
	}
	estimate, err := s.packageExpiryQuery.Resolve(ctx, assetType, assetID)
	if err != nil {
		return err
	}
	resp.PackageExpiryEstimate = estimate
	return nil
}

// fillUsageSummary 填充当前世代流量汇总字段。
// 仅在 includeUsageSummary=true 时被调用，无套餐时返回 0.0（非 null）。
func (s *Service) fillUsageSummary(ctx context.Context, resp *dto.AssetResolveResponse, carrierType string, carrierID uint) {
	usages, err := s.packageUsageStore.GetCurrentGenerationUsagesForSummary(ctx, carrierType, carrierID)
	if err != nil {
		logger.GetAppLogger().Error("查询流量汇总失败", zap.String("carrierType", carrierType), zap.Uint("carrierID", carrierID), zap.Error(err))
		return
	}
	totalUsed, totalRemaining := computeUsageSummary(usages)
	resp.TotalVirtualUsedMB = &totalUsed
	resp.TotalVirtualRemainingMB = &totalRemaining
}

// fillShopName 填充店铺名称
func (s *Service) fillShopName(ctx context.Context, resp *dto.AssetResolveResponse) {
	if resp.ShopID == nil || *resp.ShopID == 0 {
		return
	}
	shop, err := s.shopStore.GetByID(ctx, *resp.ShopID)
	if err == nil && shop != nil {
		resp.ShopName = shop.ShopName
	}
}

// fillSeriesName 填充套餐系列名称
func (s *Service) fillSeriesName(ctx context.Context, resp *dto.AssetResolveResponse) {
	if resp.SeriesID == nil || *resp.SeriesID == 0 {
		return
	}
	series, err := s.packageSeriesStore.GetByID(ctx, *resp.SeriesID)
	if err == nil && series != nil {
		resp.SeriesName = series.SeriesName
	}
}

// GetRealtimeStatus 获取资产实时状态（只读DB/Redis）
func (s *Service) GetRealtimeStatus(ctx context.Context, assetType string, id uint) (*dto.AssetRealtimeStatusResponse, error) {
	resp := &dto.AssetRealtimeStatusResponse{
		AssetType: assetType,
		AssetID:   id,
	}

	switch assetType {
	case "card":
		card, err := s.iotCardStore.GetByID(ctx, id)
		if err != nil {
			return nil, errors.Wrap(errors.CodeNotFound, err, "卡不存在")
		}
		resp.NetworkStatus = card.NetworkStatus
		resp.GatewayExtend = card.GatewayExtend
		resp.GatewayCardIMEI = card.GatewayCardIMEI
		resp.RealNameStatus = card.RealNameStatus
		resp.CurrentMonthUsageMB = card.CurrentMonthUsageMB
		resp.LastGatewayReadingMB = card.LastGatewayReadingMB
		resp.LastSyncTime = card.LastSyncTime
		resp.LastDataCheckAt = card.LastDataCheckAt
		resp.LastRealNameCheckAt = card.LastRealNameCheckAt
		resp.LastCardStatusCheckAt = card.LastCardStatusCheckAt

	case "device":
		// 查绑定卡状态列表
		bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, id)
		if err == nil && len(bindings) > 0 {
			cardIDs := make([]uint, 0, len(bindings))
			slotMap := make(map[uint]int, len(bindings))
			isCurrentMap := make(map[uint]bool, len(bindings))
			for _, b := range bindings {
				cardIDs = append(cardIDs, b.IotCardID)
				slotMap[b.IotCardID] = b.SlotPosition
				isCurrentMap[b.IotCardID] = b.IsCurrent
			}
			cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
			if err != nil {
				logger.GetAppLogger().Warn("查询设备绑定卡信息失败，结果可能不完整",
					zap.Uints("card_ids", cardIDs),
					zap.Error(err))
			}
			for _, c := range cards {
				resp.Cards = append(resp.Cards, dto.BoundCardInfo{
					CardID:                c.ID,
					ICCID:                 c.ICCID,
					MSISDN:                c.MSISDN,
					NetworkStatus:         c.NetworkStatus,
					RealNameStatus:        c.RealNameStatus,
					SlotPosition:          slotMap[c.ID],
					IsCurrent:             isCurrentMap[c.ID],
					CarrierID:             c.CarrierID,
					CarrierType:           c.CarrierType,
					CarrierName:           c.CarrierName,
					RealnamePolicy:        c.RealnamePolicy,
					GatewayExtend:         c.GatewayExtend,
					LastSyncTime:          c.LastSyncTime,
					LastDataCheckAt:       c.LastDataCheckAt,
					LastRealNameCheckAt:   c.LastRealNameCheckAt,
					LastCardStatusCheckAt: c.LastCardStatusCheckAt,
				})
			}
		}
		resp.DeviceProtectStatus = s.getDeviceProtectStatus(ctx, id)

		// 实时查询 Gateway 设备状态（不缓存，per D-05）
		// Gateway 失败不阻断主流程，DeviceRealtime 始终非 nil，失败时仅填 GatewayMsg（per D-06）
		resp.DeviceRealtime = s.fetchDeviceGatewayInfo(ctx, id)

	default:
		return nil, errors.New(errors.CodeInvalidParam, "不支持的资产类型，仅支持 card 或 device")
	}

	return resp, nil
}

// fetchDeviceGatewayInfo 查询设备 Gateway 实时状态，始终返回非 nil 对象
// Gateway 成功时填充全量字段，失败时仅填充 GatewayMsg 供前端展示错误原因
func (s *Service) fetchDeviceGatewayInfo(ctx context.Context, deviceID uint) *dto.DeviceGatewayInfo {
	if s.gatewayClient == nil {
		msg := "Gateway 客户端未配置"
		return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
	}

	device, err := s.deviceStore.GetByID(ctx, deviceID)
	if err != nil {
		msg := "查询设备信息失败: " + err.Error()
		return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
	}

	// IMEI 优先，无则用 SN（与 Refresh 中 updateDeviceFromSyncInfo 保持一致）
	cardNo := device.IMEI
	if cardNo == "" {
		cardNo = device.SN
	}
	if cardNo == "" {
		msg := "设备缺少 IMEI 和 SN，无法查询 Gateway 实时状态"
		return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
	}

	syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{
		CardNo: cardNo,
	})
	if syncErr != nil {
		msg := syncErr.Error()
		logger.GetAppLogger().Warn("fetchDeviceGatewayInfo: sync-info 调用失败",
			zap.Uint("device_id", deviceID),
			zap.Error(syncErr))
		return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
	}

	// 实时状态成功后异步回写 DB，保持 Resolve 接口的缓存与 Gateway 同步
	// 使用 context.Background() 避免请求结束后 ctx 取消导致写入失败
	go s.updateDeviceFromSyncInfo(context.Background(), deviceID, syncResp)

	return mapSyncRespToDeviceGatewayInfo(syncResp)
}

// mapSyncRespToDeviceGatewayInfo 将 Gateway SyncDeviceInfoResp 映射为 B 端 DeviceGatewayInfo DTO
// 使用指针字段，未赋值的字段保持 nil（omitempty）
func mapSyncRespToDeviceGatewayInfo(r *gateway.SyncDeviceInfoResp) *dto.DeviceGatewayInfo {
	info := &dto.DeviceGatewayInfo{
		OnlineStatus:    flexIntPtr(r.OnlineStatus),
		RunTime:         flexStrPtr(r.RunTime),
		ConnectTime:     flexStrPtr(r.ConnectTime),
		LastOnlineTime:  flexStrPtr(r.LastOnlineTime),
		LastUpdateTime:  flexStrPtr(r.LastUpdateTime),
		Rsrp:            flexIntPtr(r.Rsrp),
		Rsrq:            flexIntPtr(r.Rsrq),
		Rssi:            flexStrPtr(r.RSSI),
		Sinr:            flexIntPtr(r.Sinr),
		SSID:            flexStrPtr(r.SSID),
		WifiEnabled:     flexBoolPtr(r.WifiEnabled),
		WifiPassword:    flexStrPtr(r.WifiPassword),
		IPAddress:       flexStrPtr(r.IPAddress),
		WANIP:           flexStrPtr(r.WANIP),
		LANIP:           flexStrPtr(r.LANIP),
		MACAddress:      flexStrPtr(r.MacAddress),
		DailyUsage:      flexStrPtr(r.DailyUsage),
		DLStats:         flexStrPtr(r.DLStats),
		ULStats:         flexStrPtr(r.ULStats),
		LimitSpeed:      flexIntPtr(r.LimitSpeed),
		CurrentIccid:    flexStrPtr(r.CurrentIccid),
		MaxClients:      flexIntPtr(r.MaxClients),
		ClientNumber:    flexIntPtr(r.ClientNumber),
		SoftwareVersion: flexStrPtr(r.SoftwareVersion),
		SwitchMode:      flexStrPtr(r.SwitchMode),
		SyncInterval:    flexIntPtr(r.SyncInterval),
		DeviceID:        flexStrPtr(r.DeviceID),
		DeviceName:      flexStrPtr(r.DeviceName),
		DeviceType:      flexStrPtr(r.DeviceType),
		IMEI:            flexStrPtr(r.IMEI),
		IMSI:            flexStrPtr(r.IMSI),
		Status:          flexIntPtr(r.Status),
	}
	signalQuality, signalBadReason := buildSignalSummary(info.Rsrp, info.Rsrq, info.Rssi, info.Sinr)
	info.SignalQuality = strPtr(signalQuality)
	info.SignalBadReason = strPtr(signalBadReason)
	if r.BatteryLevel != nil {
		n := int(*r.BatteryLevel)
		info.BatteryLevel = &n
	}
	return info
}

// strPtr 将空字符串转为 nil，非空字符串返回指针
// 避免 JSON 响应中出现大量空字符串字段（omitempty 对空字符串无效）
func strPtr(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

// flexStrPtr 将 FlexString 转为字符串指针
func flexStrPtr(s gateway.FlexString) *string {
	return strPtr(string(s))
}

func flexBoolPtr(fb gateway.FlexBool) *bool {
	b := bool(fb)
	return &b
}

func flexIntPtr(fi gateway.FlexInt) *int {
	n := int(fi)
	return &n
}

// Refresh 刷新资产数据（调网关同步）
func (s *Service) Refresh(ctx context.Context, assetType string, id uint) (*dto.AssetRealtimeStatusResponse, error) {
	switch assetType {
	case "card":
		card, err := s.iotCardStore.GetByID(ctx, id)
		if err != nil {
			return nil, errors.Wrap(errors.CodeNotFound, err, "卡不存在")
		}
		if err := s.iotCardService.RefreshCardDataFromGateway(ctx, card.ICCID); err != nil {
			return nil, errors.Wrap(errors.CodeInternalError, err, "刷新卡数据失败")
		}
		return s.GetRealtimeStatus(ctx, "card", id)

	case "device":
		totalStart := time.Now()

		// 检查冷却期
		cooldownKey := constants.RedisDeviceRefreshCooldownKey(id)
		if s.redis.Exists(ctx, cooldownKey).Val() > 0 {
			return nil, errors.New(errors.CodeTooManyRequests, "刷新过于频繁，请30秒后再试")
		}

		// 查所有绑定卡，逐一刷新
		bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, id)
		if err != nil {
			return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定卡失败")
		}

		cardIDs := make([]uint, 0, len(bindings))
		for _, b := range bindings {
			cardIDs = append(cardIDs, b.IotCardID)
		}
		cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
		if err != nil {
			return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定卡详情失败")
		}

		cardMap := make(map[uint]*model.IotCard, len(cards))
		for _, card := range cards {
			cardMap[card.ID] = card
		}

		cardRefreshStart := time.Now()
		refreshedCardCount := 0
		for _, b := range bindings {
			card, ok := cardMap[b.IotCardID]
			if !ok {
				logger.GetAppLogger().Warn("刷新设备绑定卡失败：未找到卡信息",
					zap.Uint("device_id", id),
					zap.Uint("card_id", b.IotCardID),
				)
				continue
			}
			if refreshErr := s.iotCardService.RefreshCardDataFromGateway(ctx, card.ICCID); refreshErr != nil {
				logger.GetAppLogger().Warn("刷新设备绑定卡失败：网关调用失败",
					zap.Uint("device_id", id),
					zap.String("iccid", card.ICCID),
					zap.Error(refreshErr))
				continue
			}
			refreshedCardCount++
		}
		cardRefreshDuration := time.Since(cardRefreshStart)

		// 设置冷却 Key
		s.redis.Set(ctx, cooldownKey, 1, constants.DeviceRefreshCooldownDuration)

		realtimeStart := time.Now()
		result, realtimeErr := s.GetRealtimeStatus(ctx, "device", id)
		logger.GetAppLogger().Info("资产刷新完成",
			zap.Uint("device_id", id),
			zap.Int("binding_count", len(bindings)),
			zap.Int("refreshed_card_count", refreshedCardCount),
			zap.Duration("card_refresh_duration", cardRefreshDuration),
			zap.Duration("realtime_status_duration", time.Since(realtimeStart)),
			zap.Duration("total_duration", time.Since(totalStart)),
		)
		if realtimeErr != nil {
			return nil, realtimeErr
		}
		return result, nil

	default:
		return nil, errors.New(errors.CodeInvalidParam, "不支持的资产类型，仅支持 card 或 device")
	}
}

// updateDeviceFromSyncInfo 根据 Gateway sync-info 响应更新设备状态字段和当前卡标识
// 失败时只记录日志，不返回错误（非关键步骤）
func (s *Service) updateDeviceFromSyncInfo(ctx context.Context, deviceID uint, resp *gateway.SyncDeviceInfoResp) {
	now := time.Now()

	// Gateway 时间字段可能返回 RFC3339 或 Unix 时间戳，统一解析后再回写数据库
	lastOnlineTime := parseGatewayTime(resp.LastOnlineTime)

	// 更新设备 5 个存储字段
	updates := map[string]any{
		"online_status":        int(resp.OnlineStatus),
		"software_version":     string(resp.SoftwareVersion),
		"switch_mode":          string(resp.SwitchMode),
		"last_gateway_sync_at": now,
	}
	if lastOnlineTime != nil {
		updates["last_online_time"] = lastOnlineTime
	}

	if err := s.db.WithContext(ctx).
		Model(&model.Device{}).
		Where("id = ?", deviceID).
		Updates(updates).Error; err != nil {
		logger.GetAppLogger().Warn("sync-info 更新设备字段失败",
			zap.Uint("device_id", deviceID),
			zap.Error(err))
		return
	}

	// 更新 is_current 标识（事务原子操作，按 current_slot_number 逻辑槽位更新）
	if err := s.deviceSimBindingStore.UpdateIsCurrentByDeviceID(ctx, deviceID, int(resp.CurrentSlotNo)); err != nil {
		logger.GetAppLogger().Warn("sync-info 更新 is_current 失败",
			zap.Uint("device_id", deviceID),
			zap.Int("current_slot_number", int(resp.CurrentSlotNo)),
			zap.Error(err))
	}
}

// parseGatewayTime 兼容解析 Gateway 时间字段
// 支持 RFC3339 字符串、Unix 秒时间戳、Unix 毫秒时间戳。
func parseGatewayTime(raw gateway.FlexString) *time.Time {
	value := string(raw)
	if value == "" {
		return nil
	}

	if t, err := time.Parse(time.RFC3339, value); err == nil {
		return &t
	}

	// 兼容网关返回的纯数字时间戳（秒或毫秒）
	ts, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		return nil
	}

	var t time.Time
	if len(value) >= 13 {
		t = time.UnixMilli(ts)
	} else {
		t = time.Unix(ts, 0)
	}
	return &t
}

// GetPackages 获取资产的所有套餐列表（支持分页和状态筛选）
// callerAccountType: 调用方账号类型，"platform" 时返回成本价（paid_amount），其他类型不返回
// page 默认 1，pageSize 默认 50，pageSize 最大 100
func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int, status *int, callerAccountType string) (*dto.AssetPackagesResult, error) {
	// 分页参数边界处理
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 50
	}
	if pageSize > 100 {
		pageSize = 100
	}
	if status != nil && (*status < constants.PackageUsageStatusPending || *status > constants.PackageUsageStatusInvalidated) {
		return nil, errors.New(errors.CodeInvalidParam, "套餐状态非法")
	}

	// assetType 对应 Store 中的 carrierType：card→iot_card, device→device
	carrierType := assetType
	if assetType == "card" {
		carrierType = "iot_card"
	}

	usages, err := s.packageUsageStore.ListByCarrier(ctx, carrierType, id, status)
	if err != nil {
		return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐使用记录失败")
	}

	// 收集所有 PackageID 并批量查询
	pkgIDSet := make(map[uint]struct{}, len(usages))
	for _, u := range usages {
		pkgIDSet[u.PackageID] = struct{}{}
	}
	pkgIDs := make([]uint, 0, len(pkgIDSet))
	for id := range pkgIDSet {
		pkgIDs = append(pkgIDs, id)
	}
	packages, pkgErr := s.packageStore.GetByIDsUnscoped(ctx, pkgIDs)
	if pkgErr != nil {
		logger.GetAppLogger().Warn("批量查询套餐信息失败，套餐名称可能缺失",
			zap.Uints("package_ids", pkgIDs),
			zap.Error(pkgErr))
	}
	pkgMap := make(map[uint]*model.Package, len(packages))
	for _, p := range packages {
		pkgMap[p.ID] = p
	}

	all := make([]*dto.AssetPackageResponse, 0, len(usages))
	for _, u := range usages {
		pkg := pkgMap[u.PackageID]
		metrics := u.BuildTrafficMetrics()
		pkgName := u.PackageName
		pkgType := ""
		expiryBase := ""
		if pkg != nil {
			if pkgName == "" {
				pkgName = pkg.PackageName
			}
			pkgType = pkg.PackageType
			expiryBase = pkg.ExpiryBase
		}
		var paidAmount *int64
		if callerAccountType == constants.OwnerTypePlatform {
			paidAmount = u.PaidAmount
		}
		item := &dto.AssetPackageResponse{
			PackageUsageID:    u.ID,
			PackageID:         u.PackageID,
			PackageName:       pkgName,
			PackageType:       pkgType,
			ExpiryBase:        expiryBase,
			OrderID:           u.OrderID,
			OrderNo:           u.OrderNo,
			RefundID:          u.RefundID,
			RefundNo:          u.RefundNo,
			UsageType:         u.UsageType,
			Status:            u.Status,
			StatusName:        packageStatusName(u.Status),
			RealTotalMB:       metrics.RealTotalMB,
			RealUsedMB:        metrics.RealUsedMB,
			VirtualTotalMB:    metrics.VirtualTotalMB,
			VirtualUsedMB:     metrics.VirtualUsedMB,
			ReductionPct:      metrics.ReductionPct,
			EnableVirtualData: u.EnableVirtualDataSnapshot,
			ActivatedAt:       u.ActivatedAt,
			ExpiresAt:         u.ExpiresAt,
			MasterUsageID:     u.MasterUsageID,
			Priority:          u.Priority,
			PaidAmount:        paidAmount,
			RetailAmount:      u.RetailAmount,
			CreatedAt:         u.CreatedAt,
		}
		all = append(all, item)
	}

	// 按 created_at DESC 排序
	sort.Slice(all, func(i, j int) bool {
		return all[i].CreatedAt.After(all[j].CreatedAt)
	})

	total := int64(len(all))
	offset := (page - 1) * pageSize
	end := offset + pageSize
	if offset >= len(all) {
		offset = len(all)
	}
	if end > len(all) {
		end = len(all)
	}
	items := all[offset:end]

	return &dto.AssetPackagesResult{
		Total:    total,
		Page:     page,
		PageSize: pageSize,
		Items:    items,
	}, nil
}

// GetCurrentPackage 获取资产当前生效的主套餐
// callerAccountType: 调用方账号类型，"platform" 时返回成本价（paid_amount），其他类型不返回
func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id uint, callerAccountType string) (*dto.AssetPackageResponse, error) {
	carrierType := assetType
	if assetType == "card" {
		carrierType = "iot_card"
	}

	usage, err := s.packageUsageStore.GetActiveMainPackage(ctx, carrierType, id)
	if err != nil {
		// 记录不存在表示当前无生效套餐，属于正常业务状态，返回 nil 而非错误
		// 其他 DB 错误才作为服务端错误处理
		if stderrors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询当前生效套餐失败")
	}

	return s.buildAssetPackageResponse(ctx, usage, callerAccountType), nil
}

// UpdatePackageExpiresAt 修改资产套餐过期时间。
func (s *Service) UpdatePackageExpiresAt(ctx context.Context, assetType string, assetID uint, assetIdentifier string, packageUsageID uint, expiresAt time.Time) (*dto.AssetPackageResponse, error) {
	carrierType, err := normalizeAssetCarrierType(assetType)
	if err != nil {
		return nil, err
	}

	before, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
	if err != nil {
		return nil, wrapPackageUsageNotFound(err)
	}

	beforeData := packageUsageExpiresAtAuditData(before)
	rows, err := s.packageUsageStore.UpdateExpiresAtForCarrier(ctx, packageUsageID, carrierType, assetID, expiresAt)
	if err != nil {
		appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐过期时间失败")
		s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
			"package_usage_id": packageUsageID,
			"expires_at":       expiresAt,
		}, appErr)
		return nil, appErr
	}
	if rows == 0 {
		appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
		s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
			"package_usage_id": packageUsageID,
			"expires_at":       expiresAt,
		}, appErr)
		return nil, appErr
	}

	updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
	if err != nil {
		appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
		s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
			"package_usage_id": packageUsageID,
			"expires_at":       expiresAt,
		}, appErr)
		return nil, appErr
	}

	s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageExpiresAtAuditData(updated), nil)
	return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil
}

// UpdatePackageUsage 修改资产套餐已用量。
func (s *Service) UpdatePackageUsage(ctx context.Context, assetType string, assetID uint, assetIdentifier string, packageUsageID uint, dataUsageMB int64) (*dto.AssetPackageResponse, error) {
	if dataUsageMB < 0 {
		return nil, errors.New(errors.CodeInvalidParam)
	}

	carrierType, err := normalizeAssetCarrierType(assetType)
	if err != nil {
		return nil, err
	}

	before, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
	if err != nil {
		return nil, wrapPackageUsageNotFound(err)
	}

	beforeData := packageUsageTrafficAuditData(before)
	nextStatus := statusForManualDataUsage(before, dataUsageMB)
	rows, err := s.packageUsageStore.UpdateDataUsageForCarrier(ctx, packageUsageID, carrierType, assetID, dataUsageMB, nextStatus)
	if err != nil {
		appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐已用量失败")
		s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
			"package_usage_id": packageUsageID,
			"data_usage_mb":    dataUsageMB,
		}, appErr)
		return nil, appErr
	}
	if rows == 0 {
		appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
		s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
			"package_usage_id": packageUsageID,
			"data_usage_mb":    dataUsageMB,
		}, appErr)
		return nil, appErr
	}

	updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
	if err != nil {
		appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
		s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
			"package_usage_id": packageUsageID,
			"data_usage_mb":    dataUsageMB,
		}, appErr)
		return nil, appErr
	}

	s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageTrafficAuditData(updated), nil)
	return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil
}

// getDeviceProtectStatus 查询设备保护期状态
func (s *Service) getDeviceProtectStatus(ctx context.Context, deviceID uint) string {
	stopKey := constants.RedisDeviceProtectKey(deviceID, "stop")
	startKey := constants.RedisDeviceProtectKey(deviceID, "start")
	if s.redis.Exists(ctx, stopKey).Val() > 0 {
		return "stop"
	}
	if s.redis.Exists(ctx, startKey).Val() > 0 {
		return "start"
	}
	return "none"
}

// nonZeroTimePtr 将非零时间转为指针，零值返回 nil（避免序列化出历史时区偏移 +08:05）
func nonZeroTimePtr(t time.Time) *time.Time {
	if t.IsZero() {
		return nil
	}
	return &t
}

// GetOrders 查询资产历史订单，支持换货链跨代追溯
// page/pageSize 仅对本代订单生效；前代订单每代最多返回20条
// 最多追溯10代，超出后 truncated=true
func (s *Service) GetOrders(ctx context.Context, identifier string, page, pageSize int, includePrevious bool) (*dto.AssetOrdersResponse, error) {
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}

	asset, err := s.Resolve(ctx, identifier, false)
	if err != nil {
		return nil, err
	}

	currentGeneration := s.resolveAssetGeneration(ctx, asset.AssetType, asset.AssetID)

	orders, total, err := s.orderStore.ListByAssetIdentifier(ctx, identifier, page, pageSize)
	if err != nil {
		return nil, errors.Wrap(errors.CodeInternalError, err, "查询本代订单失败")
	}

	orderItems := s.fetchOrderItemsMap(ctx, orders)
	currentItems := make([]*dto.AssetOrderItem, 0, len(orders))
	for _, o := range orders {
		currentItems = append(currentItems, buildAssetOrderItem(o, orderItems[o.ID]))
	}

	resp := &dto.AssetOrdersResponse{
		CurrentGeneration: &dto.GenerationOrders{
			Generation: currentGeneration,
			Identifier: identifier,
			AssetType:  asset.AssetType,
			Total:      total,
			Page:       page,
			PageSize:   pageSize,
			Items:      currentItems,
		},
		PreviousGenerations: nil,
		Truncated:           false,
	}

	if !includePrevious {
		return resp, nil
	}

	prevGens, truncated := s.tracePreviousGenerations(ctx, asset.AssetType, asset.AssetID)
	resp.PreviousGenerations = prevGens
	resp.Truncated = truncated

	return resp, nil
}

func (s *Service) resolveAssetGeneration(ctx context.Context, assetType string, assetID uint) int {
	switch assetType {
	case "card":
		card, err := s.iotCardStore.GetByID(ctx, assetID)
		if err == nil && card != nil {
			return card.Generation
		}
	case "device":
		device, err := s.deviceStore.GetByID(ctx, assetID)
		if err == nil && device != nil {
			return device.Generation
		}
	}
	return 1
}

func (s *Service) buildAssetPackageResponse(ctx context.Context, usage *model.PackageUsage, callerAccountType string) *dto.AssetPackageResponse {
	if usage == nil {
		return nil
	}

	pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
	metrics := usage.BuildTrafficMetrics()
	pkgName := usage.PackageName
	pkgType := ""
	expiryBase := ""
	if pkgErr == nil && pkg != nil {
		if pkgName == "" {
			pkgName = pkg.PackageName
		}
		pkgType = pkg.PackageType
		expiryBase = pkg.ExpiryBase
	}
	var paidAmount *int64
	if callerAccountType == constants.OwnerTypePlatform {
		paidAmount = usage.PaidAmount
	}

	return &dto.AssetPackageResponse{
		PackageUsageID:    usage.ID,
		PackageID:         usage.PackageID,
		PackageName:       pkgName,
		PackageType:       pkgType,
		ExpiryBase:        expiryBase,
		OrderID:           usage.OrderID,
		OrderNo:           usage.OrderNo,
		RefundID:          usage.RefundID,
		RefundNo:          usage.RefundNo,
		UsageType:         usage.UsageType,
		Status:            usage.Status,
		StatusName:        packageStatusName(usage.Status),
		RealTotalMB:       metrics.RealTotalMB,
		RealUsedMB:        metrics.RealUsedMB,
		VirtualTotalMB:    metrics.VirtualTotalMB,
		VirtualUsedMB:     metrics.VirtualUsedMB,
		ReductionPct:      metrics.ReductionPct,
		EnableVirtualData: usage.EnableVirtualDataSnapshot,
		ActivatedAt:       usage.ActivatedAt,
		ExpiresAt:         usage.ExpiresAt,
		MasterUsageID:     usage.MasterUsageID,
		Priority:          usage.Priority,
		PaidAmount:        paidAmount,
		RetailAmount:      usage.RetailAmount,
		CreatedAt:         usage.CreatedAt,
	}
}

func normalizeAssetCarrierType(assetType string) (string, error) {
	switch assetType {
	case "card", constants.AssetTypeIotCard:
		return constants.AssetTypeIotCard, nil
	case constants.AssetTypeDevice:
		return constants.AssetTypeDevice, nil
	default:
		return "", errors.New(errors.CodeInvalidParam, "不支持的资产类型，仅支持 card 或 device")
	}
}

func wrapPackageUsageNotFound(err error) error {
	if stderrors.Is(err, gorm.ErrRecordNotFound) {
		return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
	}
	return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
}

func statusForManualDataUsage(usage *model.PackageUsage, dataUsageMB int64) *int {
	if usage == nil {
		return nil
	}

	threshold := usage.EffectiveThresholdMB()
	if threshold <= 0 {
		return nil
	}
	switch usage.Status {
	case constants.PackageUsageStatusActive:
		if dataUsageMB >= threshold {
			status := constants.PackageUsageStatusDepleted
			return &status
		}
	case constants.PackageUsageStatusDepleted:
		if dataUsageMB < threshold {
			status := constants.PackageUsageStatusActive
			return &status
		}
	}
	return nil
}

func packageUsageExpiresAtAuditData(usage *model.PackageUsage) map[string]any {
	if usage == nil {
		return nil
	}
	return map[string]any{
		"package_usage_id": usage.ID,
		"expires_at":       usage.ExpiresAt,
		"status":           usage.Status,
		"status_name":      packageStatusName(usage.Status),
	}
}

func packageUsageTrafficAuditData(usage *model.PackageUsage) map[string]any {
	if usage == nil {
		return nil
	}
	metrics := usage.BuildTrafficMetrics()
	return map[string]any{
		"package_usage_id":    usage.ID,
		"data_usage_mb":       usage.DataUsageMB,
		"virtual_used_mb":     metrics.VirtualUsedMB,
		"real_total_mb":       metrics.RealTotalMB,
		"virtual_total_mb":    metrics.VirtualTotalMB,
		"status":              usage.Status,
		"status_name":         packageStatusName(usage.Status),
		"enable_virtual_data": usage.EnableVirtualDataSnapshot,
	}
}

func (s *Service) logPackageAdjustmentAudit(ctx context.Context, operationType, operationDesc, resultStatus, assetType string, assetID uint, assetIdentifier string, beforeData, afterData map[string]any, err error) {
	if s == nil || s.assetAuditService == nil {
		return
	}
	errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
	wrappedBefore, wrappedAfter := assetAuditSvc.WrapOperationContent(beforeData, afterData, map[string]any{
		"asset_type":       assetAuditSvc.NormalizeAssetType(assetType),
		"asset_id":         assetID,
		"asset_identifier": assetIdentifier,
	})
	s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, assetAuditSvc.BuildLogParams{
		Operator:        assetAuditSvc.OperatorFromContext(ctx),
		AssetType:       assetType,
		AssetID:         assetID,
		AssetIdentifier: assetIdentifier,
		OperationType:   operationType,
		OperationDesc:   operationDesc,
		BeforeData:      wrappedBefore,
		AfterData:       wrappedAfter,
		ResultStatus:    resultStatus,
		ErrorCode:       errorCode,
		ErrorMsg:        errorMsg,
	}))
}

// tracePreviousGenerations 通过换货链逆向追溯前代资产的订单，最多追溯10代
func (s *Service) tracePreviousGenerations(ctx context.Context, assetType string, assetID uint) ([]*dto.PreviousGenerationOrders, bool) {
	const maxDepth = 10
	const prevPageSize = 20

	prevGens := make([]*dto.PreviousGenerationOrders, 0)
	currentType := assetType
	currentID := assetID
	truncated := false

	for i := 0; i < maxDepth; i++ {
		// 转换为 ExchangeOrder 中使用的资产类型（card→iot_card）
		storeType := currentType
		if storeType == "card" {
			storeType = "iot_card"
		}

		exchOrder, err := s.exchangeOrderStore.FindByNewAssetID(ctx, storeType, currentID)
		if err != nil || exchOrder == nil {
			break
		}

		oldOrders, oldTotal, err := s.orderStore.ListByAssetIdentifier(ctx, exchOrder.OldAssetIdentifier, 1, prevPageSize)
		if err != nil {
			logger.GetAppLogger().Warn("查询前代订单失败",
				zap.String("old_identifier", exchOrder.OldAssetIdentifier),
				zap.Error(err))
			oldOrders = nil
			oldTotal = 0
		}

		oldItems := s.fetchOrderItemsMap(ctx, oldOrders)
		orderItems := make([]*dto.AssetOrderItem, 0, len(oldOrders))
		for _, o := range oldOrders {
			orderItems = append(orderItems, buildAssetOrderItem(o, oldItems[o.ID]))
		}

		oldAssetType := exchOrder.OldAssetType
		if oldAssetType == "iot_card" {
			oldAssetType = "card"
		}

		oldGeneration := s.resolveAssetGeneration(ctx, oldAssetType, exchOrder.OldAssetID)
		exchangedAt := exchOrder.UpdatedAt
		if exchOrder.CompletedAt != nil && !exchOrder.CompletedAt.IsZero() {
			exchangedAt = *exchOrder.CompletedAt
		}

		prevGens = append(prevGens, &dto.PreviousGenerationOrders{
			Generation:  oldGeneration,
			Identifier:  exchOrder.OldAssetIdentifier,
			AssetType:   oldAssetType,
			ExchangeNo:  exchOrder.ExchangeNo,
			ExchangedAt: exchangedAt,
			Total:       oldTotal,
			Items:       orderItems,
		})

		currentType = oldAssetType
		currentID = exchOrder.OldAssetID

		if i == maxDepth-1 {
			truncated = true
		}
	}

	return prevGens, truncated
}

// fetchOrderItemsMap 批量查询订单明细并按 orderID 分组
func (s *Service) fetchOrderItemsMap(ctx context.Context, orders []*model.Order) map[uint][]*model.OrderItem {
	if len(orders) == 0 {
		return nil
	}
	orderIDs := make([]uint, 0, len(orders))
	for _, o := range orders {
		orderIDs = append(orderIDs, o.ID)
	}
	allItems, err := s.orderItemStore.ListByOrderIDs(ctx, orderIDs)
	if err != nil {
		logger.GetAppLogger().Warn("批量查询订单明细失败", zap.Error(err))
		return nil
	}
	result := make(map[uint][]*model.OrderItem, len(orders))
	for _, item := range allItems {
		result[item.OrderID] = append(result[item.OrderID], item)
	}
	return result
}

func buildAssetOrderItem(o *model.Order, items []*model.OrderItem) *dto.AssetOrderItem {
	details := make([]*dto.AssetOrderItemDetail, 0, len(items))
	for _, it := range items {
		details = append(details, &dto.AssetOrderItemDetail{
			PackageName: it.PackageName,
			Quantity:    it.Quantity,
			UnitPrice:   it.UnitPrice,
			Amount:      it.Amount,
		})
	}
	return &dto.AssetOrderItem{
		OrderNo:           o.OrderNo,
		OrderType:         o.OrderType,
		PaymentStatus:     o.PaymentStatus,
		PaymentStatusText: paymentStatusText(o.PaymentStatus),
		TotalAmount:       o.TotalAmount,
		PaymentMethod:     o.PaymentMethod,
		PaidAt:            o.PaidAt,
		Generation:        o.Generation,
		Items:             details,
		CreatedAt:         o.CreatedAt,
	}
}

func paymentStatusText(status int) string {
	switch status {
	case 1:
		return "待支付"
	case 2:
		return "已支付"
	case 3:
		return "已取消"
	case 4:
		return "已退款"
	default:
		return "未知"
	}
}

// GetEffectiveRealnamePolicy 获取资产的有效实名认证策略
// 设备卡使用设备策略，单独卡使用卡策略
func GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string {
	if device != nil {
		return device.RealnamePolicy
	}
	if card != nil {
		return card.RealnamePolicy
	}
	return constants.RealnamePolicyNone
}

// HasValidRechargeOrPaidOrder 检查资产在当前世代是否有有效充值或已支付订单
// 用于 after_order 策略的生效条件判断
func (s *Service) HasValidRechargeOrPaidOrder(ctx context.Context, assetType string, assetID uint) (bool, error) {
	generation := s.resolveAssetGeneration(ctx, assetType, assetID)

	if assetType == "card" {
		var rechargeCount int64
		err := s.db.WithContext(ctx).Model(&model.RechargeOrder{}).
			Where("iot_card_id = ? AND generation = ? AND status = ?", assetID, generation, model.RechargeOrderStatusPaid).
			Count(&rechargeCount).Error
		if err != nil {
			return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡充值订单失败")
		}
		if rechargeCount > 0 {
			return true, nil
		}

		var orderCount int64
		err = s.db.WithContext(ctx).Model(&model.Order{}).
			Where("iot_card_id = ? AND generation = ? AND payment_status = ?", assetID, generation, model.PaymentStatusPaid).
			Count(&orderCount).Error
		if err != nil {
			return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡订单失败")
		}
		if orderCount > 0 {
			return true, nil
		}
	} else if assetType == "device" {
		var rechargeCount int64
		err := s.db.WithContext(ctx).Model(&model.RechargeOrder{}).
			Where("device_id = ? AND generation = ? AND status = ?", assetID, generation, model.RechargeOrderStatusPaid).
			Count(&rechargeCount).Error
		if err != nil {
			return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备充值订单失败")
		}
		if rechargeCount > 0 {
			return true, nil
		}

		var orderCount int64
		err = s.db.WithContext(ctx).Model(&model.Order{}).
			Where("device_id = ? AND generation = ? AND payment_status = ?", assetID, generation, model.PaymentStatusPaid).
			Count(&orderCount).Error
		if err != nil {
			return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备订单失败")
		}
		if orderCount > 0 {
			return true, nil
		}
	}

	return false, nil
}

// packageStatusName 套餐状态码转中文名称
func packageStatusName(status int) string {
	switch status {
	case 0:
		return "待生效"
	case 1:
		return "生效中"
	case 2:
		return "已用完"
	case 3:
		return "已过期"
	case 4:
		return "已失效"
	default:
		return "未知"
	}
}
