package iot_card

import (
	"context"
	stderrors "errors"
	"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"
	assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
	"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"
)

// StopResumeServiceInterface 停复机服务接口
// 供 4 个 Task Handler 通过接口注入，避免循环依赖
type StopResumeServiceInterface interface {
	// EvaluateAndAct 停复机统一入口，根据卡的当前状态自动判断并执行停机或复机
	EvaluateAndAct(ctx context.Context, card *model.IotCard) error
}

// 编译时验证 StopResumeService 实现了 StopResumeServiceInterface
var _ StopResumeServiceInterface = (*StopResumeService)(nil)

// StopResumeService 停复机服务
// 处理 IoT 卡的自动停机、复机和手动停复机逻辑
type StopResumeService struct {
	db                      *gorm.DB
	redis                   *redis.Client
	iotCardStore            *postgres.IotCardStore
	packageUsageStore       *postgres.PackageUsageStore
	deviceSimBindingStore   *postgres.DeviceSimBindingStore
	gatewayClient           *gateway.Client
	logger                  *zap.Logger
	assetAuditService       AssetAuditService
	pollingCallback         PollingCallback
	observationSeriesEvents cardObservationApp.SeriesEventWriter

	maxRetries    int
	retryInterval time.Duration
}

// SetObservationSeriesEventWriter 注入停复机成功观测序列 Outbox Writer。
func (s *StopResumeService) SetObservationSeriesEventWriter(db *gorm.DB, writer cardObservationApp.SeriesEventWriter) {
	s.db = db
	s.observationSeriesEvents = writer
}

// NewStopResumeService 创建停复机服务
func NewStopResumeService(
	redis *redis.Client,
	iotCardStore *postgres.IotCardStore,
	packageUsageStore *postgres.PackageUsageStore,
	deviceSimBindingStore *postgres.DeviceSimBindingStore,
	gatewayClient *gateway.Client,
	logger *zap.Logger,
	assetAuditService AssetAuditService,
) *StopResumeService {
	return &StopResumeService{
		redis:                 redis,
		iotCardStore:          iotCardStore,
		packageUsageStore:     packageUsageStore,
		deviceSimBindingStore: deviceSimBindingStore,
		gatewayClient:         gatewayClient,
		logger:                logger,
		assetAuditService:     assetAuditService,
		maxRetries:            3,
		retryInterval:         2 * time.Second,
	}
}

// SetPollingCallback 设置轮询生命周期回调。
// 停复机成功改变卡状态后，需要立即按新状态重排轮询队列，避免继续使用旧 score。
func (s *StopResumeService) SetPollingCallback(callback PollingCallback) {
	s.pollingCallback = callback
}

// EvaluateAndAct 停复机统一入口
// 根据卡的当前网络状态，自动判断并执行停机或复机操作
// 设备/单卡维度通过 card.IsStandalone 推导（false 则为设备维度，停/复机覆盖设备下所有卡）
func (s *StopResumeService) EvaluateAndAct(ctx context.Context, card *model.IotCard) error {
	switch card.NetworkStatus {
	case constants.NetworkStatusOnline:
		// 卡在线，检查是否需要停机
		reasons, err := s.checkStopReasons(ctx, card)
		if err != nil {
			return err
		}
		if len(reasons) == 0 {
			return nil
		}
		// 取最高优先级原因（第一个）
		primaryReason := reasons[0]
		if !card.IsStandalone && isDeviceScopeReason(primaryReason) {
			deviceID, bound, lookupErr := s.getCardDeviceID(ctx, card)
			if lookupErr != nil {
				s.logger.Error("查询设备绑定关系失败，降级为单卡停机",
					zap.Uint("card_id", card.ID), zap.Error(lookupErr))
				// 降级：查不到绑定时按单卡处理
			} else if bound {
				return s.stopDeviceCards(ctx, deviceID, primaryReason)
			}
		}
		return s.stopCardWithRetry(ctx, card, primaryReason)

	case constants.NetworkStatusOffline:
		// 卡停机，检查是否可以复机
		// 只处理轮询系统引起的停机原因（手动停机等不自动复机）
		if !isPollingStopReason(card.StopReason) {
			return nil
		}
		shouldResume, err := s.shouldResume(ctx, card)
		if err != nil {
			return err
		}
		if !shouldResume {
			return nil
		}
		if !card.IsStandalone {
			deviceID, bound, lookupErr := s.getCardDeviceID(ctx, card)
			if lookupErr != nil {
				s.logger.Error("查询设备绑定关系失败，降级为单卡复机",
					zap.Uint("card_id", card.ID), zap.Error(lookupErr))
			} else if bound {
				return s.resumeDeviceCards(ctx, deviceID)
			}
		}
		return s.resumeSingleCard(ctx, card.ID)

	default:
		return nil
	}
}

// isRiskGatewayExtend 判断网关扩展状态是否为风险状态（风险停机或已销户）
// 独立卡命中此状态时禁止发起复机
func isRiskGatewayExtend(extend string) bool {
	trimmed := strings.TrimSpace(extend)
	return trimmed == constants.GatewayCardExtendRiskStop ||
		trimmed == constants.GatewayCardExtendCancelled
}

// isPollingStopReason 判断停机原因是否为轮询系统引起（可自动复机）
func isPollingStopReason(reason string) bool {
	switch reason {
	case constants.StopReasonTrafficExhausted,
		constants.StopReasonNoPackage,
		constants.StopReasonNotRealname:
		return true
	}
	return false
}

// isDeviceScopeReason 判断停机原因是否应扩散到设备维度
// 套餐/流量是设备共享资源，需整体停机；实名是单卡属性，只停该卡
func isDeviceScopeReason(reason string) bool {
	return reason == constants.StopReasonNoPackage ||
		reason == constants.StopReasonTrafficExhausted
}

// getCardDeviceID 获取卡所属设备ID（非独立卡时有效）
// 返回：deviceID, isDeviceBound, error
func (s *StopResumeService) getCardDeviceID(ctx context.Context, card *model.IotCard) (uint, bool, error) {
	if card.IsStandalone {
		return 0, false, nil
	}
	binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
	if err != nil {
		if stderrors.Is(err, gorm.ErrRecordNotFound) {
			return 0, false, nil
		}
		return 0, false, err
	}
	if binding == nil {
		return 0, false, nil
	}
	return binding.DeviceID, true, nil
}

// hasValidPackage 检查卡是否有有效套餐（status IN 0,1）
// 修复Bug1：绑定设备的卡查 device_id，独立卡查 iot_card_id
func (s *StopResumeService) hasValidPackage(ctx context.Context, card *model.IotCard) (bool, error) {
	if !card.IsStandalone {
		deviceID, bound, lookupErr := s.getCardDeviceID(ctx, card)
		if lookupErr != nil {
			return false, lookupErr
		}
		if bound {
			return s.packageUsageStore.HasValidByCarrier(ctx, "device", deviceID)
		}
	}
	return s.packageUsageStore.HasValidByCarrier(ctx, "iot_card", card.ID)
}

// isTrafficExhausted 检查卡的流量是否已耗尽
// 条件：活跃套餐 status=2（已用完），或 data_usage_mb >= data_limit_mb（data_limit_mb > 0）
// 业务约定：data_limit_mb=0 表示无限流量套餐，永远不判定为耗尽
func (s *StopResumeService) isTrafficExhausted(ctx context.Context, card *model.IotCard) (bool, error) {
	carrierType := "iot_card"
	carrierID := card.ID

	if !card.IsStandalone {
		deviceID, bound, lookupErr := s.getCardDeviceID(ctx, card)
		if lookupErr != nil {
			return false, lookupErr
		}
		if bound {
			carrierType = "device"
			carrierID = deviceID
		}
	}

	packages, err := s.packageUsageStore.ListActiveOrDepletedByCarrier(ctx, carrierType, carrierID)
	if err != nil {
		return false, err
	}
	if len(packages) == 0 {
		return false, nil
	}

	for _, pkg := range packages {
		if pkg == nil {
			continue
		}
		if !pkg.IsTrafficDepleted() {
			return false, nil
		}
	}
	return true, nil
}

// isRealnameOK 检查卡是否满足实名要求
// 行业卡（card_category='industry'）无需实名；其他卡需 real_name_status=1
func (s *StopResumeService) isRealnameOK(card *model.IotCard) bool {
	return card.CardCategory == constants.CardCategoryIndustry ||
		card.RealNameStatus == constants.RealNameStatusVerified
}

// checkStopReasons 检查卡的停机原因列表，按优先级排序
// 优先级：no_package > traffic_exhausted > not_realname
func (s *StopResumeService) checkStopReasons(ctx context.Context, card *model.IotCard) ([]string, error) {
	var reasons []string

	// 条件A：流量耗尽
	exhausted, err := s.isTrafficExhausted(ctx, card)
	if err != nil {
		return nil, err
	}
	if exhausted {
		reasons = append(reasons, constants.StopReasonTrafficExhausted)
	}

	// 条件B：无有效套餐（仅在未命中流量耗尽时判断）
	hasPackage, err := s.hasValidPackage(ctx, card)
	if err != nil {
		return nil, err
	}
	if !hasPackage && !exhausted {
		reasons = append(reasons, constants.StopReasonNoPackage)
	}

	// 条件C：非行业卡且未实名
	if !s.isRealnameOK(card) {
		reasons = append(reasons, constants.StopReasonNotRealname)
	}

	return reasons, nil
}

// shouldResume 检查停机卡是否满足复机条件
// 须同时满足：有有效套餐 + 流量未耗尽 + 实名OK
func (s *StopResumeService) shouldResume(ctx context.Context, card *model.IotCard) (bool, error) {
	hasPackage, err := s.hasValidPackage(ctx, card)
	if err != nil {
		return false, err
	}
	if !hasPackage {
		return false, nil
	}

	exhausted, err := s.isTrafficExhausted(ctx, card)
	if err != nil {
		return false, err
	}
	if exhausted {
		return false, nil
	}

	return s.isRealnameOK(card), nil
}

// stopDeviceCards 停机设备下所有在线卡（含设备维度幂等锁）
// 防止设备下多张卡并发触发 EvaluateAndAct 导致重复 Gateway 停机调用
func (s *StopResumeService) stopDeviceCards(ctx context.Context, deviceID uint, stopReason string) error {
	// 设备维度幂等锁：防止多协程同时对同一设备停机
	lockKey := constants.RedisPollingDeviceOpLockKey(deviceID)
	locked, lockErr := s.redis.SetNX(ctx, lockKey, "1", 30*time.Second).Result()
	if lockErr != nil {
		return errors.Wrap(errors.CodeRedisError, lockErr, "获取设备停机锁失败")
	}
	if !locked {
		s.logger.Debug("设备停机操作已在进行中，跳过重复调用",
			zap.Uint("device_id", deviceID))
		return nil
	}
	defer s.redis.Del(ctx, lockKey)

	// 查询设备下所有在线卡
	cards, err := s.iotCardStore.ListByDeviceIDAndNetworkStatus(ctx, deviceID, constants.NetworkStatusOnline)
	if err != nil {
		s.logger.Error("查询设备在线卡失败",
			zap.Uint("device_id", deviceID), zap.Error(err))
		return errors.Wrap(errors.CodeDatabaseError, err, "查询设备在线卡失败")
	}

	var cardErrors []error
	for _, card := range cards {
		if stopErr := s.stopCardWithRetry(ctx, card, stopReason); stopErr != nil {
			cardErrors = append(cardErrors, stopErr)
			s.logger.Warn("设备卡停机失败，继续处理其他卡",
				zap.Uint("device_id", deviceID),
				zap.Uint("card_id", card.ID),
				zap.Error(stopErr))
		}
	}
	if len(cardErrors) > 0 {
		return errors.Wrap(errors.CodeInternalError, stderrors.Join(cardErrors...), "设备停机存在未成功处理的卡")
	}
	return nil
}

// resumeDeviceCards 复机设备下满足条件的停机卡（含设备维度幂等锁）
// 遍历时对每张卡检查实名状态：未实名普通卡更新 stop_reason='not_realname' 后跳过
func (s *StopResumeService) resumeDeviceCards(ctx context.Context, deviceID uint) error {
	// 设备维度幂等锁：与 stopDeviceCards 共用，防止停/复机并发
	lockKey := constants.RedisPollingDeviceOpLockKey(deviceID)
	locked, lockErr := s.redis.SetNX(ctx, lockKey, "1", 30*time.Second).Result()
	if lockErr != nil {
		return errors.Wrap(errors.CodeRedisError, lockErr, "获取设备复机锁失败")
	}
	if !locked {
		s.logger.Debug("设备复机操作已在进行中，跳过重复调用",
			zap.Uint("device_id", deviceID))
		return nil
	}
	defer s.redis.Del(ctx, lockKey)

	// 查询设备下因轮询原因停机的卡
	cards, err := s.iotCardStore.ListByDeviceIDAndPollingStopReasons(ctx, deviceID)
	if err != nil {
		s.logger.Error("查询设备停机卡失败",
			zap.Uint("device_id", deviceID), zap.Error(err))
		return errors.Wrap(errors.CodeDatabaseError, err, "查询设备停机卡失败")
	}

	var cardErrors []error
	for _, card := range cards {
		if !s.isRealnameOK(card) {
			if updateErr := s.iotCardStore.UpdateStopReason(ctx, card.ID, constants.StopReasonNotRealname); updateErr != nil {
				cardErrors = append(cardErrors, updateErr)
				s.logger.Warn("更新未实名卡停机原因失败",
					zap.Uint("card_id", card.ID), zap.Error(updateErr))
			}
			continue
		}
		if resumeErr := s.resumeSingleCard(ctx, card.ID); resumeErr != nil {
			cardErrors = append(cardErrors, resumeErr)
			s.logger.Warn("设备卡复机失败，继续处理其他卡",
				zap.Uint("device_id", deviceID),
				zap.Uint("card_id", card.ID),
				zap.Error(resumeErr))
		}
	}
	if len(cardErrors) > 0 {
		return errors.Wrap(errors.CodeInternalError, stderrors.Join(cardErrors...), "设备复机存在未成功处理的卡")
	}
	return nil
}

// CheckAndStopCard 检查流量耗尽并停机（旧入口，保留兼容性）
// 新代码应优先使用 EvaluateAndAct
func (s *StopResumeService) CheckAndStopCard(ctx context.Context, cardID uint) error {
	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		return err
	}
	return s.EvaluateAndAct(ctx, card)
}

// ResumeCardIfStopped 套餐激活或流量重置后自动复机入口
// 支持 iot_card 和 device 两种载体类型
func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, carrierType string, carrierID uint) error {
	switch carrierType {
	case "iot_card":
		return s.resumeSingleCard(ctx, carrierID)
	case "device":
		return s.resumeDeviceCards(ctx, carrierID)
	default:
		return nil
	}
}

// resumeSingleCard 对单张卡执行复机逻辑
// 依次检查：已开机则跳过 → 非轮询停机原因则跳过 → 不满足复机条件则跳过 → 加锁 → 调 Gateway → 更新 DB
func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) error {
	card, err := s.iotCardStore.GetByID(ctx, cardID)
	if err != nil {
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			Operator:      assetAuditSvc.SystemOperator("系统任务"),
			OperationType: constants.AssetAuditOpCardAutoStart,
			OperationDesc: "自动复机执行失败",
			ResultStatus:  constants.AssetAuditResultFailed,
			ErrorCode:     errorCode,
			ErrorMsg:      errorMsg,
			AssetID:       cardID,
		})
		return err
	}

	if card.NetworkStatus == constants.NetworkStatusOnline {
		return nil
	}

	// 只处理轮询系统引起的停机
	if !isPollingStopReason(card.StopReason) {
		s.logger.Debug("卡非轮询停机原因，不自动复机",
			zap.Uint("card_id", cardID),
			zap.String("stop_reason", card.StopReason))
		return nil
	}

	// 检查复机条件
	resume, err := s.shouldResume(ctx, card)
	if err != nil {
		return err
	}
	if !resume {
		return nil
	}

	// 分布式锁防止同一张卡并发触发多次 Gateway 调用，TTL 30s
	lockKey := constants.RedisCardResumeLockKey(cardID)
	locked, lockErr := s.redis.SetNX(ctx, lockKey, "1", 30*time.Second).Result()
	if lockErr != nil {
		s.logger.Warn("获取复机分布式锁失败，跳过本次复机",
			zap.Uint("card_id", cardID),
			zap.Error(lockErr))
		return nil
	}
	if !locked {
		s.logger.Debug("复机操作进行中，跳过", zap.Uint("card_id", cardID))
		return nil
	}
	defer s.redis.Del(ctx, lockKey)

	if err := s.resumeCardWithRetry(ctx, card); err != nil {
		s.logger.Error("调用运营商复机接口失败",
			zap.Uint("card_id", cardID),
			zap.String("iccid", card.ICCID),
			zap.Error(err))
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			Operator:        assetAuditSvc.SystemOperator("系统任务"),
			OperationType:   constants.AssetAuditOpCardAutoStart,
			OperationDesc:   "自动复机执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"network_status": card.NetworkStatus,
				"stop_reason":    card.StopReason,
			},
		})
		return err
	}

	now := time.Now()
	if err := s.updateCardAndAppendNetworkSeries(ctx, cardID, map[string]any{
		"network_status": constants.NetworkStatusOnline,
		"resumed_at":     now,
		"stop_reason":    "",
	}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString()); err != nil {
		s.logger.Error("复机 Gateway 成功但 DB 更新失败",
			zap.Uint("card_id", cardID),
			zap.String("iccid", card.ICCID),
			zap.Error(err))
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			Operator:        assetAuditSvc.SystemOperator("系统任务"),
			OperationType:   constants.AssetAuditOpCardAutoStart,
			OperationDesc:   "自动复机执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"network_status": constants.NetworkStatusOffline,
				"stop_reason":    card.StopReason,
			},
			AfterData: map[string]any{
				"network_status": constants.NetworkStatusOnline,
				"stop_reason":    "",
			},
		})
		return err
	}

	s.reschedulePolling(ctx, card.ID)

	s.logger.Info("卡已自动复机",
		zap.Uint("card_id", cardID),
		zap.String("iccid", card.ICCID))

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		Operator:        assetAuditSvc.SystemOperator("系统任务"),
		OperationType:   constants.AssetAuditOpCardAutoStart,
		OperationDesc:   "自动复机",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData: map[string]any{
			"network_status": constants.NetworkStatusOffline,
			"stop_reason":    card.StopReason,
		},
		AfterData: map[string]any{
			"network_status": constants.NetworkStatusOnline,
			"stop_reason":    "",
		},
	})

	return nil
}

// stopCardWithRetry 调用运营商停机接口（带重试机制），并更新 DB 停机原因
func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.IotCard, stopReason string) error {
	operator := assetAuditSvc.SystemOperator("系统任务")
	operationType := constants.AssetAuditOpCardAutoStop
	operationDesc := "自动停卡"
	if stopReason == constants.StopReasonManual {
		operator = assetAuditSvc.OperatorFromContext(ctx)
		operationType = constants.AssetAuditOpCardManualStop
		operationDesc = "手动停卡"
	}

	if s.gatewayClient == nil {
		failErr := errors.New(errors.CodeInternalError, "Gateway 未配置，停复机操作不可用")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(failErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			Operator:        operator,
			OperationType:   operationType,
			OperationDesc:   operationDesc + "执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		return failErr
	}

	s.logger.Info("调用网关停机",
		zap.Uint("card_id", card.ID),
		zap.String("iccid", card.ICCID),
		zap.String("stop_reason", stopReason))

	var lastErr error
	for i := 0; i < s.maxRetries; i++ {
		if i > 0 {
			s.logger.Debug("重试调用停机接口",
				zap.Int("attempt", i+1),
				zap.String("iccid", card.ICCID))
			time.Sleep(s.retryInterval)
		}

		err := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{
			CardNo: card.ICCID,
		})
		if err == nil {
			s.logger.Info("网关停机成功",
				zap.Uint("card_id", card.ID),
				zap.String("iccid", card.ICCID))

			now := time.Now()
			if updateErr := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
				"network_status": constants.NetworkStatusOffline,
				"stopped_at":     now,
				"stop_reason":    stopReason,
			}, constants.CardObservationSceneBusinessStop, "offline", uuid.NewString()); updateErr != nil {
				s.logger.Error("停机 Gateway 成功但 DB 更新失败",
					zap.Uint("card_id", card.ID),
					zap.String("iccid", card.ICCID),
					zap.Error(updateErr))
				errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(updateErr)
				s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
					Operator:        operator,
					OperationType:   operationType,
					OperationDesc:   operationDesc + "执行失败",
					ResultStatus:    constants.AssetAuditResultFailed,
					ErrorCode:       errorCode,
					ErrorMsg:        errorMsg,
					AssetID:         card.ID,
					AssetIdentifier: card.ICCID,
					BeforeData: map[string]any{
						"network_status": card.NetworkStatus,
						"stop_reason":    card.StopReason,
					},
					AfterData: map[string]any{
						"network_status": constants.NetworkStatusOffline,
						"stop_reason":    stopReason,
					},
				})
				return updateErr
			}

			s.reschedulePolling(ctx, card.ID)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				Operator:        operator,
				OperationType:   operationType,
				OperationDesc:   operationDesc,
				ResultStatus:    constants.AssetAuditResultSuccess,
				AssetID:         card.ID,
				AssetIdentifier: card.ICCID,
				BeforeData: map[string]any{
					"network_status": card.NetworkStatus,
					"stop_reason":    card.StopReason,
				},
				AfterData: map[string]any{
					"network_status": constants.NetworkStatusOffline,
					"stop_reason":    stopReason,
				},
			})
			return nil
		}

		lastErr = err
		s.logger.Warn("调用停机接口失败，准备重试",
			zap.Int("attempt", i+1),
			zap.String("iccid", card.ICCID),
			zap.Error(err))
	}

	errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(lastErr)
	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		Operator:        operator,
		OperationType:   operationType,
		OperationDesc:   operationDesc + "执行失败",
		ResultStatus:    constants.AssetAuditResultFailed,
		ErrorCode:       errorCode,
		ErrorMsg:        errorMsg,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData: map[string]any{
			"network_status": card.NetworkStatus,
			"stop_reason":    card.StopReason,
		},
		AfterData: map[string]any{
			"network_status": constants.NetworkStatusOffline,
			"stop_reason":    stopReason,
		},
	})

	return lastErr
}

// resumeCardWithRetry 调用运营商复机接口（带重试机制）
func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model.IotCard) error {
	if s.gatewayClient == nil {
		return errors.New(errors.CodeInternalError, "Gateway 未配置，停复机操作不可用")
	}

	s.logger.Info("调用网关复机",
		zap.Uint("card_id", card.ID),
		zap.String("iccid", card.ICCID))

	var lastErr error
	for i := 0; i < s.maxRetries; i++ {
		if i > 0 {
			s.logger.Debug("重试调用复机接口",
				zap.Int("attempt", i+1),
				zap.String("iccid", card.ICCID))
			time.Sleep(s.retryInterval)
		}

		req := &gateway.CardOperationReq{CardNo: card.ICCID}
		if strings.TrimSpace(card.GatewayExtend) == constants.GatewayCardExtendMachineSeparated {
			req.Extend = constants.GatewayCardStartExtendMachineSeparated
		}
		err := s.gatewayClient.StartCard(ctx, req)
		if err == nil {
			s.logger.Info("网关复机成功",
				zap.Uint("card_id", card.ID),
				zap.String("iccid", card.ICCID))
			return nil
		}

		lastErr = err
		s.logger.Warn("调用复机接口失败，准备重试",
			zap.Int("attempt", i+1),
			zap.String("iccid", card.ICCID),
			zap.Error(err))
	}

	return lastErr
}

// StartMachineSeparatedCard 对机卡分离停机卡执行复机
// 调用上游复机前检查本地落库的 Gateway extend，只有“机卡分离停机”才允许复机。
func (s *StopResumeService) StartMachineSeparatedCard(ctx context.Context, card *model.IotCard) error {
	if card == nil {
		return errors.New(errors.CodeInvalidParam)
	}
	if s.gatewayClient == nil {
		return errors.New(errors.CodeInternalError, "Gateway 未配置，停复机操作不可用")
	}

	gatewayExtend := strings.TrimSpace(card.GatewayExtend)

	// 独立卡处于风险停机或已销户状态时，优先返回明确的拒绝原因
	if card.IsStandalone && isRiskGatewayExtend(gatewayExtend) {
		var denyMsg string
		if gatewayExtend == constants.GatewayCardExtendRiskStop {
			denyMsg = "该卡已被运营商风险停机，不允许复机"
		} else {
			denyMsg = "该卡已被运营商销户，不允许复机"
		}
		denyErr := errors.New(errors.CodeForbidden, denyMsg)
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "机卡分离复机被拒绝（风险状态）",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		return denyErr
	}

	if gatewayExtend != constants.GatewayCardExtendMachineSeparated {
		denyErr := errors.New(errors.CodeForbidden, constants.AgentOpenAPIResumeOnlyMachineSeparatedMessage)
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "机卡分离复机被拒绝",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
			AfterData: map[string]any{
				"gateway_extend": gatewayExtend,
			},
		})
		return denyErr
	}

	if err := s.resumeCardWithRetry(ctx, card); err != nil {
		wrapErr := errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败，请稍后重试")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "机卡分离复机执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
			AfterData: map[string]any{
				"gateway_extend": gatewayExtend,
			},
		})
		return wrapErr
	}

	now := time.Now()
	if err := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
		"network_status": constants.NetworkStatusOnline,
		"resumed_at":     now,
		"stop_reason":    "",
		"gateway_extend": "",
	}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString()); err != nil {
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "机卡分离复机执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"network_status": card.NetworkStatus,
				"stop_reason":    card.StopReason,
				"gateway_extend": gatewayExtend,
			},
			AfterData: map[string]any{
				"network_status": constants.NetworkStatusOnline,
				"stop_reason":    "",
				"gateway_extend": "",
			},
		})
		return wrapErr
	}

	s.reschedulePolling(ctx, card.ID)
	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType:   constants.AssetAuditOpCardManualStart,
		OperationDesc:   "机卡分离复机",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData: map[string]any{
			"network_status": card.NetworkStatus,
			"stop_reason":    card.StopReason,
			"gateway_extend": gatewayExtend,
		},
		AfterData: map[string]any{
			"network_status": constants.NetworkStatusOnline,
			"stop_reason":    "",
			"gateway_extend": "",
		},
	})

	return nil
}

// ManualStopCard 手动停机单张卡（通过ICCID）
func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) error {
	card, err := s.iotCardStore.GetByICCID(ctx, iccid)
	if err != nil {
		denyErr := errors.New(errors.CodeNotFound, "卡不存在")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStop,
			OperationDesc:   "手动停卡被拒绝",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetIdentifier: iccid,
		})
		return denyErr
	}

	if card.RealNameStatus != constants.RealNameStatusVerified {
		denyErr := errors.New(errors.CodeForbidden, "卡未实名，无法操作")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStop,
			OperationDesc:   "手动停卡被拒绝",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		return denyErr
	}

	// 检查绑定设备是否在复机保护期
	if s.deviceSimBindingStore != nil && s.redis != nil {
		binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
		if bindErr == nil && binding != nil {
			exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "start")).Result()
			if exists > 0 {
				denyErr := errors.New(errors.CodeForbidden, "设备复机保护期内，禁止停机")
				errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
				s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
					OperationType:   constants.AssetAuditOpCardManualStop,
					OperationDesc:   "手动停卡被拒绝",
					ResultStatus:    constants.AssetAuditResultDenied,
					ErrorCode:       errorCode,
					ErrorMsg:        errorMsg,
					AssetID:         card.ID,
					AssetIdentifier: card.ICCID,
					BeforeData:      cardSnapshot(card),
					AfterData: map[string]any{
						"device_id": binding.DeviceID,
					},
				})
				return denyErr
			}
		} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
			wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType:   constants.AssetAuditOpCardManualStop,
				OperationDesc:   "手动停卡执行失败",
				ResultStatus:    constants.AssetAuditResultFailed,
				ErrorCode:       errorCode,
				ErrorMsg:        errorMsg,
				AssetID:         card.ID,
				AssetIdentifier: card.ICCID,
				BeforeData:      cardSnapshot(card),
			})
			return wrapErr
		}
	}

	if err := s.stopCardWithRetry(ctx, card, constants.StopReasonManual); err != nil {
		return errors.Wrap(errors.CodeGatewayError, err, "调用运营商停机失败，请稍后重试")
	}

	return nil
}

// ManualStartCard 手动复机单张卡（通过ICCID）
func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) error {
	card, err := s.iotCardStore.GetByICCID(ctx, iccid)
	if err != nil {
		denyErr := errors.New(errors.CodeNotFound, "卡不存在")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "手动复机被拒绝",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetIdentifier: iccid,
		})
		return denyErr
	}

	// 独立卡处于风险停机或已销户状态时，拒绝复机
	if card.IsStandalone && isRiskGatewayExtend(card.GatewayExtend) {
		var denyMsg string
		if strings.TrimSpace(card.GatewayExtend) == constants.GatewayCardExtendRiskStop {
			denyMsg = "该卡已被运营商风险停机，不允许复机"
		} else {
			denyMsg = "该卡已被运营商销户，不允许复机"
		}
		denyErr := errors.New(errors.CodeForbidden, denyMsg)
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "手动复机被拒绝",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		return denyErr
	}

	if card.RealNameStatus != constants.RealNameStatusVerified {
		denyErr := errors.New(errors.CodeForbidden, "卡未实名，无法操作")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "手动复机被拒绝",
			ResultStatus:    constants.AssetAuditResultDenied,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		return denyErr
	}

	// 检查绑定设备是否在停机保护期
	if s.deviceSimBindingStore != nil && s.redis != nil {
		binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
		if bindErr == nil && binding != nil {
			exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "stop")).Result()
			if exists > 0 {
				denyErr := errors.New(errors.CodeForbidden, "设备停机保护期内，禁止复机")
				errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
				s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
					OperationType:   constants.AssetAuditOpCardManualStart,
					OperationDesc:   "手动复机被拒绝",
					ResultStatus:    constants.AssetAuditResultDenied,
					ErrorCode:       errorCode,
					ErrorMsg:        errorMsg,
					AssetID:         card.ID,
					AssetIdentifier: card.ICCID,
					BeforeData:      cardSnapshot(card),
					AfterData: map[string]any{
						"device_id": binding.DeviceID,
					},
				})
				return denyErr
			}
		} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
			wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
			errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
			s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
				OperationType:   constants.AssetAuditOpCardManualStart,
				OperationDesc:   "手动复机执行失败",
				ResultStatus:    constants.AssetAuditResultFailed,
				ErrorCode:       errorCode,
				ErrorMsg:        errorMsg,
				AssetID:         card.ID,
				AssetIdentifier: card.ICCID,
				BeforeData:      cardSnapshot(card),
			})
			return wrapErr
		}
	}

	if err := s.resumeCardWithRetry(ctx, card); err != nil {
		wrapErr := errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败，请稍后重试")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "手动复机执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData:      cardSnapshot(card),
		})
		return wrapErr
	}

	now := time.Now()
	if err := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
		"network_status": constants.NetworkStatusOnline,
		"resumed_at":     now,
		"stop_reason":    "",
	}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString()); err != nil {
		wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
		errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
		s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
			OperationType:   constants.AssetAuditOpCardManualStart,
			OperationDesc:   "手动复机执行失败",
			ResultStatus:    constants.AssetAuditResultFailed,
			ErrorCode:       errorCode,
			ErrorMsg:        errorMsg,
			AssetID:         card.ID,
			AssetIdentifier: card.ICCID,
			BeforeData: map[string]any{
				"network_status": card.NetworkStatus,
				"stop_reason":    card.StopReason,
			},
			AfterData: map[string]any{
				"network_status": constants.NetworkStatusOnline,
				"stop_reason":    "",
			},
		})
		return wrapErr
	}

	s.reschedulePolling(ctx, card.ID)

	s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
		OperationType:   constants.AssetAuditOpCardManualStart,
		OperationDesc:   "手动复机",
		ResultStatus:    constants.AssetAuditResultSuccess,
		AssetID:         card.ID,
		AssetIdentifier: card.ICCID,
		BeforeData: map[string]any{
			"network_status": card.NetworkStatus,
			"stop_reason":    card.StopReason,
		},
		AfterData: map[string]any{
			"network_status": constants.NetworkStatusOnline,
			"stop_reason":    "",
		},
	})

	return nil
}

func (s *StopResumeService) updateCardAndAppendNetworkSeries(ctx context.Context, cardID uint, fields map[string]any, scene, expected, operationID string) error {
	if s.db == nil || s.observationSeriesEvents == nil {
		return errors.New(errors.CodeInternalError, "停复机观测 Outbox 能力未配置")
	}
	requestID := requestIDFromContext(ctx)
	if requestID == "" {
		requestID = operationID
	}
	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 cardObservationApp.IsSeriesTriggerSuppressed(ctx) {
			return nil
		}
		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,
		})
	})
}

// invalidatePollingCache 删除轮询卡缓存，下次轮询自动从 DB 重建
func (s *StopResumeService) invalidatePollingCache(ctx context.Context, cardID uint) {
	if s.redis == nil {
		return
	}
	key := constants.RedisPollingCardInfoKey(cardID)
	if err := s.redis.Del(ctx, key).Err(); err != nil {
		s.logger.Warn("删除轮询卡缓存失败", zap.Uint("card_id", cardID))
	}
}

// reschedulePolling 按卡最新状态重排轮询队列。
// 未注入生命周期回调时退化为只清缓存，保持原有行为。
func (s *StopResumeService) reschedulePolling(ctx context.Context, cardID uint) {
	if s.pollingCallback == nil {
		s.invalidatePollingCache(ctx, cardID)
		return
	}
	s.pollingCallback.OnCardStatusChanged(ctx, cardID)
}
