package iot_card

import (
	"context"
	stderrors "errors"
	"strings"
	"time"

	cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
	carddomain "github.com/break/junhong_cmp_fiber/internal/domain/cardobservation"
	"github.com/break/junhong_cmp_fiber/internal/gateway"
	"github.com/break/junhong_cmp_fiber/internal/infrastructure/cardtrafficlock"
	"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"
	"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"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"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)
	// OnBatchCardsStatusChanged 批量卡状态变化时的回调（批量分配/回收场景）
	OnBatchCardsStatusChanged(ctx context.Context, cardIDs []uint)
	// OnCardDeleted 卡删除时的回调
	OnCardDeleted(ctx context.Context, cardID uint)
	// OnCardEnabled 卡启用轮询时的回调
	OnCardEnabled(ctx context.Context, cardID uint)
	// OnCardDisabled 卡禁用轮询时的回调
	OnCardDisabled(ctx context.Context, cardID uint)
}

// RealnameActivator 实名激活回调接口
// 用于在手动实名后触发待实名套餐激活，避免循环依赖
type RealnameActivator interface {
	// ActivateByRealname 根据载体触发待实名套餐激活
	ActivateByRealname(ctx context.Context, carrierType string, carrierID uint) 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
	realnameActivator          RealnameActivator
	stopResumeService          StopResumeServiceInterface
	deviceSimBindingStore      *postgres.DeviceSimBindingStore
	redis                      *redis.Client
	assetIdentifierStore       *postgres.AssetIdentifierStore
	assetAuditService          AssetAuditService
	enterpriseCardAuthStore    *postgres.EnterpriseCardAuthorizationStore
	enterpriseStore            *postgres.EnterpriseStore
	packageExpiryQuery         *packageexpiry.Query
	cardObservation            *cardapp.Service
	observationSeries          cardapp.BestEffortSeriesDispatcher
	trafficLock                *cardtrafficlock.Lock
	speedTierIntegration       speedTierIntegrationLog
}

// SetObservationSeriesDispatcher 注入获取实名链接后的后台观测端口。
func (s *Service) SetObservationSeriesDispatcher(dispatcher cardapp.BestEffortSeriesDispatcher) {
	s.observationSeries = dispatcher
}

// SetCardObservationService 注入统一卡实名观测写入用例。
func (s *Service) SetCardObservationService(service *cardapp.Service) {
	s.cardObservation = service
}

// SetSpeedTierIntegrationLog 注入卡固定限速的 Integration Log 接缝。
func (s *Service) SetSpeedTierIntegrationLog(integration speedTierIntegrationLog) {
	s.speedTierIntegration = integration
}

// SetPackageExpiryQuery 注入套餐最终到期查询，供列表使用批量投影。
func (s *Service) SetPackageExpiryQuery(query *packageexpiry.Query) {
	s.packageExpiryQuery = query
}

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,
	assetAuditService AssetAuditService,
) *Service {
	return &Service{
		db:                         db,
		iotCardStore:               iotCardStore,
		shopStore:                  shopStore,
		assetAllocationRecordStore: assetAllocationRecordStore,
		shopPackageAllocationStore: shopPackageAllocationStore,
		shopSeriesAllocationStore:  shopSeriesAllocationStore,
		packageSeriesStore:         packageSeriesStore,
		gatewayClient:              gatewayClient,
		logger:                     logger,
		assetAuditService:          assetAuditService,
		packageExpiryQuery:         packageexpiry.NewQuery(db),
	}
}

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

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

// SetEnterpriseCardAuthStore 注入企业卡授权 store（用于列表响应回填企业信息）
func (s *Service) SetEnterpriseCardAuthStore(store *postgres.EnterpriseCardAuthorizationStore) {
	s.enterpriseCardAuthStore = store
}

// SetEnterpriseStore 注入企业 store（用于批量加载企业名称）
func (s *Service) SetEnterpriseStore(store *postgres.EnterpriseStore) {
	s.enterpriseStore = store
}

// SetRealnameActivator 设置实名激活回调
// 在应用启动时由 bootstrap 注入套餐激活服务
func (s *Service) SetRealnameActivator(activator RealnameActivator) {
	s.realnameActivator = activator
}

// SetStopResumeService 设置停复机服务
// 在应用启动时由 bootstrap 注入，确保手动实名后可立即评估停复机
func (s *Service) SetStopResumeService(stopResumeService StopResumeServiceInterface) {
	s.stopResumeService = stopResumeService
}

// SetDeviceSimBindingStore 设置设备绑定存储
// 在手动实名成功后用于补触发设备维度待实名套餐激活
func (s *Service) SetDeviceSimBindingStore(deviceSimBindingStore *postgres.DeviceSimBindingStore) {
	s.deviceSimBindingStore = deviceSimBindingStore
}

// SetRedisClient 设置 Redis 客户端
// 用于清理实名逆转计数器，避免历史计数干扰手动实名后的判定
func (s *Service) SetRedisClient(redisClient *redis.Client) {
	s.redis = redisClient
	s.trafficLock = cardtrafficlock.New(redisClient)
}

// acquireCardTrafficSyncLock 获取卡流量同步锁，避免主动刷新与轮询重复统计同一上游读数。
func (s *Service) acquireCardTrafficSyncLock(ctx context.Context, cardID uint) (string, bool, error) {
	return s.trafficLock.Acquire(ctx, cardID)
}

// releaseCardTrafficSyncLock 释放卡流量同步锁。
func (s *Service) releaseCardTrafficSyncLock(ctx context.Context, cardID uint, token string) {
	if err := s.trafficLock.Release(ctx, cardID, token); err != nil {
		s.logger.Warn("释放卡流量同步锁失败", zap.Uint("card_id", cardID), zap.Error(err))
	}
}

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
	}
	shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
	if hasShopIDs {
		filters["shop_ids"] = shopIDs
	} else if req.ShopID != nil {
		if *req.ShopID == 0 {
			filters["shop_ids"] = []uint{}
		} else {
			filters["shop_id"] = *req.ShopID
		}
	}
	if req.ICCID != "" {
		filters["iccid"] = req.ICCID
	}
	if req.VirtualNo != "" {
		filters["virtual_no"] = req.VirtualNo
	}
	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
	}
	if req.CarrierName != "" {
		filters["carrier_name"] = req.CarrierName
	}
	if req.HasActivePackage != nil {
		filters["has_active_package"] = *req.HasActivePackage
	}
	if req.Keyword != "" {
		filters["keyword"] = req.Keyword
	}
	if req.NetworkStatus != nil {
		filters["network_status"] = *req.NetworkStatus
	}
	if req.RealNameStatus != nil {
		filters["real_name_status"] = *req.RealNameStatus
	}
	if req.AuthorizedEnterpriseID != nil {
		filters["authorized_enterprise_id"] = *req.AuthorizedEnterpriseID
	}
	if req.IsAuthorizedToEnterprise != nil {
		filters["is_authorized_to_enterprise"] = *req.IsAuthorizedToEnterprise
	}

	// 代理用户注入 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 {
				if hasShopIDs {
					scopedShopIDs := intersectShopIDs(shopIDs, subordinateIDs)
					filters["shop_ids"] = scopedShopIDs
					if len(scopedShopIDs) > 1 {
						filters["subordinate_shop_ids"] = scopedShopIDs
					}
				} else if len(subordinateIDs) > 1 {
					filters["subordinate_shop_ids"] = subordinateIDs
				}
			}
		}
	}

	cards, total, err := s.iotCardStore.ListStandalone(ctx, opts, filters)
	if err != nil {
		return nil, err
	}
	cardIDs := make([]uint, 0, len(cards))
	for _, card := range cards {
		cardIDs = append(cardIDs, card.ID)
	}
	expiryEstimates, err := s.packageExpiryQuery.ResolveBatch(ctx, constants.AssetTypeIotCard, cardIDs)
	if err != nil {
		return nil, err
	}

	shopMap := s.loadShopNames(ctx, cards)
	//TODO 这里不对,现在已经快照了,这里如果还这样处理明显是浪费的
	seriesMap := s.loadSeriesNames(ctx, cards)

	// 批量加载企业授权信息
	cardAuthMap := make(map[uint]uint)
	enterpriseNameMap := make(map[uint]string)
	if s.enterpriseCardAuthStore != nil && len(cards) > 0 {
		cardIDs := make([]uint, 0, len(cards))
		for _, card := range cards {
			cardIDs = append(cardIDs, card.ID)
		}
		authMap, err := s.enterpriseCardAuthStore.GetActiveAuthEnterpriseByCardIDs(ctx, cardIDs)
		if err != nil {
			s.logger.Error("批量加载卡企业授权失败", zap.Error(err))
		} else {
			cardAuthMap = authMap
		}
		if s.enterpriseStore != nil && len(cardAuthMap) > 0 {
			eIDs := make([]uint, 0, len(cardAuthMap))
			seen := make(map[uint]struct{})
			for _, eid := range cardAuthMap {
				if _, ok := seen[eid]; !ok {
					seen[eid] = struct{}{}
					eIDs = append(eIDs, eid)
				}
			}
			nameMap, err := s.enterpriseStore.GetNameMapByIDs(ctx, eIDs)
			if err != nil {
				s.logger.Error("批量加载企业名称失败", zap.Error(err))
			} else {
				enterpriseNameMap = nameMap
			}
		}
	}

	list := make([]*dto.StandaloneIotCardResponse, 0, len(cards))
	for _, card := range cards {
		item := s.toStandaloneResponse(card, shopMap, seriesMap)
		item.PackageExpiryEstimate = expiryEstimates[card.ID]
		if eid, ok := cardAuthMap[card.ID]; ok {
			item.AuthorizedEnterpriseID = &eid
			item.AuthorizedEnterpriseName = enterpriseNameMap[eid]
		}
		list = append(list, item)
	}

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

func normalizeShopIDs(ids []uint) ([]uint, bool) {
	if len(ids) == 0 {
		return nil, false
	}
	seen := make(map[uint]struct{}, len(ids))
	result := make([]uint, 0, len(ids))
	for _, id := range ids {
		if id == 0 {
			continue
		}
		if _, exists := seen[id]; exists {
			continue
		}
		seen[id] = struct{}{}
		result = append(result, id)
	}
	return result, true
}

func intersectShopIDs(selectedIDs, allowedIDs []uint) []uint {
	if len(selectedIDs) == 0 || len(allowedIDs) == 0 {
		return []uint{}
	}
	allowed := make(map[uint]struct{}, len(allowedIDs))
	for _, id := range allowedIDs {
		allowed[id] = struct{}{}
	}
	result := make([]uint, 0, len(selectedIDs))
	for _, id := range selectedIDs {
		if _, ok := allowed[id]; ok {
			result = append(result, id)
		}
	}
	return result
}

// 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),
		GatewayExtend:         card.GatewayExtend,
		GatewayCardIMEI:       card.GatewayCardIMEI,
		DataUsageMB:           card.DataUsageMB,
		CurrentMonthUsageMB:   card.CurrentMonthUsageMB,
		LastGatewayReadingMB:  card.LastGatewayReadingMB,
		CurrentMonthStartDate: card.CurrentMonthStartDate,
		LastMonthTotalMB:      card.LastMonthTotalMB,
		LastDataCheckAt:       card.LastDataCheckAt,
		LastRealNameCheckAt:   card.LastRealNameCheckAt,
		EnablePolling:         card.EnablePolling,
		SeriesID:              card.SeriesID,
		DeviceVirtualNo:       card.DeviceVirtualNo,
		AssetStatus:           card.AssetStatus,
		AssetStatusName:       constants.GetAssetStatusName(card.AssetStatus),
		Generation:            card.Generation,
		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 {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardAllocate,
			OperationDesc: "单卡分配被拒绝",
			ResultStatus:  constants.AssetAuditResultDenied,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AfterData: map[string]any{
				"to_shop_id":      req.ToShopID,
				"selection_type":  req.SelectionType,
				"requested_count": len(req.ICCIDs),
			},
		})
		return nil, err
	}

	cards, err := s.getCardsForAllocation(ctx, req, operatorShopID)
	if err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardAllocate,
			OperationDesc: "单卡分配执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AfterData: map[string]any{
				"to_shop_id":      req.ToShopID,
				"selection_type":  req.SelectionType,
				"requested_count": len(req.ICCIDs),
			},
		})
		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 {
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardAllocate,
			OperationDesc: "单卡分配被拒绝",
			ResultStatus:  constants.AssetAuditResultDenied,
			ErrorMsg:      "无可分配卡",
			BatchTotal:    len(cards),
			FailCount:     len(failedItems),
			AfterData: map[string]any{
				"to_shop_id":     req.ToShopID,
				"failed_items":   failedItems,
				"selection_type": req.SelectionType,
			},
		})
		return &dto.AllocateStandaloneCardsResponse{
			TotalCount:   len(cards),
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	newStatus := constants.IotCardStatusDistributed
	toShopID := req.ToShopID
	allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)

	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
		}

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

	if err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardAllocate,
			OperationDesc: "单卡分配执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			BatchTotal:    len(cards),
			SuccessCount:  len(cardIDs),
			FailCount:     len(failedItems),
			AfterData: map[string]any{
				"to_shop_id":   req.ToShopID,
				"failed_items": failedItems,
			},
		})
		return nil, err
	}
	s.iotCardStore.InvalidateListCountCache(ctx)

	// 通知轮询调度器状态变化（异步执行 + 批量操作，避免 N 次单卡 DB 查询打满连接池）
	if s.pollingCallback != nil && len(cardIDs) > 0 {
		cardIDsCopy := make([]uint, len(cardIDs))
		copy(cardIDsCopy, cardIDs)
		cb := s.pollingCallback
		go func() {
			cb.OnBatchCardsStatusChanged(context.Background(), cardIDsCopy)
		}()
	}

	shopMap := s.loadShopNames(ctx, cards)
	targetShopName := s.getAuditShopName(ctx, &req.ToShopID)
	beforeData := make([]map[string]any, 0, len(cards))
	for _, card := range cards {
		beforeData = append(beforeData, map[string]any{
			"id":        card.ID,
			"iccid":     card.ICCID,
			"shop_id":   card.ShopID,
			"shop_name": shopMapValue(shopMap, card.ShopID),
			"status":    card.Status,
		})
	}
	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType: constants.AssetAuditOpCardAllocate,
		OperationDesc: "单卡分配",
		ResultStatus:  constants.AssetAuditResultSuccess,
		BatchTotal:    len(cards),
		SuccessCount:  len(cardIDs),
		FailCount:     len(failedItems),
		BeforeData: map[string]any{
			"cards": beforeData,
		},
		AfterData: map[string]any{
			"to_shop_id":   req.ToShopID,
			"to_shop_name": targetShopName,
			"new_status":   newStatus,
			"failed_items": failedItems,
		},
	})

	return &dto.AllocateStandaloneCardsResponse{
		TotalCount:   len(cards),
		SuccessCount: len(cardIDs),
		FailCount:    len(failedItems),
		AllocationNo: allocationNo,
		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 {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardRecall,
			OperationDesc: "单卡回收执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AfterData: map[string]any{
				"selection_type":  req.SelectionType,
				"requested_count": len(req.ICCIDs),
			},
		})
		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 {
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardRecall,
			OperationDesc: "单卡回收被拒绝",
			ResultStatus:  constants.AssetAuditResultDenied,
			ErrorMsg:      "无可回收卡",
			BatchTotal:    len(cards),
			FailCount:     len(failedItems),
			AfterData: map[string]any{
				"failed_items":   failedItems,
				"selection_type": req.SelectionType,
			},
		})
		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 {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardRecall,
			OperationDesc: "单卡回收执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			BatchTotal:    len(cards),
			SuccessCount:  len(cardIDs),
			FailCount:     len(failedItems),
			AfterData: map[string]any{
				"failed_items": failedItems,
			},
		})
		return nil, err
	}
	s.iotCardStore.InvalidateListCountCache(ctx)

	// 通知轮询调度器状态变化（异步批量执行，避免回收大批卡时打满 DB 连接池）
	if s.pollingCallback != nil && len(cardIDs) > 0 {
		cardIDsCopy := make([]uint, len(cardIDs))
		copy(cardIDsCopy, cardIDs)
		cb := s.pollingCallback
		go func() {
			cb.OnBatchCardsStatusChanged(context.Background(), cardIDsCopy)
		}()
	}

	shopMap := s.loadShopNames(ctx, successCards)
	targetShopName := s.getAuditRecallTargetShopName(ctx, newShopID)
	beforeData := make([]map[string]any, 0, len(successCards))
	for _, card := range successCards {
		beforeData = append(beforeData, map[string]any{
			"id":        card.ID,
			"iccid":     card.ICCID,
			"shop_id":   card.ShopID,
			"shop_name": shopMapValue(shopMap, card.ShopID),
			"status":    card.Status,
		})
	}
	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType: constants.AssetAuditOpCardRecall,
		OperationDesc: "单卡回收",
		ResultStatus:  constants.AssetAuditResultSuccess,
		BatchTotal:    len(cards),
		SuccessCount:  len(cardIDs),
		FailCount:     len(failedItems),
		BeforeData: map[string]any{
			"cards": beforeData,
		},
		AfterData: map[string]any{
			"to_shop_id":       newShopID,
			"target_shop_name": targetShopName,
			"new_status":       newStatus,
			"failed_items":     failedItems,
		},
	})

	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 {
		return nil
	}

	// 代理仅允许分配给直属下级店铺
	if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID {
		return errors.ErrNotDirectSubordinate
	}

	return nil
}

func (s *Service) getAuditShopName(ctx context.Context, shopID *uint) string {
	if shopID == nil || *shopID == 0 {
		return ""
	}
	var shop model.Shop
	if err := s.db.WithContext(ctx).Unscoped().First(&shop, *shopID).Error; err != nil {
		return ""
	}
	return shop.ShopName
}

func (s *Service) getAuditRecallTargetShopName(ctx context.Context, shopID *uint) string {
	if shopID == nil {
		return "平台库存"
	}
	return s.getAuditShopName(ctx, shopID)
}

func shopMapValue(shopMap map[uint]string, shopID *uint) string {
	if shopID == nil {
		return ""
	}
	return shopMap[*shopID]
}

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) {
	selectionType, err := normalizeCardSeriesBindingSelection(req)
	if err != nil {
		return nil, err
	}

	cards, err := s.getCardsForSeriesBinding(ctx, req, selectionType)
	batchTotal := cardSeriesBindingBatchTotal(req, selectionType, cards)
	auditData := cardSeriesBindingAuditData(req, selectionType)
	if err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardSeriesBinding,
			OperationDesc: "卡系列绑定执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			BatchTotal:    batchTotal,
			AfterData:     auditData,
		})
		return nil, err
	}

	if len(cards) == 0 {
		failedItems := []dto.CardSeriesBindngFailedItem{}
		if selectionType == dto.SelectionTypeList {
			failedItems = s.buildCardNotFoundFailedItems(req.ICCIDs)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardSeriesBinding,
				OperationDesc: "卡系列绑定被拒绝",
				ResultStatus:  constants.AssetAuditResultDenied,
				ErrorMsg:      "卡不存在",
				BatchTotal:    batchTotal,
				FailCount:     len(failedItems),
				AfterData:     auditData,
			})
		}
		return &dto.BatchSetCardSeriesBindngResponse{
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	cardMap := indexCardsByICCID(cards)

	// 验证系列存在（仅当 SeriesID > 0 时）
	if req.SeriesID > 0 {
		packageSeries, err := s.packageSeriesStore.GetByID(ctx, req.SeriesID)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				denyErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
				errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
				s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
					OperationType: constants.AssetAuditOpCardSeriesBinding,
					OperationDesc: "卡系列绑定被拒绝",
					ResultStatus:  constants.AssetAuditResultDenied,
					ErrorCode:     errorCode,
					ErrorMsg:      errorMsg,
					BatchTotal:    batchTotal,
					AfterData:     auditData,
				})
				return nil, denyErr
			}
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardSeriesBinding,
				OperationDesc: "卡系列绑定执行失败",
				ResultStatus:  constants.AssetAuditResultFailed,
				ErrorCode:     errorCode,
				ErrorMsg:      errorMsg,
				BatchTotal:    batchTotal,
				AfterData:     auditData,
			})
			return nil, err
		}
		if packageSeries.Status != 1 {
			denyErr := errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardSeriesBinding,
				OperationDesc: "卡系列绑定被拒绝",
				ResultStatus:  constants.AssetAuditResultDenied,
				ErrorCode:     errorCode,
				ErrorMsg:      errorMsg,
				BatchTotal:    batchTotal,
				AfterData:     auditData,
			})
			return nil, denyErr
		}
	}

	var successCardIDs []uint
	var failedItems []dto.CardSeriesBindngFailedItem
	successCardIDSet := make(map[uint]struct{})

	hasSeriesAllocation := true
	if operatorShopID != nil && req.SeriesID > 0 {
		hasSeriesAllocation, err = s.hasAvailableSeriesAllocation(ctx, *operatorShopID, req.SeriesID)
		if err != nil {
			return nil, err
		}
	}

	addCard := func(card *model.IotCard, requestedICCID string) {
		if card == nil {
			failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
				ICCID:  requestedICCID,
				Reason: "卡不存在",
			})
			return
		}

		if !hasSeriesAllocation {
			failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
				ICCID:  card.ICCID,
				Reason: "您没有权限分配该套餐系列",
			})
			return
		}

		// 代理只能操作自己店铺名下的卡，保持旧接口权限语义不变。
		if operatorShopID != nil && (card.ShopID == nil || *card.ShopID != *operatorShopID) {
			failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
				ICCID:  card.ICCID,
				Reason: "无权操作此卡",
			})
			return
		}

		if _, exists := successCardIDSet[card.ID]; exists {
			return
		}
		successCardIDSet[card.ID] = struct{}{}
		successCardIDs = append(successCardIDs, card.ID)
	}

	if selectionType == dto.SelectionTypeList {
		for _, iccid := range req.ICCIDs {
			addCard(cardMap[iccid], iccid)
		}
	} else {
		for _, card := range cards {
			addCard(card, card.ICCID)
		}
	}

	if len(successCardIDs) > 0 {
		var seriesIDPtr *uint
		if req.SeriesID > 0 {
			seriesIDPtr = &req.SeriesID
		}
		if err := s.iotCardStore.BatchUpdateSeriesID(ctx, successCardIDs, seriesIDPtr); err != nil {
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardSeriesBinding,
				OperationDesc: "卡系列绑定执行失败",
				ResultStatus:  constants.AssetAuditResultFailed,
				ErrorCode:     errorCode,
				ErrorMsg:      errorMsg,
				BatchTotal:    batchTotal,
				SuccessCount:  len(successCardIDs),
				FailCount:     len(failedItems),
				AfterData: map[string]any{
					"selection_type": selectionType,
					"series_id":      req.SeriesID,
					"success_ids":    successCardIDs,
					"failed_items":   failedItems,
				},
			})
			return nil, err
		}
	}

	resultStatus := constants.AssetAuditResultSuccess
	operationDesc := "批量设置卡系列绑定"
	errorMsg := ""
	if len(successCardIDs) == 0 && len(failedItems) > 0 {
		resultStatus = constants.AssetAuditResultDenied
		operationDesc = "卡系列绑定被拒绝"
		errorMsg = "无可操作卡"
	}
	beforeCards := make([]map[string]any, 0, len(successCardIDs))
	successSet := make(map[uint]struct{}, len(successCardIDs))
	for _, id := range successCardIDs {
		successSet[id] = struct{}{}
	}
	for _, card := range cards {
		if _, ok := successSet[card.ID]; !ok {
			continue
		}
		beforeCards = append(beforeCards, map[string]any{
			"id":        card.ID,
			"iccid":     card.ICCID,
			"series_id": card.SeriesID,
		})
	}
	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType: constants.AssetAuditOpCardSeriesBinding,
		OperationDesc: operationDesc,
		ResultStatus:  resultStatus,
		ErrorMsg:      errorMsg,
		BatchTotal:    batchTotal,
		SuccessCount:  len(successCardIDs),
		FailCount:     len(failedItems),
		BeforeData: map[string]any{
			"cards": beforeCards,
		},
		AfterData: map[string]any{
			"selection_type": selectionType,
			"series_id":      req.SeriesID,
			"success_ids":    successCardIDs,
			"failed_items":   failedItems,
		},
	})

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

func normalizeCardSeriesBindingSelection(req *dto.BatchSetCardSeriesBindngRequest) (string, error) {
	switch req.SelectionType {
	case dto.SelectionTypeList:
		if len(req.ICCIDs) == 0 {
			return "", errors.New(errors.CodeInvalidParam, "selection_type=list时iccids不能为空")
		}
		return dto.SelectionTypeList, nil
	case dto.SelectionTypeRange:
		if req.ICCIDStart == "" || req.ICCIDEnd == "" {
			return "", errors.New(errors.CodeInvalidParam, "selection_type=range时iccid_start和iccid_end不能为空")
		}
		if req.ICCIDStart > req.ICCIDEnd {
			return "", errors.New(errors.CodeInvalidParam, "ICCID起始号不能大于结束号")
		}
		return dto.SelectionTypeRange, nil
	case dto.SelectionTypeFilter:
		return dto.SelectionTypeFilter, nil
	case "":
		if len(req.ICCIDs) > 0 {
			return dto.SelectionTypeList, nil
		}
		if req.ICCIDStart != "" || req.ICCIDEnd != "" {
			if req.ICCIDStart == "" || req.ICCIDEnd == "" {
				return "", errors.New(errors.CodeInvalidParam, "iccid_start和iccid_end必须同时传入")
			}
			if req.ICCIDStart > req.ICCIDEnd {
				return "", errors.New(errors.CodeInvalidParam, "ICCID起始号不能大于结束号")
			}
			return dto.SelectionTypeRange, nil
		}
		if hasCardSeriesBindingFilters(req) {
			return dto.SelectionTypeFilter, nil
		}
		return "", errors.New(errors.CodeInvalidParam, "请选择要设置套餐系列的卡")
	default:
		return "", errors.New(errors.CodeInvalidParam, "无效的选卡方式")
	}
}

func hasCardSeriesBindingFilters(req *dto.BatchSetCardSeriesBindngRequest) bool {
	return req.Status != nil ||
		req.CarrierID != nil ||
		req.ShopID != nil ||
		len(req.ShopIDs) > 0 ||
		req.FilterSeriesID != nil ||
		req.ICCID != "" ||
		req.MSISDN != "" ||
		req.IsStandalone != nil ||
		req.BatchNo != "" ||
		req.PackageID != nil ||
		req.IsDistributed != nil ||
		req.IsReplaced != nil ||
		req.CarrierName != ""
}

func (s *Service) getCardsForSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, selectionType string) ([]*model.IotCard, error) {
	switch selectionType {
	case dto.SelectionTypeList:
		return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
	case dto.SelectionTypeRange:
		filters := map[string]any{
			"iccid_start": req.ICCIDStart,
			"iccid_end":   req.ICCIDEnd,
		}
		return s.iotCardStore.GetBySeriesBindingFilters(ctx, filters)
	case dto.SelectionTypeFilter:
		return s.iotCardStore.GetBySeriesBindingFilters(ctx, buildCardSeriesBindingFilters(req))
	default:
		return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
	}
}

func buildCardSeriesBindingFilters(req *dto.BatchSetCardSeriesBindngRequest) map[string]any {
	filters := make(map[string]any)
	if req.Status != nil {
		filters["status"] = *req.Status
	}
	if req.CarrierID != nil {
		filters["carrier_id"] = *req.CarrierID
	}
	shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
	if hasShopIDs {
		filters["shop_ids"] = shopIDs
	} else if req.ShopID != nil {
		if *req.ShopID == 0 {
			filters["shop_ids"] = []uint{}
		} else {
			filters["shop_id"] = *req.ShopID
		}
	}
	if req.FilterSeriesID != nil {
		filters["series_id"] = *req.FilterSeriesID
	}
	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.CarrierName != "" {
		filters["carrier_name"] = req.CarrierName
	}
	return filters
}

func (s *Service) hasAvailableSeriesAllocation(ctx context.Context, shopID uint, seriesID uint) (bool, error) {
	seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
	if err != nil {
		return false, err
	}
	for _, alloc := range seriesAllocations {
		if alloc.SeriesID == seriesID && alloc.Status == 1 {
			return true, nil
		}
	}
	return false, nil
}

func indexCardsByICCID(cards []*model.IotCard) map[string]*model.IotCard {
	cardMap := make(map[string]*model.IotCard, len(cards))
	for _, card := range cards {
		if card.ICCID != "" {
			cardMap[card.ICCID] = card
		}
		if card.ICCID19 != "" {
			cardMap[card.ICCID19] = card
		}
		if card.ICCID20 != nil && *card.ICCID20 != "" {
			cardMap[*card.ICCID20] = card
		}
	}
	return cardMap
}

func cardSeriesBindingBatchTotal(req *dto.BatchSetCardSeriesBindngRequest, selectionType string, cards []*model.IotCard) int {
	if selectionType == dto.SelectionTypeList {
		return len(req.ICCIDs)
	}
	return len(cards)
}

func cardSeriesBindingAuditData(req *dto.BatchSetCardSeriesBindngRequest, selectionType string) map[string]any {
	data := map[string]any{
		"selection_type": selectionType,
		"series_id":      req.SeriesID,
	}
	switch selectionType {
	case dto.SelectionTypeList:
		data["iccids"] = req.ICCIDs
	case dto.SelectionTypeRange:
		data["iccid_start"] = req.ICCIDStart
		data["iccid_end"] = req.ICCIDEnd
	case dto.SelectionTypeFilter:
		data["filters"] = buildCardSeriesBindingFilters(req)
	}
	return data
}

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
}

// RefreshCardDataByID 通过卡 ID 从 Gateway 同步卡数据
// 内部查询 ICCID 后委托给 RefreshCardDataFromGateway，供套餐失效前流量同步使用
func (s *Service) RefreshCardDataByID(ctx context.Context, cardID uint) error {
	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		return err
	}
	return s.RefreshCardDataFromGateway(ctx, card.ICCID)
}

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

	lockToken, locked, lockErr := s.acquireCardTrafficSyncLock(ctx, card.ID)
	if lockErr != nil {
		return errors.Wrap(errors.CodeInternalError, lockErr, "获取卡流量同步锁失败")
	} else if !locked {
		return errors.New(errors.CodeTooManyRequests, "卡流量正在同步，请稍后重试")
	} else {
		defer s.releaseCardTrafficSyncLock(ctx, card.ID, lockToken)
		latestCard, loadErr := s.iotCardStore.GetByID(ctx, card.ID)
		if loadErr != nil {
			return errors.Wrap(errors.CodeInternalError, loadErr, "刷新卡数据失败")
		}
		card = latestCard
	}

	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 if strings.TrimSpace(statusResp.ICCID) == "" {
			s.logger.Warn("刷新卡数据：网络状态响应缺少 ICCID，跳过本次网络观测", zap.Uint("card_id", card.ID))
		} else if s.cardObservation == nil {
			return errors.New(errors.CodeInternalError, "卡网络观测能力未配置")
		} else {
			requestID := requestIDFromContext(ctx)
			decision, applyErr := s.cardObservation.ApplyNetworkObservation(ctx, carddomain.NetworkObservation{
				CardID: card.ID, GatewayStatus: statusResp.CardStatus,
				GatewayExtend: statusResp.Extend, GatewayIMEI: statusResp.IMEI,
				Metadata: carddomain.ObservationMetadata{
					ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualSync,
					Scene: constants.CardObservationSceneManualRefresh, ObservedAt: syncTime,
					RequestID: requestID, CorrelationID: requestID,
				},
			})
			if applyErr != nil {
				return applyErr
			}
			if !decision.StatusKnown {
				s.logger.Warn("刷新卡数据：未知 Gateway 卡状态",
					zap.String("iccid", iccid),
					zap.String("card_status", statusResp.CardStatus),
					zap.String("extend", strings.TrimSpace(statusResp.Extend)))
			}
		}

		// 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 if strings.TrimSpace(realnameResp.ICCID) == "" {
			s.logger.Warn("刷新卡数据：实名响应缺少 ICCID，跳过本次实名观测", zap.Uint("card_id", card.ID))
		} else if s.cardObservation == nil {
			return errors.New(errors.CodeInternalError, "卡实名观测能力未配置")
		} else {
			requestID := requestIDFromContext(ctx)
			_, applyErr := s.cardObservation.ApplyCardObservation(ctx, carddomain.RealnameObservation{
				CardID:   card.ID,
				Verified: realnameResp.RealStatus,
				Metadata: carddomain.ObservationMetadata{
					ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualSync,
					Scene: constants.CardObservationSceneManualRefresh, ObservedAt: syncTime,
					RequestID: requestID, CorrelationID: requestID,
				},
			})
			if applyErr != nil {
				return applyErr
			}
		}

		// 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 if s.cardObservation == nil {
			return errors.New(errors.CodeInternalError, "卡流量观测能力未配置")
		} else {
			requestID := requestIDFromContext(ctx)
			resetDay := postgres.NewCarrierStore(s.db).GetDataResetDay(ctx, card.CarrierID)
			decision, applyErr := s.cardObservation.ApplyTrafficObservation(ctx, carddomain.TrafficObservation{
				CardID: card.ID, GatewayReadingMB: float64(flowResp.Used), ResetDay: resetDay,
				Metadata: carddomain.ObservationMetadata{
					ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualSync,
					Scene: constants.CardObservationSceneManualRefresh, ObservedAt: syncTime,
					RequestID: requestID, CorrelationID: requestID,
				},
			})
			if applyErr != nil {
				return applyErr
			}
			flowIncrementMB = decision.IncrementMB
		}
	}

	if err := s.iotCardStore.UpdateFields(ctx, card.ID, updates); err != nil {
		return errors.Wrap(errors.CodeInternalError, 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
}

// 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 {
			denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType:   constants.AssetAuditOpCardPollingStatus,
				OperationDesc:   "更新卡轮询状态被拒绝",
				ResultStatus:    constants.AssetAuditResultDenied,
				ErrorCode:       errorCode,
				ErrorMsg:        errorMsg,
				AssetID:         cardID,
				AssetIdentifier: "",
				AfterData: map[string]any{
					"enable_polling": enablePolling,
				},
			})
			return denyErr
		}
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardPollingStatus,
			OperationDesc: "更新卡轮询状态执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AssetID:       cardID,
			AfterData: map[string]any{
				"enable_polling": enablePolling,
			},
		})
		return err
	}

	// 检查是否需要更新
	if card.EnablePolling == enablePolling {
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardPollingStatus,
			OperationDesc:   "更新卡轮询状态",
			ResultStatus:    constants.AssetAuditResultSuccess,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"enable_polling": card.EnablePolling,
			},
			AfterData: map[string]any{
				"enable_polling": enablePolling,
			},
		})
		return nil // 状态未变化
	}

	// 更新数据库
	card.EnablePolling = enablePolling
	if err := s.iotCardStore.Update(ctx, card); err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardPollingStatus,
			OperationDesc:   "更新卡轮询状态执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"enable_polling": !enablePolling,
			},
			AfterData: map[string]any{
				"enable_polling": enablePolling,
			},
		})
		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)
		}
	}

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType:   constants.AssetAuditOpCardPollingStatus,
		OperationDesc:   "更新卡轮询状态",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData: map[string]any{
			"enable_polling": !enablePolling,
		},
		AfterData: map[string]any{
			"enable_polling": enablePolling,
		},
	})

	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 {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardPollingStatus,
			OperationDesc: "批量更新卡轮询状态执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			BatchTotal:    len(cardIDs),
			FailCount:     len(cardIDs),
			AfterData: map[string]any{
				"card_ids":       cardIDs,
				"enable_polling": enablePolling,
				"trigger_source": "batch",
			},
		})
		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)
			}
		}
	}

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType: constants.AssetAuditOpCardPollingStatus,
		OperationDesc: "批量更新卡轮询状态",
		ResultStatus:  constants.AssetAuditResultSuccess,
		BatchTotal:    len(cardIDs),
		SuccessCount:  len(cardIDs),
		AfterData: map[string]any{
			"card_ids":       cardIDs,
			"enable_polling": enablePolling,
			"trigger_source": "batch",
		},
	})

	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 {
			denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardDelete,
				OperationDesc: "删除卡被拒绝",
				ResultStatus:  constants.AssetAuditResultDenied,
				ErrorCode:     errorCode,
				ErrorMsg:      errorMsg,
				AssetID:       cardID,
			})
			return denyErr
		}
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardDelete,
			OperationDesc: "删除卡执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AssetID:       cardID,
		})
		return err
	}

	if err := s.iotCardStore.Delete(ctx, cardID); err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardDelete,
			OperationDesc:   "删除卡执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		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)
	}

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType:   constants.AssetAuditOpCardDelete,
		OperationDesc:   "删除卡",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData:      cardSnapshot(card),
		AfterData: map[string]any{
			"deleted": true,
		},
	})

	return nil
}

// BatchDeleteCards 批量删除卡（软删除）
func (s *Service) BatchDeleteCards(ctx context.Context, cardIDs []uint) error {
	if len(cardIDs) == 0 {
		return nil
	}
	cards, queryErr := s.iotCardStore.GetByIDs(ctx, cardIDs)
	if queryErr != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(queryErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardBatchDelete,
			OperationDesc: "批量删除卡执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			BatchTotal:    len(cardIDs),
			FailCount:     len(cardIDs),
			AfterData: map[string]any{
				"card_ids": cardIDs,
			},
		})
		return queryErr
	}

	// 批量软删除
	if err := s.iotCardStore.BatchDelete(ctx, cardIDs); err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardBatchDelete,
			OperationDesc: "批量删除卡执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			BatchTotal:    len(cardIDs),
			FailCount:     len(cardIDs),
			AfterData: map[string]any{
				"card_ids": cardIDs,
			},
		})
		return err
	}

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

	// 通知轮询调度器
	if s.pollingCallback != nil {
		for _, cardID := range cardIDs {
			s.pollingCallback.OnCardDeleted(ctx, cardID)
		}
	}
	beforeCards := make([]map[string]any, 0, len(cards))
	for _, card := range cards {
		beforeCards = append(beforeCards, cardSnapshot(card))
	}
	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType: constants.AssetAuditOpCardBatchDelete,
		OperationDesc: "批量删除卡",
		ResultStatus:  constants.AssetAuditResultSuccess,
		BatchTotal:    len(cardIDs),
		SuccessCount:  len(cardIDs),
		BeforeData: map[string]any{
			"cards": beforeCards,
		},
		AfterData: map[string]any{
			"card_ids": cardIDs,
			"deleted":  true,
		},
	})

	return nil
}

// UpdateRealnamePolicy 更新卡的实名认证策略
func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnamePolicy string) error {
	// 检查卡是否存在
	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardRealnamePolicy,
				OperationDesc: "更新卡实名认证策略被拒绝",
				ResultStatus:  constants.AssetAuditResultDenied,
				ErrorCode:     errorCode,
				ErrorMsg:      errorMsg,
				AssetID:       cardID,
				AfterData: map[string]any{
					"realname_policy": realnamePolicy,
				},
			})
			return denyErr
		}
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardRealnamePolicy,
			OperationDesc: "更新卡实名认证策略执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AssetID:       cardID,
			AfterData: map[string]any{
				"realname_policy": realnamePolicy,
			},
		})
		return wrapErr
	}

	// 幂等检查
	if card.RealnamePolicy == realnamePolicy {
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardRealnamePolicy,
			OperationDesc:   "更新卡实名认证策略",
			ResultStatus:    constants.AssetAuditResultSuccess,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"realname_policy": card.RealnamePolicy,
			},
			AfterData: map[string]any{
				"realname_policy": realnamePolicy,
			},
		})
		return nil
	}

	// 更新数据库
	if err := s.iotCardStore.UpdateRealnamePolicy(ctx, cardID, realnamePolicy); err != nil {
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardRealnamePolicy,
			OperationDesc:   "更新卡实名认证策略执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"realname_policy": card.RealnamePolicy,
			},
			AfterData: map[string]any{
				"realname_policy": realnamePolicy,
			},
		})
		return wrapErr
	}

	s.logger.Info("更新卡实名认证策略",
		zap.Uint("card_id", cardID),
		zap.String("realname_policy", realnamePolicy),
	)

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType:   constants.AssetAuditOpCardRealnamePolicy,
		OperationDesc:   "更新卡实名认证策略",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData: map[string]any{
			"realname_policy": card.RealnamePolicy,
		},
		AfterData: map[string]any{
			"realname_policy": realnamePolicy,
		},
	})

	return nil
}

// ManualUpdateRealnameStatus 手动更新卡实名状态
// 用于人工纠偏实名状态，并同步触发实名相关业务（套餐激活、停复机评估、轮询缓存失效）
func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, realNameStatus int) (*model.IotCard, error) {
	if realNameStatus != constants.RealNameStatusNotVerified &&
		realNameStatus != constants.RealNameStatusVerified {
		denyErr := errors.New(errors.CodeInvalidParam, "无效的实名状态")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardRealnameStatus,
			OperationDesc: "手动更新卡实名状态被拒绝",
			ResultStatus:  constants.AssetAuditResultDenied,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AssetID:       cardID,
			AfterData: map[string]any{
				"real_name_status": realNameStatus,
			},
		})
		return nil, denyErr
	}

	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType: constants.AssetAuditOpCardRealnameStatus,
				OperationDesc: "手动更新卡实名状态被拒绝",
				ResultStatus:  constants.AssetAuditResultDenied,
				ErrorCode:     errorCode,
				ErrorMsg:      errorMsg,
				AssetID:       cardID,
				AfterData: map[string]any{
					"real_name_status": realNameStatus,
				},
			})
			return nil, denyErr
		}
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType: constants.AssetAuditOpCardRealnameStatus,
			OperationDesc: "手动更新卡实名状态执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AssetID:       cardID,
			AfterData: map[string]any{
				"real_name_status": realNameStatus,
			},
		})
		return nil, wrapErr
	}

	oldStatus := card.RealNameStatus
	now := time.Now()
	if s.cardObservation == nil {
		return nil, errors.New(errors.CodeInternalError, "卡实名观测能力未配置")
	}
	requestID := requestIDFromContext(ctx)
	if _, err := s.cardObservation.ApplyCardObservation(ctx, carddomain.RealnameObservation{
		CardID:   cardID,
		Verified: realNameStatus == constants.RealNameStatusVerified,
		Metadata: carddomain.ObservationMetadata{
			ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualOverride,
			Scene: constants.CardObservationSceneManualRefresh, ObservedAt: now,
			RequestID: requestID, CorrelationID: requestID,
		},
	}); err != nil {
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡实名状态失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardRealnameStatus,
			OperationDesc:   "手动更新卡实名状态执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"real_name_status": oldStatus,
			},
			AfterData: map[string]any{
				"real_name_status": realNameStatus,
			},
		})
		return nil, wrapErr
	}

	freshCard, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "查询更新后的IoT卡失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardRealnameStatus,
			OperationDesc:   "手动更新卡实名状态执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"real_name_status": oldStatus,
			},
			AfterData: map[string]any{
				"real_name_status": realNameStatus,
			},
		})
		return nil, wrapErr
	}

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

	s.logger.Info("手动更新卡实名状态",
		zap.Uint("card_id", cardID),
		zap.Int("old_status", oldStatus),
		zap.Int("new_status", realNameStatus),
		zap.Uint("operator_id", middleware.GetUserIDFromContext(ctx)))

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType:   constants.AssetAuditOpCardRealnameStatus,
		OperationDesc:   "手动更新卡实名状态",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         freshCard.ID,
		AssetIdentifier: freshCard.ICCID,
		BeforeData: map[string]any{
			"real_name_status":  oldStatus,
			"first_realname_at": card.FirstRealnameAt,
		},
		AfterData: map[string]any{
			"real_name_status":  freshCard.RealNameStatus,
			"first_realname_at": freshCard.FirstRealnameAt,
		},
	})

	return freshCard, nil
}

func requestIDFromContext(ctx context.Context) string {
	requestID := middleware.GetRequestIDFromContext(ctx)
	if requestID == nil {
		return ""
	}
	return *requestID
}

// handleManualRealnameStatusChanged 处理手动实名状态变更后的联动逻辑
func (s *Service) handleManualRealnameStatusChanged(ctx context.Context, oldStatus int, card *model.IotCard) {
	if card == nil {
		return
	}

	s.clearRealnameReversalCounter(ctx, card.ID)

	if oldStatus != constants.RealNameStatusVerified &&
		card.RealNameStatus == constants.RealNameStatusVerified {
		s.triggerRealnameActivation(ctx, constants.AssetTypeIotCard, card.ID)

		if s.deviceSimBindingStore != nil {
			binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
			if err == nil && binding != nil {
				s.triggerRealnameActivation(ctx, constants.AssetTypeDevice, binding.DeviceID)
			} else if err != nil && !stderrors.Is(err, gorm.ErrRecordNotFound) {
				s.logger.Warn("手动实名后查询绑定设备失败",
					zap.Uint("card_id", card.ID),
					zap.Error(err))
			}
		}
	}

	if s.stopResumeService != nil {
		if err := s.stopResumeService.EvaluateAndAct(ctx, card); err != nil {
			s.logger.Warn("手动实名后触发停复机评估失败",
				zap.Uint("card_id", card.ID),
				zap.Error(err))
		}
	}
}

// triggerRealnameActivation 触发待实名套餐激活
func (s *Service) triggerRealnameActivation(ctx context.Context, carrierType string, carrierID uint) {
	if s.realnameActivator == nil {
		return
	}
	if err := s.realnameActivator.ActivateByRealname(ctx, carrierType, carrierID); err != nil {
		s.logger.Warn("手动实名后触发待实名套餐激活失败",
			zap.String("carrier_type", carrierType),
			zap.Uint("carrier_id", carrierID),
			zap.Error(err))
	}
}

// clearRealnameReversalCounter 清理实名逆转连续计数器
func (s *Service) clearRealnameReversalCounter(ctx context.Context, cardID uint) {
	if s.redis == nil {
		return
	}
	if err := s.redis.Del(ctx, constants.RedisPollingRealnameReversalCountKey(cardID)).Err(); err != nil {
		s.logger.Warn("清理实名逆转计数器失败",
			zap.Uint("card_id", cardID),
			zap.Error(err))
	}
}
