package packageexpiry

import (
	"context"
	"sort"
	"strings"
	"time"

	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/model/dto"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/errors"
	"github.com/break/junhong_cmp_fiber/pkg/middleware"
	"gorm.io/gorm"
)

const expiryWindowDays = 15

// ListResult 是临期资产 Query 的分页结果。
type ListResult struct {
	Items   []dto.ExpiringAssetItem
	Total   int64
	Page    int
	Size    int
	Summary dto.ExpiringAssetSummary
}

type assetCandidate struct {
	AssetType  string
	AssetID    uint
	Identifier string
	ShopID     *uint
}

// List 查询当前权限范围内的临期资产，并返回同口径数量汇总。
func (q *Query) List(ctx context.Context, request dto.ExpiringAssetListRequest) (ListResult, error) {
	if q == nil || q.db == nil {
		return ListResult{}, errors.New(errors.CodeInternalError, "套餐临期查询未配置")
	}
	if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeEnterprise {
		return ListResult{}, errors.New(errors.CodeForbidden, "企业账号无权查看临期资产列表")
	}
	request = normalizeListRequest(request)
	if err := validateListRequest(request); err != nil {
		return ListResult{}, err
	}
	items, err := q.collect(ctx, request)
	if err != nil {
		return ListResult{}, err
	}
	summary := summarize(items)
	total := int64(len(items))
	start := (request.Page - 1) * request.PageSize
	if start > len(items) {
		start = len(items)
	}
	end := start + request.PageSize
	if end > len(items) {
		end = len(items)
	}
	return ListResult{Items: items[start:end], Total: total, Page: request.Page, Size: request.PageSize, Summary: summary}, nil
}

// ReminderCandidates 查询当天精确命中 15、7、3 天节点的全部资产，供每日通知任务复用。
func (q *Query) ReminderCandidates(ctx context.Context) ([]dto.ExpiringAssetItem, error) {
	items, err := q.collect(ctx, normalizeListRequest(dto.ExpiringAssetListRequest{}))
	if err != nil {
		return nil, err
	}
	result := make([]dto.ExpiringAssetItem, 0)
	for _, item := range items {
		if item.DaysUntilFinalExpiry == nil {
			continue
		}
		switch *item.DaysUntilFinalExpiry {
		case 15, 7, 3:
			result = append(result, item)
		}
	}
	return result, nil
}

func (q *Query) collect(ctx context.Context, request dto.ExpiringAssetListRequest) ([]dto.ExpiringAssetItem, error) {
	candidates := make([]assetCandidate, 0)
	if request.AssetType == "" || request.AssetType == constants.AssetTypeIotCard {
		cards, err := q.findCardCandidates(ctx, request)
		if err != nil {
			return nil, err
		}
		candidates = append(candidates, cards...)
	}
	if request.AssetType == "" || request.AssetType == constants.AssetTypeDevice {
		devices, err := q.findDeviceCandidates(ctx, request)
		if err != nil {
			return nil, err
		}
		candidates = append(candidates, devices...)
	}
	items, err := q.resolveCandidates(ctx, candidates, request)
	if err != nil {
		return nil, err
	}
	if err := q.fillShopNames(ctx, items); err != nil {
		return nil, err
	}
	sort.SliceStable(items, func(i, j int) bool {
		if items[i].IsPriority != items[j].IsPriority {
			return items[i].IsPriority
		}
		left, right := items[i].EstimatedFinalExpiresAt, items[j].EstimatedFinalExpiresAt
		if left != nil && right != nil && !left.Equal(*right) {
			return left.Before(*right)
		}
		if items[i].AssetType != items[j].AssetType {
			return items[i].AssetType < items[j].AssetType
		}
		return items[i].AssetID < items[j].AssetID
	})
	return items, nil
}

func (q *Query) findCardCandidates(ctx context.Context, request dto.ExpiringAssetListRequest) ([]assetCandidate, error) {
	var rows []model.IotCard
	query := q.db.WithContext(ctx).Model(&model.IotCard{}).
		Select("id, iccid, shop_id")
	query = applyStrictShopScope(ctx, query)
	if request.ShopID != nil {
		query = query.Where("shop_id = ?", *request.ShopID)
	}
	if request.Keyword != "" {
		keyword := "%" + strings.TrimSpace(request.Keyword) + "%"
		query = query.Where("iccid ILIKE ? OR msisdn ILIKE ? OR virtual_no ILIKE ?", keyword, keyword, keyword)
	}
	query = query.Where(`EXISTS (
		SELECT 1 FROM tb_package_usage pu
		WHERE pu.iot_card_id = tb_iot_card.id AND pu.deleted_at IS NULL
		AND pu.master_usage_id IS NULL AND pu.refund_id IS NULL
		AND pu.status IN ? AND pu.expires_at IS NOT NULL AND pu.expires_at < ?
	)`, []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}, dateInShanghai(q.now()).AddDate(0, 0, expiryWindowDays+1))
	if err := query.Find(&rows).Error; err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询临期卡候选失败")
	}
	result := make([]assetCandidate, 0, len(rows))
	for _, row := range rows {
		result = append(result, assetCandidate{AssetType: constants.AssetTypeIotCard, AssetID: row.ID, Identifier: row.ICCID, ShopID: row.ShopID})
	}
	return result, nil
}

func (q *Query) findDeviceCandidates(ctx context.Context, request dto.ExpiringAssetListRequest) ([]assetCandidate, error) {
	var rows []model.Device
	query := q.db.WithContext(ctx).Model(&model.Device{}).
		Select("id, virtual_no, imei, shop_id")
	query = applyStrictShopScope(ctx, query)
	if request.ShopID != nil {
		query = query.Where("shop_id = ?", *request.ShopID)
	}
	if request.Keyword != "" {
		keyword := "%" + strings.TrimSpace(request.Keyword) + "%"
		query = query.Where("virtual_no ILIKE ? OR imei ILIKE ?", keyword, keyword)
	}
	query = query.Where(`EXISTS (
		SELECT 1 FROM tb_package_usage pu
		WHERE pu.device_id = tb_device.id AND pu.deleted_at IS NULL
		AND pu.master_usage_id IS NULL AND pu.refund_id IS NULL
		AND pu.status IN ? AND pu.expires_at IS NOT NULL AND pu.expires_at < ?
	)`, []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}, dateInShanghai(q.now()).AddDate(0, 0, expiryWindowDays+1))
	if err := query.Find(&rows).Error; err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询临期设备候选失败")
	}
	result := make([]assetCandidate, 0, len(rows))
	for _, row := range rows {
		identifier := row.VirtualNo
		if identifier == "" {
			identifier = row.IMEI
		}
		result = append(result, assetCandidate{AssetType: constants.AssetTypeDevice, AssetID: row.ID, Identifier: identifier, ShopID: row.ShopID})
	}
	return result, nil
}

func (q *Query) resolveCandidates(ctx context.Context, candidates []assetCandidate, request dto.ExpiringAssetListRequest) ([]dto.ExpiringAssetItem, error) {
	groupedIDs := map[string][]uint{constants.AssetTypeIotCard: {}, constants.AssetTypeDevice: {}}
	for _, candidate := range candidates {
		groupedIDs[candidate.AssetType] = append(groupedIDs[candidate.AssetType], candidate.AssetID)
	}
	estimates := make(map[string]map[uint]dto.PackageExpiryEstimate, 2)
	finalUsages := make(map[string]map[uint]*model.PackageUsage, 2)
	for _, assetType := range []string{constants.AssetTypeIotCard, constants.AssetTypeDevice} {
		var err error
		estimates[assetType], err = q.ResolveBatch(ctx, assetType, groupedIDs[assetType])
		if err != nil {
			return nil, err
		}
		finalUsages[assetType], err = q.loadFinalUsages(ctx, assetType, groupedIDs[assetType])
		if err != nil {
			return nil, err
		}
	}
	from, to, err := parseExpiryRange(request)
	if err != nil {
		return nil, err
	}
	items := make([]dto.ExpiringAssetItem, 0, len(candidates))
	for _, candidate := range candidates {
		estimate := estimates[candidate.AssetType][candidate.AssetID]
		usage := finalUsages[candidate.AssetType][candidate.AssetID]
		if usage == nil || estimate.ExpiryEstimateStatus != constants.PackageExpiryEstimateStatusExact || estimate.DaysUntilFinalExpiry == nil || estimate.EstimatedFinalExpiresAt == nil {
			continue
		}
		days := *estimate.DaysUntilFinalExpiry
		if days < 0 || days > expiryWindowDays || request.DaysMin != nil && days < *request.DaysMin || request.DaysMax != nil && days > *request.DaysMax {
			continue
		}
		expiryDate := dateInShanghai(*estimate.EstimatedFinalExpiresAt)
		if from != nil && expiryDate.Before(*from) || to != nil && expiryDate.After(*to) {
			continue
		}
		if request.PackageID != nil && usage.PackageID != *request.PackageID {
			continue
		}
		level, levelName := expiryLevel(days)
		items = append(items, dto.ExpiringAssetItem{
			AssetType: candidate.AssetType, AssetID: candidate.AssetID, Identifier: candidate.Identifier, ShopID: candidate.ShopID,
			PackageUsageID: usage.ID, PackageID: usage.PackageID, PackageName: usage.PackageName,
			PackageExpiryEstimate: estimate, ExpiryLevel: level, ExpiryLevelName: levelName, IsPriority: days <= 3,
		})
	}
	return items, nil
}

func (q *Query) loadFinalUsages(ctx context.Context, assetType string, assetIDs []uint) (map[uint]*model.PackageUsage, error) {
	result := make(map[uint]*model.PackageUsage, len(assetIDs))
	if len(assetIDs) == 0 {
		return result, nil
	}
	column, ok := packageUsageAssetColumn(assetType)
	if !ok {
		return nil, errors.New(errors.CodeInvalidParam, "资产类型无效")
	}
	var usages []*model.PackageUsage
	if err := q.db.WithContext(ctx).Where(column+" IN ?", assetIDs).
		Where("master_usage_id IS NULL AND refund_id IS NULL").
		Where("status IN ?", []int{constants.PackageUsageStatusPending, constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
		Order("priority ASC, created_at ASC, id ASC").Find(&usages).Error; err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询临期资产最终套餐失败")
	}
	for _, usage := range usages {
		assetID := usage.IotCardID
		if assetType == constants.AssetTypeDevice {
			assetID = usage.DeviceID
		}
		result[assetID] = usage
	}
	return result, nil
}

func (q *Query) fillShopNames(ctx context.Context, items []dto.ExpiringAssetItem) error {
	ids := make([]uint, 0)
	seen := make(map[uint]struct{})
	for _, item := range items {
		if item.ShopID == nil {
			continue
		}
		if _, exists := seen[*item.ShopID]; !exists {
			seen[*item.ShopID] = struct{}{}
			ids = append(ids, *item.ShopID)
		}
	}
	if len(ids) == 0 {
		return nil
	}
	var shops []model.Shop
	if err := q.db.WithContext(ctx).Select("id, shop_name").Where("id IN ?", ids).Find(&shops).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询临期资产店铺名称失败")
	}
	names := make(map[uint]string, len(shops))
	for _, shop := range shops {
		names[shop.ID] = shop.ShopName
	}
	for index := range items {
		if items[index].ShopID != nil {
			items[index].ShopName = names[*items[index].ShopID]
		}
	}
	return nil
}

func applyStrictShopScope(ctx context.Context, query *gorm.DB) *gorm.DB {
	if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeAgent {
		return query
	}
	shopIDs := middleware.GetSubordinateShopIDs(ctx)
	if len(shopIDs) == 0 {
		return query.Where("1 = 0")
	}
	return query.Where("shop_id IN ?", shopIDs)
}

func normalizeListRequest(request dto.ExpiringAssetListRequest) dto.ExpiringAssetListRequest {
	if request.Page <= 0 {
		request.Page = 1
	}
	if request.PageSize <= 0 {
		request.PageSize = constants.DefaultPageSize
	}
	return request
}

func validateListRequest(request dto.ExpiringAssetListRequest) error {
	if request.PageSize > constants.MaxPageSize || request.Page < 1 {
		return errors.New(errors.CodeInvalidParam)
	}
	if request.AssetType != "" && request.AssetType != constants.AssetTypeIotCard && request.AssetType != constants.AssetTypeDevice {
		return errors.New(errors.CodeInvalidParam)
	}
	if request.DaysMin != nil && (*request.DaysMin < 0 || *request.DaysMin > expiryWindowDays) || request.DaysMax != nil && (*request.DaysMax < 0 || *request.DaysMax > expiryWindowDays) {
		return errors.New(errors.CodeInvalidParam)
	}
	if request.DaysMin != nil && request.DaysMax != nil && *request.DaysMin > *request.DaysMax {
		return errors.New(errors.CodeInvalidParam, "最小剩余天数不能大于最大剩余天数")
	}
	_, _, err := parseExpiryRange(request)
	return err
}

func parseExpiryRange(request dto.ExpiringAssetListRequest) (*time.Time, *time.Time, error) {
	parse := func(value string) (*time.Time, error) {
		if value == "" {
			return nil, nil
		}
		result, err := time.ParseInLocation("2006-01-02", value, shanghaiLocation)
		if err != nil {
			return nil, errors.New(errors.CodeInvalidParam, "到期日期格式无效")
		}
		return &result, nil
	}
	from, err := parse(request.ExpiresFrom)
	if err != nil {
		return nil, nil, err
	}
	to, err := parse(request.ExpiresTo)
	if err != nil {
		return nil, nil, err
	}
	if from != nil && to != nil && from.After(*to) {
		return nil, nil, errors.New(errors.CodeInvalidParam, "到期开始日期不能晚于结束日期")
	}
	return from, to, nil
}

func expiryLevel(days int) (string, string) {
	if days <= 3 {
		return "red", "红色"
	}
	if days <= 7 {
		return "purple", "紫色"
	}
	return "pink", "粉红色"
}

func summarize(items []dto.ExpiringAssetItem) dto.ExpiringAssetSummary {
	result := dto.ExpiringAssetSummary{WindowDays: expiryWindowDays, TotalCount: int64(len(items))}
	for _, item := range items {
		if item.AssetType == constants.AssetTypeIotCard {
			result.CardCount++
		} else if item.AssetType == constants.AssetTypeDevice {
			result.DeviceCount++
		}
	}
	return result
}
