package device

import (
	"context"
	"strings"
	"time"

	cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/break/junhong_cmp_fiber/internal/gateway"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/model/dto"
	packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
	"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/logger"
	"github.com/break/junhong_cmp_fiber/pkg/middleware"
)

type Service struct {
	db                         *gorm.DB
	redis                      *redis.Client
	deviceStore                *postgres.DeviceStore
	deviceSimBindingStore      *postgres.DeviceSimBindingStore
	iotCardStore               *postgres.IotCardStore
	shopStore                  *postgres.ShopStore
	assetAllocationRecordStore *postgres.AssetAllocationRecordStore
	shopPackageAllocationStore *postgres.ShopPackageAllocationStore
	shopSeriesAllocationStore  *postgres.ShopSeriesAllocationStore
	packageSeriesStore         *postgres.PackageSeriesStore
	gatewayClient              *gateway.Client
	assetIdentifierStore       *postgres.AssetIdentifierStore
	assetAuditService          AssetAuditService
	enterpriseDeviceAuthStore  *postgres.EnterpriseDeviceAuthorizationStore
	enterpriseStore            *postgres.EnterpriseStore
	packageExpiryQuery         *packageexpiry.Query
	observationSeriesEvents    cardObservationApp.SeriesEventWriter
	observationSeries          cardObservationApp.BestEffortSeriesDispatcher
}

// SetObservationSeriesEventWriter 注入设备停复机成功观测序列 Outbox Writer。
func (s *Service) SetObservationSeriesEventWriter(writer cardObservationApp.SeriesEventWriter) {
	s.observationSeriesEvents = writer
}

// SetObservationSeriesDispatcher 注入设备控制成功后的后台观测分发器。
func (s *Service) SetObservationSeriesDispatcher(dispatcher cardObservationApp.BestEffortSeriesDispatcher) {
	s.observationSeries = dispatcher
}

type deviceControlObservationSnapshot struct {
	SourceCardID uint
	TargetCardID uint
	BoundCardIDs []uint
	TargetICCID  string
}

func (s *Service) captureDeviceControlObservation(ctx context.Context, deviceID, knownTargetCardID uint, targetICCID string) deviceControlObservationSnapshot {
	snapshot := deviceControlObservationSnapshot{TargetICCID: targetICCID}
	bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
	if err != nil {
		logger.GetAppLogger().Warn("冻结设备控制观测绑定快照失败，继续执行原操作", zap.Uint("device_id", deviceID), zap.Error(err))
		return snapshot
	}
	bound := make(map[uint]struct{}, len(bindings))
	for _, binding := range bindings {
		if binding == nil || binding.IotCardID == 0 {
			continue
		}
		snapshot.BoundCardIDs = append(snapshot.BoundCardIDs, binding.IotCardID)
		bound[binding.IotCardID] = struct{}{}
		if binding.IsCurrent {
			snapshot.SourceCardID = binding.IotCardID
		}
	}
	if knownTargetCardID != 0 {
		if _, ok := bound[knownTargetCardID]; ok {
			snapshot.TargetCardID = knownTargetCardID
		}
		return snapshot
	}
	if strings.TrimSpace(targetICCID) == "" || len(snapshot.BoundCardIDs) == 0 {
		return snapshot
	}
	cards, err := s.iotCardStore.GetByIDs(ctx, snapshot.BoundCardIDs)
	if err != nil {
		logger.GetAppLogger().Warn("冻结设备控制观测目标卡快照失败，继续执行原操作", zap.Uint("device_id", deviceID), zap.String("target_iccid", targetICCID), zap.Error(err))
		return snapshot
	}
	for _, card := range cards {
		if card != nil && cardMatchesICCID(card, targetICCID) {
			snapshot.TargetCardID = card.ID
			break
		}
	}
	return snapshot
}

func cardMatchesICCID(card *model.IotCard, expected string) bool {
	expected = strings.TrimSpace(expected)
	return strings.EqualFold(strings.TrimSpace(card.ICCID), expected) ||
		strings.EqualFold(strings.TrimSpace(card.ICCID19), expected) ||
		(card.ICCID20 != nil && strings.EqualFold(strings.TrimSpace(*card.ICCID20), expected))
}

func (s *Service) dispatchDeviceControlObservation(ctx context.Context, deviceID uint, scene string, snapshot deviceControlObservationSnapshot, includeTargetTraffic bool) {
	if s.observationSeries == nil || deviceID == 0 {
		return
	}
	requestID := ""
	if value := middleware.GetRequestIDFromContext(ctx); value != nil {
		requestID = *value
	}
	request := cardObservationApp.SeriesRequest{
		Scene: scene, Source: constants.CardObservationSourceBusinessEvent,
		RequestID: requestID, CorrelationID: requestID,
	}
	s.observationSeries.DispatchDeviceControl(ctx, cardObservationApp.DeviceControlSeriesRequest{
		DeviceID: deviceID, TargetICCID: snapshot.TargetICCID,
		SourceCardID: snapshot.SourceCardID, TargetCardID: snapshot.TargetCardID,
		BoundCardIDs:         snapshot.BoundCardIDs,
		IncludeTargetTraffic: includeTargetTraffic, Request: request,
	})
}

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

func New(
	db *gorm.DB,
	rds *redis.Client,
	deviceStore *postgres.DeviceStore,
	deviceSimBindingStore *postgres.DeviceSimBindingStore,
	iotCardStore *postgres.IotCardStore,
	shopStore *postgres.ShopStore,
	assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
	shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
	shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
	packageSeriesStore *postgres.PackageSeriesStore,
	gatewayClient *gateway.Client,
	assetIdentifierStore *postgres.AssetIdentifierStore,
	assetAuditService AssetAuditService,
	enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore,
	enterpriseStore *postgres.EnterpriseStore,
) *Service {
	return &Service{
		db:                         db,
		redis:                      rds,
		deviceStore:                deviceStore,
		deviceSimBindingStore:      deviceSimBindingStore,
		iotCardStore:               iotCardStore,
		shopStore:                  shopStore,
		assetAllocationRecordStore: assetAllocationRecordStore,
		shopPackageAllocationStore: shopPackageAllocationStore,
		shopSeriesAllocationStore:  shopSeriesAllocationStore,
		packageSeriesStore:         packageSeriesStore,
		gatewayClient:              gatewayClient,
		assetIdentifierStore:       assetIdentifierStore,
		assetAuditService:          assetAuditService,
		enterpriseDeviceAuthStore:  enterpriseDeviceAuthStore,
		enterpriseStore:            enterpriseStore,
		packageExpiryQuery:         packageexpiry.NewQuery(db),
	}
}

// List 获取设备列表
func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.ListDeviceResponse, 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.VirtualNo != "" {
		filters["virtual_no"] = req.VirtualNo
	}
	if req.IMEI != "" {
		filters["imei"] = req.IMEI
	}
	if req.Keyword != "" {
		filters["keyword"] = req.Keyword
	}
	if req.DeviceName != "" {
		filters["device_name"] = req.DeviceName
	}
	if req.Status != nil {
		filters["status"] = *req.Status
	}
	if req.ActivationStatus != nil {
		filters["activation_status"] = *req.ActivationStatus
	}
	if req.RealNameStatus != nil {
		filters["real_name_status"] = *req.RealNameStatus
	}
	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.BatchNo != "" {
		filters["batch_no"] = req.BatchNo
	}
	if req.DeviceType != "" {
		filters["device_type"] = req.DeviceType
	}
	if req.Manufacturer != "" {
		filters["manufacturer"] = req.Manufacturer
	}
	if req.CreatedAtStart != nil {
		filters["created_at_start"] = *req.CreatedAtStart
	}
	if req.CreatedAtEnd != nil {
		filters["created_at_end"] = *req.CreatedAtEnd
	}
	if req.SeriesID != nil {
		filters["series_id"] = *req.SeriesID
	}
	if req.HasActivePackage != nil {
		filters["has_active_package"] = *req.HasActivePackage
	}
	if req.AuthorizedEnterpriseID != nil {
		filters["authorized_enterprise_id"] = *req.AuthorizedEnterpriseID
	}
	if req.IsAuthorizedToEnterprise != nil {
		filters["is_authorized_to_enterprise"] = *req.IsAuthorizedToEnterprise
	}

	devices, total, err := s.deviceStore.List(ctx, opts, filters)
	if err != nil {
		return nil, err
	}
	deviceIDs := s.extractDeviceIDs(devices)
	expiryEstimates, err := s.packageExpiryQuery.ResolveBatch(ctx, constants.AssetTypeDevice, deviceIDs)
	if err != nil {
		return nil, err
	}

	shopMap := s.loadShopData(ctx, devices)
	seriesMap := s.loadSeriesNames(ctx, devices)
	bindingCounts, err := s.getBindingCounts(ctx, deviceIDs)
	if err != nil {
		return nil, err
	}
	activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, deviceIDs)
	if err != nil {
		return nil, err
	}

	// 批量查询有效企业授权（避免 N+1 查询）
	deviceEnterpriseMap, err := s.enterpriseDeviceAuthStore.GetActiveAuthEnterpriseByDeviceIDs(ctx, deviceIDs)
	if err != nil {
		return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备企业授权失败")
	}
	enterpriseIDs := make([]uint, 0)
	seen := make(map[uint]struct{})
	for _, eid := range deviceEnterpriseMap {
		if _, ok := seen[eid]; !ok {
			seen[eid] = struct{}{}
			enterpriseIDs = append(enterpriseIDs, eid)
		}
	}
	enterpriseNameMap, err := s.enterpriseStore.GetNameMapByIDs(ctx, enterpriseIDs)
	if err != nil {
		return nil, errors.Wrap(errors.CodeInternalError, err, "查询企业名称失败")
	}

	list := make([]*dto.DeviceResponse, 0, len(devices))
	for _, device := range devices {
		item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses)
		item.PackageExpiryEstimate = expiryEstimates[device.ID]
		if eid, ok := deviceEnterpriseMap[device.ID]; ok {
			item.AuthorizedEnterpriseID = &eid
			item.AuthorizedEnterpriseName = enterpriseNameMap[eid]
		}
		list = append(list, item)
	}

	return &dto.ListDeviceResponse{
		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
}

// Get 获取设备详情
func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error) {
	device, err := s.deviceStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeNotFound, "设备不存在")
		}
		return nil, err
	}

	shopMap := s.loadShopData(ctx, []*model.Device{device})
	seriesMap := s.loadSeriesNames(ctx, []*model.Device{device})
	bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
	if err != nil {
		return nil, err
	}
	activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
	if err != nil {
		return nil, err
	}

	return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
}

// GetByDeviceNo 通过设备号获取设备详情
func (s *Service) GetByDeviceNo(ctx context.Context, deviceNo string) (*dto.DeviceResponse, error) {
	device, err := s.deviceStore.GetByDeviceNo(ctx, deviceNo)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeNotFound, "设备不存在")
		}
		return nil, err
	}

	shopMap := s.loadShopData(ctx, []*model.Device{device})
	seriesMap := s.loadSeriesNames(ctx, []*model.Device{device})
	bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
	if err != nil {
		return nil, err
	}
	activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
	if err != nil {
		return nil, err
	}

	return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
}

// GetByIdentifier 通过任意标识符获取设备详情
// 支持 device_no（虚拟号）、imei、sn 三个字段的自动匹配
func (s *Service) GetByIdentifier(ctx context.Context, identifier string) (*dto.DeviceResponse, error) {
	device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeNotFound, "设备不存在")
		}
		return nil, err
	}

	shopMap := s.loadShopData(ctx, []*model.Device{device})
	seriesMap := s.loadSeriesNames(ctx, []*model.Device{device})
	bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
	if err != nil {
		return nil, err
	}
	activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
	if err != nil {
		return nil, err
	}

	return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
}

// GetDeviceByIdentifier 通过任意标识符获取设备模型（内部使用，不转为 DTO）
// 用于 Handler 层获取设备后提取 IMEI 调用 Gateway API
func (s *Service) GetDeviceByIdentifier(ctx context.Context, identifier string) (*model.Device, error) {
	device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeNotFound, "设备不存在或无权限访问")
		}
		return nil, err
	}
	return device, nil
}

func (s *Service) Delete(ctx context.Context, id uint) error {
	auditDevice := &model.Device{}
	auditDevice.ID = id
	device, err := s.deviceStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			appErr := errors.New(errors.CodeNotFound, "设备不存在")
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceDelete,
				"删除设备失败",
				constants.AssetAuditResultFailed,
				auditDevice,
				nil,
				nil,
				0,
				0,
				0,
				appErr,
			)
			return appErr
		}
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceDelete,
			"删除设备失败",
			constants.AssetAuditResultFailed,
			auditDevice,
			nil,
			nil,
			0,
			0,
			0,
			err,
		)
		return err
	}
	beforeData := deviceSnapshot(device)

	if err := s.deviceSimBindingStore.UnbindByDeviceID(ctx, device.ID); err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceDelete,
			"删除设备失败",
			constants.AssetAuditResultFailed,
			device,
			beforeData,
			nil,
			0,
			0,
			0,
			err,
		)
		return err
	}

	if err := s.deviceStore.Delete(ctx, id); err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceDelete,
			"删除设备失败",
			constants.AssetAuditResultFailed,
			device,
			beforeData,
			nil,
			0,
			0,
			0,
			err,
		)
		return err
	}

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

	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpDeviceDelete,
		"删除设备",
		constants.AssetAuditResultSuccess,
		device,
		beforeData,
		map[string]any{
			"deleted": true,
		},
		0,
		0,
		0,
		nil,
	)

	return nil
}

// GetByVirtualNo 通过虚拟号获取设备
func (s *Service) GetByVirtualNo(ctx context.Context, virtualNo string) (*model.Device, error) {
	return s.deviceStore.GetByIdentifier(ctx, virtualNo)
}

// GetCardByICCID 通过 ICCID 获取 IoT 卡
func (s *Service) GetCardByICCID(ctx context.Context, iccid string) (*model.IotCard, error) {
	cards, err := s.iotCardStore.GetByICCIDs(ctx, []string{iccid})
	if err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡失败")
	}
	if len(cards) == 0 {
		return nil, errors.New(errors.CodeNotFound, "卡不存在")
	}
	return cards[0], nil
}

// AllocateDevices 批量分配设备
func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateDevicesResponse, error) {
	// 代理仅可分配给直属下级；平台/超级管理员可跨级分配
	if err := s.validateDirectSubordinate(ctx, operatorShopID, req.TargetShopID); err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceAllocate,
			"设备分配被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{
				"target_shop_id": req.TargetShopID,
				"device_ids":     req.DeviceIDs,
			},
			len(req.DeviceIDs),
			0,
			len(req.DeviceIDs),
			err,
		)
		return nil, err
	}

	devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
	if err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceAllocate,
			"设备分配失败",
			constants.AssetAuditResultFailed,
			nil,
			nil,
			map[string]any{
				"target_shop_id": req.TargetShopID,
				"device_ids":     req.DeviceIDs,
			},
			len(req.DeviceIDs),
			0,
			len(req.DeviceIDs),
			err,
		)
		return nil, err
	}

	if len(devices) == 0 {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceAllocate,
			"设备分配被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{
				"target_shop_id": req.TargetShopID,
				"device_ids":     req.DeviceIDs,
				"reason":         "未找到可分配设备",
			},
			len(req.DeviceIDs),
			0,
			len(req.DeviceIDs),
			errors.New(errors.CodeNotFound, "未找到可分配设备"),
		)
		return &dto.AllocateDevicesResponse{
			SuccessCount: 0,
			FailCount:    0,
			FailedItems:  []dto.AllocationDeviceFailedItem{},
		}, nil
	}

	var deviceIDs []uint
	var failedItems []dto.AllocationDeviceFailedItem

	isPlatform := operatorShopID == nil

	for _, device := range devices {
		// 平台只能分配 shop_id=NULL 的设备
		if isPlatform && device.ShopID != nil {
			failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
				DeviceID:  device.ID,
				VirtualNo: device.VirtualNo,
				Reason:    "平台只能分配库存设备",
			})
			continue
		}

		// 代理只能分配自己店铺的设备
		if !isPlatform && (device.ShopID == nil || *device.ShopID != *operatorShopID) {
			failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
				DeviceID:  device.ID,
				VirtualNo: device.VirtualNo,
				Reason:    "设备不属于当前店铺",
			})
			continue
		}

		deviceIDs = append(deviceIDs, device.ID)
	}

	if len(deviceIDs) == 0 {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceAllocate,
			"设备分配被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{
				"target_shop_id": req.TargetShopID,
				"failed_items":   failedItems,
			},
			len(devices),
			0,
			len(failedItems),
			errors.New(errors.CodeForbidden, "无可分配设备"),
		)
		return &dto.AllocateDevicesResponse{
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	newStatus := constants.DeviceStatusDistributed
	targetShopID := req.TargetShopID
	targetShopName := s.getAuditShopName(ctx, &targetShopID)
	shopMap := s.loadShopData(ctx, devices)
	beforeData := make([]map[string]any, 0, len(devices))
	for _, device := range devices {
		snapshot := deviceSnapshot(device)
		snapshot["shop_name"] = deviceShopMapValue(shopMap, device.ShopID)
		beforeData = append(beforeData, snapshot)
	}

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

		if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, &targetShopID, newStatus); err != nil {
			return err
		}

		boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs)
		if err != nil {
			return err
		}

		if len(boundCardIDs) > 0 {
			if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, &targetShopID, constants.IotCardStatusDistributed); err != nil {
				return err
			}
		}

		allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
		records := s.buildAllocationRecords(devices, deviceIDs, operatorShopID, targetShopID, operatorID, allocationNo, req.Remark)
		return txRecordStore.BatchCreate(ctx, records)
	})

	if err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceAllocate,
			"设备分配失败",
			constants.AssetAuditResultFailed,
			nil,
			map[string]any{"devices": beforeData},
			map[string]any{
				"target_shop_id":   req.TargetShopID,
				"target_shop_name": targetShopName,
				"failed_items":     failedItems,
			},
			len(devices),
			len(deviceIDs),
			len(failedItems),
			err,
		)
		return nil, err
	}
	s.iotCardStore.InvalidateListCountCache(ctx)

	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpDeviceAllocate,
		"设备分配",
		constants.AssetAuditResultSuccess,
		nil,
		map[string]any{"devices": beforeData},
		map[string]any{
			"target_shop_id":   req.TargetShopID,
			"target_shop_name": targetShopName,
			"device_ids":       deviceIDs,
			"failed_items":     failedItems,
		},
		len(devices),
		len(deviceIDs),
		len(failedItems),
		nil,
	)

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

// RecallDevices 批量回收设备
func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.RecallDevicesResponse, error) {
	devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
	if err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceRecall,
			"设备回收失败",
			constants.AssetAuditResultFailed,
			nil,
			nil,
			map[string]any{
				"device_ids": req.DeviceIDs,
			},
			len(req.DeviceIDs),
			0,
			len(req.DeviceIDs),
			err,
		)
		return nil, err
	}

	if len(devices) == 0 {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceRecall,
			"设备回收被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{
				"device_ids": req.DeviceIDs,
				"reason":     "未找到可回收设备",
			},
			len(req.DeviceIDs),
			0,
			len(req.DeviceIDs),
			errors.New(errors.CodeNotFound, "未找到可回收设备"),
		)
		return &dto.RecallDevicesResponse{
			SuccessCount: 0,
			FailCount:    0,
			FailedItems:  []dto.AllocationDeviceFailedItem{},
		}, nil
	}

	var deviceIDs []uint
	var failedItems []dto.AllocationDeviceFailedItem

	isPlatform := operatorShopID == nil

	for _, device := range devices {
		// 验证设备所属店铺是否为直属下级
		if device.ShopID == nil {
			failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
				DeviceID:  device.ID,
				VirtualNo: device.VirtualNo,
				Reason:    "设备已在平台库存中",
			})
			continue
		}

		// 验证直属下级关系（平台用户可以回收所有店铺的设备）
		if !isPlatform {
			if err := s.validateDirectSubordinate(ctx, operatorShopID, *device.ShopID); err != nil {
				failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
					DeviceID:  device.ID,
					VirtualNo: device.VirtualNo,
					Reason:    "只能回收直属下级店铺的设备",
				})
				continue
			}
		}

		deviceIDs = append(deviceIDs, device.ID)
	}

	if len(deviceIDs) == 0 {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceRecall,
			"设备回收被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{
				"device_ids":   req.DeviceIDs,
				"failed_items": failedItems,
			},
			len(devices),
			0,
			len(failedItems),
			errors.New(errors.CodeForbidden, "无可回收设备"),
		)
		return &dto.RecallDevicesResponse{
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	var newShopID *uint
	var newStatus int
	if isPlatform {
		newShopID = nil
		newStatus = constants.DeviceStatusInStock
	} else {
		newShopID = operatorShopID
		newStatus = constants.DeviceStatusDistributed
	}
	targetShopName := s.getAuditRecallTargetShopName(ctx, newShopID)
	shopMap := s.loadShopData(ctx, devices)
	beforeData := make([]map[string]any, 0, len(devices))
	for _, device := range devices {
		snapshot := deviceSnapshot(device)
		snapshot["shop_name"] = deviceShopMapValue(shopMap, device.ShopID)
		beforeData = append(beforeData, snapshot)
	}

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

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

		boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs)
		if err != nil {
			return err
		}

		if len(boundCardIDs) > 0 {
			var cardStatus int
			if isPlatform {
				cardStatus = constants.IotCardStatusInStock
			} else {
				cardStatus = constants.IotCardStatusDistributed
			}
			if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, newShopID, cardStatus); err != nil {
				return err
			}
		}

		allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall)
		records := s.buildRecallRecords(devices, deviceIDs, operatorShopID, newShopID, operatorID, allocationNo, req.Remark)
		return txRecordStore.BatchCreate(ctx, records)
	})

	if err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceRecall,
			"设备回收失败",
			constants.AssetAuditResultFailed,
			nil,
			map[string]any{"devices": beforeData},
			map[string]any{
				"device_ids":       deviceIDs,
				"failed_items":     failedItems,
				"target_shop_name": targetShopName,
			},
			len(devices),
			len(deviceIDs),
			len(failedItems),
			err,
		)
		return nil, err
	}
	s.iotCardStore.InvalidateListCountCache(ctx)

	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpDeviceRecall,
		"设备回收",
		constants.AssetAuditResultSuccess,
		nil,
		map[string]any{"devices": beforeData},
		map[string]any{
			"device_ids":       deviceIDs,
			"failed_items":     failedItems,
			"target_shop":      newShopID,
			"target_shop_name": targetShopName,
		},
		len(devices),
		len(deviceIDs),
		len(failedItems),
		nil,
	)

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

// 辅助方法

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 deviceShopMapValue(shopMap map[uint]string, shopID *uint) string {
	if shopID == nil {
		return ""
	}
	return shopMap[*shopID]
}

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

	for _, device := range devices {
		if device.ShopID != nil && *device.ShopID > 0 && !shopIDSet[*device.ShopID] {
			shopIDs = append(shopIDs, *device.ShopID)
			shopIDSet[*device.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, devices []*model.Device) map[uint]string {
	seriesIDs := make([]uint, 0)
	seriesIDSet := make(map[uint]bool)

	for _, device := range devices {
		if device.SeriesID != nil && *device.SeriesID > 0 && !seriesIDSet[*device.SeriesID] {
			seriesIDs = append(seriesIDs, *device.SeriesID)
			seriesIDSet[*device.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) getBindingCounts(ctx context.Context, deviceIDs []uint) (map[uint]int64, error) {
	result := make(map[uint]int64)
	if len(deviceIDs) == 0 {
		return result, nil
	}

	bindings, err := s.deviceSimBindingStore.ListByDeviceIDs(ctx, deviceIDs)
	if err != nil {
		return nil, err
	}

	for _, binding := range bindings {
		result[binding.DeviceID]++
	}

	return result, nil
}

func (s *Service) extractDeviceIDs(devices []*model.Device) []uint {
	ids := make([]uint, len(devices))
	for i, device := range devices {
		ids[i] = device.ID
	}
	return ids
}

func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, seriesMap map[uint]string, bindingCounts map[uint]int64, activationStatuses map[uint]int) *dto.DeviceResponse {
	activationStatus := activationStatuses[device.ID]
	resp := &dto.DeviceResponse{
		ID:                   device.ID,
		VirtualNo:            device.VirtualNo,
		IMEI:                 device.IMEI,
		SN:                   device.SN,
		DeviceName:           device.DeviceName,
		DeviceModel:          device.DeviceModel,
		DeviceType:           device.DeviceType,
		MaxSimSlots:          device.MaxSimSlots,
		Manufacturer:         device.Manufacturer,
		BatchNo:              device.BatchNo,
		ShopID:               device.ShopID,
		Status:               device.Status,
		StatusName:           constants.GetDeviceStatusName(device.Status),
		ActivationStatus:     activationStatus,
		ActivationStatusName: constants.GetActivationStatusName(activationStatus),
		BoundCardCount:       int(bindingCounts[device.ID]),
		SeriesID:             device.SeriesID,
		ActivatedAt:          device.ActivatedAt,
		CreatedAt:            device.CreatedAt,
		UpdatedAt:            device.UpdatedAt,
		OnlineStatus:         device.OnlineStatus,
		LastOnlineTime:       device.LastOnlineTime,
		SoftwareVersion:      device.SoftwareVersion,
		SwitchMode:           device.SwitchMode,
		LastGatewaySyncAt:    device.LastGatewaySyncAt,
		AssetStatus:          device.AssetStatus,
		AssetStatusName:      constants.GetAssetStatusName(device.AssetStatus),
		Generation:           device.Generation,
	}

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

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

	return resp
}

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

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

		record := &model.AssetAllocationRecord{
			AllocationNo:    allocationNo,
			AllocationType:  constants.AssetAllocationTypeAllocate,
			AssetType:       constants.AssetTypeDevice,
			AssetID:         device.ID,
			AssetIdentifier: device.VirtualNo,
			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(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
	successIDSet := make(map[uint]bool)
	for _, id := range successDeviceIDs {
		successIDSet[id] = true
	}

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

		record := &model.AssetAllocationRecord{
			AllocationNo:    allocationNo,
			AllocationType:  constants.AssetAllocationTypeRecall,
			AssetType:       constants.AssetTypeDevice,
			AssetID:         device.ID,
			AssetIdentifier: device.VirtualNo,
			OperatorID:      operatorID,
			Remark:          remark,
		}

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

		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.BatchSetDeviceSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetDeviceSeriesBindngResponse, error) {
	selectionType, err := normalizeDeviceSeriesBindingSelection(req)
	if err != nil {
		return nil, err
	}

	devices, err := s.getDevicesForSeriesBinding(ctx, req, selectionType)
	batchTotal := deviceSeriesBindingBatchTotal(req, selectionType, devices)
	auditData := deviceSeriesBindingAuditData(req, selectionType, operatorShopID)
	if err != nil {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceSeriesBinding,
			"设备系列绑定失败",
			constants.AssetAuditResultFailed,
			nil,
			nil,
			auditData,
			batchTotal,
			0,
			batchTotal,
			err,
		)
		return nil, err
	}

	if len(devices) == 0 {
		failedItems := []dto.DeviceSeriesBindngFailedItem{}
		if selectionType == dto.SelectionTypeList {
			failedItems = s.buildDeviceNotFoundFailedItems(req.DeviceIDs)
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceSeriesBinding,
				"设备系列绑定被拒绝",
				constants.AssetAuditResultDenied,
				nil,
				nil,
				auditData,
				batchTotal,
				0,
				len(failedItems),
				errors.New(errors.CodeNotFound, "设备不存在"),
			)
		}
		return &dto.BatchSetDeviceSeriesBindngResponse{
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	deviceMap := make(map[uint]*model.Device)
	for _, device := range devices {
		deviceMap[device.ID] = device
	}

	// 验证系列存在（仅当 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 {
				appErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
				s.logDeviceOperation(
					ctx,
					constants.AssetAuditOpDeviceSeriesBinding,
					"设备系列绑定被拒绝",
					constants.AssetAuditResultDenied,
					nil,
					nil,
					auditData,
					batchTotal,
					0,
					batchTotal,
					appErr,
				)
				return nil, appErr
			}
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceSeriesBinding,
				"设备系列绑定失败",
				constants.AssetAuditResultFailed,
				nil,
				nil,
				auditData,
				batchTotal,
				0,
				batchTotal,
				err,
			)
			return nil, err
		}
		if packageSeries.Status != 1 {
			appErr := errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceSeriesBinding,
				"设备系列绑定被拒绝",
				constants.AssetAuditResultDenied,
				nil,
				nil,
				auditData,
				batchTotal,
				0,
				batchTotal,
				appErr,
			)
			return nil, appErr
		}
	}

	var successDeviceIDs []uint
	var failedItems []dto.DeviceSeriesBindngFailedItem
	successDeviceIDSet := make(map[uint]struct{})

	hasSeriesAllocation := true
	if operatorShopID != nil && req.SeriesID > 0 {
		hasSeriesAllocation, err = s.hasAvailableSeriesAllocation(ctx, *operatorShopID, req.SeriesID)
		if err != nil {
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceSeriesBinding,
				"设备系列绑定失败",
				constants.AssetAuditResultFailed,
				nil,
				nil,
				auditData,
				batchTotal,
				0,
				batchTotal,
				err,
			)
			return nil, err
		}
	}

	addDevice := func(device *model.Device, requestedDeviceID uint) {
		if device == nil {
			failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
				DeviceID:  requestedDeviceID,
				VirtualNo: "",
				Reason:    "设备不存在",
			})
			return
		}

		if !hasSeriesAllocation {
			failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
				DeviceID:  device.ID,
				VirtualNo: device.VirtualNo,
				Reason:    "您没有权限分配该套餐系列",
			})
			return
		}

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

		if _, exists := successDeviceIDSet[device.ID]; exists {
			return
		}
		successDeviceIDSet[device.ID] = struct{}{}
		successDeviceIDs = append(successDeviceIDs, device.ID)
	}

	if selectionType == dto.SelectionTypeList {
		for _, deviceID := range req.DeviceIDs {
			addDevice(deviceMap[deviceID], deviceID)
		}
	} else {
		for _, device := range devices {
			addDevice(device, device.ID)
		}
	}

	if len(successDeviceIDs) > 0 {
		var seriesIDPtr *uint
		if req.SeriesID > 0 {
			seriesIDPtr = &req.SeriesID
		}
		if err := s.deviceStore.BatchUpdateSeriesID(ctx, successDeviceIDs, seriesIDPtr); err != nil {
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceSeriesBinding,
				"设备系列绑定失败",
				constants.AssetAuditResultFailed,
				nil,
				nil,
				map[string]any{
					"series_id":          req.SeriesID,
					"selection_type":     selectionType,
					"success_device_ids": successDeviceIDs,
					"failed_items":       failedItems,
				},
				batchTotal,
				len(successDeviceIDs),
				len(failedItems),
				err,
			)
			return nil, err
		}
	}

	beforeData := make([]map[string]any, 0, len(devices))
	for _, device := range devices {
		beforeData = append(beforeData, deviceSnapshot(device))
	}
	resultStatus := constants.AssetAuditResultSuccess
	var resultErr error
	if len(successDeviceIDs) == 0 && len(failedItems) > 0 {
		resultStatus = constants.AssetAuditResultDenied
		resultErr = errors.New(errors.CodeForbidden, "无可绑定设备")
	}
	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpDeviceSeriesBinding,
		"设备系列绑定",
		resultStatus,
		nil,
		map[string]any{
			"devices": beforeData,
		},
		map[string]any{
			"selection_type":     selectionType,
			"series_id":          req.SeriesID,
			"success_device_ids": successDeviceIDs,
			"failed_items":       failedItems,
		},
		batchTotal,
		len(successDeviceIDs),
		len(failedItems),
		resultErr,
	)

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

func normalizeDeviceSeriesBindingSelection(req *dto.BatchSetDeviceSeriesBindngRequest) (string, error) {
	switch req.SelectionType {
	case dto.SelectionTypeList:
		if len(req.DeviceIDs) == 0 {
			return "", errors.New(errors.CodeInvalidParam, "selection_type=list时device_ids不能为空")
		}
		return dto.SelectionTypeList, nil
	case dto.SelectionTypeRange:
		if req.VirtualNoStart == "" || req.VirtualNoEnd == "" {
			return "", errors.New(errors.CodeInvalidParam, "selection_type=range时virtual_no_start和virtual_no_end不能为空")
		}
		if req.VirtualNoStart > req.VirtualNoEnd {
			return "", errors.New(errors.CodeInvalidParam, "设备虚拟号起始值不能大于结束值")
		}
		return dto.SelectionTypeRange, nil
	case dto.SelectionTypeFilter:
		return dto.SelectionTypeFilter, nil
	case "":
		if len(req.DeviceIDs) > 0 {
			return dto.SelectionTypeList, nil
		}
		if req.VirtualNoStart != "" || req.VirtualNoEnd != "" {
			if req.VirtualNoStart == "" || req.VirtualNoEnd == "" {
				return "", errors.New(errors.CodeInvalidParam, "virtual_no_start和virtual_no_end必须同时传入")
			}
			if req.VirtualNoStart > req.VirtualNoEnd {
				return "", errors.New(errors.CodeInvalidParam, "设备虚拟号起始值不能大于结束值")
			}
			return dto.SelectionTypeRange, nil
		}
		if hasDeviceSeriesBindingFilters(req) {
			return dto.SelectionTypeFilter, nil
		}
		return "", errors.New(errors.CodeInvalidParam, "请选择要设置套餐系列的设备")
	default:
		return "", errors.New(errors.CodeInvalidParam, "无效的选设备方式")
	}
}

func hasDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) bool {
	return req.VirtualNo != "" ||
		req.DeviceName != "" ||
		req.Status != nil ||
		req.ActivationStatus != nil ||
		req.ShopID != nil ||
		len(req.ShopIDs) > 0 ||
		req.FilterSeriesID != nil ||
		req.BatchNo != "" ||
		req.DeviceType != "" ||
		req.Manufacturer != "" ||
		req.CreatedAtStart != nil ||
		req.CreatedAtEnd != nil
}

func (s *Service) getDevicesForSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string) ([]*model.Device, error) {
	switch selectionType {
	case dto.SelectionTypeList:
		return s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
	case dto.SelectionTypeRange:
		return s.deviceStore.GetByFilters(ctx, map[string]any{
			"virtual_no_start": req.VirtualNoStart,
			"virtual_no_end":   req.VirtualNoEnd,
		})
	case dto.SelectionTypeFilter:
		return s.deviceStore.GetByFilters(ctx, buildDeviceSeriesBindingFilters(req))
	default:
		return nil, errors.New(errors.CodeInvalidParam, "无效的选设备方式")
	}
}

func buildDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) map[string]any {
	filters := make(map[string]any)
	if req.VirtualNo != "" {
		filters["virtual_no"] = req.VirtualNo
	}
	if req.VirtualNoStart != "" {
		filters["virtual_no_start"] = req.VirtualNoStart
	}
	if req.VirtualNoEnd != "" {
		filters["virtual_no_end"] = req.VirtualNoEnd
	}
	if req.DeviceName != "" {
		filters["device_name"] = req.DeviceName
	}
	if req.Status != nil {
		filters["status"] = *req.Status
	}
	if req.ActivationStatus != nil {
		filters["activation_status"] = *req.ActivationStatus
	}
	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.BatchNo != "" {
		filters["batch_no"] = req.BatchNo
	}
	if req.DeviceType != "" {
		filters["device_type"] = req.DeviceType
	}
	if req.Manufacturer != "" {
		filters["manufacturer"] = req.Manufacturer
	}
	if req.CreatedAtStart != nil {
		filters["created_at_start"] = *req.CreatedAtStart
	}
	if req.CreatedAtEnd != nil {
		filters["created_at_end"] = *req.CreatedAtEnd
	}
	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 deviceSeriesBindingBatchTotal(req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string, devices []*model.Device) int {
	if selectionType == dto.SelectionTypeList {
		return len(req.DeviceIDs)
	}
	return len(devices)
}

func deviceSeriesBindingAuditData(req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string, operatorShopID *uint) map[string]any {
	data := map[string]any{
		"selection_type":   selectionType,
		"series_id":        req.SeriesID,
		"operator_shop_id": operatorShopID,
	}
	switch selectionType {
	case dto.SelectionTypeList:
		data["device_ids"] = req.DeviceIDs
	case dto.SelectionTypeRange:
		data["virtual_no_start"] = req.VirtualNoStart
		data["virtual_no_end"] = req.VirtualNoEnd
	case dto.SelectionTypeFilter:
		data["filters"] = buildDeviceSeriesBindingFilters(req)
	}
	return data
}

func (s *Service) buildDeviceNotFoundFailedItems(deviceIDs []uint) []dto.DeviceSeriesBindngFailedItem {
	items := make([]dto.DeviceSeriesBindngFailedItem, len(deviceIDs))
	for i, id := range deviceIDs {
		items[i] = dto.DeviceSeriesBindngFailedItem{
			DeviceID:  id,
			VirtualNo: "",
			Reason:    "设备不存在",
		}
	}
	return items
}

// StopDevice 设备停机
// POST /api/admin/assets/device/:device_id/stop
// 查找设备绑定的所有已实名且已开机的卡，逐一调网关停机
func (s *Service) StopDevice(ctx context.Context, deviceID uint) (*dto.DeviceSuspendResponse, error) {
	log := logger.GetAppLogger()

	userID := middleware.GetUserIDFromContext(ctx)
	if userID == 0 {
		appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStop,
			"设备停机被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{"device_id": deviceID},
			0,
			0,
			0,
			appErr,
		)
		return nil, appErr
	}

	device, err := s.deviceStore.GetByID(ctx, deviceID)
	if err != nil {
		appErr := errors.New(errors.CodeNotFound, "设备不存在")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStop,
			"设备停机失败",
			constants.AssetAuditResultFailed,
			nil,
			nil,
			map[string]any{"device_id": deviceID},
			0,
			0,
			0,
			appErr,
		)
		return nil, appErr
	}

	// 复机保护期内禁止停机
	if s.redis != nil {
		exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "start")).Result()
		if exists > 0 {
			appErr := errors.New(errors.CodeForbidden, "设备复机保护期内，禁止停机")
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceStop,
				"设备停机被拒绝",
				constants.AssetAuditResultDenied,
				device,
				map[string]any{"device": deviceSnapshot(device)},
				nil,
				0,
				0,
				0,
				appErr,
			)
			return nil, appErr
		}
	}

	bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
	if err != nil {
		appErr := errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStop,
			"设备停机失败",
			constants.AssetAuditResultFailed,
			device,
			map[string]any{"device": deviceSnapshot(device)},
			nil,
			0,
			0,
			0,
			appErr,
		)
		return nil, appErr
	}

	if len(bindings) == 0 {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStop,
			"设备停机",
			constants.AssetAuditResultSuccess,
			device,
			map[string]any{"device": deviceSnapshot(device)},
			map[string]any{
				"success_count": 0,
				"fail_count":    0,
				"skip_count":    0,
			},
			0,
			0,
			0,
			nil,
		)
		return &dto.DeviceSuspendResponse{}, nil
	}

	cardIDs := make([]uint, 0, len(bindings))
	for _, b := range bindings {
		cardIDs = append(cardIDs, b.IotCardID)
	}

	cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
	if err != nil {
		appErr := errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStop,
			"设备停机失败",
			constants.AssetAuditResultFailed,
			device,
			map[string]any{"device": deviceSnapshot(device)},
			nil,
			len(cardIDs),
			0,
			len(cardIDs),
			appErr,
		)
		return nil, appErr
	}

	beforeCards := make([]map[string]any, 0, len(cards))
	for _, card := range cards {
		beforeCards = append(beforeCards, map[string]any{
			"id":               card.ID,
			"iccid":            card.ICCID,
			"network_status":   card.NetworkStatus,
			"real_name_status": card.RealNameStatus,
			"stop_reason":      card.StopReason,
		})
	}

	var successCount, skipCount int
	var failedItems []dto.DeviceSuspendFailItem

	for _, card := range cards {
		if card.RealNameStatus != constants.RealNameStatusVerified || card.NetworkStatus != constants.NetworkStatusOnline {
			skipCount++
			continue
		}

		if s.gatewayClient != nil {
			log.Info("调用网关停机（设备）",
				zap.Uint("device_id", deviceID),
				zap.String("iccid", card.ICCID))
			if gwErr := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); gwErr != nil {
				log.Error("设备停机-调网关停机失败",
					zap.Uint("device_id", deviceID),
					zap.String("iccid", card.ICCID),
					zap.Error(gwErr))
				failedItems = append(failedItems, dto.DeviceSuspendFailItem{
					ICCID:  card.ICCID,
					Reason: "网关停机失败",
				})
				continue
			}
			log.Info("网关停机成功（设备）",
				zap.Uint("device_id", deviceID),
				zap.String("iccid", card.ICCID))
		}

		now := time.Now()
		if dbErr := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
			"network_status": constants.NetworkStatusOffline,
			"stopped_at":     now,
			"stop_reason":    constants.StopReasonManual,
		}, constants.CardObservationSceneBusinessStop, "offline", s.gatewayClient != nil); dbErr != nil {
			log.Error("设备停机-更新卡状态失败",
				zap.Uint("card_id", card.ID),
				zap.Error(dbErr))
			failedItems = append(failedItems, dto.DeviceSuspendFailItem{
				ICCID:  card.ICCID,
				Reason: "更新卡状态失败",
			})
			continue
		}

		s.invalidatePollingCardCache(card.ID)
		successCount++
	}

	// 成功停机至少一张卡后设置保护期
	if successCount > 0 && s.redis != nil {
		s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"), uuid.NewString(), constants.DeviceProtectPeriodDuration)
		s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "start"))
	}

	resultStatus := constants.AssetAuditResultSuccess
	var resultErr error
	if successCount == 0 && len(failedItems) > 0 {
		resultStatus = constants.AssetAuditResultFailed
		resultErr = errors.New(errors.CodeGatewayError, "设备停机失败，未成功处理任何卡")
	}
	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpDeviceStop,
		"设备停机",
		resultStatus,
		device,
		map[string]any{
			"device": deviceSnapshot(device),
			"cards":  beforeCards,
		},
		map[string]any{
			"success_count": successCount,
			"fail_count":    len(failedItems),
			"skip_count":    skipCount,
			"failed_items":  failedItems,
		},
		len(cards),
		successCount,
		len(failedItems),
		resultErr,
	)

	return &dto.DeviceSuspendResponse{
		SuccessCount: successCount,
		FailCount:    len(failedItems),
		SkipCount:    skipCount,
		FailedItems:  failedItems,
	}, nil
}

// StartDevice 设备复机
// POST /api/admin/assets/device/:device_id/start
// 查找设备绑定的所有已实名且已停机的卡，逐一调网关复机
func (s *Service) StartDevice(ctx context.Context, deviceID uint) error {
	log := logger.GetAppLogger()

	userID := middleware.GetUserIDFromContext(ctx)
	if userID == 0 {
		appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStart,
			"设备复机被拒绝",
			constants.AssetAuditResultDenied,
			nil,
			nil,
			map[string]any{"device_id": deviceID},
			0,
			0,
			0,
			appErr,
		)
		return appErr
	}

	device, err := s.deviceStore.GetByID(ctx, deviceID)
	if err != nil {
		appErr := errors.New(errors.CodeNotFound, "设备不存在")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStart,
			"设备复机失败",
			constants.AssetAuditResultFailed,
			nil,
			nil,
			map[string]any{"device_id": deviceID},
			0,
			0,
			0,
			appErr,
		)
		return appErr
	}

	// 停机保护期内禁止复机
	if s.redis != nil {
		exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")).Result()
		if exists > 0 {
			appErr := errors.New(errors.CodeForbidden, "设备停机保护期内，禁止复机")
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpDeviceStart,
				"设备复机被拒绝",
				constants.AssetAuditResultDenied,
				device,
				map[string]any{"device": deviceSnapshot(device)},
				nil,
				0,
				0,
				0,
				appErr,
			)
			return appErr
		}
	}

	bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
	if err != nil {
		appErr := errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStart,
			"设备复机失败",
			constants.AssetAuditResultFailed,
			device,
			map[string]any{"device": deviceSnapshot(device)},
			nil,
			0,
			0,
			0,
			appErr,
		)
		return appErr
	}

	if len(bindings) == 0 {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStart,
			"设备复机",
			constants.AssetAuditResultSuccess,
			device,
			map[string]any{"device": deviceSnapshot(device)},
			map[string]any{
				"success_count": 0,
				"fail_count":    0,
			},
			0,
			0,
			0,
			nil,
		)
		return nil
	}

	cardIDs := make([]uint, 0, len(bindings))
	for _, b := range bindings {
		cardIDs = append(cardIDs, b.IotCardID)
	}

	cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
	if err != nil {
		appErr := errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStart,
			"设备复机失败",
			constants.AssetAuditResultFailed,
			device,
			map[string]any{"device": deviceSnapshot(device)},
			nil,
			len(cardIDs),
			0,
			len(cardIDs),
			appErr,
		)
		return appErr
	}

	beforeCards := make([]map[string]any, 0, len(cards))
	for _, card := range cards {
		beforeCards = append(beforeCards, map[string]any{
			"id":               card.ID,
			"iccid":            card.ICCID,
			"network_status":   card.NetworkStatus,
			"real_name_status": card.RealNameStatus,
			"stop_reason":      card.StopReason,
		})
	}

	var successCount int
	var failCount int
	var lastErr error

	for _, card := range cards {
		if card.RealNameStatus != constants.RealNameStatusVerified || card.NetworkStatus != constants.NetworkStatusOffline {
			continue
		}

		if s.gatewayClient != nil {
			log.Info("调用网关复机（设备）",
				zap.Uint("device_id", deviceID),
				zap.String("iccid", card.ICCID))
			if gwErr := s.gatewayClient.StartCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); gwErr != nil {
				log.Error("设备复机-调网关复机失败",
					zap.Uint("device_id", deviceID),
					zap.String("iccid", card.ICCID),
					zap.Error(gwErr))
				lastErr = gwErr
				failCount++
				continue
			}
			log.Info("网关复机成功（设备）",
				zap.Uint("device_id", deviceID),
				zap.String("iccid", card.ICCID))
		}

		now := time.Now()
		if dbErr := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
			"network_status": constants.NetworkStatusOnline,
			"resumed_at":     now,
			"stop_reason":    "",
		}, constants.CardObservationSceneBusinessResume, "online", s.gatewayClient != nil); dbErr != nil {
			log.Error("设备复机-更新卡状态失败",
				zap.Uint("card_id", card.ID),
				zap.Error(dbErr))
			lastErr = dbErr
			failCount++
			continue
		}

		s.invalidatePollingCardCache(card.ID)
		successCount++
	}

	// 成功复机至少一张卡后设置保护期
	if successCount > 0 && s.redis != nil {
		s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "start"), uuid.NewString(), constants.DeviceProtectPeriodDuration)
		s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"))
	}

	// 全部失败时返回 error
	if successCount == 0 && lastErr != nil {
		appErr := errors.Wrap(errors.CodeGatewayError, lastErr, "设备复机失败，所有卡均复机失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpDeviceStart,
			"设备复机失败",
			constants.AssetAuditResultFailed,
			device,
			map[string]any{
				"device": deviceSnapshot(device),
				"cards":  beforeCards,
			},
			map[string]any{
				"success_count": successCount,
				"fail_count":    failCount,
			},
			len(cards),
			successCount,
			failCount,
			appErr,
		)
		return appErr
	}

	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpDeviceStart,
		"设备复机",
		constants.AssetAuditResultSuccess,
		device,
		map[string]any{
			"device": deviceSnapshot(device),
			"cards":  beforeCards,
		},
		map[string]any{
			"success_count": successCount,
			"fail_count":    failCount,
		},
		len(cards),
		successCount,
		failCount,
		nil,
	)

	return nil
}

// UpdateRealnamePolicy 更新设备的实名认证策略
func (s *Service) UpdateRealnamePolicy(ctx context.Context, deviceID uint, realnamePolicy string) error {
	// 检查设备是否存在
	device, err := s.deviceStore.GetByID(ctx, deviceID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			appErr := errors.New(errors.CodeNotFound, "设备不存在")
			s.logDeviceOperation(
				ctx,
				constants.AssetAuditOpAssetRealnamePolicy,
				"更新设备实名策略失败",
				constants.AssetAuditResultFailed,
				nil,
				nil,
				map[string]any{
					"device_id":       deviceID,
					"realname_policy": realnamePolicy,
				},
				0,
				0,
				0,
				appErr,
			)
			return appErr
		}
		appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpAssetRealnamePolicy,
			"更新设备实名策略失败",
			constants.AssetAuditResultFailed,
			nil,
			nil,
			map[string]any{
				"device_id":       deviceID,
				"realname_policy": realnamePolicy,
			},
			0,
			0,
			0,
			appErr,
		)
		return appErr
	}
	beforeData := map[string]any{
		"realname_policy": device.RealnamePolicy,
		"device":          deviceSnapshot(device),
	}

	// 幂等检查
	if device.RealnamePolicy == realnamePolicy {
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpAssetRealnamePolicy,
			"更新设备实名策略被拒绝",
			constants.AssetAuditResultDenied,
			device,
			beforeData,
			map[string]any{"realname_policy": realnamePolicy},
			0,
			0,
			0,
			errors.New(errors.CodeConflict, "实名认证策略未变化"),
		)
		return nil
	}

	// 更新数据库
	if err := s.deviceStore.UpdateRealnamePolicy(ctx, deviceID, realnamePolicy); err != nil {
		appErr := errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
		s.logDeviceOperation(
			ctx,
			constants.AssetAuditOpAssetRealnamePolicy,
			"更新设备实名策略失败",
			constants.AssetAuditResultFailed,
			device,
			beforeData,
			map[string]any{"realname_policy": realnamePolicy},
			0,
			0,
			0,
			appErr,
		)
		return appErr
	}

	s.logDeviceOperation(
		ctx,
		constants.AssetAuditOpAssetRealnamePolicy,
		"更新设备实名策略",
		constants.AssetAuditResultSuccess,
		device,
		beforeData,
		map[string]any{
			"realname_policy": realnamePolicy,
		},
		0,
		0,
		0,
		nil,
	)

	return nil
}

func (s *Service) invalidatePollingCardCache(cardID uint) {
	if s.redis == nil {
		return
	}
	_ = s.redis.Del(context.Background(), constants.RedisPollingCardInfoKey(cardID)).Err()
}

func (s *Service) updateCardAndAppendNetworkSeries(ctx context.Context, cardID uint, fields map[string]any, scene, expected string, upstreamCalled bool) error {
	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).Updates(fields).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "更新设备绑定卡停复机状态失败")
		}
		if !upstreamCalled || cardObservationApp.IsSeriesTriggerSuppressed(ctx) {
			return nil
		}
		if s.observationSeriesEvents == nil {
			return errors.New(errors.CodeInternalError, "设备停复机观测 Outbox Writer 未配置")
		}
		operationID := uuid.NewString()
		requestID := operationID
		if value := middleware.GetRequestIDFromContext(ctx); value != nil && *value != "" {
			requestID = *value
		}
		return s.observationSeriesEvents.AppendSeriesRequested(ctx, tx, cardObservationApp.SeriesRequestedEvent{
			EventID: "card-observation:network-command:" + operationID,
			Scene:   scene, ResourceType: constants.CardObservationResourceTypeCard, ResourceID: cardID,
			SyncTypes: []string{constants.CardObservationSyncTypeNetwork}, ExpectedValue: expected,
			Source: constants.CardObservationSourceBusinessEvent, OccurredAt: time.Now().UTC(),
			RequestID: requestID, CorrelationID: requestID,
		})
	})
}
