// Package packageexpiry 提供资产主套餐最终到期时间的统一只读推算。
package packageexpiry

import (
	"context"
	"sort"
	"time"

	packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/model/dto"
	packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/errors"
	"gorm.io/gorm"
)

var shanghaiLocation = time.FixedZone("Asia/Shanghai", 8*60*60)

// Query 查询单个或批量资产的套餐最终到期时间。
type Query struct {
	db  *gorm.DB
	now func() time.Time
}

// NewQuery 创建套餐最终到期查询。
func NewQuery(db *gorm.DB) *Query {
	return &Query{db: db, now: time.Now}
}

// Resolve 查询单个资产的最终到期推算结果。
func (q *Query) Resolve(ctx context.Context, assetType string, assetID uint) (dto.PackageExpiryEstimate, error) {
	results, err := q.ResolveBatch(ctx, assetType, []uint{assetID})
	if err != nil {
		return dto.PackageExpiryEstimate{}, err
	}
	return results[assetID], nil
}

// ResolveBatch 批量查询同类型资产的最终到期推算结果，避免列表场景逐资产读取。
func (q *Query) ResolveBatch(ctx context.Context, assetType string, assetIDs []uint) (map[uint]dto.PackageExpiryEstimate, error) {
	results := make(map[uint]dto.PackageExpiryEstimate, len(assetIDs))
	if len(assetIDs) == 0 {
		return results, nil
	}
	column, ok := packageUsageAssetColumn(assetType)
	if !ok {
		return nil, errors.New(errors.CodeInvalidParam, "资产类型无效")
	}
	usages := make([]*model.PackageUsage, 0)
	if err := q.db.WithContext(ctx).
		Where(column+" IN ?", assetIDs).
		Where("master_usage_id IS NULL").
		Where("status IN ?", []int{constants.PackageUsageStatusPending, constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
		Where("refund_id IS NULL").
		Order("priority ASC, created_at ASC, id ASC").
		Find(&usages).Error; err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐最终到期记录失败")
	}

	packages, err := q.loadFallbackPackages(ctx, usages)
	if err != nil {
		return nil, err
	}
	grouped := make(map[uint][]*model.PackageUsage, len(assetIDs))
	for _, usage := range usages {
		assetID := usage.IotCardID
		if assetType == constants.AssetTypeDevice {
			assetID = usage.DeviceID
		}
		grouped[assetID] = append(grouped[assetID], usage)
	}
	for _, assetID := range assetIDs {
		results[assetID] = Calculate(grouped[assetID], packages, q.now())
	}
	return results, nil
}

func (q *Query) loadFallbackPackages(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, error) {
	ids := make([]uint, 0)
	seen := make(map[uint]struct{})
	for _, usage := range usages {
		if !isEmptyHistoricalTerms(usage) {
			continue
		}
		if _, exists := seen[usage.PackageID]; !exists {
			seen[usage.PackageID] = struct{}{}
			ids = append(ids, usage.PackageID)
		}
	}
	packages := make(map[uint]*model.Package, len(ids))
	if len(ids) == 0 {
		return packages, nil
	}
	var rows []*model.Package
	if err := q.db.WithContext(ctx).Where("id IN ?", ids).Find(&rows).Error; err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询历史套餐计时条款失败")
	}
	for _, pkg := range rows {
		packages[pkg.ID] = pkg
	}
	return packages, nil
}

// Calculate 使用已批量读取的记录和历史套餐回退数据计算最终到期时间。
func Calculate(usages []*model.PackageUsage, packages map[uint]*model.Package, now time.Time) dto.PackageExpiryEstimate {
	eligible := make([]*model.PackageUsage, 0, len(usages))
	for _, usage := range usages {
		if usage == nil || usage.DeletedAt.Valid || usage.RefundID != nil || usage.MasterUsageID != nil {
			continue
		}
		if usage.Status == constants.PackageUsageStatusPending || usage.Status == constants.PackageUsageStatusActive || usage.Status == constants.PackageUsageStatusDepleted {
			eligible = append(eligible, usage)
		}
	}
	if len(eligible) == 0 {
		return newEstimate(constants.PackageExpiryEstimateStatusNone, nil, now)
	}
	sort.SliceStable(eligible, func(i, j int) bool {
		if eligible[i].Priority != eligible[j].Priority {
			return eligible[i].Priority < eligible[j].Priority
		}
		if !eligible[i].CreatedAt.Equal(eligible[j].CreatedAt) {
			return eligible[i].CreatedAt.Before(eligible[j].CreatedAt)
		}
		return eligible[i].ID < eligible[j].ID
	})

	currentIndex := -1
	for i, usage := range eligible {
		if usage.Status == constants.PackageUsageStatusActive || usage.Status == constants.PackageUsageStatusDepleted {
			if currentIndex >= 0 || usage.ExpiresAt == nil {
				return newEstimate(constants.PackageExpiryEstimateStatusInvalidData, nil, now)
			}
			currentIndex = i
		}
	}
	if currentIndex < 0 {
		first := eligible[0]
		terms, ok := resolveTerms(first, packages)
		if !ok {
			return newEstimate(constants.PackageExpiryEstimateStatusInvalidData, nil, now)
		}
		if first.PendingRealnameActivation || terms.ExpiryBase == constants.PackageExpiryBaseFromActivation {
			return newEstimate(constants.PackageExpiryEstimateStatusWaitingActivation, nil, now)
		}
		return newEstimate(constants.PackageExpiryEstimateStatusInvalidData, nil, now)
	}
	for i := 0; i < currentIndex; i++ {
		if eligible[i].Status == constants.PackageUsageStatusPending {
			return newEstimate(constants.PackageExpiryEstimateStatusInvalidData, nil, now)
		}
	}
	cursor := eligible[currentIndex].ExpiresAt.In(shanghaiLocation)
	for _, usage := range eligible[currentIndex+1:] {
		if usage.Status != constants.PackageUsageStatusPending {
			return newEstimate(constants.PackageExpiryEstimateStatusInvalidData, nil, now)
		}
		terms, ok := resolveTerms(usage, packages)
		if !ok {
			return newEstimate(constants.PackageExpiryEstimateStatusInvalidData, nil, now)
		}
		start := cursor.Add(time.Second)
		cursor = packagepkg.CalculateExpiryTime(terms.CalendarType, start, terms.DurationMonths, terms.DurationDays).In(shanghaiLocation)
	}
	return newEstimate(constants.PackageExpiryEstimateStatusExact, &cursor, now)
}

func resolveTerms(usage *model.PackageUsage, packages map[uint]*model.Package) (packagedomain.TermsSnapshot, bool) {
	terms := packagedomain.TermsSnapshotFromUsage(usage)
	if terms.IsValid() {
		return terms, true
	}
	if !isEmptyHistoricalTerms(usage) {
		return packagedomain.TermsSnapshot{}, false
	}
	pkg := packages[usage.PackageID]
	terms, err := packagedomain.ResolveTermsSnapshot(pkg, nil)
	return terms, err == nil
}

func isEmptyHistoricalTerms(usage *model.PackageUsage) bool {
	// UR55 的 000163 迁移已在测试环境生效：触发器拒绝新记录写入空快照，
	// 因此整组空值只可能是迁移前遗留记录；任意部分空值仍按异常处理。
	return usage.ExpiryBaseSnapshot == "" && usage.CalendarTypeSnapshot == "" && usage.DurationMonthsSnapshot == 0 && usage.DurationDaysSnapshot == 0
}

func packageUsageAssetColumn(assetType string) (string, bool) {
	switch assetType {
	case constants.AssetTypeIotCard, constants.AssetResolveTypeCard:
		return "iot_card_id", true
	case constants.AssetTypeDevice:
		return "device_id", true
	default:
		return "", false
	}
}

func newEstimate(status string, expiresAt *time.Time, now time.Time) dto.PackageExpiryEstimate {
	result := dto.PackageExpiryEstimate{ExpiryEstimateStatus: status, ExpiryEstimateStatusName: constants.GetPackageExpiryEstimateStatusName(status)}
	if status != constants.PackageExpiryEstimateStatusExact || expiresAt == nil {
		return result
	}
	days := dateInShanghai(*expiresAt).Sub(dateInShanghai(now))
	daysUntil := int(days.Hours() / 24)
	result.EstimatedFinalExpiresAt = expiresAt
	result.DaysUntilFinalExpiry = &daysUntil
	result.IsExpiring = daysUntil >= 0 && daysUntil <= 15
	return result
}

func dateInShanghai(value time.Time) time.Time {
	local := value.In(shanghaiLocation)
	return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghaiLocation)
}
