package postgres

import (
	"context"
	"fmt"
	"hash/fnv"
	"sort"
	"sync"
	"time"

	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/store"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/logger"
	"github.com/break/junhong_cmp_fiber/pkg/middleware"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const listCountCacheTTL = 30 * time.Second

type IotCardStore struct {
	db    *gorm.DB
	redis *redis.Client
}

func NewIotCardStore(db *gorm.DB, redis *redis.Client) *IotCardStore {
	return &IotCardStore{
		db:    db,
		redis: redis,
	}
}

func (s *IotCardStore) Create(ctx context.Context, card *model.IotCard) error {
	if err := s.db.WithContext(ctx).Create(card).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

func (s *IotCardStore) CreateBatch(ctx context.Context, cards []*model.IotCard) error {
	if len(cards) == 0 {
		return nil
	}
	if err := s.db.WithContext(ctx).CreateInBatches(cards, 100).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

func (s *IotCardStore) GetByID(ctx context.Context, id uint) (*model.IotCard, error) {
	var card model.IotCard
	query := s.db.WithContext(ctx).Where("id = ?", id)
	// 应用数据权限过滤（NULL shop_id 对代理用户不可见）
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.First(&card).Error; err != nil {
		return nil, err
	}
	return &card, nil
}

func (s *IotCardStore) GetByICCID(ctx context.Context, iccid string) (*model.IotCard, error) {
	var card model.IotCard
	var column string
	switch len(iccid) {
	case 19:
		column = "iccid_19"
	case 20:
		column = "iccid_20"
	default:
		logger.GetAppLogger().Error("GetByICCID: ICCID 长度异常，拒绝查询",
			zap.String("iccid", iccid),
			zap.Int("length", len(iccid)),
		)
		return nil, gorm.ErrRecordNotFound
	}
	query := s.db.WithContext(ctx).Where(column+" = ?", iccid)
	query = middleware.ApplyShopFilter(ctx, query)
	err := query.First(&card).Error
	if err == gorm.ErrRecordNotFound {
		logger.GetAppLogger().Warn("GetByICCID: 未命中，上游 ICCID 格式可能与存储不一致",
			zap.String("iccid", iccid),
			zap.String("column_used", column),
		)
		return nil, err
	}
	return &card, err
}

// GetByIdentifier 通过 ICCID、虚拟号或 MSISDN 精确查询 IoT 卡
func (s *IotCardStore) GetByIdentifier(ctx context.Context, identifier string) (*model.IotCard, error) {
	var card model.IotCard
	query := s.db.WithContext(ctx).Where(
		"virtual_no = ? OR iccid = ? OR msisdn = ? OR iccid_19 = ? OR iccid_20 = ?",
		identifier,
		identifier,
		identifier,
		identifier,
		identifier,
	)
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.First(&card).Error; err != nil {
		return nil, err
	}
	return &card, nil
}

func (s *IotCardStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.IotCard, error) {
	if len(ids) == 0 {
		return []*model.IotCard{}, nil
	}
	var cards []*model.IotCard
	query := s.db.WithContext(ctx).Where("id IN ?", ids)
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

func (s *IotCardStore) ExistsByICCID(ctx context.Context, iccid string) (bool, error) {
	var column string
	switch len(iccid) {
	case 19:
		column = "iccid_19"
	case 20:
		column = "iccid_20"
	default:
		logger.GetAppLogger().Error("ExistsByICCID: ICCID 长度异常，拒绝查询",
			zap.String("iccid", iccid),
			zap.Int("length", len(iccid)),
		)
		return false, nil
	}
	var count int64
	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).Where(column+" = ?", iccid).Count(&count).Error; err != nil {
		return false, err
	}
	return count > 0, nil
}

func (s *IotCardStore) ExistsByICCIDBatch(ctx context.Context, iccids []string) (map[string]bool, error) {
	if len(iccids) == 0 {
		return make(map[string]bool), nil
	}

	var list19, list20 []string
	for _, id := range iccids {
		switch len(id) {
		case 19:
			list19 = append(list19, id)
		case 20:
			list20 = append(list20, id)
		}
	}

	result := make(map[string]bool)

	if len(list19) > 0 {
		var found []string
		if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
			Where("iccid_19 IN ?", list19).
			Pluck("iccid_19", &found).Error; err != nil {
			return nil, err
		}
		for _, v := range found {
			result[v] = true
		}
	}

	if len(list20) > 0 {
		var found []string
		if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
			Where("iccid_20 IN ?", list20).
			Pluck("iccid_20", &found).Error; err != nil {
			return nil, err
		}
		for _, v := range found {
			result[v] = true
		}
	}

	return result, nil
}

func (s *IotCardStore) Update(ctx context.Context, card *model.IotCard) error {
	if err := s.db.WithContext(ctx).Save(card).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

// UpdateFields 按 ID 部分更新卡字段
// 当更新包含 network_status/real_name_status/card_category 时，自动同步 activation_status
func (s *IotCardStore) UpdateFields(ctx context.Context, cardID uint, fields map[string]any) error {
	if len(fields) == 0 {
		return nil
	}

	updates := make(map[string]any, len(fields)+1)
	for k, v := range fields {
		updates[k] = v
	}

	if shouldSyncActivationStatus(fields) {
		updates["activation_status"] = buildActivationStatusExpr(fields)
		// 激活状态从 0→1 时补写激活时间；仅写首次，不覆盖已有值。
		if _, hasActivatedAt := fields["activated_at"]; !hasActivatedAt {
			updates["activated_at"] = buildActivatedAtExpr(fields, time.Now())
		}
	}

	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id = ?", cardID).
		Updates(updates).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

func shouldSyncActivationStatus(fields map[string]any) bool {
	if fields == nil {
		return false
	}
	if _, ok := fields["network_status"]; ok {
		return true
	}
	if _, ok := fields["real_name_status"]; ok {
		return true
	}
	if _, ok := fields["card_category"]; ok {
		return true
	}
	return false
}

// buildActivationStatusExpr 构造激活状态表达式（与本次更新同 SQL 执行）
// 规则：开机 且（行业卡 或 已实名）=> 已激活；否则未激活
// 若本次未更新某字段，则回退使用该字段当前值。
func buildActivationStatusExpr(fields map[string]any) any {
	var networkStatus any
	var cardCategory any
	var realNameStatus any

	if v, ok := fields["network_status"]; ok {
		networkStatus = v
	}
	if v, ok := fields["card_category"]; ok {
		cardCategory = v
	}
	if v, ok := fields["real_name_status"]; ok {
		realNameStatus = v
	}

	return gorm.Expr(`
			CASE
				WHEN COALESCE(CAST(? AS integer), network_status) = CAST(? AS integer)
					AND (
						COALESCE(CAST(? AS text), card_category) = CAST(? AS text)
						OR COALESCE(CAST(? AS integer), real_name_status) = CAST(? AS integer)
					)
				THEN CAST(? AS integer)
				ELSE CAST(? AS integer)
			END`,
		networkStatus,
		constants.NetworkStatusOnline,
		cardCategory,
		constants.CardCategoryIndustry,
		realNameStatus,
		constants.RealNameStatusVerified,
		constants.ActivationStatusActive,
		constants.ActivationStatusInactive,
	)
}

// buildActivatedAtExpr 构造激活时间表达式（与本次更新同 SQL 执行）
// 仅在激活状态从 0→1 时写入当前时间，且不覆盖已有 activated_at。
func buildActivatedAtExpr(fields map[string]any, now time.Time) any {
	var networkStatus any
	var cardCategory any
	var realNameStatus any

	if v, ok := fields["network_status"]; ok {
		networkStatus = v
	}
	if v, ok := fields["card_category"]; ok {
		cardCategory = v
	}
	if v, ok := fields["real_name_status"]; ok {
		realNameStatus = v
	}

	return gorm.Expr(`
			CASE
				WHEN activation_status = CAST(? AS integer)
					AND COALESCE(CAST(? AS integer), network_status) = CAST(? AS integer)
					AND (
						COALESCE(CAST(? AS text), card_category) = CAST(? AS text)
						OR COALESCE(CAST(? AS integer), real_name_status) = CAST(? AS integer)
					)
				THEN COALESCE(activated_at, CAST(? AS timestamptz))
				ELSE activated_at
			END`,
		constants.ActivationStatusInactive,
		networkStatus,
		constants.NetworkStatusOnline,
		cardCategory,
		constants.CardCategoryIndustry,
		realNameStatus,
		constants.RealNameStatusVerified,
		now,
	)
}

// UpdatePollingStatus 更新卡的轮询启用状态
func (s *IotCardStore) UpdatePollingStatus(ctx context.Context, cardID uint, enablePolling bool) error {
	return s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id = ?", cardID).
		Update("enable_polling", enablePolling).Error
}

// UpdateRealnamePolicy 更新卡的实名认证策略
func (s *IotCardStore) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnamePolicy string) error {
	return s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id = ?", cardID).
		Update("realname_policy", realnamePolicy).Error
}

func (s *IotCardStore) Delete(ctx context.Context, id uint) error {
	if err := s.db.WithContext(ctx).Delete(&model.IotCard{}, id).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

func (s *IotCardStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
	var cards []*model.IotCard
	var total int64

	query := s.db.WithContext(ctx).Model(&model.IotCard{})
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)

	// 企业用户特殊处理：只能看到授权给自己的卡
	// 子查询跳过数据权限过滤，权限已由外层查询的 GORM callback 保证
	// 子查询无需数据权限过滤（在不同表上执行）
	if enterpriseID, ok := filters["authorized_enterprise_id"].(uint); ok && enterpriseID > 0 {
		query = query.Where("id IN (?)",
			s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
				Select("card_id").
				Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID))
	}

	// 基础过滤条件
	if status, ok := filters["status"].(int); ok && status > 0 {
		query = query.Where("status = ?", status)
	}
	if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
		query = query.Where("carrier_id = ?", carrierID)
	}
	if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 {
		query = query.Where("shop_id = ?", shopID)
	}
	if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
		query = query.Where("iccid LIKE ?", "%"+iccid+"%")
	}
	if msisdn, ok := filters["msisdn"].(string); ok && msisdn != "" {
		query = query.Where("msisdn LIKE ?", "%"+msisdn+"%")
	}
	if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
		query = query.Where("batch_no = ?", batchNo)
	}
	if packageID, ok := filters["package_id"].(uint); ok && packageID > 0 {
		query = query.Where("id IN (?)",
			s.db.WithContext(ctx).Table("tb_package_usage").
				Select("iot_card_id").
				Where("package_id = ? AND deleted_at IS NULL", packageID))
	}
	if isDistributed, ok := filters["is_distributed"].(bool); ok {
		if isDistributed {
			query = query.Where("shop_id IS NOT NULL")
		} else {
			query = query.Where("shop_id IS NULL")
		}
	}
	if iccidStart, ok := filters["iccid_start"].(string); ok && iccidStart != "" {
		query = query.Where("iccid >= ?", iccidStart)
	}
	if iccidEnd, ok := filters["iccid_end"].(string); ok && iccidEnd != "" {
		query = query.Where("iccid <= ?", iccidEnd)
	}
	if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
		query = query.Where("series_id = ?", seriesID)
	}

	if cachedTotal, ok, cacheVersion := s.getCachedCount(ctx, "iot_card", filters); ok {
		total = cachedTotal
	} else {
		if err := query.Count(&total).Error; err != nil {
			return nil, 0, err
		}
		s.cacheCount(ctx, "iot_card", filters, total, cacheVersion)
	}

	// 分页处理
	if opts == nil {
		opts = &store.QueryOptions{
			Page:     1,
			PageSize: constants.DefaultPageSize,
		}
	}
	offset := (opts.Page - 1) * opts.PageSize
	query = query.Offset(offset).Limit(opts.PageSize)

	// 排序
	if opts.OrderBy != "" {
		query = query.Order(opts.OrderBy)
	} else {
		query = query.Order("created_at DESC")
	}

	// 查询结果
	if err := query.Find(&cards).Error; err != nil {
		return nil, 0, err
	}

	return cards, total, nil
}

// standaloneListColumns 列表查询只选取响应需要的列，避免 SELECT * 的宽行 I/O
var standaloneListColumns = []string{
	"id", "iccid", "virtual_no", "card_category", "carrier_id", "carrier_type", "carrier_name",
	"imsi", "msisdn", "batch_no", "supplier", "status", "shop_id", "activated_at",
	"activation_status", "real_name_status", "network_status", "data_usage_mb",
	"current_month_usage_mb", "last_gateway_reading_mb", "current_month_start_date", "last_month_total_mb",
	"gateway_extend", "gateway_card_imei", "last_data_check_at", "last_real_name_check_at", "enable_polling", "series_id", "device_virtual_no", "realname_policy",
	"created_at", "updated_at",
}

// ListStandalone 独立卡列表查询入口
// 路由逻辑：
//   - ICCID/MSISDN 模糊搜索 → listStandaloneDefault（trigram GIN 索引）
//   - 多店铺（代理用户） → listStandaloneParallel（per-shop 并行，内含深度分页两阶段优化）
//   - 深度分页（page >= 50）→ listStandaloneTwoPhase（覆盖索引延迟 Join）
//   - 其他 → listStandaloneDefault
func (s *IotCardStore) ListStandalone(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
	if opts == nil {
		opts = &store.QueryOptions{
			Page:     1,
			PageSize: constants.DefaultPageSize,
		}
	}

	_, hasICCID := filters["iccid"].(string)
	_, hasMSISDN := filters["msisdn"].(string)
	_, hasVirtualNo := filters["virtual_no"].(string)
	_, hasKeyword := filters["keyword"].(string)
	useFuzzySearch := (hasICCID && filters["iccid"] != "") || (hasMSISDN && filters["msisdn"] != "") || (hasVirtualNo && filters["virtual_no"] != "") || (hasKeyword && filters["keyword"] != "")

	if !useFuzzySearch {
		if shopIDs, ok := filters["subordinate_shop_ids"].([]uint); ok && len(shopIDs) > 1 {
			return s.listStandaloneParallel(ctx, opts, filters, shopIDs)
		}
	}

	// 非多店铺场景的深度分页走两阶段路径（单店铺或平台用户）
	if !useFuzzySearch && opts.Page >= 50 {
		return s.listStandaloneTwoPhase(ctx, opts, filters)
	}

	return s.listStandaloneDefault(ctx, opts, filters)
}

// listStandaloneTwoPhase 单路径两阶段延迟 Join
// 适用于非多店铺场景（平台用户、单店铺）的深度分页（page >= 50）
// Phase 1: SELECT id 走覆盖索引；Phase 2: WHERE id IN 回表
func (s *IotCardStore) listStandaloneTwoPhase(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
	var total int64

	query := s.db.WithContext(ctx).Model(&model.IotCard{})
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)
	query = s.applyStandaloneFilters(ctx, query, filters)

	if cachedTotal, ok, cacheVersion := s.getCachedCount(ctx, "standalone", filters); ok {
		total = cachedTotal
	} else {
		if err := query.Count(&total).Error; err != nil {
			return nil, 0, err
		}
		s.cacheCount(ctx, "standalone", filters, total, cacheVersion)
	}

	offset := (opts.Page - 1) * opts.PageSize

	// Phase 1: 仅取 ID（覆盖索引 Index Only Scan）
	var ids []uint
	if err := query.Select("id").
		Order("created_at DESC, id").
		Offset(offset).Limit(opts.PageSize).
		Pluck("id", &ids).Error; err != nil {
		return nil, 0, err
	}

	if len(ids) == 0 {
		return []*model.IotCard{}, total, nil
	}

	// Phase 2: 用 ID 精确回表
	var cards []*model.IotCard
	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Select(standaloneListColumns).
		Where("id IN ?", ids).
		Find(&cards).Error; err != nil {
		return nil, 0, err
	}

	idOrder := make(map[uint]int, len(ids))
	for i, id := range ids {
		idOrder[id] = i
	}
	sort.Slice(cards, func(i, j int) bool {
		return idOrder[cards[i].ID] < idOrder[cards[j].ID]
	})

	logger.GetAppLogger().Debug("两阶段查询完成",
		zap.Int("page", opts.Page),
		zap.Int("phase2_fetched", len(cards)),
		zap.Int64("total_count", total),
	)

	return cards, total, nil
}

// listStandaloneDefault 原始查询路径
// 适用于平台/超管用户（无 shop_id 过滤）或单店铺场景
func (s *IotCardStore) listStandaloneDefault(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
	var cards []*model.IotCard
	var total int64

	query := s.db.WithContext(ctx).Model(&model.IotCard{})
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)
	query = s.applyStandaloneFilters(ctx, query, filters)

	if cachedTotal, ok, cacheVersion := s.getCachedCount(ctx, "standalone", filters); ok {
		total = cachedTotal
	} else {
		if err := query.Count(&total).Error; err != nil {
			return nil, 0, err
		}
		s.cacheCount(ctx, "standalone", filters, total, cacheVersion)
	}

	offset := (opts.Page - 1) * opts.PageSize

	if opts.OrderBy != "" {
		query = query.Order(opts.OrderBy)
	} else {
		query = query.Order("created_at DESC")
	}

	if err := query.Select(standaloneListColumns).Offset(offset).Limit(opts.PageSize).Find(&cards).Error; err != nil {
		return nil, 0, err
	}

	return cards, total, nil
}

// listStandaloneParallel 并行查询路径
// 将 shop_id IN (...) 拆分为 per-shop 独立查询，每个查询走 Index Scan
// 然后在应用层归并排序，避免 PG 对多值 IN + ORDER BY 选择全表扫描
func (s *IotCardStore) listStandaloneParallel(ctx context.Context, opts *store.QueryOptions, filters map[string]any, shopIDs []uint) ([]*model.IotCard, int64, error) {
	// 子查询无需数据权限过滤（在不同表上执行）

	fetchLimit := (opts.Page-1)*opts.PageSize + opts.PageSize

	// 深度分页（page >= 50）走两阶段延迟 Join：
	// Phase 1 只取 ID（覆盖索引 Index Only Scan），Phase 2 用 ID 回表
	// 避免 OFFSET 跳过大量宽行数据
	if opts.Page >= 50 {
		return s.listStandaloneParallelTwoPhase(ctx, opts, filters, shopIDs)
	}

	type shopResult struct {
		cards []*model.IotCard
		count int64
		err   error
	}

	results := make([]shopResult, len(shopIDs))
	var wg sync.WaitGroup

	cachedTotal, hasCachedTotal, cacheVersion := s.getCachedCount(ctx, "standalone", filters)

	for i, shopID := range shopIDs {
		wg.Add(1)
		go func(idx int, sid uint) {
			defer wg.Done()

			q := s.db.WithContext(ctx).Model(&model.IotCard{}).
				Where("deleted_at IS NULL AND shop_id = ?", sid)
			q = s.applyStandaloneFilters(ctx, q, filters)

			var cards []*model.IotCard
			if err := q.Select(standaloneListColumns).
				Order("created_at DESC, id").
				Limit(fetchLimit).
				Find(&cards).Error; err != nil {
				results[idx] = shopResult{err: err}
				return
			}

			var count int64
			if !hasCachedTotal {
				countQ := s.db.WithContext(ctx).Model(&model.IotCard{}).
					Where("deleted_at IS NULL AND shop_id = ?", sid)
				countQ = s.applyStandaloneFilters(ctx, countQ, filters)
				if err := countQ.Count(&count).Error; err != nil {
					results[idx] = shopResult{err: err}
					return
				}
			}

			results[idx] = shopResult{cards: cards, count: count}
		}(i, shopID)
	}

	wg.Wait()

	allCards := make([]*model.IotCard, 0, fetchLimit)
	var totalCount int64
	if hasCachedTotal {
		totalCount = cachedTotal
	}

	for _, r := range results {
		if r.err != nil {
			return nil, 0, r.err
		}
		allCards = append(allCards, r.cards...)
		if !hasCachedTotal {
			totalCount += r.count
		}
	}

	if !hasCachedTotal && totalCount > 0 {
		s.cacheCount(ctx, "standalone", filters, totalCount, cacheVersion)
	}

	sort.Slice(allCards, func(i, j int) bool {
		if allCards[i].CreatedAt.Equal(allCards[j].CreatedAt) {
			return allCards[i].ID < allCards[j].ID
		}
		return allCards[i].CreatedAt.After(allCards[j].CreatedAt)
	})

	offset := (opts.Page - 1) * opts.PageSize
	end := offset + opts.PageSize
	if offset >= len(allCards) {
		return []*model.IotCard{}, totalCount, nil
	}
	if end > len(allCards) {
		end = len(allCards)
	}

	logger.GetAppLogger().Debug("并行查询完成",
		zap.Int("shop_count", len(shopIDs)),
		zap.Int("total_fetched", len(allCards)),
		zap.Int("offset", offset),
		zap.Int("page_size", opts.PageSize),
		zap.Int64("total_count", totalCount),
	)

	return allCards[offset:end], totalCount, nil
}

// cardIDWithTime 仅存储 ID 和排序键，用于两阶段查询的轻量归并排序
type cardIDWithTime struct {
	ID        uint
	CreatedAt time.Time
}

// listStandaloneParallelTwoPhase 并行+两阶段延迟 Join 查询路径
// 解决深度分页（page >= 50）时 OFFSET 跳过大量宽行数据的性能问题：
// Phase 1: 每个 shop 并行查询 SELECT id, created_at（走覆盖索引 Index Only Scan，~20字节/元组）
// 归并排序后取目标页的 20 个 ID
// Phase 2: SELECT 完整列 WHERE id IN (20 IDs)（PK 精确回表）
func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts *store.QueryOptions, filters map[string]any, shopIDs []uint) ([]*model.IotCard, int64, error) {
	// 子查询无需数据权限过滤（在不同表上执行）

	fetchLimit := (opts.Page-1)*opts.PageSize + opts.PageSize

	type shopResult struct {
		ids   []cardIDWithTime
		count int64
		err   error
	}

	results := make([]shopResult, len(shopIDs))
	var wg sync.WaitGroup

	cachedTotal, hasCachedTotal, cacheVersion := s.getCachedCount(ctx, "standalone", filters)

	// Phase 1: 并行获取每个 shop 的 (id, created_at)，走覆盖索引
	for i, shopID := range shopIDs {
		wg.Add(1)
		go func(idx int, sid uint) {
			defer wg.Done()

			q := s.db.WithContext(ctx).Model(&model.IotCard{}).
				Where("deleted_at IS NULL AND shop_id = ?", sid)
			q = s.applyStandaloneFilters(ctx, q, filters)

			var ids []cardIDWithTime
			if err := q.Select("id, created_at").
				Order("created_at DESC, id").
				Limit(fetchLimit).
				Find(&ids).Error; err != nil {
				results[idx] = shopResult{err: err}
				return
			}

			var count int64
			if !hasCachedTotal {
				countQ := s.db.WithContext(ctx).Model(&model.IotCard{}).
					Where("deleted_at IS NULL AND shop_id = ?", sid)
				countQ = s.applyStandaloneFilters(ctx, countQ, filters)
				if err := countQ.Count(&count).Error; err != nil {
					results[idx] = shopResult{err: err}
					return
				}
			}

			results[idx] = shopResult{ids: ids, count: count}
		}(i, shopID)
	}

	wg.Wait()

	allIDs := make([]cardIDWithTime, 0, fetchLimit)
	var totalCount int64
	if hasCachedTotal {
		totalCount = cachedTotal
	}

	for _, r := range results {
		if r.err != nil {
			return nil, 0, r.err
		}
		allIDs = append(allIDs, r.ids...)
		if !hasCachedTotal {
			totalCount += r.count
		}
	}

	if !hasCachedTotal && totalCount > 0 {
		s.cacheCount(ctx, "standalone", filters, totalCount, cacheVersion)
	}

	sort.Slice(allIDs, func(i, j int) bool {
		if allIDs[i].CreatedAt.Equal(allIDs[j].CreatedAt) {
			return allIDs[i].ID < allIDs[j].ID
		}
		return allIDs[i].CreatedAt.After(allIDs[j].CreatedAt)
	})

	offset := (opts.Page - 1) * opts.PageSize
	end := offset + opts.PageSize
	if offset >= len(allIDs) {
		return []*model.IotCard{}, totalCount, nil
	}
	if end > len(allIDs) {
		end = len(allIDs)
	}

	pageIDs := make([]uint, 0, opts.PageSize)
	for _, item := range allIDs[offset:end] {
		pageIDs = append(pageIDs, item.ID)
	}

	if len(pageIDs) == 0 {
		return []*model.IotCard{}, totalCount, nil
	}

	// Phase 2: 用 ID 精确回表获取完整数据（PK Index Scan，仅 20 行）
	var cards []*model.IotCard
	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Select(standaloneListColumns).
		Where("id IN ?", pageIDs).
		Find(&cards).Error; err != nil {
		return nil, 0, err
	}

	// Phase 2 结果按原始排序顺序重排
	idOrder := make(map[uint]int, len(pageIDs))
	for i, id := range pageIDs {
		idOrder[id] = i
	}
	sort.Slice(cards, func(i, j int) bool {
		return idOrder[cards[i].ID] < idOrder[cards[j].ID]
	})

	logger.GetAppLogger().Debug("两阶段并行查询完成",
		zap.Int("shop_count", len(shopIDs)),
		zap.Int("phase1_total_ids", len(allIDs)),
		zap.Int("phase2_fetched", len(cards)),
		zap.Int("page", opts.Page),
		zap.Int64("total_count", totalCount),
	)

	return cards, totalCount, nil
}

// applyStandaloneFilters 应用独立卡列表的通用过滤条件
// 注意：不包含 deleted_at 条件（由调用方控制）
// subordinate_shop_ids 仅用于路由选择，不作为查询条件
// is_standalone 仅在 filters["is_standalone"] 非 nil 时拼接（不传则不过滤）
func (s *IotCardStore) applyStandaloneFilters(ctx context.Context, query *gorm.DB, filters map[string]any) *gorm.DB {
	// is_standalone 为可选过滤条件，仅在明确传入时应用
	if isStandalone, ok := filters["is_standalone"].(*bool); ok && isStandalone != nil {
		query = query.Where("is_standalone = ?", *isStandalone)
	}
	// 子查询无需数据权限过滤（在不同表上执行）

	if status, ok := filters["status"].(int); ok && status > 0 {
		query = query.Where("status = ?", status)
	}
	if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
		query = query.Where("carrier_id = ?", carrierID)
	}
	// 并行路径下 shop_id 已由调用方设置，此处仅处理显式店铺筛选。
	if shopIDs, ok := filters["shop_ids"].([]uint); ok {
		if len(shopIDs) == 0 {
			query = query.Where("1 = 0")
		} else {
			query = query.Where("shop_id IN ?", shopIDs)
		}
	} else if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 {
		query = query.Where("shop_id = ?", shopID)
	}
	if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
		query = query.Where("iccid LIKE ?", "%"+iccid+"%")
	}
	if msisdn, ok := filters["msisdn"].(string); ok && msisdn != "" {
		query = query.Where("msisdn LIKE ?", "%"+msisdn+"%")
	}
	if virtualNo, ok := filters["virtual_no"].(string); ok && virtualNo != "" {
		query = query.Where("virtual_no LIKE ?", "%"+virtualNo+"%")
	}
	if keyword, ok := filters["keyword"].(string); ok && keyword != "" {
		query = query.Where("iccid LIKE ? OR virtual_no LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
	}
	if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
		query = query.Where("batch_no = ?", batchNo)
	}
	if packageID, ok := filters["package_id"].(uint); ok && packageID > 0 {
		query = query.Where("id IN (?)",
			s.db.WithContext(ctx).Table("tb_package_usage").
				Select("iot_card_id").
				Where("package_id = ? AND deleted_at IS NULL", packageID))
	}
	if isDistributed, ok := filters["is_distributed"].(bool); ok {
		if isDistributed {
			query = query.Where("shop_id IS NOT NULL")
		} else {
			query = query.Where("shop_id IS NULL")
		}
	}
	if iccidStart, ok := filters["iccid_start"].(string); ok && iccidStart != "" {
		query = query.Where("iccid >= ?", iccidStart)
	}
	if iccidEnd, ok := filters["iccid_end"].(string); ok && iccidEnd != "" {
		query = query.Where("iccid <= ?", iccidEnd)
	}
	if isReplaced, ok := filters["is_replaced"].(bool); ok {
		if isReplaced {
			query = query.Where("id IN (?)",
				s.db.WithContext(ctx).Table("tb_exchange_order").
					Select("old_asset_id").
					Where("old_asset_type = ? AND status IN ? AND deleted_at IS NULL", constants.ExchangeAssetTypeIotCard, []int{constants.ExchangeStatusShipped, constants.ExchangeStatusCompleted}))
		} else {
			query = query.Where("id NOT IN (?)",
				s.db.WithContext(ctx).Table("tb_exchange_order").
					Select("old_asset_id").
					Where("old_asset_type = ? AND status IN ? AND deleted_at IS NULL", constants.ExchangeAssetTypeIotCard, []int{constants.ExchangeStatusShipped, constants.ExchangeStatusCompleted}))
		}
	}
	if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
		query = query.Where("series_id = ?", seriesID)
	}
	if carrierName, ok := filters["carrier_name"].(string); ok && carrierName != "" {
		query = query.Where("carrier_name LIKE ?", "%"+carrierName+"%")
	}
	if networkStatus, ok := filters["network_status"].(int); ok {
		query = query.Where("network_status = ?", networkStatus)
	}
	if realNameStatus, ok := filters["real_name_status"].(int); ok {
		query = query.Where("real_name_status = ?", realNameStatus)
	}
	if enterpriseID, ok := filters["authorized_enterprise_id"].(uint); ok && enterpriseID > 0 {
		// 只返回有效授权给该企业的卡
		query = query.Where("id IN (?)",
			s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
				Select("card_id").
				Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID))
	}
	if isAuthorized, ok := filters["is_authorized_to_enterprise"].(bool); ok {
		subQ := s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
			Select("card_id").
			Where("revoked_at IS NULL AND deleted_at IS NULL")
		if isAuthorized {
			query = query.Where("id IN (?)", subQ)
		} else {
			query = query.Where("id NOT IN (?)", subQ)
		}
	}
	// has_active_package 过滤：是否存在生效中的主套餐记录
	if hasActivePackage, ok := filters["has_active_package"].(bool); ok {
		subQuery := s.db.WithContext(ctx).Table("tb_package_usage pu").Select("1").
			Where("pu.iot_card_id = tb_iot_card.id AND pu.status = ? AND pu.master_usage_id IS NULL AND pu.deleted_at IS NULL", constants.PackageUsageStatusActive)
		if hasActivePackage {
			query = query.Where("EXISTS (?)", subQuery)
		} else {
			query = query.Where("NOT EXISTS (?)", subQuery)
		}
	}

	return query
}

func (s *IotCardStore) GetByICCIDs(ctx context.Context, iccids []string) ([]*model.IotCard, error) {
	if len(iccids) == 0 {
		return nil, nil
	}

	var list19, list20 []string
	for _, id := range iccids {
		switch len(id) {
		case 19:
			list19 = append(list19, id)
		case 20:
			list20 = append(list20, id)
		}
	}

	seen := make(map[uint]struct{})
	var all []*model.IotCard

	if len(list19) > 0 {
		var cards []*model.IotCard
		query := s.db.WithContext(ctx).Where("iccid_19 IN ?", list19)
		query = middleware.ApplyShopFilter(ctx, query)
		if err := query.Find(&cards).Error; err != nil {
			return nil, err
		}
		for _, c := range cards {
			if _, dup := seen[c.ID]; !dup {
				seen[c.ID] = struct{}{}
				all = append(all, c)
			}
		}
	}

	if len(list20) > 0 {
		var cards []*model.IotCard
		query := s.db.WithContext(ctx).Where("iccid_20 IN ?", list20)
		query = middleware.ApplyShopFilter(ctx, query)
		if err := query.Find(&cards).Error; err != nil {
			return nil, err
		}
		for _, c := range cards {
			if _, dup := seen[c.ID]; !dup {
				seen[c.ID] = struct{}{}
				all = append(all, c)
			}
		}
	}

	return all, nil
}

func (s *IotCardStore) GetStandaloneByICCIDRange(ctx context.Context, iccidStart, iccidEnd string, shopID *uint) ([]*model.IotCard, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true").
		Where("iccid >= ? AND iccid <= ?", iccidStart, iccidEnd)
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)

	if shopID == nil {
		query = query.Where("shop_id IS NULL")
	} else {
		query = query.Where("shop_id = ?", *shopID)
	}

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetDistributedStandaloneByICCIDRange 根据号段范围查询已分配给店铺的单卡（用于回收）
func (s *IotCardStore) GetDistributedStandaloneByICCIDRange(ctx context.Context, iccidStart, iccidEnd string) ([]*model.IotCard, error) {
	var cards []*model.IotCard
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true").
		Where("shop_id IS NOT NULL").
		Where("iccid >= ? AND iccid <= ?", iccidStart, iccidEnd)
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

func (s *IotCardStore) GetStandaloneByFilters(ctx context.Context, filters map[string]any, shopID *uint) ([]*model.IotCard, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true")
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)

	if shopID == nil {
		query = query.Where("shop_id IS NULL")
	} else {
		query = query.Where("shop_id = ?", *shopID)
	}

	if status, ok := filters["status"].(int); ok && status > 0 {
		query = query.Where("status = ?", status)
	}
	if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
		query = query.Where("carrier_id = ?", carrierID)
	}
	if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
		query = query.Where("batch_no = ?", batchNo)
	}

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetDistributedStandaloneByFilters 根据筛选条件查询已分配给店铺的单卡（用于回收）
func (s *IotCardStore) GetDistributedStandaloneByFilters(ctx context.Context, filters map[string]any) ([]*model.IotCard, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true").
		Where("shop_id IS NOT NULL")
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)

	if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
		query = query.Where("carrier_id = ?", carrierID)
	}
	if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
		query = query.Where("batch_no = ?", batchNo)
	}

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetBySeriesBindingFilters 根据系列绑定筛选条件查询卡
// 用于批量设置套餐系列时选择完整筛选结果，不受列表分页限制。
func (s *IotCardStore) GetBySeriesBindingFilters(ctx context.Context, filters map[string]any) ([]*model.IotCard, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{})
	query = middleware.ApplyShopFilter(ctx, query)
	query = s.applyStandaloneFilters(ctx, query, filters)

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetStandaloneByICCIDRangeForAuth 按号段范围查询单卡（企业授权用），应用代理数据权限过滤但不限制店铺
func (s *IotCardStore) GetStandaloneByICCIDRangeForAuth(ctx context.Context, iccidStart, iccidEnd string) ([]*model.IotCard, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true AND iccid >= ? AND iccid <= ?", iccidStart, iccidEnd)
	query = middleware.ApplyShopFilter(ctx, query)
	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetStandaloneByAuthFilters 按条件查询单卡（企业授权用），应用代理数据权限过滤
func (s *IotCardStore) GetStandaloneByAuthFilters(ctx context.Context, filters map[string]any) ([]*model.IotCard, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true")
	query = middleware.ApplyShopFilter(ctx, query)

	if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
		query = query.Where("iccid LIKE ?", "%"+iccid+"%")
	}
	if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
		query = query.Where("batch_no = ?", batchNo)
	}
	if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
		query = query.Where("carrier_id = ?", carrierID)
	}
	if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 {
		query = query.Where("shop_id = ?", shopID)
	}
	if shopIDs, ok := filters["shop_ids"].([]uint); ok && len(shopIDs) > 0 {
		query = query.Where("shop_id IN ?", shopIDs)
	}

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetAuthorizedStandaloneByICCIDRange 按号段范围查询已授权给指定企业的单卡（企业回收用）
func (s *IotCardStore) GetAuthorizedStandaloneByICCIDRange(ctx context.Context, enterpriseID uint, iccidStart, iccidEnd string) ([]*model.IotCard, error) {
	subQ := s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
		Select("card_id").
		Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID)
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true AND iccid >= ? AND iccid <= ? AND id IN (?)", iccidStart, iccidEnd, subQ)
	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// GetAuthorizedStandaloneByFilters 按条件查询已授权给指定企业的单卡（企业回收用）
func (s *IotCardStore) GetAuthorizedStandaloneByFilters(ctx context.Context, enterpriseID uint, filters map[string]any) ([]*model.IotCard, error) {
	subQ := s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
		Select("card_id").
		Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID)
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("is_standalone = true AND id IN (?)", subQ)

	if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
		query = query.Where("iccid LIKE ?", "%"+iccid+"%")
	}
	if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
		query = query.Where("batch_no = ?", batchNo)
	}
	if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
		query = query.Where("carrier_id = ?", carrierID)
	}

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

func (s *IotCardStore) BatchUpdateShopIDAndStatus(ctx context.Context, cardIDs []uint, shopID *uint, status int) error {
	if len(cardIDs) == 0 {
		return nil
	}
	updates := map[string]any{
		"shop_id": shopID,
		"status":  status,
	}
	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id IN ?", cardIDs).
		Updates(updates).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

func (s *IotCardStore) GetBoundCardIDs(ctx context.Context, cardIDs []uint) ([]uint, error) {
	if len(cardIDs) == 0 {
		return nil, nil
	}
	var boundIDs []uint
	err := s.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).
		Where("iot_card_id IN ? AND bind_status = ?", cardIDs, 1).
		Pluck("iot_card_id", &boundIDs).Error
	return boundIDs, err
}

func (s *IotCardStore) GetByIDsWithEnterpriseFilter(ctx context.Context, cardIDs []uint, enterpriseID *uint) ([]*model.IotCard, error) {
	if len(cardIDs) == 0 {
		return []*model.IotCard{}, nil
	}
	query := s.db.WithContext(ctx).Model(&model.IotCard{})

	if enterpriseID != nil && *enterpriseID > 0 {
		// 子查询无需数据权限过滤（在不同表上执行）
		query = query.Where("id IN (?) AND id IN (?)",
			cardIDs,
			s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
				Select("card_id").
				Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", *enterpriseID))
	} else {
		query = query.Where("id IN ?", cardIDs)
	}

	var cards []*model.IotCard
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

// UpdateDeviceVirtualNo 更新卡的设备虚拟号快照
// 绑定设备时写入 virtual_no，解绑时写入空字符串
func (s *IotCardStore) UpdateDeviceVirtualNo(ctx context.Context, cardID uint, deviceVirtualNo string) error {
	return s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id = ?", cardID).
		Update("device_virtual_no", deviceVirtualNo).Error
}

// BatchUpdateDeviceVirtualNo 批量更新卡的设备虚拟号快照（设备导入时使用）
func (s *IotCardStore) BatchUpdateDeviceVirtualNo(ctx context.Context, cardIDs []uint, deviceVirtualNo string) error {
	if len(cardIDs) == 0 {
		return nil
	}
	return s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id IN ?", cardIDs).
		Update("device_virtual_no", deviceVirtualNo).Error
}

// BatchUpdateSeriesID 批量更新卡的套餐系列ID
// 用于批量设置或清除卡与套餐系列的关联关系
func (s *IotCardStore) BatchUpdateSeriesID(ctx context.Context, cardIDs []uint, seriesID *uint) error {
	if len(cardIDs) == 0 {
		return nil
	}
	return s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id IN ?", cardIDs).
		Update("series_id", seriesID).Error
}

// ListBySeriesID 根据套餐系列ID查询卡列表
// 用于查询某个套餐系列下的所有卡
func (s *IotCardStore) ListBySeriesID(ctx context.Context, seriesID uint) ([]*model.IotCard, error) {
	var cards []*model.IotCard
	query := s.db.WithContext(ctx).Where("series_id = ?", seriesID)
	// 应用数据权限过滤
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.Find(&cards).Error; err != nil {
		return nil, err
	}
	return cards, nil
}

func (s *IotCardStore) UpdateRechargeTrackingFields(ctx context.Context, cardID uint, accumulatedJSON, triggeredJSON string) error {
	return s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("id = ?", cardID).
		Updates(map[string]any{
			"accumulated_recharge_by_series":     accumulatedJSON,
			"first_recharge_triggered_by_series": triggeredJSON,
		}).Error
}

// IotCardQueryFilter 卡查询过滤条件
type IotCardQueryFilter struct {
	ShopID        *uint   // 店铺ID
	CardStatus    *string // 卡状态
	CarrierCode   *string // 运营商代码
	CardType      *string // 卡类型
	EnablePolling *bool   // 是否启用轮询
	Limit         int     // 限制数量
}

// QueryIDsByFilter 根据过滤条件查询卡ID列表
func (s *IotCardStore) QueryIDsByFilter(ctx context.Context, filter *IotCardQueryFilter) ([]uint, error) {
	query := s.db.WithContext(ctx).Model(&model.IotCard{}).Select("id")

	// 应用过滤条件
	if filter.ShopID != nil {
		query = query.Where("shop_id = ?", *filter.ShopID)
	}
	if filter.CardStatus != nil && *filter.CardStatus != "" {
		query = query.Where("card_status = ?", *filter.CardStatus)
	}
	if filter.CarrierCode != nil && *filter.CarrierCode != "" {
		query = query.Where("carrier_code = ?", *filter.CarrierCode)
	}
	if filter.CardType != nil && *filter.CardType != "" {
		query = query.Where("card_type = ?", *filter.CardType)
	}
	if filter.EnablePolling != nil {
		query = query.Where("enable_polling = ?", *filter.EnablePolling)
	}

	// 应用限制
	if filter.Limit > 0 {
		query = query.Limit(filter.Limit)
	}

	var cardIDs []uint
	if err := query.Pluck("id", &cardIDs).Error; err != nil {
		return nil, err
	}

	return cardIDs, nil
}

// BatchUpdatePollingStatus 批量更新卡的轮询状态
func (s *IotCardStore) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint, enablePolling bool) error {
	if len(cardIDs) == 0 {
		return nil
	}
	return s.db.WithContext(ctx).
		Model(&model.IotCard{}).
		Where("id IN ?", cardIDs).
		Update("enable_polling", enablePolling).Error
}

// BatchDelete 批量删除卡（软删除）
func (s *IotCardStore) BatchDelete(ctx context.Context, cardIDs []uint) error {
	if len(cardIDs) == 0 {
		return nil
	}
	if err := s.db.WithContext(ctx).
		Where("id IN ?", cardIDs).
		Delete(&model.IotCard{}).Error; err != nil {
		return err
	}
	s.invalidateIotCardListCountCache(ctx)
	return nil
}

// ExistsByVirtualNoBatch 批量检查 virtual_no 是否已存在
func (s *IotCardStore) ExistsByVirtualNoBatch(ctx context.Context, virtualNos []string) (map[string]bool, error) {
	result := make(map[string]bool)
	if len(virtualNos) == 0 {
		return result, nil
	}

	var existingNos []string
	if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
		Where("virtual_no IN ? AND virtual_no <> ''", virtualNos).
		Pluck("virtual_no", &existingNos).Error; err != nil {
		return nil, err
	}

	for _, no := range existingNos {
		result[no] = true
	}
	return result, nil
}

// ==================== 轮询系统专用查询 ====================

// ListByDeviceIDAndNetworkStatus 查询设备下指定网络状态的所有卡
// 通过 tb_device_sim_binding 关联查询（项目禁止外键约束，用子查询实现）
func (s *IotCardStore) ListByDeviceIDAndNetworkStatus(ctx context.Context, deviceID uint, networkStatus int) ([]*model.IotCard, error) {
	var cards []*model.IotCard
	err := s.db.WithContext(ctx).
		Where("id IN (SELECT iot_card_id FROM tb_device_sim_binding WHERE device_id = ? AND bind_status = 1 AND deleted_at IS NULL)", deviceID).
		Where("network_status = ? AND deleted_at IS NULL", networkStatus).
		Find(&cards).Error
	return cards, err
}

// ListByDeviceIDAndPollingStopReasons 查询设备下因轮询原因停机的所有卡
// 用于 resumeDeviceCards：只复机由轮询系统停机的卡（不干预手动停机）
func (s *IotCardStore) ListByDeviceIDAndPollingStopReasons(ctx context.Context, deviceID uint) ([]*model.IotCard, error) {
	pollingReasons := []string{
		constants.StopReasonTrafficExhausted,
		constants.StopReasonNoPackage,
		constants.StopReasonNotRealname,
	}
	var cards []*model.IotCard
	err := s.db.WithContext(ctx).
		Where("id IN (SELECT iot_card_id FROM tb_device_sim_binding WHERE device_id = ? AND bind_status = 1 AND deleted_at IS NULL)", deviceID).
		Where("network_status = 0 AND stop_reason IN ? AND deleted_at IS NULL", pollingReasons).
		Find(&cards).Error
	return cards, err
}

// UpdateStopReason 更新卡的停机原因字段
func (s *IotCardStore) UpdateStopReason(ctx context.Context, cardID uint, reason string) error {
	return s.db.WithContext(ctx).
		Model(&model.IotCard{}).
		Where("id = ?", cardID).
		Update("stop_reason", reason).Error
}

// CountForPolling 统计启用轮询的卡总数（仅供轮询初始化使用，不应用数据权限过滤）
func (s *IotCardStore) CountForPolling(ctx context.Context) (int64, error) {
	var total int64
	err := s.db.WithContext(ctx).
		Model(&model.IotCard{}).
		Where("enable_polling = true AND deleted_at IS NULL").
		Count(&total).Error
	return total, err
}

// ListForPollingBatch 分批查询启用轮询的卡，按 id ASC 游标翻页（仅供轮询初始化使用，不应用数据权限过滤）
// lastID: 上批最后一条卡的 id（首次传 0）；limit: 每批大小
func (s *IotCardStore) ListForPollingBatch(ctx context.Context, lastID uint, limit int) ([]*model.IotCard, error) {
	var cards []*model.IotCard
	err := s.db.WithContext(ctx).
		Where("id > ? AND enable_polling = true AND deleted_at IS NULL", lastID).
		Order("id ASC").
		Limit(limit).
		Find(&cards).Error
	return cards, err
}

// ==================== 列表计数缓存 ====================

func (s *IotCardStore) getCachedCount(ctx context.Context, table string, filters map[string]any) (int64, bool, int64) {
	if s.redis == nil {
		return 0, false, 0
	}
	version := s.listCountCacheVersion(ctx, table)
	key := s.listCountCacheKey(ctx, table, filters, version)
	val, err := s.redis.Get(ctx, key).Int64()
	if err != nil {
		return 0, false, version
	}
	return val, true, version
}

func (s *IotCardStore) cacheCount(ctx context.Context, table string, filters map[string]any, total int64, version int64) {
	if s.redis == nil {
		return
	}
	if s.listCountCacheVersion(ctx, table) != version {
		return
	}
	key := s.listCountCacheKey(ctx, table, filters, version)
	_ = s.redis.Set(ctx, key, total, listCountCacheTTL).Err()
}

func (s *IotCardStore) listCountCacheKey(ctx context.Context, table string, filters map[string]any, version int64) string {
	return constants.RedisListCountKey(
		table,
		version,
		middleware.GetUserIDFromContext(ctx),
		hashDataScope(ctx),
		hashFilters(filters),
	)
}

func (s *IotCardStore) listCountCacheVersion(ctx context.Context, table string) int64 {
	version, err := s.redis.Get(ctx, constants.RedisListCountVersionKey(table)).Int64()
	if err != nil {
		return 0
	}
	return version
}

// InvalidateListCountCache 失效 IoT 卡相关列表计数缓存。
func (s *IotCardStore) InvalidateListCountCache(ctx context.Context) {
	s.invalidateIotCardListCountCache(ctx)
}

func (s *IotCardStore) invalidateIotCardListCountCache(ctx context.Context) {
	s.invalidateListCountCache(ctx, "iot_card", "standalone")
}

func (s *IotCardStore) invalidateListCountCache(ctx context.Context, tables ...string) {
	if s.redis == nil {
		return
	}

	seen := make(map[string]struct{}, len(tables))
	for _, table := range tables {
		if table == "" {
			continue
		}
		if _, ok := seen[table]; ok {
			continue
		}
		seen[table] = struct{}{}
		_ = s.redis.Incr(ctx, constants.RedisListCountVersionKey(table)).Err()
	}
}

func hashFilters(filters map[string]any) string {
	if len(filters) == 0 {
		return "0"
	}
	keys := make([]string, 0, len(filters))
	for k := range filters {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	h := fnv.New32a()
	for _, k := range keys {
		_, _ = h.Write([]byte(k))
		_, _ = fmt.Fprint(h, filters[k])
	}
	return fmt.Sprintf("%08x", h.Sum32())
}

func hashDataScope(ctx context.Context) string {
	userType := middleware.GetUserTypeFromContext(ctx)
	shopID := middleware.GetShopIDFromContext(ctx)
	enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
	subordinateIDs := middleware.GetSubordinateShopIDs(ctx)

	h := fnv.New32a()
	_, _ = fmt.Fprintf(h, "user_type=%d;shop_id=%d;enterprise_id=%d;", userType, shopID, enterpriseID)
	if subordinateIDs == nil {
		_, _ = h.Write([]byte("subordinate=nil"))
		return fmt.Sprintf("%08x", h.Sum32())
	}

	ids := append([]uint(nil), subordinateIDs...)
	sort.Slice(ids, func(i, j int) bool {
		return ids[i] < ids[j]
	})
	_, _ = fmt.Fprint(h, "subordinate=")
	for _, id := range ids {
		_, _ = fmt.Fprintf(h, "%d,", id)
	}
	return fmt.Sprintf("%08x", h.Sum32())
}
