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

import (
	"context"
	stderrors "errors"
	"sort"
	"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"
	"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
}

// 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
}

// 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,
) *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,
	}
}

// Resolve 通过任意标识符解析资产
// 主路径：查注册表（精确匹配 ICCID 或 VirtualNo）
// Fallback：原有跨表 OR 查询（处理 IMEI/SN/MSISDN 等非注册标识符）
func (s *Service) Resolve(ctx context.Context, identifier string) (*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
					}
					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
					}
					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
		}
		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
		}
		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.GetIotCardStatusName(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,
	}

	// 查绑定卡
	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,
			})
		}

		// 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)

	// 查 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,
		NetworkStatus:    card.NetworkStatus,
		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,
		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)

	// 查 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
	}

	pkg, err := s.packageStore.GetByID(ctx, usage.PackageID)
	if err != nil || pkg == nil {
		return
	}

	resp.CurrentPackage = pkg.PackageName
	ratio := safeVirtualRatio(pkg.VirtualRatio)
	resp.PackageTotalMB = int64(float64(usage.DataLimitMB) / ratio)
	resp.PackageUsedMB = float64(usage.DataUsageMB) / ratio
	resp.PackageRemainMB = float64(usage.DataLimitMB-usage.DataUsageMB) / ratio
}

// 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.RealNameStatus = card.RealNameStatus
		resp.CurrentMonthUsageMB = card.CurrentMonthUsageMB
		resp.LastSyncTime = card.LastSyncTime

	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,
				})
			}
		}
		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:         strPtr(r.RunTime),
		ConnectTime:     strPtr(r.ConnectTime),
		LastOnlineTime:  strPtr(r.LastOnlineTime),
		LastUpdateTime:  strPtr(r.LastUpdateTime),
		Rsrp:            flexIntPtr(r.Rsrp),
		Rsrq:            flexIntPtr(r.Rsrq),
		Rssi:            strPtr(r.RSSI),
		Sinr:            flexIntPtr(r.Sinr),
		SSID:            strPtr(r.SSID),
		WifiEnabled:     flexBoolPtr(r.WifiEnabled),
		WifiPassword:    strPtr(r.WifiPassword),
		IPAddress:       strPtr(r.IPAddress),
		WANIP:           strPtr(r.WANIP),
		LANIP:           strPtr(r.LANIP),
		MACAddress:      strPtr(r.MacAddress),
		DailyUsage:      strPtr(r.DailyUsage),
		DLStats:         strPtr(r.DLStats),
		ULStats:         strPtr(r.ULStats),
		LimitSpeed:      flexIntPtr(r.LimitSpeed),
		CurrentIccid:    strPtr(r.CurrentIccid),
		MaxClients:      flexIntPtr(r.MaxClients),
		ClientNumber:    flexIntPtr(r.ClientNumber),
		SoftwareVersion: strPtr(r.SoftwareVersion),
		SwitchMode:      strPtr(r.SwitchMode),
		SyncInterval:    flexIntPtr(r.SyncInterval),
		DeviceID:        strPtr(r.DeviceID),
		DeviceName:      strPtr(r.DeviceName),
		DeviceType:      strPtr(r.DeviceType),
		IMEI:            strPtr(r.IMEI),
		IMSI:            strPtr(r.IMSI),
		Status:          flexIntPtr(r.Status),
	}
	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
}

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":
		// 检查冷却期
		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, "查询绑定卡失败")
		}
		for _, b := range bindings {
			card, cardErr := s.iotCardStore.GetByID(ctx, b.IotCardID)
			if cardErr != nil {
				logger.GetAppLogger().Warn("刷新设备绑定卡失败：查卡失败",
					zap.Uint("device_id", id),
					zap.Uint("card_id", b.IotCardID),
					zap.Error(cardErr))
				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))
			}
		}

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

		// 刷新设备自身信息（在线状态、当前卡、固件版本等），失败不阻断刷新流程（per D-15）
		if s.gatewayClient != nil {
			device, devErr := s.deviceStore.GetByID(ctx, id)
			if devErr == nil {
				// IMEI 优先，无则用 SN（per D-3 注释说明）
				cardNo := device.IMEI
				if cardNo == "" {
					cardNo = device.SN
				}
				if cardNo != "" {
					if syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{
						CardNo: cardNo,
					}); syncErr == nil {
						s.updateDeviceFromSyncInfo(ctx, id, syncResp)
					} else {
						logger.GetAppLogger().Warn("sync-info 调用失败",
							zap.Uint("device_id", id),
							zap.Error(syncErr))
					}
				}
			}
		}

		return s.GetRealtimeStatus(ctx, "device", id)

	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()

	// 解析 LastOnlineTime（ISO 8601 字符串 → *time.Time）
	var lastOnlineTime *time.Time
	if resp.LastOnlineTime != "" {
		if t, err := time.Parse(time.RFC3339, resp.LastOnlineTime); err == nil {
			lastOnlineTime = &t
		}
	}

	// 更新设备 5 个存储字段
	updates := map[string]any{
		"online_status":        int(resp.OnlineStatus),
		"software_version":     resp.SoftwareVersion,
		"switch_mode":          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 标识（事务原子操作，per D-14 决策）
	if err := s.deviceSimBindingStore.UpdateIsCurrentByDeviceID(ctx, deviceID, resp.CurrentIccid); err != nil {
		logger.GetAppLogger().Warn("sync-info 更新 is_current 失败",
			zap.Uint("device_id", deviceID),
			zap.Error(err))
	}
}

// GetPackages 获取资产的所有套餐列表（支持分页）
// page 默认 1，pageSize 默认 50，pageSize 最大 100
func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int) (*dto.AssetPackagesResult, error) {
	// 分页参数边界处理
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 50
	}
	if pageSize > 100 {
		pageSize = 100
	}

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

	usages, err := s.packageUsageStore.ListByCarrier(ctx, carrierType, id)
	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]
		ratio := 1.0
		pkgName := ""
		pkgType := ""
		enableVirtualData := false
		if pkg != nil {
			ratio = safeVirtualRatio(pkg.VirtualRatio)
			pkgName = pkg.PackageName
			pkgType = pkg.PackageType
			enableVirtualData = pkg.EnableVirtualData
		}

		item := &dto.AssetPackageResponse{
			PackageUsageID:    u.ID,
			PackageID:         u.PackageID,
			PackageName:       pkgName,
			PackageType:       pkgType,
			UsageType:         u.UsageType,
			Status:            u.Status,
			StatusName:        packageStatusName(u.Status),
			DataLimitMB:       u.DataLimitMB,
			VirtualLimitMB:    int64(float64(u.DataLimitMB) / ratio),
			DataUsageMB:       u.DataUsageMB,
			VirtualUsedMB:     float64(u.DataUsageMB) / ratio,
			VirtualRemainMB:   float64(u.DataLimitMB-u.DataUsageMB) / ratio,
			VirtualRatio:      ratio,
			EnableVirtualData: enableVirtualData,
			ActivatedAt:       nonZeroTimePtr(u.ActivatedAt),
			ExpiresAt:         nonZeroTimePtr(u.ExpiresAt),
			MasterUsageID:     u.MasterUsageID,
			Priority:          u.Priority,
			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 获取资产当前生效的主套餐
func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id uint) (*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, "查询当前生效套餐失败")
	}

	pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
	ratio := 1.0
	pkgName := ""
	pkgType := ""
	enableVirtualData := false
	if pkgErr == nil && pkg != nil {
		ratio = safeVirtualRatio(pkg.VirtualRatio)
		pkgName = pkg.PackageName
		pkgType = pkg.PackageType
		enableVirtualData = pkg.EnableVirtualData
	}

	return &dto.AssetPackageResponse{
		PackageUsageID:    usage.ID,
		PackageID:         usage.PackageID,
		PackageName:       pkgName,
		PackageType:       pkgType,
		UsageType:         usage.UsageType,
		Status:            usage.Status,
		StatusName:        packageStatusName(usage.Status),
		DataLimitMB:       usage.DataLimitMB,
		VirtualLimitMB:    int64(float64(usage.DataLimitMB) / ratio),
		DataUsageMB:       usage.DataUsageMB,
		VirtualUsedMB:     float64(usage.DataUsageMB) / ratio,
		VirtualRemainMB:   float64(usage.DataLimitMB-usage.DataUsageMB) / ratio,
		VirtualRatio:      ratio,
		EnableVirtualData: enableVirtualData,
		ActivatedAt:       nonZeroTimePtr(usage.ActivatedAt),
		ExpiresAt:         nonZeroTimePtr(usage.ExpiresAt),
		MasterUsageID:     usage.MasterUsageID,
		Priority:          usage.Priority,
		CreatedAt:         usage.CreatedAt,
	}, 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
}

// safeVirtualRatio 安全获取虚流量比例，避免除零
func safeVirtualRatio(ratio float64) float64 {
	if ratio <= 0 {
		return 1.0
	}
	return ratio
}

// 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)
	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
}

// 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)

		prevGens = append(prevGens, &dto.PreviousGenerationOrders{
			Generation:  oldGeneration,
			Identifier:  exchOrder.OldAssetIdentifier,
			AssetType:   oldAssetType,
			ExchangeNo:  exchOrder.ExchangeNo,
			ExchangedAt: exchOrder.UpdatedAt,
			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 "未知"
	}
}

// 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 "未知"
	}
}
