// Package asset 提供资产详情读取投影。
package asset

import (
	"context"

	"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"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// ExchangeTraceQuery 查询资产单节点换货链路并投影关联资产可见性。
type ExchangeTraceQuery struct {
	db     *gorm.DB
	logger *zap.Logger
}

// ExchangeTraceAssetRef 标识批量换货关系查询中的当前资产。
type ExchangeTraceAssetRef struct {
	// AssetType 为统一资产详情或换货模型中的资产类型。
	AssetType string
	// AssetID 为资产数据库 ID。
	AssetID uint
}

// NewExchangeTraceQuery 创建资产换货链路查询。
func NewExchangeTraceQuery(db *gorm.DB, logger *zap.Logger) *ExchangeTraceQuery {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &ExchangeTraceQuery{db: db, logger: logger}
}

// Resolve 查询当前资产的前代与后代换货投影。
func (q *ExchangeTraceQuery) Resolve(ctx context.Context, assetType string, assetID uint) (*dto.AssetExchangeTrace, error) {
	trace := &dto.AssetExchangeTrace{}
	assetType = normalizeExchangeAssetType(assetType)
	ref := ExchangeTraceAssetRef{AssetType: assetType, AssetID: assetID}
	previousBatch, err := q.FindPreviousCompleted(ctx, []ExchangeTraceAssetRef{ref})
	if err != nil {
		return nil, q.wrapQueryError(constants.ExchangeTraceDirectionPrevious, assetType, assetID, err)
	}
	nextBatch, err := q.FindNextCompleted(ctx, []ExchangeTraceAssetRef{ref})
	if err != nil {
		return nil, q.wrapQueryError(constants.ExchangeTraceDirectionNext, assetType, assetID, err)
	}

	previousOrders := previousBatch[ref]
	nextOrders := nextBatch[ref]
	previous := q.selectLatest(constants.ExchangeTraceDirectionPrevious, assetType, assetID, previousOrders)
	next := q.selectLatest(constants.ExchangeTraceDirectionNext, assetType, assetID, nextOrders)
	visibility, err := q.loadVisibility(ctx, previous, next)
	if err != nil {
		q.logger.Error("查询换货关联资产可见性失败",
			zap.String("asset_type", assetType),
			zap.Uint("asset_id", assetID),
			zap.Error(err))
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货关联资产可见性失败")
	}

	if previous != nil {
		key := relatedAssetKey{assetType: previous.OldAssetType, assetID: previous.OldAssetID}
		previousAssetID := previous.OldAssetID
		trace.PreviousAsset = newTraceItem(previous.OldAssetType, &previousAssetID, previous.OldAssetIdentifier, previous.ExchangeNo, visibility[key])
	}
	if next != nil {
		var canView bool
		if next.NewAssetID != nil {
			key := relatedAssetKey{assetType: next.NewAssetType, assetID: *next.NewAssetID}
			canView = visibility[key]
		} else {
			q.logger.Warn("已完成换货记录缺少新资产 ID",
				zap.String("asset_type", assetType),
				zap.Uint("asset_id", assetID),
				zap.Uint("exchange_id", next.ID),
				zap.String("exchange_no", next.ExchangeNo))
		}
		trace.NextAsset = newTraceItem(next.NewAssetType, next.NewAssetID, next.NewAssetIdentifier, next.ExchangeNo, canView)
	}
	return trace, nil
}

func normalizeExchangeAssetType(assetType string) string {
	if assetType == constants.AssetResolveTypeCard {
		return constants.ExchangeAssetTypeIotCard
	}
	return assetType
}

type relatedAssetKey struct {
	assetType string
	assetID   uint
}

// FindPreviousCompleted 批量查询当前资产作为新资产时的已完成换货记录。
func (q *ExchangeTraceQuery) FindPreviousCompleted(ctx context.Context, refs []ExchangeTraceAssetRef) (map[ExchangeTraceAssetRef][]*model.ExchangeOrder, error) {
	return q.findRelatedOrdersBatch(ctx, constants.ExchangeTraceDirectionPrevious, refs)
}

// FindNextCompleted 批量查询当前资产作为旧资产时的已完成换货记录。
func (q *ExchangeTraceQuery) FindNextCompleted(ctx context.Context, refs []ExchangeTraceAssetRef) (map[ExchangeTraceAssetRef][]*model.ExchangeOrder, error) {
	return q.findRelatedOrdersBatch(ctx, constants.ExchangeTraceDirectionNext, refs)
}

func (q *ExchangeTraceQuery) findRelatedOrdersBatch(ctx context.Context, direction string, refs []ExchangeTraceAssetRef) (map[ExchangeTraceAssetRef][]*model.ExchangeOrder, error) {
	result := make(map[ExchangeTraceAssetRef][]*model.ExchangeOrder, len(refs))
	if len(refs) == 0 {
		return result, nil
	}
	var orders []*model.ExchangeOrder
	query := q.db.WithContext(ctx).Where("status = ?", constants.ExchangeStatusCompleted)
	assetConditions := q.db.Session(&gorm.Session{NewDB: true}).Where("1 = 0")
	for _, ref := range refs {
		ref.AssetType = normalizeExchangeAssetType(ref.AssetType)
		if direction == constants.ExchangeTraceDirectionPrevious {
			assetConditions = assetConditions.Or("new_asset_type = ? AND new_asset_id = ?", ref.AssetType, ref.AssetID)
		} else {
			assetConditions = assetConditions.Or("old_asset_type = ? AND old_asset_id = ?", ref.AssetType, ref.AssetID)
		}
	}
	err := query.Where(assetConditions).
		Order("completed_at DESC NULLS LAST, id DESC").
		Find(&orders).Error
	if err != nil {
		return nil, err
	}
	for _, order := range orders {
		ref := ExchangeTraceAssetRef{AssetType: order.OldAssetType, AssetID: order.OldAssetID}
		if direction == constants.ExchangeTraceDirectionPrevious && order.NewAssetID != nil {
			ref = ExchangeTraceAssetRef{AssetType: order.NewAssetType, AssetID: *order.NewAssetID}
		}
		result[ref] = append(result[ref], order)
	}
	return result, nil
}

func (q *ExchangeTraceQuery) selectLatest(direction string, assetType string, assetID uint, orders []*model.ExchangeOrder) *model.ExchangeOrder {
	if len(orders) == 0 {
		return nil
	}
	selected := orders[0]
	if len(orders) > 1 {
		q.logger.Warn("检测到同方向多条已完成换货记录",
			zap.String("direction", string(direction)),
			zap.String("asset_type", assetType),
			zap.Uint("asset_id", assetID),
			zap.Int("candidate_count", len(orders)),
			zap.Uint("selected_exchange_id", selected.ID),
			zap.String("selected_exchange_no", selected.ExchangeNo))
	}
	return selected
}

func (q *ExchangeTraceQuery) loadVisibility(ctx context.Context, previous, next *model.ExchangeOrder) (map[relatedAssetKey]bool, error) {
	result := make(map[relatedAssetKey]bool, 2)
	cardIDs, deviceIDs := relatedAssetIDs(previous, next)
	if len(cardIDs) > 0 {
		var cards []*model.IotCard
		if err := q.db.WithContext(ctx).Select("id", "shop_id").Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
			return nil, err
		}
		for _, card := range cards {
			result[relatedAssetKey{assetType: constants.ExchangeAssetTypeIotCard, assetID: card.ID}] = canViewShopAsset(ctx, card.ShopID)
		}
	}
	if len(deviceIDs) > 0 {
		var devices []*model.Device
		if err := q.db.WithContext(ctx).Select("id", "shop_id").Where("id IN ?", deviceIDs).Find(&devices).Error; err != nil {
			return nil, err
		}
		for _, device := range devices {
			result[relatedAssetKey{assetType: constants.ExchangeAssetTypeDevice, assetID: device.ID}] = canViewShopAsset(ctx, device.ShopID)
		}
	}
	return result, nil
}

func canViewShopAsset(ctx context.Context, shopID *uint) bool {
	if middleware.IsUnrestricted(ctx) {
		return true
	}
	return shopID != nil && middleware.ContainsShopID(ctx, *shopID)
}

func relatedAssetIDs(previous, next *model.ExchangeOrder) ([]uint, []uint) {
	cardIDs := make([]uint, 0, 2)
	deviceIDs := make([]uint, 0, 2)
	appendID := func(assetType string, assetID uint) {
		if assetType == constants.ExchangeAssetTypeIotCard {
			cardIDs = append(cardIDs, assetID)
		} else if assetType == constants.ExchangeAssetTypeDevice {
			deviceIDs = append(deviceIDs, assetID)
		}
	}
	if previous != nil {
		appendID(previous.OldAssetType, previous.OldAssetID)
	}
	if next != nil && next.NewAssetID != nil {
		appendID(next.NewAssetType, *next.NewAssetID)
	}
	return cardIDs, deviceIDs
}

func (q *ExchangeTraceQuery) wrapQueryError(direction string, assetType string, assetID uint, err error) error {
	q.logger.Error("查询资产换货关系失败",
		zap.String("direction", string(direction)),
		zap.String("asset_type", assetType),
		zap.Uint("asset_id", assetID),
		zap.Error(err))
	return errors.Wrap(errors.CodeDatabaseError, err, "查询资产换货关系失败")
}

func newTraceItem(assetType string, assetID *uint, identifier, exchangeNo string, canView bool) *dto.AssetExchangeTraceItem {
	item := &dto.AssetExchangeTraceItem{
		AssetType:  assetType,
		Identifier: identifier,
		ExchangeNo: exchangeNo,
		CanView:    canView,
	}
	if canView && assetID != nil {
		item.AssetID = assetID
	}
	return item
}
