package iot_card

import (
	"context"
	"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"
	"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/middleware"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// PollingCallback 轮询回调接口
// 用于在卡生命周期事件发生时通知轮询调度器
type PollingCallback interface {
	// OnCardCreated 单卡创建时的回调
	OnCardCreated(ctx context.Context, card *model.IotCard)
	// OnCardStatusChanged 卡状态变化时的回调
	OnCardStatusChanged(ctx context.Context, cardID uint)
	// OnCardDeleted 卡删除时的回调
	OnCardDeleted(ctx context.Context, cardID uint)
	// OnCardEnabled 卡启用轮询时的回调
	OnCardEnabled(ctx context.Context, cardID uint)
	// OnCardDisabled 卡禁用轮询时的回调
	OnCardDisabled(ctx context.Context, cardID uint)
}

// DataDeductor 流量扣减回调接口
// 用于在手动刷新流量后触发套餐扣减，避免循环依赖
type DataDeductor interface {
	// DeductDataUsage 按优先级扣减套餐流量
	DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB int64) error
}

type Service struct {
	db                         *gorm.DB
	iotCardStore               *postgres.IotCardStore
	shopStore                  *postgres.ShopStore
	assetAllocationRecordStore *postgres.AssetAllocationRecordStore
	shopPackageAllocationStore *postgres.ShopPackageAllocationStore
	shopSeriesAllocationStore  *postgres.ShopSeriesAllocationStore
	packageSeriesStore         *postgres.PackageSeriesStore
	gatewayClient              *gateway.Client
	logger                     *zap.Logger
	pollingCallback            PollingCallback
	dataDeductor               DataDeductor
	assetIdentifierStore       *postgres.AssetIdentifierStore
}

func New(
	db *gorm.DB,
	iotCardStore *postgres.IotCardStore,
	shopStore *postgres.ShopStore,
	assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
	shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
	shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
	packageSeriesStore *postgres.PackageSeriesStore,
	gatewayClient *gateway.Client,
	logger *zap.Logger,
) *Service {
	return &Service{
		db:                         db,
		iotCardStore:               iotCardStore,
		shopStore:                  shopStore,
		assetAllocationRecordStore: assetAllocationRecordStore,
		shopPackageAllocationStore: shopPackageAllocationStore,
		shopSeriesAllocationStore:  shopSeriesAllocationStore,
		packageSeriesStore:         packageSeriesStore,
		gatewayClient:              gatewayClient,
		logger:                     logger,
	}
}

// SetPollingCallback 设置轮询回调
func (s *Service) SetPollingCallback(callback PollingCallback) {
	s.pollingCallback = callback
}

// SetAssetIdentifierStore 设置资产标识符注册表存储
func (s *Service) SetAssetIdentifierStore(store *postgres.AssetIdentifierStore) {
	s.assetIdentifierStore = store
}

// SetDataDeductor 设置流量扣减回调
// 在应用启动时由 bootstrap 调用，注入套餐扣减服务
func (s *Service) SetDataDeductor(deductor DataDeductor) {
	s.dataDeductor = deductor
}

func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIotCardRequest) (*dto.ListStandaloneIotCardResponse, error) {
	page := req.Page
	pageSize := req.PageSize
	if page == 0 {
		page = 1
	}
	if pageSize == 0 {
		pageSize = constants.DefaultPageSize
	}

	opts := &store.QueryOptions{
		Page:     page,
		PageSize: pageSize,
	}

	filters := make(map[string]interface{})
	if req.Status != nil {
		filters["status"] = *req.Status
	}
	if req.CarrierID != nil {
		filters["carrier_id"] = *req.CarrierID
	}
	if req.ShopID != nil {
		filters["shop_id"] = *req.ShopID
	}
	if req.ICCID != "" {
		filters["iccid"] = req.ICCID
	}
	if req.MSISDN != "" {
		filters["msisdn"] = req.MSISDN
	}
	if req.IsStandalone != nil {
		filters["is_standalone"] = req.IsStandalone
	}
	if req.BatchNo != "" {
		filters["batch_no"] = req.BatchNo
	}
	if req.PackageID != nil {
		filters["package_id"] = *req.PackageID
	}
	if req.IsDistributed != nil {
		filters["is_distributed"] = *req.IsDistributed
	}
	if req.ICCIDStart != "" {
		filters["iccid_start"] = req.ICCIDStart
	}
	if req.ICCIDEnd != "" {
		filters["iccid_end"] = req.ICCIDEnd
	}
	if req.IsReplaced != nil {
		filters["is_replaced"] = *req.IsReplaced
	}
	if req.SeriesID != nil {
		filters["series_id"] = *req.SeriesID
	}

	// 代理用户注入 subordinate_shop_ids，让 Store 层走并行查询路径
	// 避免 PG 对 shop_id IN (...) + ORDER BY 选择全表扫描
	userType := middleware.GetUserTypeFromContext(ctx)
	if userType == constants.UserTypeAgent {
		shopID := middleware.GetShopIDFromContext(ctx)
		if shopID > 0 {
			subordinateIDs, err := s.shopStore.GetSubordinateShopIDs(ctx, shopID)
			if err == nil && len(subordinateIDs) > 1 {
				filters["subordinate_shop_ids"] = subordinateIDs
			}
		}
	}

	cards, total, err := s.iotCardStore.ListStandalone(ctx, opts, filters)
	if err != nil {
		return nil, err
	}

	shopMap := s.loadShopNames(ctx, cards)
	seriesMap := s.loadSeriesNames(ctx, cards)

	list := make([]*dto.StandaloneIotCardResponse, 0, len(cards))
	for _, card := range cards {
		item := s.toStandaloneResponse(card, shopMap, seriesMap)
		list = append(list, item)
	}

	return &dto.ListStandaloneIotCardResponse{
		List:     list,
		Total:    total,
		Page:     page,
		PageSize: pageSize,
	}, nil
}

// GetByICCID 通过 ICCID 获取单卡详情
func (s *Service) GetByICCID(ctx context.Context, iccid string) (*dto.IotCardDetailResponse, error) {
	card, err := s.iotCardStore.GetByICCID(ctx, iccid)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeNotFound, "IoT卡不存在")
		}
		return nil, err
	}

	shopMap := s.loadShopNames(ctx, []*model.IotCard{card})
	seriesMap := s.loadSeriesNames(ctx, []*model.IotCard{card})
	standaloneResp := s.toStandaloneResponse(card, shopMap, seriesMap)

	return &dto.IotCardDetailResponse{
		StandaloneIotCardResponse: *standaloneResp,
	}, nil
}

func (s *Service) loadShopNames(ctx context.Context, cards []*model.IotCard) map[uint]string {
	shopIDs := make([]uint, 0)
	shopIDSet := make(map[uint]bool)

	for _, card := range cards {
		if card.ShopID != nil && *card.ShopID > 0 && !shopIDSet[*card.ShopID] {
			shopIDs = append(shopIDs, *card.ShopID)
			shopIDSet[*card.ShopID] = true
		}
	}

	shopMap := make(map[uint]string)
	if len(shopIDs) > 0 {
		var shops []model.Shop
		// 使用 Unscoped() 包含已删除的店铺，确保能显示店铺名称
		s.db.WithContext(ctx).Unscoped().Where("id IN ?", shopIDs).Find(&shops)
		for _, shop := range shops {
			shopMap[shop.ID] = shop.ShopName
		}
	}

	return shopMap
}

func (s *Service) loadSeriesNames(ctx context.Context, cards []*model.IotCard) map[uint]string {
	seriesIDs := make([]uint, 0)
	seriesIDSet := make(map[uint]bool)

	for _, card := range cards {
		if card.SeriesID != nil && *card.SeriesID > 0 && !seriesIDSet[*card.SeriesID] {
			seriesIDs = append(seriesIDs, *card.SeriesID)
			seriesIDSet[*card.SeriesID] = true
		}
	}

	seriesMap := make(map[uint]string)
	if len(seriesIDs) > 0 {
		var seriesList []model.PackageSeries
		s.db.WithContext(ctx).Where("id IN ?", seriesIDs).Find(&seriesList)
		for _, series := range seriesList {
			seriesMap[series.ID] = series.SeriesName
		}
	}

	return seriesMap
}

func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]string, seriesMap map[uint]string) *dto.StandaloneIotCardResponse {
	resp := &dto.StandaloneIotCardResponse{
		ID:                    card.ID,
		ICCID:                 card.ICCID,
		VirtualNo:             card.VirtualNo,
		CardCategory:          card.CardCategory,
		CarrierID:             card.CarrierID,
		CarrierType:           card.CarrierType,
		CarrierName:           card.CarrierName,
		IMSI:                  card.IMSI,
		MSISDN:                card.MSISDN,
		BatchNo:               card.BatchNo,
		Supplier:              card.Supplier,
		Status:                card.Status,
		StatusName:            constants.GetIotCardStatusName(card.Status),
		ShopID:                card.ShopID,
		ActivatedAt:           card.ActivatedAt,
		ActivationStatus:      card.ActivationStatus,
		ActivationStatusName:  constants.GetActivationStatusName(card.ActivationStatus),
		RealNameStatus:        card.RealNameStatus,
		RealNameStatusName:    constants.GetRealNameStatusName(card.RealNameStatus),
		NetworkStatus:         card.NetworkStatus,
		NetworkStatusName:     constants.GetNetworkStatusName(card.NetworkStatus),
		DataUsageMB:           card.DataUsageMB,
		CurrentMonthUsageMB:   card.CurrentMonthUsageMB,
		CurrentMonthStartDate: card.CurrentMonthStartDate,
		LastMonthTotalMB:      card.LastMonthTotalMB,
		LastDataCheckAt:       card.LastDataCheckAt,
		LastRealNameCheckAt:   card.LastRealNameCheckAt,
		EnablePolling:         card.EnablePolling,
		SeriesID:              card.SeriesID,
		DeviceVirtualNo:       card.DeviceVirtualNo,
		CreatedAt:             card.CreatedAt,
		UpdatedAt:             card.UpdatedAt,
	}

	if card.ShopID != nil && *card.ShopID > 0 {
		resp.ShopName = shopMap[*card.ShopID]
	}

	if card.SeriesID != nil && *card.SeriesID > 0 {
		resp.SeriesName = seriesMap[*card.SeriesID]
	}

	return resp
}

func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateStandaloneCardsResponse, error) {
	if err := s.validateDirectSubordinate(ctx, operatorShopID, req.ToShopID); err != nil {
		return nil, err
	}

	cards, err := s.getCardsForAllocation(ctx, req, operatorShopID)
	if err != nil {
		return nil, err
	}

	if len(cards) == 0 {
		return &dto.AllocateStandaloneCardsResponse{
			TotalCount:   0,
			SuccessCount: 0,
			FailCount:    0,
			FailedItems:  []dto.AllocationFailedItem{},
		}, nil
	}

	var cardIDs []uint
	var failedItems []dto.AllocationFailedItem

	boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, s.extractCardIDs(cards))
	if err != nil {
		return nil, err
	}
	boundCardIDSet := make(map[uint]bool)
	for _, id := range boundCardIDs {
		boundCardIDSet[id] = true
	}

	isPlatform := operatorShopID == nil

	for _, card := range cards {
		if boundCardIDSet[card.ID] {
			failedItems = append(failedItems, dto.AllocationFailedItem{
				ICCID:  card.ICCID,
				Reason: "已绑定设备的卡不能单独分配",
			})
			continue
		}

		if isPlatform && card.Status != constants.IotCardStatusInStock {
			failedItems = append(failedItems, dto.AllocationFailedItem{
				ICCID:  card.ICCID,
				Reason: "平台只能分配在库状态的卡",
			})
			continue
		}

		if !isPlatform && card.Status != constants.IotCardStatusDistributed {
			failedItems = append(failedItems, dto.AllocationFailedItem{
				ICCID:  card.ICCID,
				Reason: "代理只能分配已分销状态的卡",
			})
			continue
		}

		cardIDs = append(cardIDs, card.ID)
	}

	if len(cardIDs) == 0 {
		return &dto.AllocateStandaloneCardsResponse{
			TotalCount:   len(cards),
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	newStatus := constants.IotCardStatusDistributed
	toShopID := req.ToShopID

	err = s.db.Transaction(func(tx *gorm.DB) error {
		txIotCardStore := postgres.NewIotCardStore(tx, nil)
		txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)

		if err := txIotCardStore.BatchUpdateShopIDAndStatus(ctx, cardIDs, &toShopID, newStatus); err != nil {
			return err
		}

		allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
		records := s.buildAllocationRecords(cards, cardIDs, operatorShopID, toShopID, operatorID, allocationNo, req.Remark)
		return txRecordStore.BatchCreate(ctx, records)
	})

	if err != nil {
		return nil, err
	}

	// 通知轮询调度器状态变化（卡被分配后可能需要重新匹配配置）
	if s.pollingCallback != nil && len(cardIDs) > 0 {
		for _, cardID := range cardIDs {
			s.pollingCallback.OnCardStatusChanged(ctx, cardID)
		}
	}

	return &dto.AllocateStandaloneCardsResponse{
		TotalCount:   len(cards),
		SuccessCount: len(cardIDs),
		FailCount:    len(failedItems),
		AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate),
		FailedItems:  failedItems,
	}, nil
}

func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.RecallStandaloneCardsResponse, error) {
	// 1. 查询卡列表
	cards, err := s.getCardsForRecall(ctx, req)
	if err != nil {
		return nil, err
	}

	if len(cards) == 0 {
		return &dto.RecallStandaloneCardsResponse{
			TotalCount:   0,
			SuccessCount: 0,
			FailCount:    0,
			FailedItems:  []dto.AllocationFailedItem{},
		}, nil
	}

	// 2. 收集所有卡的店铺 ID，批量查询店铺信息以验证直属下级关系
	shopIDSet := make(map[uint]bool)
	for _, card := range cards {
		if card.ShopID != nil {
			shopIDSet[*card.ShopID] = true
		}
	}
	shopIDs := make([]uint, 0, len(shopIDSet))
	for shopID := range shopIDSet {
		shopIDs = append(shopIDs, shopID)
	}

	// 3. 批量查询店铺，验证哪些是直属下级
	directSubordinateSet := make(map[uint]bool)
	if len(shopIDs) > 0 {
		shops, err := s.shopStore.GetByIDs(ctx, shopIDs)
		if err != nil {
			return nil, err
		}
		for _, shop := range shops {
			if s.isDirectSubordinate(operatorShopID, shop) {
				directSubordinateSet[shop.ID] = true
			}
		}

	}

	// 4. 检查绑定设备的卡
	var cardIDs []uint
	var successCards []*model.IotCard
	var failedItems []dto.AllocationFailedItem

	boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, s.extractCardIDs(cards))
	if err != nil {
		return nil, err
	}
	boundCardIDSet := make(map[uint]bool)
	for _, id := range boundCardIDs {
		boundCardIDSet[id] = true
	}

	// 5. 逐卡验证：绑定设备、所属店铺是否是直属下级
	for _, card := range cards {
		if boundCardIDSet[card.ID] {
			failedItems = append(failedItems, dto.AllocationFailedItem{
				ICCID:  card.ICCID,
				Reason: "已绑定设备的卡不能单独回收",
			})
			continue
		}

		if card.ShopID == nil {
			failedItems = append(failedItems, dto.AllocationFailedItem{
				ICCID:  card.ICCID,
				Reason: "卡未分配给任何店铺",
			})
			continue
		}
		userType := middleware.GetUserTypeFromContext(ctx)

		if userType == constants.UserTypeAgent {
			if !directSubordinateSet[*card.ShopID] {
				failedItems = append(failedItems, dto.AllocationFailedItem{
					ICCID:  card.ICCID,
					Reason: "卡所属店铺不是您的直属下级",
				})
				continue
			}
		}
		cardIDs = append(cardIDs, card.ID)
		successCards = append(successCards, card)
	}

	if len(cardIDs) == 0 {
		return &dto.RecallStandaloneCardsResponse{
			TotalCount:   len(cards),
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	// 6. 执行回收
	isPlatform := operatorShopID == nil
	var newShopID *uint
	var newStatus int
	if isPlatform {
		newShopID = nil
		newStatus = constants.IotCardStatusInStock
	} else {
		newShopID = operatorShopID
		newStatus = constants.IotCardStatusDistributed
	}

	allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall)

	err = s.db.Transaction(func(tx *gorm.DB) error {
		txIotCardStore := postgres.NewIotCardStore(tx, nil)
		txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)

		if err := txIotCardStore.BatchUpdateShopIDAndStatus(ctx, cardIDs, newShopID, newStatus); err != nil {
			return err
		}

		records := s.buildRecallRecords(successCards, operatorShopID, operatorID, allocationNo, req.Remark)
		return txRecordStore.BatchCreate(ctx, records)
	})

	if err != nil {
		return nil, err
	}

	// 通知轮询调度器状态变化（卡被回收后可能需要重新匹配配置）
	if s.pollingCallback != nil && len(cardIDs) > 0 {
		for _, cardID := range cardIDs {
			s.pollingCallback.OnCardStatusChanged(ctx, cardID)
		}
	}

	return &dto.RecallStandaloneCardsResponse{
		TotalCount:   len(cards),
		SuccessCount: len(cardIDs),
		FailCount:    len(failedItems),
		AllocationNo: allocationNo,
		FailedItems:  failedItems,
	}, nil
}

// isDirectSubordinate 检查店铺是否是操作者的可回收范围
// 平台用户可以回收所有店铺的卡，代理用户只能回收直属下级店铺的卡
func (s *Service) isDirectSubordinate(operatorShopID *uint, shop *model.Shop) bool {
	if operatorShopID == nil {
		// 平台用户：可以回收所有店铺的卡
		return true
	}
	// 代理用户：直属下级是 parent_id 等于自己的店铺
	return shop.ParentID != nil && *shop.ParentID == *operatorShopID
}

func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID *uint, targetShopID uint) error {
	if operatorShopID != nil && *operatorShopID == targetShopID {
		return errors.ErrCannotAllocateToSelf
	}

	targetShop, err := s.shopStore.GetByID(ctx, targetShopID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeShopNotFound)
		}
		return err
	}

	if operatorShopID == nil {
		if targetShop.ParentID != nil {
			return errors.ErrNotDirectSubordinate
		}
	} else {
		if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID {
			return errors.ErrNotDirectSubordinate
		}
	}

	return nil
}

func (s *Service) getCardsForAllocation(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorShopID *uint) ([]*model.IotCard, error) {
	switch req.SelectionType {
	case dto.SelectionTypeList:
		return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
	case dto.SelectionTypeRange:
		return s.iotCardStore.GetStandaloneByICCIDRange(ctx, req.ICCIDStart, req.ICCIDEnd, operatorShopID)
	case dto.SelectionTypeFilter:
		filters := make(map[string]any)
		if req.CarrierID != nil {
			filters["carrier_id"] = *req.CarrierID
		}
		if req.BatchNo != "" {
			filters["batch_no"] = req.BatchNo
		}
		if req.Status != nil {
			filters["status"] = *req.Status
		}
		return s.iotCardStore.GetStandaloneByFilters(ctx, filters, operatorShopID)
	default:
		return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
	}
}

func (s *Service) getCardsForRecall(ctx context.Context, req *dto.RecallStandaloneCardsRequest) ([]*model.IotCard, error) {
	switch req.SelectionType {
	case dto.SelectionTypeList:
		return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
	case dto.SelectionTypeRange:
		// 查询已分配给店铺的单卡（回收场景）
		return s.iotCardStore.GetDistributedStandaloneByICCIDRange(ctx, req.ICCIDStart, req.ICCIDEnd)
	case dto.SelectionTypeFilter:
		filters := make(map[string]any)
		if req.CarrierID != nil {
			filters["carrier_id"] = *req.CarrierID
		}
		if req.BatchNo != "" {
			filters["batch_no"] = req.BatchNo
		}
		// 查询已分配给店铺的单卡（回收场景）
		return s.iotCardStore.GetDistributedStandaloneByFilters(ctx, filters)
	default:
		return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
	}
}

func (s *Service) extractCardIDs(cards []*model.IotCard) []uint {
	ids := make([]uint, len(cards))
	for i, card := range cards {
		ids[i] = card.ID
	}
	return ids
}

func (s *Service) buildAllocationRecords(cards []*model.IotCard, successCardIDs []uint, fromShopID *uint, toShopID uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
	successIDSet := make(map[uint]bool)
	for _, id := range successCardIDs {
		successIDSet[id] = true
	}

	var records []*model.AssetAllocationRecord
	for _, card := range cards {
		if !successIDSet[card.ID] {
			continue
		}

		record := &model.AssetAllocationRecord{
			AllocationNo:    allocationNo,
			AllocationType:  constants.AssetAllocationTypeAllocate,
			AssetType:       constants.AssetTypeIotCard,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			ToOwnerType:     constants.OwnerTypeShop,
			ToOwnerID:       toShopID,
			OperatorID:      operatorID,
			Remark:          remark,
		}

		if fromShopID == nil {
			record.FromOwnerType = constants.OwnerTypePlatform
			record.FromOwnerID = nil
		} else {
			record.FromOwnerType = constants.OwnerTypeShop
			record.FromOwnerID = fromShopID
		}

		records = append(records, record)
	}

	return records
}

func (s *Service) buildRecallRecords(successCards []*model.IotCard, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
	var records []*model.AssetAllocationRecord
	for _, card := range successCards {
		record := &model.AssetAllocationRecord{
			AllocationNo:    allocationNo,
			AllocationType:  constants.AssetAllocationTypeRecall,
			AssetType:       constants.AssetTypeIotCard,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			FromOwnerType:   constants.OwnerTypeShop,
			FromOwnerID:     card.ShopID, // 从卡的当前所属店铺获取
			OperatorID:      operatorID,
			Remark:          remark,
		}

		if toShopID == nil {
			record.ToOwnerType = constants.OwnerTypePlatform
			record.ToOwnerID = 0
		} else {
			record.ToOwnerType = constants.OwnerTypeShop
			record.ToOwnerID = *toShopID
		}

		records = append(records, record)
	}

	return records
}

// BatchSetSeriesBinding 批量设置卡的套餐系列绑定
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) {
	cards, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
	if err != nil {
		return nil, err
	}

	if len(cards) == 0 {
		return &dto.BatchSetCardSeriesBindngResponse{
			SuccessCount: 0,
			FailCount:    len(req.ICCIDs),
			FailedItems:  s.buildCardNotFoundFailedItems(req.ICCIDs),
		}, nil
	}

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

	// 验证系列存在（仅当 SeriesID > 0 时）
	var packageSeries *model.PackageSeries
	if req.SeriesID > 0 {
		packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
			}
			return nil, err
		}
		if packageSeries.Status != 1 {
			return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
		}
	}

	var successCardIDs []uint
	var failedItems []dto.CardSeriesBindngFailedItem

	for _, iccid := range req.ICCIDs {
		card, exists := cardMap[iccid]
		if !exists {
			failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
				ICCID:  iccid,
				Reason: "卡不存在",
			})
			continue
		}

		// 验证操作者权限（仅代理用户）
		if operatorShopID != nil && req.SeriesID > 0 {
			seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
			if err != nil {
				return nil, err
			}
			hasSeriesAllocation := false
			for _, alloc := range seriesAllocations {
				if alloc.SeriesID == req.SeriesID && alloc.Status == 1 {
					hasSeriesAllocation = true
					break
				}
			}
			if !hasSeriesAllocation {
				failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
					ICCID:  iccid,
					Reason: "您没有权限分配该套餐系列",
				})
				continue
			}
		}

		// 验证卡权限（基于 card.ShopID）
		if operatorShopID != nil {
			if card.ShopID == nil || *card.ShopID != *operatorShopID {
				failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
					ICCID:  iccid,
					Reason: "无权操作此卡",
				})
				continue
			}
		}

		successCardIDs = append(successCardIDs, card.ID)
	}

	if len(successCardIDs) > 0 {
		var seriesIDPtr *uint
		if req.SeriesID > 0 {
			seriesIDPtr = &req.SeriesID
		}
		if err := s.iotCardStore.BatchUpdateSeriesID(ctx, successCardIDs, seriesIDPtr); err != nil {
			return nil, err
		}
	}

	return &dto.BatchSetCardSeriesBindngResponse{
		SuccessCount: len(successCardIDs),
		FailCount:    len(failedItems),
		FailedItems:  failedItems,
	}, nil
}

func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeriesBindngFailedItem {
	items := make([]dto.CardSeriesBindngFailedItem, len(iccids))
	for i, iccid := range iccids {
		items[i] = dto.CardSeriesBindngFailedItem{
			ICCID:  iccid,
			Reason: "卡不存在",
		}
	}
	return items
}

// RefreshCardDataFromGateway 从 Gateway 完整同步卡数据
// 调用网关查询网络状态、实名状态、本月流量，并写回数据库
// 流量采用增量计算（与轮询逻辑一致）：increment = 当前网关读数 - 上次网关读数
func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string) error {
	card, err := s.iotCardStore.GetByICCID(ctx, iccid)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeNotFound, "IoT卡不存在")
		}
		return err
	}

	syncTime := time.Now()
	updates := map[string]any{
		"last_sync_time": syncTime,
	}
	var flowIncrementMB float64

	if s.gatewayClient != nil {
		// 1. 查询网络状态（卡的开/停机状态）
		statusResp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
			CardNo: iccid,
		})
		if err != nil {
			s.logger.Warn("刷新卡数据：查询网络状态失败", zap.String("iccid", iccid), zap.Error(err))
		} else {
			networkStatus := parseNetworkStatus(statusResp.CardStatus)
			updates["network_status"] = networkStatus
		}

		// 2. 查询实名状态
		realnameResp, err := s.gatewayClient.QueryRealnameStatus(ctx, &gateway.CardStatusReq{
			CardNo: iccid,
		})
		if err != nil {
			s.logger.Warn("刷新卡数据：查询实名状态失败", zap.String("iccid", iccid), zap.Error(err))
		} else {
			realNameStatus := parseGatewayRealnameStatus(realnameResp.RealStatus)
			updates["real_name_status"] = realNameStatus
			// 检测 0→1 变化：旧状态非已实名且新状态为已实名，补写 first_realname_at
			if card.RealNameStatus != constants.RealNameStatusVerified && realNameStatus == constants.RealNameStatusVerified {
				updates["first_realname_at"] = syncTime
			}
		}

		// 3. 查询本月流量用量 — 使用增量计算（与轮询 calculateFlowUpdates 逻辑一致）
		flowResp, err := s.gatewayClient.QueryFlow(ctx, &gateway.FlowQueryReq{
			CardNo: iccid,
		})
		if err != nil {
			s.logger.Warn("刷新卡数据：查询流量失败", zap.String("iccid", iccid), zap.Error(err))
		} else {
			flowIncrementMB = s.calculateRefreshFlowUpdates(card, flowResp.Used, syncTime, updates)
		}
	}

	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id = ?", card.ID).
		Updates(updates).Error; err != nil {
		return errors.Wrap(errors.CodeInternalError, err, "更新卡数据失败")
	}

	// 有增量时触发套餐流量扣减
	if flowIncrementMB > 0 && s.dataDeductor != nil {
		if err := s.dataDeductor.DeductDataUsage(ctx, "iot_card", card.ID, int64(flowIncrementMB)); err != nil {
			s.logger.Warn("手动刷新：套餐流量扣减失败",
				zap.Uint("card_id", card.ID),
				zap.Float64("increment_mb", flowIncrementMB),
				zap.Error(err))
		}
	}

	// 失效轮询缓存，确保轮询系统读到最新的 network_status/real_name_status/流量
	if s.pollingCallback != nil {
		s.pollingCallback.OnCardStatusChanged(ctx, card.ID)
	}

	s.logger.Info("刷新卡数据成功",
		zap.String("iccid", iccid),
		zap.Uint("card_id", card.ID),
		zap.Float64("flow_increment_mb", flowIncrementMB))
	return nil
}

// calculateRefreshFlowUpdates 计算手动刷新时的流量增量并填充 updates
// 算法与轮询 calculateFlowUpdates 一致：增量 = 当前网关读数 - 上次网关读数
// 返回本次增量值（MB），用于触发套餐扣减
func (s *Service) calculateRefreshFlowUpdates(card *model.IotCard, gatewayFlowMB float64, now time.Time, updates map[string]any) float64 {
	increment := gatewayFlowMB - card.LastGatewayReadingMB

	if increment < 0 {
		// 当前值比上次小，检查是否为上游运营商正常重置
		resetDay := s.getCarrierResetDay(card.CarrierID)
		if isRefreshResetWindow(now, resetDay) {
			// 在重置日窗口内，本次原始值即为增量
			increment = gatewayFlowMB
			s.logger.Info("手动刷新：检测到上游运营商重置",
				zap.Uint("card_id", card.ID),
				zap.Float64("gateway_flow", gatewayFlowMB),
				zap.Float64("last_reading", card.LastGatewayReadingMB),
				zap.Int("reset_day", resetDay))
		} else {
			// 非重置日出现值下降，异常，不计入
			s.logger.Warn("手动刷新：流量异常，非重置日出现值下降",
				zap.Uint("card_id", card.ID),
				zap.Float64("gateway_flow", gatewayFlowMB),
				zap.Float64("last_reading", card.LastGatewayReadingMB))
			increment = 0
		}
	}

	// 始终更新网关读数基准
	updates["last_gateway_reading_mb"] = gatewayFlowMB

	// 检测跨自然月
	currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
	isCrossMonth := card.CurrentMonthStartDate == nil ||
		card.CurrentMonthStartDate.Before(currentMonthStart)

	if isCrossMonth {
		s.logger.Info("手动刷新：检测到跨月，重置流量计数",
			zap.Uint("card_id", card.ID),
			zap.Float64("last_month_total", card.CurrentMonthUsageMB))
		updates["last_month_total_mb"] = card.CurrentMonthUsageMB
		updates["current_month_start_date"] = currentMonthStart
		if increment > 0 {
			updates["current_month_usage_mb"] = increment
		} else {
			updates["current_month_usage_mb"] = float64(0)
		}
	} else if increment > 0 {
		// 同月内，增量累加（使用 gorm.Expr 保证原子性）
		updates["current_month_usage_mb"] = gorm.Expr("current_month_usage_mb + ?", increment)
	}

	// 更新卡生命周期总用量
	if increment > 0 {
		updates["data_usage_mb"] = gorm.Expr("data_usage_mb + ?", int64(increment))
	}

	// 首次流量查询初始化
	if card.CurrentMonthStartDate == nil {
		updates["current_month_start_date"] = currentMonthStart
	}

	return increment
}

// getCarrierResetDay 读取运营商的上游流量重置日
func (s *Service) getCarrierResetDay(carrierID uint) int {
	var carrier model.Carrier
	if err := s.db.Select("data_reset_day").First(&carrier, carrierID).Error; err != nil {
		return 1
	}
	if carrier.DataResetDay == 0 {
		return 1
	}
	return carrier.DataResetDay
}

// isRefreshResetWindow 判断今天是否在运营商重置日窗口内
// 窗口 = 重置日当天 + 前一天（容错网关数据延迟）
func isRefreshResetWindow(now time.Time, resetDay int) bool {
	today := now.Day()
	if today == resetDay {
		return true
	}
	resetDate := time.Date(now.Year(), now.Month(), resetDay, 0, 0, 0, 0, now.Location())
	prevDay := resetDate.AddDate(0, 0, -1).Day()
	return today == prevDay
}

// parseNetworkStatus 将网关返回的卡状态字符串转换为 network_status 数值
// 停机→0，其他（准备/正常）→1
func parseNetworkStatus(cardStatus string) int {
	if cardStatus == "停机" {
		return 0
	}
	return 1
}

// parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值
// true=已实名(1)，false=未实名(0)
func parseGatewayRealnameStatus(realStatus bool) int {
	if realStatus {
		return constants.RealNameStatusVerified
	}
	return constants.RealNameStatusNotVerified
}

// UpdatePollingStatus 更新卡的轮询状态
// 启用或禁用卡的轮询功能
func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePolling bool) error {
	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeNotFound, "IoT卡不存在")
		}
		return err
	}

	// 检查是否需要更新
	if card.EnablePolling == enablePolling {
		return nil // 状态未变化
	}

	// 更新数据库
	card.EnablePolling = enablePolling
	if err := s.iotCardStore.Update(ctx, card); err != nil {
		return err
	}

	s.logger.Info("更新卡轮询状态",
		zap.Uint("card_id", cardID),
		zap.Bool("enable_polling", enablePolling),
	)

	// 通知轮询调度器
	if s.pollingCallback != nil {
		if enablePolling {
			s.pollingCallback.OnCardEnabled(ctx, cardID)
		} else {
			s.pollingCallback.OnCardDisabled(ctx, cardID)
		}
	}

	return nil
}

// BatchUpdatePollingStatus 批量更新卡的轮询状态
func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint, enablePolling bool) error {
	if len(cardIDs) == 0 {
		return nil
	}

	// 批量更新数据库
	if err := s.iotCardStore.BatchUpdatePollingStatus(ctx, cardIDs, enablePolling); err != nil {
		return err
	}

	s.logger.Info("批量更新卡轮询状态",
		zap.Int("count", len(cardIDs)),
		zap.Bool("enable_polling", enablePolling),
	)

	// 通知轮询调度器
	if s.pollingCallback != nil {
		for _, cardID := range cardIDs {
			if enablePolling {
				s.pollingCallback.OnCardEnabled(ctx, cardID)
			} else {
				s.pollingCallback.OnCardDisabled(ctx, cardID)
			}
		}
	}

	return nil
}

// DeleteCard 删除卡（软删除）
func (s *Service) DeleteCard(ctx context.Context, cardID uint) error {
	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeNotFound, "IoT卡不存在")
		}
		return err
	}

	if err := s.iotCardStore.Delete(ctx, cardID); err != nil {
		return err
	}

	if s.assetIdentifierStore != nil {
		_ = s.assetIdentifierStore.DeleteByAsset(ctx, model.AssetTypeIotCard, cardID)
	}

	s.logger.Info("删除卡", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID))

	if s.pollingCallback != nil {
		s.pollingCallback.OnCardDeleted(ctx, cardID)
	}

	return nil
}

// BatchDeleteCards 批量删除卡（软删除）
func (s *Service) BatchDeleteCards(ctx context.Context, cardIDs []uint) error {
	if len(cardIDs) == 0 {
		return nil
	}

	// 批量软删除
	if err := s.iotCardStore.BatchDelete(ctx, cardIDs); err != nil {
		return err
	}

	s.logger.Info("批量删除卡", zap.Int("count", len(cardIDs)))

	// 通知轮询调度器
	if s.pollingCallback != nil {
		for _, cardID := range cardIDs {
			s.pollingCallback.OnCardDeleted(ctx, cardID)
		}
	}

	return nil
}
