package device

import (
	"context"
	"time"

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

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

// 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.DeviceName != "" {
		filters["device_name"] = req.DeviceName
	}
	if req.Status != nil {
		filters["status"] = *req.Status
	}
	if req.ShopID != nil {
		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
	}

	devices, total, err := s.deviceStore.List(ctx, opts, filters)
	if err != nil {
		return nil, err
	}

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

	list := make([]*dto.DeviceResponse, 0, len(devices))
	for _, device := range devices {
		item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts)
		list = append(list, item)
	}

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

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

	return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), 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
	}

	return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), 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
	}

	return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), 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 {
	device, err := s.deviceStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeNotFound, "设备不存在")
		}
		return err
	}

	if err := s.deviceSimBindingStore.UnbindByDeviceID(ctx, device.ID); err != nil {
		return err
	}

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

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

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

	devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
	if err != nil {
		return nil, err
	}

	if len(devices) == 0 {
		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 {
		return &dto.AllocateDevicesResponse{
			SuccessCount: 0,
			FailCount:    len(failedItems),
			FailedItems:  failedItems,
		}, nil
	}

	newStatus := constants.DeviceStatusDistributed
	targetShopID := req.TargetShopID

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

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

	if len(devices) == 0 {
		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 {
		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
	}

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

	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 {
		if targetShop.ParentID != nil {
			return errors.ErrNotDirectSubordinate
		}
	} else {
		if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID {
			return errors.ErrNotDirectSubordinate
		}
	}

	return nil
}

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) *dto.DeviceResponse {
	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:        s.getDeviceStatusName(device.Status),
		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,
	}

	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) getDeviceStatusName(status int) string {
	switch status {
	case constants.DeviceStatusInStock:
		return "在库"
	case constants.DeviceStatusDistributed:
		return "已分销"
	case constants.DeviceStatusActivated:
		return "已激活"
	case constants.DeviceStatusSuspended:
		return "已停用"
	default:
		return "未知"
	}
}

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) {
	devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
	if err != nil {
		return nil, err
	}

	if len(devices) == 0 {
		return &dto.BatchSetDeviceSeriesBindngResponse{
			SuccessCount: 0,
			FailCount:    len(req.DeviceIDs),
			FailedItems:  s.buildDeviceNotFoundFailedItems(req.DeviceIDs),
		}, 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 {
				return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
			}
			return nil, err
		}
		if packageSeries.Status != 1 {
			return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
		}
	}

	var successDeviceIDs []uint
	var failedItems []dto.DeviceSeriesBindngFailedItem

	for _, deviceID := range req.DeviceIDs {
		device, exists := deviceMap[deviceID]
		if !exists {
			failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
				DeviceID:  deviceID,
				VirtualNo: "",
				Reason:    "设备不存在",
			})
			continue
		}

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

		// 验证设备权限（基于 device.ShopID）
		if operatorShopID != nil {
			if device.ShopID == nil || *device.ShopID != *operatorShopID {
				failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
					DeviceID:  device.ID,
					VirtualNo: device.VirtualNo,
					Reason:    "无权操作此设备",
				})
				continue
			}
		}

		successDeviceIDs = append(successDeviceIDs, 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 {
			return nil, err
		}
	}

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

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 {
		return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
	}

	device, err := s.deviceStore.GetByID(ctx, deviceID)
	if err != nil {
		return nil, errors.New(errors.CodeNotFound, "设备不存在")
	}
	_ = device

	// 复机保护期内禁止停机
	if s.redis != nil {
		exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "start")).Result()
		if exists > 0 {
			return nil, errors.New(errors.CodeForbidden, "设备复机保护期内，禁止停机")
		}
	}

	bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
	if err != nil {
		return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
	}

	if len(bindings) == 0 {
		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 {
		return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
	}

	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.db.WithContext(ctx).Model(&model.IotCard{}).
			Where("id = ?", card.ID).
			Updates(map[string]any{
				"network_status": constants.NetworkStatusOffline,
				"stopped_at":     now,
				"stop_reason":    constants.StopReasonManual,
			}).Error; 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"), 1, constants.DeviceProtectPeriodDuration)
		s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "start"))
	}

	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 {
		return errors.New(errors.CodeUnauthorized, "未授权访问")
	}

	device, err := s.deviceStore.GetByID(ctx, deviceID)
	if err != nil {
		return errors.New(errors.CodeNotFound, "设备不存在")
	}
	_ = device

	// 停机保护期内禁止复机
	if s.redis != nil {
		exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")).Result()
		if exists > 0 {
			return errors.New(errors.CodeForbidden, "设备停机保护期内，禁止复机")
		}
	}

	bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
	if err != nil {
		return errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
	}

	if len(bindings) == 0 {
		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 {
		return errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
	}

	var successCount 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
				continue
			}
			log.Info("网关复机成功（设备）",
				zap.Uint("device_id", deviceID),
				zap.String("iccid", card.ICCID))
		}

		now := time.Now()
		if dbErr := s.db.WithContext(ctx).Model(&model.IotCard{}).
			Where("id = ?", card.ID).
			Updates(map[string]any{
				"network_status": constants.NetworkStatusOnline,
				"resumed_at":     now,
				"stop_reason":    "",
			}).Error; dbErr != nil {
			log.Error("设备复机-更新卡状态失败",
				zap.Uint("card_id", card.ID),
				zap.Error(dbErr))
			lastErr = dbErr
			continue
		}

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

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

	// 全部失败时返回 error
	if successCount == 0 && lastErr != nil {
		return errors.Wrap(errors.CodeGatewayError, lastErr, "设备复机失败，所有卡均复机失败")
	}

	return nil
}

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