package app

import (
	"context"
	"sort"
	"strconv"
	"strings"
	"time"

	cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
	"github.com/break/junhong_cmp_fiber/internal/middleware"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/model/dto"
	asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
	customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
	packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
	"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
	rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
	"github.com/break/junhong_cmp_fiber/internal/store"
	"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"
	pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
	"github.com/break/junhong_cmp_fiber/pkg/response"
	"github.com/gofiber/fiber/v2"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// ClientAssetHandler C 端资产信息处理器
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
type ClientAssetHandler struct {
	assetService               *asset.Service
	customerBinding            *customerBinding.Service
	assetWalletStore           *postgres.AssetWalletStore
	packageStore               *postgres.PackageStore
	shopPackageAllocationStore *postgres.ShopPackageAllocationStore
	iotCardStore               *postgres.IotCardStore
	deviceStore                *postgres.DeviceStore
	db                         *gorm.DB
	logger                     *zap.Logger
	observationSeries          cardObservationApp.BestEffortSeriesDispatcher
	paymentMethodPolicy        ClientPaymentMethodPolicy
	forceRechargeChecker       ClientForceRechargeChecker
}

// ClientPaymentMethodPolicy 提供 C 端支付方式读取和校验能力。
type ClientPaymentMethodPolicy interface {
	AllowedMethods(ctx context.Context, assetType string) ([]string, error)
	AllowedRechargeMethods(ctx context.Context, assetType string) ([]string, error)
	EnsureAllowed(ctx context.Context, assetType, paymentMethod string) error
	EnsureRechargeAllowed(ctx context.Context, assetType, paymentMethod string) error
}

// ClientForceRechargeChecker 提供 C 端资产当前强充资格的统一判定。
type ClientForceRechargeChecker interface {
	GetRechargeCheck(ctx context.Context, resourceType string, resourceID uint) (*rechargeSvc.ForceRechargeRequirement, error)
}

// SetObservationSeriesDispatcher 注入读取型后台观测序列端口。
func (h *ClientAssetHandler) SetObservationSeriesDispatcher(dispatcher cardObservationApp.BestEffortSeriesDispatcher) {
	h.observationSeries = dispatcher
}

// SetPaymentMethodPolicy 注入 C 端支付方式策略。
func (h *ClientAssetHandler) SetPaymentMethodPolicy(policy ClientPaymentMethodPolicy) {
	h.paymentMethodPolicy = policy
}

// SetForceRechargeChecker 注入复用现有一次性佣金状态语义的强充判定服务。
func (h *ClientAssetHandler) SetForceRechargeChecker(checker ClientForceRechargeChecker) {
	h.forceRechargeChecker = checker
}

// NewClientAssetHandler 创建 C 端资产信息处理器
func NewClientAssetHandler(
	assetService *asset.Service,
	binding *customerBinding.Service,
	assetWalletStore *postgres.AssetWalletStore,
	packageStore *postgres.PackageStore,
	shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
	iotCardStore *postgres.IotCardStore,
	deviceStore *postgres.DeviceStore,
	db *gorm.DB,
	logger *zap.Logger,
) *ClientAssetHandler {
	return &ClientAssetHandler{
		assetService:               assetService,
		customerBinding:            binding,
		assetWalletStore:           assetWalletStore,
		packageStore:               packageStore,
		shopPackageAllocationStore: shopPackageAllocationStore,
		iotCardStore:               iotCardStore,
		deviceStore:                deviceStore,
		db:                         db,
		logger:                     logger,
	}
}

type resolvedAssetContext struct {
	CustomerID          uint
	Identifier          string
	Asset               *dto.AssetResolveResponse
	Generation          int
	WalletBalance       int64
	SkipPermissionCtx   context.Context
	IsAgentChannel      bool
	SellerShopID        uint
	HasAddonMainPackage bool
}

// resolveAssetFromIdentifier 统一执行资产解析与归属校验
// 处理流程：客户鉴权 -> 标识符解析 -> 资产解析 -> 归属校验 -> 世代与钱包信息补齐
func (h *ClientAssetHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedAssetContext, error) {
	customerID, ok := middleware.GetCustomerID(c)
	if !ok || customerID == 0 {
		return nil, errors.New(errors.CodeUnauthorized)
	}

	identifier = strings.TrimSpace(identifier)
	if identifier == "" {
		identifier = strings.TrimSpace(c.Query("identifier"))
	}
	if identifier == "" {
		var req dto.AssetRefreshRequest
		if err := c.BodyParser(&req); err == nil {
			identifier = strings.TrimSpace(req.Identifier)
		}
	}
	if identifier == "" {
		return nil, errors.New(errors.CodeInvalidParam)
	}

	skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{})
	assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier, false)
	if err != nil {
		return nil, err
	}

	owned, ownErr := h.customerBinding.OwnsAsset(skipPermissionCtx, customerID, assetInfo.AssetType, assetInfo.AssetID)
	if ownErr != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
	}
	if !owned {
		return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
	}

	generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
	if genErr != nil {
		return nil, genErr
	}

	walletBalance, walletErr := h.getAssetWalletBalance(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
	if walletErr != nil {
		return nil, walletErr
	}

	ctxInfo := &resolvedAssetContext{
		CustomerID:        customerID,
		Identifier:        identifier,
		Asset:             assetInfo,
		Generation:        generation,
		WalletBalance:     walletBalance,
		SkipPermissionCtx: skipPermissionCtx,
	}

	if assetInfo.ShopID != nil && *assetInfo.ShopID > 0 {
		ctxInfo.IsAgentChannel = true
		ctxInfo.SellerShopID = *assetInfo.ShopID
	}

	return ctxInfo, nil
}

// GetAssetInfo B1 资产信息
// GET /api/c/v1/asset/info
func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
	var req dto.AssetInfoRequest
	if err := c.QueryParser(&req); err != nil {
		return errors.New(errors.CodeInvalidParam)
	}

	resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
	if err != nil {
		return err
	}
	currentPackageID, err := h.getCurrentPackageID(resolved.SkipPermissionCtx, resolved.Asset.CurrentPackageUsageID)
	if err != nil {
		return err
	}
	if h.paymentMethodPolicy == nil {
		return errors.New(errors.CodeNoPaymentConfig)
	}
	if h.forceRechargeChecker == nil {
		return errors.New(errors.CodeNoPaymentConfig)
	}
	resourceType := resolved.Asset.AssetType
	if resourceType == "card" {
		resourceType = constants.ResourceTypeIotCard
	}
	forceRecharge, err := h.forceRechargeChecker.GetRechargeCheck(resolved.SkipPermissionCtx, resourceType, resolved.Asset.AssetID)
	if err != nil {
		return err
	}
	allowedPaymentMethods, err := h.paymentMethodPolicy.AllowedMethods(resolved.SkipPermissionCtx, resolved.Asset.AssetType)
	if err != nil {
		return err
	}
	if forceRecharge.NeedForceRecharge {
		allowedPaymentMethods, err = h.paymentMethodPolicy.AllowedRechargeMethods(resolved.SkipPermissionCtx, resolved.Asset.AssetType)
		if err != nil {
			return err
		}
	}

	phone, _ := middleware.GetCustomerPhone(c)

	resp := &dto.AssetInfoResponse{
		PackageExpiryEstimate:     resolved.Asset.PackageExpiryEstimate,
		BoundPhone:                phone,
		AssetType:                 resolved.Asset.AssetType,
		AssetID:                   resolved.Asset.AssetID,
		Identifier:                resolved.Identifier,
		VirtualNo:                 resolved.Asset.VirtualNo,
		Status:                    resolved.Asset.Status,
		StatusName:                resolved.Asset.StatusName,
		RealNameStatus:            resolved.Asset.RealNameStatus,
		RealNameStatusName:        constants.GetRealNameStatusName(resolved.Asset.RealNameStatus),
		RealnamePolicy:            resolved.Asset.RealnamePolicy,
		EffectiveRealnamePolicy:   resolved.Asset.RealnamePolicy,
		RealnameRequired:          resolved.Asset.RealnamePolicy != constants.RealnamePolicyNone,
		CarrierName:               resolved.Asset.CarrierName,
		Generation:                strconv.Itoa(resolved.Generation),
		WalletBalance:             resolved.WalletBalance,
		AllowedPaymentMethods:     allowedPaymentMethods,
		ActivatedAt:               resolved.Asset.ActivatedAt,
		CurrentPackage:            resolved.Asset.CurrentPackage,
		CurrentPackageID:          currentPackageID,
		CurrentPackageUsageID:     resolved.Asset.CurrentPackageUsageID,
		CurrentPackageActivatedAt: resolved.Asset.CurrentPackageActivatedAt,
		CurrentPackageExpiresAt:   resolved.Asset.CurrentPackageExpiresAt,
		RealTotalMB:               resolved.Asset.RealTotalMB,
		RealUsedMB:                resolved.Asset.RealUsedMB,
		VirtualTotalMB:            resolved.Asset.VirtualTotalMB,
		VirtualUsedMB:             resolved.Asset.VirtualUsedMB,
		ReductionPct:              resolved.Asset.ReductionPct,
		EnableVirtualData:         resolved.Asset.EnableVirtualData,
		DeviceName:                resolved.Asset.DeviceName,
		IMEI:                      resolved.Asset.IMEI,
		SN:                        resolved.Asset.SN,
		DeviceModel:               resolved.Asset.DeviceModel,
		DeviceType:                resolved.Asset.DeviceType,
		Manufacturer:              resolved.Asset.Manufacturer,
		MaxSimSlots:               resolved.Asset.MaxSimSlots,
		BoundCardCount:            resolved.Asset.BoundCardCount,
		Cards:                     resolved.Asset.Cards,
		DeviceProtectStatus:       resolved.Asset.DeviceProtectStatus,
		ICCID:                     resolved.Asset.ICCID,
		MSISDN:                    resolved.Asset.MSISDN,
		CarrierID:                 resolved.Asset.CarrierID,
		CarrierType:               resolved.Asset.CarrierType,
		NetworkStatus:             resolved.Asset.NetworkStatus,
		NetworkStatusName:         constants.GetNetworkStatusName(resolved.Asset.NetworkStatus),
		GatewayExtend:             resolved.Asset.GatewayExtend,
		ActivationStatus:          resolved.Asset.ActivationStatus,
		ActivationStatusName:      constants.GetActivationStatusName(resolved.Asset.ActivationStatus),
		CardCategory:              resolved.Asset.CardCategory,
		BoundDeviceID:             resolved.Asset.BoundDeviceID,
		BoundDeviceNo:             resolved.Asset.BoundDeviceNo,
		BoundDeviceName:           resolved.Asset.BoundDeviceName,
	}

	// 调用服务层获取设备实时 Gateway 数据（per D-11）
	// Gateway 失败时 DeviceRealtime 仅含 gateway_msg，不阻断主流程（per D-06）
	if resp.AssetType == "device" {
		realtimeResp, realtimeErr := h.assetService.GetRealtimeStatus(
			c.UserContext(), "device", resp.AssetID,
		)
		if realtimeErr == nil && realtimeResp.DeviceRealtime != nil {
			resp.DeviceRealtime = mapDeviceGatewayInfoToClientInfo(realtimeResp.DeviceRealtime)
		}
	}
	h.dispatchAssetReadObservations(c.UserContext(), resolved.Asset)

	return response.Success(c, resp)
}

func (h *ClientAssetHandler) getCurrentPackageID(ctx context.Context, usageID *uint) (uint, error) {
	if usageID == nil || *usageID == 0 {
		return 0, nil
	}
	var usage model.PackageUsage
	if err := h.db.WithContext(ctx).Select("package_id").First(&usage, *usageID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return 0, nil
		}
		return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询当前套餐引用失败")
	}
	return usage.PackageID, nil
}

func (h *ClientAssetHandler) dispatchAssetReadObservations(ctx context.Context, asset *dto.AssetResolveResponse) {
	if h.observationSeries == nil || asset == nil {
		return
	}
	cardIDs := make([]uint, 0, len(asset.Cards)+1)
	if asset.AssetType == "card" {
		cardIDs = append(cardIDs, asset.AssetID)
	} else {
		for _, card := range asset.Cards {
			cardIDs = append(cardIDs, card.CardID)
		}
	}
	dispatchCardReadSeries(ctx, h.observationSeries, constants.CardObservationSceneClientAssetRead, cardIDs)
}

func dispatchCardReadSeries(ctx context.Context, dispatcher cardObservationApp.BestEffortSeriesDispatcher, scene string, cardIDs []uint) {
	requestID := ""
	if value := pkgMiddleware.GetRequestIDFromContext(ctx); value != nil {
		requestID = *value
	}
	for _, cardID := range cardIDs {
		resourceID := strconv.FormatUint(uint64(cardID), 10)
		for _, syncType := range []string{
			constants.CardObservationSyncTypeRealname,
			constants.CardObservationSyncTypeTraffic,
			constants.CardObservationSyncTypeNetwork,
		} {
			dispatcher.Dispatch(ctx, cardObservationApp.SeriesRequest{
				Scene: scene, ResourceType: constants.CardObservationResourceTypeCard,
				ResourceID: resourceID, SyncType: syncType, Source: constants.CardObservationSourceBusinessEvent,
				RequestID: requestID, CorrelationID: requestID,
			})
		}
	}
}

// GetAvailablePackages B2 资产可购套餐列表
// GET /api/c/v1/asset/packages
func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error {
	var req dto.AssetPackageListRequest
	if err := c.QueryParser(&req); err != nil {
		return errors.New(errors.CodeInvalidParam)
	}

	resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
	if err != nil {
		return err
	}

	if resolved.Asset.SeriesID == nil || *resolved.Asset.SeriesID == 0 {
		return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
	}

	if _, err := packagepkg.FindAttachableMainPackageForAddon(
		h.db.WithContext(resolved.SkipPermissionCtx),
		resolved.Asset.AssetType,
		resolved.Asset.AssetID,
		time.Now(),
	); err == nil {
		resolved.HasAddonMainPackage = true
	} else if err != gorm.ErrRecordNotFound {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
	}

	listCtx := resolved.SkipPermissionCtx
	if resolved.IsAgentChannel {
		listCtx = context.WithValue(listCtx, constants.ContextKeyUserType, constants.UserTypeAgent)
		listCtx = context.WithValue(listCtx, constants.ContextKeyShopID, resolved.SellerShopID)
	}

	pkgs, _, err := h.packageStore.List(listCtx, &store.QueryOptions{
		Page:     1,
		PageSize: constants.MaxPageSize,
		OrderBy:  "id DESC",
	}, map[string]any{
		"series_id":    *resolved.Asset.SeriesID,
		"status":       constants.StatusEnabled,
		"shelf_status": constants.ShelfStatusOn,
	})
	if err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询可购套餐失败")
	}

	allocationMap := make(map[uint]*model.ShopPackageAllocation)
	if resolved.IsAgentChannel {
		packageIDs := collectPackageIDs(pkgs)
		allocations, allocErr := h.shopPackageAllocationStore.GetByShopAndPackages(
			resolved.SkipPermissionCtx,
			resolved.SellerShopID,
			packageIDs,
		)
		if allocErr != nil {
			return errors.Wrap(errors.CodeDatabaseError, allocErr, "查询套餐分配记录失败")
		}
		for _, allocation := range allocations {
			allocationMap[allocation.PackageID] = allocation
		}
	}

	items := make([]dto.ClientPackageItem, 0, len(pkgs))
	for _, pkg := range pkgs {
		item, ok := buildClientPackageItem(pkg, resolved, allocationMap)
		if !ok {
			continue
		}
		items = append(items, item)
	}

	if len(items) == 0 {
		return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
	}

	sort.Slice(items, func(i, j int) bool {
		return items[i].RetailPrice < items[j].RetailPrice
	})

	return response.Success(c, &dto.AssetPackageListResponse{Packages: items})
}

// GetPackageHistory B3 资产套餐历史
// GET /api/c/v1/asset/package-history
func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
	var req dto.AssetPackageHistoryRequest
	if err := c.QueryParser(&req); err != nil {
		return errors.New(errors.CodeInvalidParam)
	}

	if req.Page < 1 {
		req.Page = 1
	}
	if req.PageSize < 1 {
		req.PageSize = constants.DefaultPageSize
	}
	if req.PageSize > constants.MaxPageSize {
		req.PageSize = constants.MaxPageSize
	}

	resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
	if err != nil {
		return err
	}

	query := h.db.WithContext(resolved.SkipPermissionCtx).Model(&model.PackageUsage{}).
		Where("generation = ?", resolved.Generation)
	if resolved.Asset.AssetType == "card" {
		query = query.Where("iot_card_id = ?", resolved.Asset.AssetID)
	} else {
		query = query.Where("device_id = ?", resolved.Asset.AssetID)
	}
	if req.Status != nil {
		query = query.Where("status = ?", *req.Status)
	}
	if req.PackageType != nil {
		query = query.Where("package_id IN (?)",
			h.db.Model(&model.Package{}).Select("id").Where("package_type = ?", *req.PackageType))
	}

	var total int64
	if err := query.Count(&total).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史总数失败")
	}

	var usages []*model.PackageUsage
	offset := (req.Page - 1) * req.PageSize
	if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&usages).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史失败")
	}

	packageMap, err := h.loadPackageMap(resolved.SkipPermissionCtx, usages)
	if err != nil {
		return err
	}

	list := make([]dto.AssetPackageResponse, 0, len(usages))
	for _, usage := range usages {
		pkg := packageMap[usage.PackageID]
		metrics := usage.BuildTrafficMetrics()
		pkgName := usage.PackageName
		pkgType := ""
		if pkg != nil {
			if pkgName == "" {
				pkgName = pkg.PackageName
			}
			pkgType = pkg.PackageType
		}

		list = append(list, dto.AssetPackageResponse{
			PackageUsageID:    usage.ID,
			PackageID:         usage.PackageID,
			PackageName:       pkgName,
			PackageType:       pkgType,
			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,
			CreatedAt:         usage.CreatedAt,
		})
	}

	return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
}

// RefreshAsset B4 资产刷新
// POST /api/c/v1/asset/refresh
func (h *ClientAssetHandler) RefreshAsset(c *fiber.Ctx) error {
	var req dto.AssetRefreshRequest
	if err := c.BodyParser(&req); err != nil {
		return errors.New(errors.CodeInvalidParam)
	}

	resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
	if err != nil {
		return err
	}

	if _, err := h.assetService.Refresh(
		resolved.SkipPermissionCtx,
		resolved.Asset.AssetType,
		resolved.Asset.AssetID,
	); err != nil {
		return err
	}

	resp := &dto.AssetRefreshResponse{
		RefreshType:     resolved.Asset.AssetType,
		Accepted:        true,
		CooldownSeconds: 0,
	}
	if resolved.Asset.AssetType == constants.ResourceTypeDevice {
		resp.CooldownSeconds = int(constants.DeviceRefreshCooldownDuration / time.Second)
	}

	return response.Success(c, resp)
}

func (h *ClientAssetHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
	switch assetType {
	case "card":
		card, err := h.iotCardStore.GetByID(ctx, assetID)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return 0, errors.New(errors.CodeAssetNotFound)
			}
			return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
		}
		return card.Generation, nil
	case "device":
		device, err := h.deviceStore.GetByID(ctx, assetID)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return 0, errors.New(errors.CodeAssetNotFound)
			}
			return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
		}
		return device.Generation, nil
	default:
		return 0, errors.New(errors.CodeInvalidParam)
	}
}

func (h *ClientAssetHandler) getAssetWalletBalance(ctx context.Context, assetType string, assetID uint) (int64, error) {
	resourceType := constants.AssetWalletResourceTypeIotCard
	if assetType == constants.ResourceTypeDevice {
		resourceType = constants.AssetWalletResourceTypeDevice
	}

	wallet, err := h.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, assetID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return 0, nil
		}
		return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
	}

	return wallet.Balance, nil
}

func (h *ClientAssetHandler) loadPackageMap(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, error) {
	ids := make([]uint, 0, len(usages))
	seen := make(map[uint]struct{}, len(usages))
	for _, usage := range usages {
		if usage == nil {
			continue
		}
		if _, ok := seen[usage.PackageID]; ok {
			continue
		}
		seen[usage.PackageID] = struct{}{}
		ids = append(ids, usage.PackageID)
	}

	packages, err := h.packageStore.GetByIDsUnscoped(ctx, ids)
	if err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
	}

	result := make(map[uint]*model.Package, len(packages))
	for _, pkg := range packages {
		result[pkg.ID] = pkg
	}

	return result, nil
}

func collectPackageIDs(pkgs []*model.Package) []uint {
	ids := make([]uint, 0, len(pkgs))
	for _, pkg := range pkgs {
		if pkg == nil {
			continue
		}
		ids = append(ids, pkg.ID)
	}
	return ids
}

func buildClientPackageItem(
	pkg *model.Package,
	resolved *resolvedAssetContext,
	allocationMap map[uint]*model.ShopPackageAllocation,
) (dto.ClientPackageItem, bool) {
	if pkg == nil || pkg.Status != constants.StatusEnabled {
		return dto.ClientPackageItem{}, false
	}
	if pkg.IsGift {
		return dto.ClientPackageItem{}, false
	}

	isAddon := pkg.PackageType == constants.PackageTypeAddon
	if isAddon && !resolved.HasAddonMainPackage {
		return dto.ClientPackageItem{}, false
	}

	retailPrice := packageprice.PackageEffectiveRetailPrice(pkg)
	costPrice := pkg.CostPrice

	if resolved.IsAgentChannel {
		allocation, ok := allocationMap[pkg.ID]
		if !ok || allocation == nil {
			return dto.ClientPackageItem{}, false
		}
		if allocation.ShelfStatus != constants.ShelfStatusOn || allocation.Status != constants.StatusEnabled {
			return dto.ClientPackageItem{}, false
		}
		retailPrice = packageprice.AllocationEffectiveRetailPrice(allocation)
		costPrice = allocation.CostPrice
	} else if pkg.ShelfStatus != constants.ShelfStatusOn {
		return dto.ClientPackageItem{}, false
	}

	if retailPrice < costPrice {
		return dto.ClientPackageItem{}, false
	}

	validityDays := packagepkg.CalculateValidityDays(pkg.CalendarType, time.Now(), pkg.DurationMonths, pkg.DurationDays)

	dataAllowance := pkg.RealDataMB
	if dataAllowance <= 0 {
		dataAllowance = pkg.RealDataMB
	}

	return dto.ClientPackageItem{
		PackageID:     pkg.ID,
		PackageName:   pkg.PackageName,
		PackageType:   pkg.PackageType,
		RetailPrice:   retailPrice,
		CostPrice:     costPrice,
		ValidityDays:  validityDays,
		IsAddon:       isAddon,
		DataAllowance: dataAllowance,
		DataUnit:      "MB",
		Description:   pkg.PackageCode,
	}, true
}

func nonZeroTimePtr(t time.Time) *time.Time {
	if t.IsZero() {
		return nil
	}
	return &t
}

func packageStatusName(status int) string {
	switch status {
	case constants.PackageUsageStatusPending:
		return "待生效"
	case constants.PackageUsageStatusActive:
		return "生效中"
	case constants.PackageUsageStatusDepleted:
		return "已用完"
	case constants.PackageUsageStatusExpired:
		return "已过期"
	case constants.PackageUsageStatusInvalidated:
		return "已失效"
	default:
		return "未知"
	}
}

// mapDeviceGatewayInfoToClientInfo 将 B 端 DeviceGatewayInfo 映射为 C 端 DeviceRealtimeInfo
// B 端和 C 端结构独立，通过映射函数转换（per D-08）
func mapDeviceGatewayInfoToClientInfo(g *dto.DeviceGatewayInfo) *dto.DeviceRealtimeInfo {
	if g == nil {
		return nil
	}
	info := &dto.DeviceRealtimeInfo{}
	if g.OnlineStatus != nil {
		v := int64(*g.OnlineStatus)
		info.OnlineStatus = &v
	}
	if g.BatteryLevel != nil {
		v := int64(*g.BatteryLevel)
		info.BatteryLevel = &v
	}
	if g.Status != nil {
		v := int64(*g.Status)
		info.Status = &v
	}
	info.RunTime = g.RunTime
	info.ConnectTime = g.ConnectTime
	info.LastOnlineTime = g.LastOnlineTime
	info.LastUpdateTime = g.LastUpdateTime
	if g.Rsrp != nil {
		v := int64(*g.Rsrp)
		info.Rsrp = &v
	}
	if g.Rsrq != nil {
		v := int64(*g.Rsrq)
		info.Rsrq = &v
	}
	info.Rssi = g.Rssi
	if g.Sinr != nil {
		v := int64(*g.Sinr)
		info.Sinr = &v
	}
	info.SignalQuality = g.SignalQuality
	info.SignalBadReason = g.SignalBadReason
	info.SSID = g.SSID
	info.WifiEnabled = g.WifiEnabled
	info.WifiPassword = g.WifiPassword
	info.IPAddress = g.IPAddress
	info.WANIP = g.WANIP
	info.LANIP = g.LANIP
	info.MACAddress = g.MACAddress
	info.DailyUsage = g.DailyUsage
	info.DLStats = g.DLStats
	info.ULStats = g.ULStats
	if g.LimitSpeed != nil {
		v := int64(*g.LimitSpeed)
		info.LimitSpeed = &v
	}
	info.CurrentIccid = g.CurrentIccid
	if g.MaxClients != nil {
		v := int64(*g.MaxClients)
		info.MaxClients = &v
	}
	if g.ClientNumber != nil {
		v := int64(*g.ClientNumber)
		info.ClientNumber = &v
	}
	info.SoftwareVersion = g.SoftwareVersion
	if g.SwitchMode != nil {
		// SwitchMode 在 B 端为字符串（"0"/"1"），C 端为整型（0/1），需解析转换
		if n, convErr := strconv.Atoi(*g.SwitchMode); convErr == nil {
			info.SwitchMode = &n
		}
	}
	if g.SyncInterval != nil {
		v := int64(*g.SyncInterval)
		info.SyncInterval = &v
	}
	info.DeviceID = g.DeviceID
	info.DeviceName = g.DeviceName
	info.DeviceType = g.DeviceType
	info.Imei = g.IMEI
	info.Imsi = g.IMSI
	info.GatewayMsg = g.GatewayMsg
	return info
}
