package packagepkg

import (
	"context"
	"strconv"
	"time"

	cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"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/redis/go-redis/v9"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// ResumeCallback 复机回调接口
// 用于在套餐激活或流量重置后触发自动复机
type ResumeCallback interface {
	ResumeCardIfStopped(ctx context.Context, carrierType string, carrierID uint) error
}

type ActivationService struct {
	db                      *gorm.DB
	redis                   *redis.Client
	packageUsageStore       *postgres.PackageUsageStore
	packageStore            *postgres.PackageStore
	packageUsageDailyRecord *postgres.PackageUsageDailyRecordStore
	logger                  *zap.Logger
	resumeCallback          ResumeCallback // 复机回调，可选
	observationSeriesEvents cardObservationApp.SeriesEventWriter
}

// SetObservationSeriesEventWriter 注入套餐激活成功观测序列 Outbox Writer。
func (s *ActivationService) SetObservationSeriesEventWriter(writer cardObservationApp.SeriesEventWriter) {
	s.observationSeriesEvents = writer
}

func NewActivationService(
	db *gorm.DB,
	redis *redis.Client,
	packageUsageStore *postgres.PackageUsageStore,
	packageStore *postgres.PackageStore,
	packageUsageDailyRecord *postgres.PackageUsageDailyRecordStore,
	logger *zap.Logger,
) *ActivationService {
	return &ActivationService{
		db:                      db,
		redis:                   redis,
		packageUsageStore:       packageUsageStore,
		packageStore:            packageStore,
		packageUsageDailyRecord: packageUsageDailyRecord,
		logger:                  logger,
	}
}

// SetResumeCallback 任务 24.7: 设置复机回调
// 在应用启动时由 bootstrap 调用，注入停复机服务
func (s *ActivationService) SetResumeCallback(callback ResumeCallback) {
	s.resumeCallback = callback
}

// ActivateByRealname 任务 9.2-9.3: 首次实名激活
// 当用户完成实名后，激活所有待实名激活的套餐
func (s *ActivationService) ActivateByRealname(ctx context.Context, carrierType string, carrierID uint) error {
	// 查询待实名激活的套餐
	var pendingUsages []*model.PackageUsage
	query := s.db.WithContext(ctx).
		Where("pending_realname_activation = ?", true).
		Where("status = ?", constants.PackageUsageStatusPending)

	if carrierType == "iot_card" {
		query = query.Where("iot_card_id = ?", carrierID)
	} else if carrierType == "device" {
		query = query.Where("device_id = ?", carrierID)
	} else {
		return errors.New(errors.CodeInvalidParam, "无效的载体类型")
	}

	if err := query.Order("priority ASC").Find(&pendingUsages).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询待实名激活套餐失败")
	}

	if len(pendingUsages) == 0 {
		s.logger.Info("没有待实名激活的套餐", zap.String("carrier_type", carrierType), zap.Uint("carrier_id", carrierID))
		return nil
	}

	now := time.Now()

	activated := false
	// 在事务中激活套餐
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		for _, usage := range pendingUsages {
			// 查询套餐信息
			var pkg model.Package
			if err := tx.First(&pkg, usage.PackageID).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
			}

			// 检查是否是主套餐
			if usage.MasterUsageID == nil {
				// 主套餐：需要检查是否有已激活或已用完（仍在有效期）的主套餐
				var activeMain model.PackageUsage
				err := tx.Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
					Where("master_usage_id IS NULL").
					Where(carrierType+"_id = ?", carrierID).
					Order("priority ASC").
					First(&activeMain).Error

				if err == nil {
					// 已有生效中或已用完（占位）的主套餐，保持排队状态
					s.logger.Warn("已有占位主套餐，跳过激活",
						zap.Uint("usage_id", usage.ID),
						zap.Uint("active_main_id", activeMain.ID),
						zap.Int("active_main_status", activeMain.Status))
					continue
				}
				if err != gorm.ErrRecordNotFound {
					return errors.Wrap(errors.CodeDatabaseError, err, "检查生效中主套餐失败")
				}
			}

			terms, termsErr := ResolveUsageTerms(usage, &pkg, s.logger)
			if termsErr != nil {
				return termsErr
			}
			// REALNAME-04: 按购买快照选择激活计时基准
			// from_purchase：购买时起算，用 PackageUsage 创建时间；否则用实名触发当前时刻
			var activatedAt time.Time
			if terms.ExpiryBase == constants.PackageExpiryBaseFromPurchase {
				activatedAt = usage.CreatedAt
			} else {
				activatedAt = now
			}
			expiresAt := CalculateExpiryTime(terms.CalendarType, activatedAt, terms.DurationMonths, terms.DurationDays)

			// 计算下次重置时间
			nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)

			// 更新套餐使用记录
			updates := map[string]interface{}{
				"status":                      constants.PackageUsageStatusActive,
				"pending_realname_activation": false,
				"activated_at":                activatedAt,
				"expires_at":                  expiresAt,
			}
			if nextResetAt != nil {
				updates["next_reset_at"] = *nextResetAt
			}

			if err := tx.Model(usage).Updates(updates).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "激活套餐失败")
			}
			if err := s.appendActivationObservation(ctx, tx, usage, carrierType, carrierID, now); err != nil {
				return err
			}

			s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)

			s.logger.Info("套餐已激活",
				zap.Uint("usage_id", usage.ID),
				zap.Uint("package_id", usage.PackageID),
				zap.Time("activated_at", activatedAt),
				zap.Time("expires_at", expiresAt))

			activated = true
		}

		return nil
	})
	if err != nil {
		return err
	}
	if activated {
		s.resumeAfterActivation(ctx, carrierType, carrierID, "实名激活后自动复机失败")
	}
	return nil
}

// ActivateQueuedPackage 任务 9.4-9.7: 排队主套餐激活
// 当主套餐过期后，激活下一个待生效的主套餐
func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierType string, carrierID uint) error {
	// 使用 Redis 分布式锁避免并发
	lockKey := constants.RedisPackageActivationLockKey(carrierType, carrierID)
	lockValue := time.Now().String()
	locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
	if err != nil {
		return errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
	}
	if !locked {
		s.logger.Warn("套餐激活正在进行中，跳过",
			zap.String("carrier_type", carrierType),
			zap.Uint("carrier_id", carrierID))
		return nil
	}
	defer s.redis.Del(ctx, lockKey)

	activated := false
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// 任务 9.5: 检测并标记过期的主套餐
		now := time.Now()
		var expiredMainUsages []*model.PackageUsage
		err := tx.Where("status = ?", constants.PackageUsageStatusActive).
			Where("master_usage_id IS NULL").
			Where("expires_at <= ?", now).
			Where(carrierType+"_id = ?", carrierID).
			Find(&expiredMainUsages).Error

		if err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "查询过期主套餐失败")
		}

		for _, expiredMain := range expiredMainUsages {
			// 更新主套餐状态为已过期
			if err := tx.Model(expiredMain).Update("status", constants.PackageUsageStatusExpired).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "更新过期主套餐状态失败")
			}

			expiresAt := now
			if expiredMain.ExpiresAt != nil {
				expiresAt = *expiredMain.ExpiresAt
			}
			s.logger.Info("主套餐已过期",
				zap.Uint("usage_id", expiredMain.ID),
				zap.Time("expires_at", expiresAt))

			// 任务 9.7: 加油包级联失效
			if err := s.invalidateAddons(ctx, tx, expiredMain.ID); err != nil {
				return err
			}

			// 任务 9.6: 激活下一个待生效主套餐
			currentActivated, err := s.activateNextMainPackage(ctx, tx, carrierType, carrierID, now)
			if err != nil {
				return err
			}
			activated = activated || currentActivated
		}

		return nil
	})
	if err != nil {
		return err
	}
	if activated {
		s.resumeAfterActivation(ctx, carrierType, carrierID, "排队激活后自动复机失败")
	}
	return nil
}

// ActivateSpecificPackage 任务 4: 激活指定的套餐使用记录
// 根据 PackageUsageID 精准激活目标套餐，而非重跑"查找过期包"流程
func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, packageUsageID uint) error {
	// 加载 PackageUsage
	var usage model.PackageUsage
	if err := s.db.WithContext(ctx).First(&usage, packageUsageID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeNotFound, "套餐使用记录不存在")
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
	}

	// 幂等检查：非待生效状态直接返回
	if usage.Status != constants.PackageUsageStatusPending {
		s.logger.Info("套餐无需激活（非待生效状态）",
			zap.Uint("usage_id", usage.ID),
			zap.Int("status", usage.Status))
		return nil
	}

	// 确定载体类型和 ID
	var carrierType string
	var carrierID uint
	if usage.IotCardID > 0 {
		carrierType = "iot_card"
		carrierID = usage.IotCardID
	} else if usage.DeviceID > 0 {
		carrierType = "device"
		carrierID = usage.DeviceID
	} else {
		return errors.New(errors.CodeInvalidParam, "套餐使用记录缺少载体信息")
	}

	// 获取分布式锁
	lockKey := constants.RedisPackageActivationLockKey(carrierType, carrierID)
	lockValue := time.Now().String()
	locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
	if err != nil {
		return errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
	}
	if !locked {
		s.logger.Warn("套餐激活正在进行中，跳过",
			zap.String("carrier_type", carrierType),
			zap.Uint("carrier_id", carrierID))
		return nil
	}
	defer s.redis.Del(ctx, lockKey)

	// 加载关联 Package
	var pkg model.Package
	if err := s.db.WithContext(ctx).First(&pkg, usage.PackageID).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
	}

	// 事务内激活
	activated := false
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// 再次幂等检查（防止锁释放期间被其他操作激活）
		var currentUsage model.PackageUsage
		if err := tx.First(&currentUsage, packageUsageID).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "重新查询套餐使用记录失败")
		}
		if currentUsage.Status != constants.PackageUsageStatusPending {
			return nil
		}

		if currentUsage.MasterUsageID == nil {
			hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
			if err != nil {
				return err
			}
			if hasActive {
				s.logger.Info("已有生效主套餐，跳过指定套餐激活",
					zap.Uint("usage_id", currentUsage.ID),
					zap.String("carrier_type", carrierType),
					zap.Uint("carrier_id", carrierID))
				return nil
			}

			canActivate, err := s.canActivatePendingUsage(ctx, tx, &currentUsage, carrierType, carrierID)
			if err != nil {
				return err
			}
			if !canActivate {
				s.logger.Info("指定套餐暂不满足激活条件",
					zap.Uint("usage_id", currentUsage.ID),
					zap.Bool("pending_realname_activation", currentUsage.PendingRealnameActivation))
				return nil
			}
		}

		if err := s.activatePendingUsage(ctx, tx, &currentUsage, &pkg, carrierType, carrierID, time.Now(), "指定套餐已激活"); err != nil {
			return err
		}
		activated = true
		return nil
	})
	if err != nil {
		return err
	}
	if activated {
		s.resumeAfterActivation(ctx, carrierType, carrierID, "激活后自动复机失败")
	}
	return nil
}

// ActivateNextPendingMainPackage 按购买顺序激活载体的下一个待生效主套餐。
// 返回 true 表示本次成功激活了一个主套餐；队首套餐未满足实名条件时返回 false。
func (s *ActivationService) ActivateNextPendingMainPackage(ctx context.Context, carrierType string, carrierID uint) (bool, error) {
	if carrierType != constants.AssetTypeIotCard && carrierType != constants.AssetTypeDevice {
		return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
	}
	if carrierID == 0 {
		return false, errors.New(errors.CodeInvalidParam, "无效的载体ID")
	}

	lockKey := constants.RedisPackageActivationLockKey(carrierType, carrierID)
	lockValue := time.Now().String()
	locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
	if err != nil {
		return false, errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
	}
	if !locked {
		s.logger.Warn("套餐激活正在进行中，跳过",
			zap.String("carrier_type", carrierType),
			zap.Uint("carrier_id", carrierID))
		return false, errors.New(errors.CodePackageActivationConflict)
	}
	defer s.redis.Del(ctx, lockKey)

	activated := false
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
		if err != nil {
			return err
		}
		if hasActive {
			return nil
		}

		nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		if err != nil {
			return err
		}

		canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
		if err != nil {
			return err
		}
		if !canActivate {
			s.logger.Info("队首待生效套餐暂不满足激活条件",
				zap.Uint("usage_id", nextMain.ID),
				zap.String("carrier_type", carrierType),
				zap.Uint("carrier_id", carrierID),
				zap.Bool("pending_realname_activation", nextMain.PendingRealnameActivation))
			return nil
		}

		var pkg model.Package
		if err := tx.First(&pkg, nextMain.PackageID).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
		}

		if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, time.Now(), "队首待生效套餐已激活"); err != nil {
			return err
		}
		activated = true
		return nil
	})
	if err != nil {
		return false, err
	}
	if activated {
		s.resumeAfterActivation(ctx, carrierType, carrierID, "队首激活后自动复机失败")
	}

	return activated, nil
}

// HasActiveMainPackage 查询载体当前是否存在生效中的主套餐。
func (s *ActivationService) HasActiveMainPackage(ctx context.Context, carrierType string, carrierID uint) (bool, error) {
	return s.hasActiveMainPackage(ctx, s.db.WithContext(ctx), carrierType, carrierID)
}

// invalidateAddons 任务 9.7: 加油包级联失效
func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, masterUsageID uint) error {
	var addons []*model.PackageUsage
	if err := tx.Where("master_usage_id = ?", masterUsageID).
		Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusPending}).
		Find(&addons).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询加油包失败")
	}

	if len(addons) == 0 {
		return nil
	}

	addonIDs := make([]uint, len(addons))
	for i, addon := range addons {
		addonIDs[i] = addon.ID
	}

	// 批量更新加油包状态为已失效
	if err := tx.Model(&model.PackageUsage{}).
		Where("id IN ?", addonIDs).
		Update("status", constants.PackageUsageStatusInvalidated).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "批量失效加油包失败")
	}

	s.logger.Info("加油包已级联失效",
		zap.Uint("master_usage_id", masterUsageID),
		zap.Int("addon_count", len(addons)))

	return nil
}

// activateNextMainPackage 任务 9.6: 激活下一个待生效主套餐
func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (bool, error) {
	// 查询下一个待生效主套餐
	nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
	if err == gorm.ErrRecordNotFound {
		s.logger.Info("没有待生效的主套餐",
			zap.String("carrier_type", carrierType),
			zap.Uint("carrier_id", carrierID))
		return false, nil
	}
	if err != nil {
		return false, err
	}

	canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
	if err != nil {
		return false, err
	}
	if !canActivate {
		s.logger.Info("下一个待生效主套餐暂不满足激活条件",
			zap.Uint("usage_id", nextMain.ID),
			zap.String("carrier_type", carrierType),
			zap.Uint("carrier_id", carrierID),
			zap.Bool("pending_realname_activation", nextMain.PendingRealnameActivation))
		return false, nil
	}

	// 查询套餐信息
	var pkg model.Package
	if err := tx.First(&pkg, nextMain.PackageID).Error; err != nil {
		return false, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
	}

	if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, now, "排队主套餐已激活"); err != nil {
		return false, err
	}
	return true, nil
}

func (s *ActivationService) getNextPendingMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (*model.PackageUsage, error) {
	var nextMain model.PackageUsage
	err := tx.WithContext(ctx).
		Where("status = ?", constants.PackageUsageStatusPending).
		Where("master_usage_id IS NULL").
		Where(carrierType+"_id = ?", carrierID).
		Order("priority ASC, created_at ASC").
		First(&nextMain).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, err
		}
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询下一个待生效主套餐失败")
	}
	return &nextMain, nil
}

func (s *ActivationService) hasActiveMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
	var count int64
	// 生效中（1）和已用完（2）都属于"占位"状态：已用完的套餐仍在有效期内，不允许提前激活下一个主套餐
	if err := tx.WithContext(ctx).
		Model(&model.PackageUsage{}).
		Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
		Where("master_usage_id IS NULL").
		Where(carrierType+"_id = ?", carrierID).
		Count(&count).Error; err != nil {
		return false, errors.Wrap(errors.CodeDatabaseError, err, "检查生效中主套餐失败")
	}
	return count > 0, nil
}

func (s *ActivationService) canActivatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, carrierType string, carrierID uint) (bool, error) {
	if usage == nil {
		return false, nil
	}
	if !usage.PendingRealnameActivation {
		return true, nil
	}
	return s.isCarrierRealnamed(ctx, tx, carrierType, carrierID)
}

func (s *ActivationService) isCarrierRealnamed(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
	switch carrierType {
	case constants.AssetTypeIotCard:
		var card model.IotCard
		if err := tx.WithContext(ctx).
			Select("real_name_status").
			First(&card, carrierID).Error; err != nil {
			return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡实名状态失败")
		}
		return card.RealNameStatus == constants.RealNameStatusVerified, nil
	case constants.AssetTypeDevice:
		var count int64
		subQuery := tx.WithContext(ctx).
			Model(&model.DeviceSimBinding{}).
			Select("iot_card_id").
			Where("device_id = ? AND bind_status = ?", carrierID, constants.BindStatusBound)
		if err := tx.WithContext(ctx).
			Model(&model.IotCard{}).
			Where("id IN (?) AND real_name_status = ?", subQuery, constants.RealNameStatusVerified).
			Count(&count).Error; err != nil {
			return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备实名状态失败")
		}
		return count > 0, nil
	default:
		return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
	}
}

func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, pkg *model.Package, carrierType string, carrierID uint, now time.Time, logMessage string) error {
	terms, err := ResolveUsageTerms(usage, pkg, s.logger)
	if err != nil {
		return err
	}
	// ExpiryBase=from_purchase 只用于"等待实名激活"场景（REALNAME-04）：套餐已购买但资产未实名，
	// 计时基准按购买时间算，实名只是解锁使用权。此函数同时被"前一个主套餐到期后排队顺延"场景复用，
	// 这种情况下 usage.PendingRealnameActivation 为 false，不应该套用购买时间，否则排队等待的天数会
	// 从到期时间里被扣掉。只有当这条记录确实是因为等实名才被搁置时，才按 ExpiryBase 选基准。
	var activatedAt time.Time
	if usage.PendingRealnameActivation && terms.ExpiryBase == constants.PackageExpiryBaseFromPurchase {
		activatedAt = usage.CreatedAt
	} else {
		activatedAt = now
	}
	expiresAt := CalculateExpiryTime(terms.CalendarType, activatedAt, terms.DurationMonths, terms.DurationDays)

	// 计算下次重置时间
	nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)

	// 更新套餐使用记录
	updates := map[string]interface{}{
		"status":                      constants.PackageUsageStatusActive,
		"pending_realname_activation": false,
		"activated_at":                activatedAt,
		"expires_at":                  expiresAt,
	}
	if nextResetAt != nil {
		updates["next_reset_at"] = *nextResetAt
	}

	if err := tx.Model(usage).Updates(updates).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "激活排队主套餐失败")
	}
	if err := s.appendActivationObservation(ctx, tx, usage, carrierType, carrierID, now); err != nil {
		return err
	}

	s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)

	s.logger.Info(logMessage,
		zap.Uint("usage_id", usage.ID),
		zap.Uint("package_id", usage.PackageID),
		zap.Time("activated_at", activatedAt),
		zap.Time("expires_at", expiresAt))

	return nil
}

// resumeAfterActivation 在激活事务提交后异步尝试复机，避免外部副作用先于业务事实提交。
func (s *ActivationService) resumeAfterActivation(ctx context.Context, carrierType string, carrierID uint, errorMessage string) {
	if s.resumeCallback == nil {
		return
	}
	resumeCtx := context.WithoutCancel(ctx)
	go func() {
		if err := s.resumeCallback.ResumeCardIfStopped(resumeCtx, carrierType, carrierID); err != nil {
			s.logger.Error(errorMessage,
				zap.String("carrier_type", carrierType),
				zap.Uint("carrier_id", carrierID),
				zap.Error(err))
		}
	}()
}

func (s *ActivationService) appendActivationObservation(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, carrierType string, carrierID uint, occurredAt time.Time) error {
	if cardObservationApp.IsSeriesTriggerSuppressed(ctx) {
		return nil
	}
	if s.observationSeriesEvents == nil {
		return errors.New(errors.CodeInternalError, "套餐激活观测 Outbox Writer 未配置")
	}
	if usage == nil || usage.ID == 0 || carrierID == 0 {
		return errors.New(errors.CodeInvalidParam, "套餐激活观测事件缺少载体")
	}
	eventID := "card-observation:package-usage:" + strconv.FormatUint(uint64(usage.ID), 10) + ":activated"
	return s.observationSeriesEvents.AppendSeriesRequested(ctx, tx, cardObservationApp.SeriesRequestedEvent{
		EventID: eventID, Scene: constants.CardObservationScenePackageChanged,
		ResourceType: carrierType, ResourceID: carrierID,
		SyncTypes: []string{constants.CardObservationSyncTypeRealname, constants.CardObservationSyncTypeTraffic, constants.CardObservationSyncTypeNetwork},
		Source:    constants.CardObservationSourceBusinessEvent, OccurredAt: occurredAt,
		RequestID: eventID, CorrelationID: eventID,
	})
}

func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, carrierType string, carrierID uint) {
	if usage == nil {
		return
	}

	if usage.UsageType == constants.PackageUsageTypeDevice || usage.DeviceID > 0 || carrierType == constants.AssetTypeDevice {
		// 设备激活状态由“生效中主套餐 + 任意绑定卡已实名”动态计算，
		// 不再写入设备归属状态字段。
		return
	}

	cardID := usage.IotCardID
	if cardID == 0 {
		cardID = carrierID
	}
	if cardID == 0 {
		return
	}

	if err := tx.WithContext(ctx).
		Model(&model.IotCard{}).
		Where("id = ? AND status < ?", cardID, constants.IotCardStatusActivated).
		Update("status", constants.IotCardStatusActivated).Error; err != nil {
		s.logger.Warn("套餐激活后更新卡状态失败",
			zap.Uint("iot_card_id", cardID),
			zap.Uint("usage_id", usage.ID),
			zap.Error(err))
	}
}

// InvalidatePackagesForRefund 退款精准失效套餐并写入退款快照
// 规则：
// 1. 优先按 packageUsageID 精准失效单条套餐（且必须属于当前退款订单）
// 2. 未传 packageUsageID 时，按 orderID 失效该订单生成的套餐
// 3. 若命中主套餐，级联失效其加油包，避免残留挂靠关系
func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, assetType string, assetID uint, orderID uint, refundID uint, refundNo string, packageUsageID *uint) error {
	if orderID == 0 {
		return errors.New(errors.CodeInvalidParam, "无效的订单ID")
	}
	if assetType != constants.AssetTypeIotCard && assetType != constants.AssetTypeDevice {
		return errors.New(errors.CodeInvalidParam, "无效的资产类型")
	}

	validStatuses := []int{
		constants.PackageUsageStatusPending,
		constants.PackageUsageStatusActive,
		constants.PackageUsageStatusDepleted,
	}

	// 换货会把套餐使用记录迁移到新资产，但原订单与套餐使用记录的关联保持不变。
	// 因此退款必须以订单和套餐使用记录为权威定位键，不能再用旧资产 ID 缩小查询范围。
	baseQuery := s.db.WithContext(ctx).Model(&model.PackageUsage{})

	var targets []model.PackageUsage
	if packageUsageID != nil && *packageUsageID > 0 {
		var usage model.PackageUsage
		err := baseQuery.
			Where("id = ? AND order_id = ?", *packageUsageID, orderID).
			Where("status IN ?", validStatuses).
			First(&usage).Error
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				s.logger.Info("退款精准失效：未命中可失效套餐",
					zap.String("asset_type", assetType),
					zap.Uint("asset_id", assetID),
					zap.Uint("order_id", orderID),
					zap.Uint("package_usage_id", *packageUsageID),
				)
				return nil
			}
			return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联套餐失败")
		}
		targets = append(targets, usage)
	} else {
		if err := baseQuery.
			Where("order_id = ?", orderID).
			Where("status IN ?", validStatuses).
			Find(&targets).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "查询退款订单套餐失败")
		}
		if len(targets) == 0 {
			s.logger.Info("退款精准失效：订单无可失效套餐",
				zap.String("asset_type", assetType),
				zap.Uint("asset_id", assetID),
				zap.Uint("order_id", orderID),
			)
			return nil
		}
	}

	targetIDSet := make(map[uint]struct{}, len(targets))
	mainUsageIDs := make([]uint, 0, len(targets))
	for _, usage := range targets {
		targetIDSet[usage.ID] = struct{}{}
		if usage.MasterUsageID == nil {
			mainUsageIDs = append(mainUsageIDs, usage.ID)
		}
	}

	if len(mainUsageIDs) > 0 {
		var addons []model.PackageUsage
		if err := s.db.WithContext(ctx).Model(&model.PackageUsage{}).
			Where("master_usage_id IN ?", mainUsageIDs).
			Where("status IN ?", validStatuses).
			Find(&addons).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐关联加油包失败")
		}
		for _, addon := range addons {
			targetIDSet[addon.ID] = struct{}{}
		}
	}

	targetIDs := make([]uint, 0, len(targetIDSet))
	for id := range targetIDSet {
		targetIDs = append(targetIDs, id)
	}
	if len(targetIDs) == 0 {
		return nil
	}

	updates := map[string]any{
		"status": constants.PackageUsageStatusInvalidated,
	}
	if refundID > 0 {
		updates["refund_id"] = refundID
	}
	if refundNo != "" {
		updates["refund_no"] = refundNo
	}

	result := s.db.WithContext(ctx).
		Model(&model.PackageUsage{}).
		Where("id IN ?", targetIDs).
		Updates(updates)
	if result.Error != nil {
		return errors.Wrap(errors.CodeDatabaseError, result.Error, "退款失效套餐失败")
	}

	s.logger.Info("退款精准失效套餐完成",
		zap.String("asset_type", assetType),
		zap.Uint("asset_id", assetID),
		zap.Uint("order_id", orderID),
		zap.Uint("refund_id", refundID),
		zap.String("refund_no", refundNo),
		zap.Int("target_count", len(targetIDs)),
		zap.Int64("affected", result.RowsAffected),
	)

	return nil
}

// InvalidateAllPackagesByAsset 批量失效资产关联的所有有效套餐
// 退款时调用：将该资产下状态为待生效(0)、生效中(1)、已用完(2)的套餐全部标记为已失效(4)
func (s *ActivationService) InvalidateAllPackagesByAsset(ctx context.Context, assetType string, assetID uint) error {
	query := s.db.WithContext(ctx).
		Model(&model.PackageUsage{}).
		Where("status IN ?", []int{
			constants.PackageUsageStatusPending,
			constants.PackageUsageStatusActive,
			constants.PackageUsageStatusDepleted,
		})

	switch assetType {
	case "iot_card":
		query = query.Where("iot_card_id = ?", assetID)
	case "device":
		query = query.Where("device_id = ?", assetID)
	default:
		return errors.New(errors.CodeInvalidParam, "无效的资产类型")
	}

	result := query.Update("status", constants.PackageUsageStatusInvalidated)
	if result.Error != nil {
		return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量失效套餐失败")
	}

	s.logger.Info("批量失效套餐完成",
		zap.String("asset_type", assetType),
		zap.Uint("asset_id", assetID),
		zap.Int64("affected", result.RowsAffected),
	)

	return nil
}
