package exchange

import (
	"context"
	"strings"
	"time"

	exchangeapp "github.com/break/junhong_cmp_fiber/internal/application/exchange"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/model/dto"
	customerBindingSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
	"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/break/junhong_cmp_fiber/pkg/middleware"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type Service struct {
	db                           *gorm.DB
	exchangeStore                *postgres.ExchangeOrderStore
	refundStore                  *postgres.RefundStore
	iotCardStore                 *postgres.IotCardStore
	deviceStore                  *postgres.DeviceStore
	assetWalletStore             *postgres.AssetWalletStore
	assetWalletTransactionStore  *postgres.AssetWalletTransactionStore
	packageUsageStore            *postgres.PackageUsageStore
	packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore
	resourceTagStore             *postgres.ResourceTagStore
	customerBinding              *customerBindingSvc.Service
	shippingCreatedNotifier      *exchangeapp.ShippingCreatedNotifier
	logger                       *zap.Logger
}

func New(
	db *gorm.DB,
	exchangeStore *postgres.ExchangeOrderStore,
	iotCardStore *postgres.IotCardStore,
	deviceStore *postgres.DeviceStore,
	assetWalletStore *postgres.AssetWalletStore,
	assetWalletTransactionStore *postgres.AssetWalletTransactionStore,
	packageUsageStore *postgres.PackageUsageStore,
	packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore,
	resourceTagStore *postgres.ResourceTagStore,
	customerBinding *customerBindingSvc.Service,
	logger *zap.Logger,
) *Service {
	return &Service{
		db:                           db,
		exchangeStore:                exchangeStore,
		refundStore:                  postgres.NewRefundStore(db),
		iotCardStore:                 iotCardStore,
		deviceStore:                  deviceStore,
		assetWalletStore:             assetWalletStore,
		assetWalletTransactionStore:  assetWalletTransactionStore,
		packageUsageStore:            packageUsageStore,
		packageUsageDailyRecordStore: packageUsageDailyRecordStore,
		resourceTagStore:             resourceTagStore,
		customerBinding:              customerBinding,
		logger:                       logger,
	}
}

// SetShippingCreatedNotifier 注入物流换货创建后的可靠通知用例。
func (s *Service) SetShippingCreatedNotifier(notifier *exchangeapp.ShippingCreatedNotifier) {
	s.shippingCreatedNotifier = notifier
}

func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*dto.ExchangeOrderResponse, error) {
	flowType := normalizeExchangeFlowType(req.FlowType)
	if !isValidExchangeFlowType(flowType) {
		return nil, errors.New(errors.CodeInvalidParam, "换货流程类型不合法")
	}
	if flowType == constants.ExchangeFlowTypeDirect && strings.TrimSpace(req.NewIdentifier) == "" {
		return nil, errors.New(errors.CodeInvalidParam, "直接换货必须填写新资产标识")
	}

	asset, err := s.resolveAssetByIdentifier(ctx, req.OldAssetType, req.OldIdentifier)
	if err != nil {
		return nil, err
	}
	if !isExchangeableAssetStatus(asset.AssetStatus) {
		return nil, oldAssetStatusError(asset.AssetStatus)
	}
	hasUnfinishedRefund, err := s.refundStore.HasUnfinishedByAsset(ctx, asset.AssetType, asset.AssetID)
	if err != nil {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产退款申请失败")
	}
	if hasUnfinishedRefund {
		return nil, errors.New(errors.CodeExchangeActiveRefund)
	}

	if _, err = s.exchangeStore.FindActiveByOldAsset(ctx, asset.AssetType, asset.AssetID); err == nil {
		return nil, errors.New(errors.CodeExchangeInProgress)
	} else if err != gorm.ErrRecordNotFound {
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
	}

	if flowType == constants.ExchangeFlowTypeDirect {
		return s.createDirectExchange(ctx, req, asset)
	}

	creator := middleware.GetUserIDFromContext(ctx)
	order := &model.ExchangeOrder{
		ExchangeNo:         model.GenerateExchangeNo(),
		FlowType:           constants.ExchangeFlowTypeShipping,
		OldAssetType:       asset.AssetType,
		OldAssetID:         asset.AssetID,
		OldAssetIdentifier: asset.Identifier,
		ExchangeReason:     req.ExchangeReason,
		Remark:             req.Remark,
		Status:             constants.ExchangeStatusPendingInfo,
		MigrationCompleted: false,
		MigrationBalance:   0,
		MigrateData:        false,
		BaseModel:          model.BaseModel{Creator: creator, Updater: creator},
	}
	if asset.ShopID != nil {
		order.ShopID = asset.ShopID
	}

	if s.shippingCreatedNotifier == nil {
		return nil, errors.New(errors.CodeInternalError, "物流换货通知服务未配置")
	}
	requestID := ""
	if value := middleware.GetRequestIDFromContext(ctx); value != nil {
		requestID = *value
	}
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if createErr := tx.WithContext(ctx).Create(order).Error; createErr != nil {
			return errors.Wrap(errors.CodeDatabaseError, createErr, "创建换货单失败")
		}
		customerIDs, queryErr := s.customerBinding.ActiveCustomerIDsByAsset(ctx, tx, asset.AssetType, asset.AssetID)
		if queryErr != nil {
			return queryErr
		}
		for _, customerID := range customerIDs {
			if notifyErr := s.shippingCreatedNotifier.Notify(ctx, tx, exchangeapp.ShippingCreatedEvent{
				ExchangeID: order.ID, ExchangeNo: order.ExchangeNo, CustomerID: customerID,
				AssetType: asset.AssetType, AssetID: asset.AssetID, AssetIdentifier: asset.Identifier,
				RequestID: requestID, CorrelationID: requestID,
			}); notifyErr != nil {
				return notifyErr
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	resp := s.toExchangeOrderResponse(order)
	resp.SubmitterName = s.loadExchangeSubmitterNameBestEffort(ctx, order.Creator)
	return resp, nil
}

func (s *Service) Get(ctx context.Context, id uint) (*dto.ExchangeOrderResponse, error) {
	order, err := s.exchangeStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeExchangeOrderNotFound)
		}
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单详情失败")
	}
	resp := s.toExchangeOrderResponse(order)
	resp.SubmitterName = s.loadExchangeSubmitterNameBestEffort(ctx, order.Creator)
	return resp, nil
}

func (s *Service) Ship(ctx context.Context, id uint, req *dto.ExchangeShipRequest) (*dto.ExchangeOrderResponse, error) {
	order, err := s.exchangeStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeExchangeOrderNotFound)
		}
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
	}
	if order.Status != constants.ExchangeStatusPendingShip {
		return nil, errors.New(errors.CodeExchangeStatusInvalid)
	}
	if !isShippingExchangeFlow(order.FlowType) {
		return nil, errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
	}

	if err = s.shipWithTx(ctx, order, req); err != nil {
		return nil, err
	}

	return s.Get(ctx, id)
}

func (s *Service) Complete(ctx context.Context, id uint) error {
	order, err := s.exchangeStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeExchangeOrderNotFound)
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
	}
	if order.Status != constants.ExchangeStatusShipped {
		return errors.New(errors.CodeExchangeStatusInvalid)
	}
	if !isShippingExchangeFlow(order.FlowType) {
		return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
	}

	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		lockedOrder, lockErr := s.lockExchangeOrderByID(ctx, tx, id)
		if lockErr != nil {
			return lockErr
		}
		if !isShippingExchangeFlow(lockedOrder.FlowType) {
			return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
		}
		if lockedOrder.Status != constants.ExchangeStatusShipped {
			return errors.New(errors.CodeExchangeStatusInvalid)
		}
		return s.completeExchangeWithTx(ctx, tx, lockedOrder, constants.ExchangeStatusShipped)
	})
}

func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRequest) error {
	order, err := s.exchangeStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeExchangeOrderNotFound)
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
	}
	if order.Status != constants.ExchangeStatusPendingInfo && order.Status != constants.ExchangeStatusPendingShip {
		return errors.New(errors.CodeExchangeStatusInvalid)
	}
	if !isShippingExchangeFlow(order.FlowType) {
		return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
	}

	updates := map[string]any{
		"updater":    middleware.GetUserIDFromContext(ctx),
		"updated_at": time.Now(),
	}
	if req != nil {
		updates["remark"] = req.Remark
	}
	if err = s.exchangeStore.UpdateStatus(ctx, id, order.Status, constants.ExchangeStatusCancelled, updates); err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeExchangeStatusInvalid)
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "取消换货失败")
	}
	return nil
}

func (s *Service) Renew(ctx context.Context, id uint) error {
	order, err := s.exchangeStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeExchangeOrderNotFound)
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
	}
	if order.Status != constants.ExchangeStatusCompleted {
		return errors.New(errors.CodeExchangeStatusInvalid)
	}

	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if order.OldAssetType == constants.ExchangeAssetTypeIotCard {
			var card model.IotCard
			if err = tx.Where("id = ?", order.OldAssetID).First(&card).Error; err != nil {
				if err == gorm.ErrRecordNotFound {
					return errors.New(errors.CodeAssetNotFound)
				}
				return errors.Wrap(errors.CodeDatabaseError, err, "查询旧卡失败")
			}
			if card.AssetStatus != constants.AssetStatusExchanged {
				return errors.New(errors.CodeExchangeAssetNotExchanged)
			}

			if err = tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
				"generation":                         card.Generation + 1,
				"asset_status":                       constants.AssetStatusInStock,
				"accumulated_recharge_by_series":     "{}",
				"first_recharge_triggered_by_series": "{}",
				"updater":                            middleware.GetUserIDFromContext(ctx),
				"updated_at":                         time.Now(),
			}).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "重置旧卡转新状态失败")
			}

			cardKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &card, VirtualNo: card.VirtualNo})
			if cardKey != "" {
				if err = tx.Where("virtual_no = ?", cardKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
					return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
				}
			}

			if err = tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "清理旧钱包失败")
			}

			shopTag := uint(0)
			if card.ShopID != nil {
				shopTag = *card.ShopID
			}
			if err = tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeIotCard, ResourceID: card.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "创建新钱包失败")
			}
			return nil
		}

		var device model.Device
		if err = tx.Where("id = ?", order.OldAssetID).First(&device).Error; err != nil {
			if err == gorm.ErrRecordNotFound {
				return errors.New(errors.CodeAssetNotFound)
			}
			return errors.Wrap(errors.CodeDatabaseError, err, "查询旧设备失败")
		}
		if device.AssetStatus != constants.AssetStatusExchanged {
			return errors.New(errors.CodeExchangeAssetNotExchanged)
		}

		if err = tx.Model(&model.Device{}).Where("id = ?", device.ID).Updates(map[string]any{
			"generation":                         device.Generation + 1,
			"asset_status":                       constants.AssetStatusInStock,
			"accumulated_recharge_by_series":     "{}",
			"first_recharge_triggered_by_series": "{}",
			"updater":                            middleware.GetUserIDFromContext(ctx),
			"updated_at":                         time.Now(),
		}).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "重置旧设备转新状态失败")
		}

		deviceKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &device, VirtualNo: device.VirtualNo})
		if deviceKey != "" {
			if err = tx.Where("virtual_no = ?", deviceKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
				return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
			}
		}

		if err = tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeDevice, device.ID).Delete(&model.AssetWallet{}).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "清理旧钱包失败")
		}

		shopTag := uint(0)
		if device.ShopID != nil {
			shopTag = *device.ShopID
		}
		if err = tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeDevice, ResourceID: device.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "创建新钱包失败")
		}
		return nil
	})
}

func (s *Service) GetPending(ctx context.Context, identifier string) (*dto.ClientExchangePendingResponse, error) {
	asset, err := s.resolveAssetByIdentifier(ctx, "", identifier)
	if err != nil {
		return nil, err
	}

	if !s.customerOwnsAsset(ctx, asset) {
		return nil, errors.New(errors.CodeAssetNotFound)
	}

	order, err := s.exchangeStore.FindShippingPendingByOldAsset(ctx, asset.AssetType, asset.AssetID)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询待处理换货单失败")
	}

	return &dto.ClientExchangePendingResponse{
		ID:             order.ID,
		ExchangeNo:     order.ExchangeNo,
		FlowType:       effectiveExchangeFlowType(order.FlowType),
		Status:         order.Status,
		StatusName:     constants.GetExchangeStatusName(order.Status),
		StatusText:     constants.GetExchangeStatusName(order.Status),
		ExchangeReason: order.ExchangeReason,
		CreatedAt:      order.CreatedAt,
	}, nil
}

func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.ClientShippingInfoRequest) error {
	order, err := s.exchangeStore.GetByID(ctx, id)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeExchangeOrderNotFound)
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
	}
	if !isShippingExchangeFlow(order.FlowType) {
		return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
	}
	if order.Status != constants.ExchangeStatusPendingInfo {
		return errors.New(errors.CodeExchangeStatusInvalid)
	}
	oldAsset, err := s.resolveAssetByID(ctx, order.OldAssetType, order.OldAssetID)
	if err != nil {
		return err
	}
	if !s.customerOwnsAsset(ctx, oldAsset) {
		return errors.New(errors.CodeExchangeOrderNotFound)
	}

	updates := map[string]any{
		"recipient_name":    req.RecipientName,
		"recipient_phone":   req.RecipientPhone,
		"recipient_address": req.RecipientAddress,
		"updated_at":        time.Now(),
	}
	if err := s.exchangeStore.UpdateStatus(ctx, id, constants.ExchangeStatusPendingInfo, constants.ExchangeStatusPendingShip, updates); err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.CodeExchangeStatusInvalid)
		}
		return errors.Wrap(errors.CodeDatabaseError, err, "提交收货信息失败")
	}
	return nil
}

type resolvedExchangeAsset struct {
	AssetType   string
	AssetID     uint
	Identifier  string
	VirtualNo   string
	AssetStatus int
	ShopID      *uint
	Card        *model.IotCard
	Device      *model.Device
}

func (s *Service) resolveAssetByIdentifier(ctx context.Context, expectedAssetType, identifier string) (*resolvedExchangeAsset, error) {
	if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeDevice {
		device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
		if err == nil {
			if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeDevice {
				return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
			}
			return newResolvedDeviceAsset(device), nil
		}
		if err != gorm.ErrRecordNotFound {
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
		}
	}

	if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeIotCard {
		card, err := s.iotCardStore.GetByIdentifier(ctx, identifier)
		if err == nil {
			if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeIotCard {
				return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
			}
			return newResolvedIotCardAsset(card), nil
		} else if err != gorm.ErrRecordNotFound {
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
		}
	}

	return nil, errors.New(errors.CodeAssetNotFound)
}

// isExchangeableAssetStatus 判断旧资产状态是否允许发起换货（在库和已销售均可换货）
func isExchangeableAssetStatus(status int) bool {
	return status == constants.AssetStatusInStock || status == constants.AssetStatusSold
}

// oldAssetStatusError 根据旧资产的实际状态返回具体的错误原因
func oldAssetStatusError(status int) error {
	switch status {
	case constants.AssetStatusExchanged:
		return errors.New(errors.CodeExchangeStatusInvalid, "该资产已完成过换货，如需再次换货请使用换货后的新资产")
	case constants.AssetStatusDeactivated:
		return errors.New(errors.CodeExchangeStatusInvalid, "该资产已停用，无法发起换货")
	default:
		return errors.New(errors.CodeExchangeStatusInvalid, "该资产当前状态不允许换货")
	}
}

func normalizeExchangeFlowType(flowType string) string {
	flowType = strings.TrimSpace(flowType)
	if flowType == "" {
		return constants.ExchangeFlowTypeShipping
	}
	return flowType
}

func effectiveExchangeFlowType(flowType string) string {
	return normalizeExchangeFlowType(flowType)
}

func isValidExchangeFlowType(flowType string) bool {
	return flowType == constants.ExchangeFlowTypeShipping || flowType == constants.ExchangeFlowTypeDirect
}

func isShippingExchangeFlow(flowType string) bool {
	return effectiveExchangeFlowType(flowType) == constants.ExchangeFlowTypeShipping
}

func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExchangeRequest, oldAsset *resolvedExchangeAsset) (*dto.ExchangeOrderResponse, error) {
	var orderID uint
	migrateData := false
	if req.MigrateData != nil {
		migrateData = *req.MigrateData
	}

	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		lockedOldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, oldAsset.AssetType, oldAsset.AssetID)
		if err != nil {
			return err
		}
		if !isExchangeableAssetStatus(lockedOldAsset.AssetStatus) {
			return oldAssetStatusError(lockedOldAsset.AssetStatus)
		}
		if err = s.ensureNoActiveExchangeWithTx(ctx, tx, lockedOldAsset.AssetType, lockedOldAsset.AssetID); err != nil {
			return err
		}

		newAsset, err := s.resolveAssetByIdentifierWithTx(ctx, tx, "", req.NewIdentifier)
		if err != nil {
			return err
		}
		if newAsset.AssetType != lockedOldAsset.AssetType {
			return errors.New(errors.CodeExchangeAssetTypeMismatch)
		}

		creator := middleware.GetUserIDFromContext(ctx)
		order := &model.ExchangeOrder{
			ExchangeNo:         model.GenerateExchangeNo(),
			FlowType:           constants.ExchangeFlowTypeDirect,
			OldAssetType:       lockedOldAsset.AssetType,
			OldAssetID:         lockedOldAsset.AssetID,
			OldAssetIdentifier: lockedOldAsset.Identifier,
			NewAssetType:       newAsset.AssetType,
			NewAssetID:         &newAsset.AssetID,
			NewAssetIdentifier: newAsset.Identifier,
			ExchangeReason:     req.ExchangeReason,
			Remark:             req.Remark,
			Status:             constants.ExchangeStatusPendingInfo,
			MigrationCompleted: false,
			MigrationBalance:   0,
			MigrateData:        migrateData,
			BaseModel:          model.BaseModel{Creator: creator, Updater: creator},
		}
		if lockedOldAsset.ShopID != nil {
			order.ShopID = lockedOldAsset.ShopID
		}
		if err = tx.WithContext(ctx).Create(order).Error; err != nil {
			return errors.Wrap(errors.CodeDatabaseError, err, "创建直接换货单失败")
		}
		if err = s.completeExchangeWithTx(ctx, tx, order, constants.ExchangeStatusPendingInfo); err != nil {
			return err
		}
		orderID = order.ID
		return nil
	})
	if err != nil {
		return nil, err
	}
	return s.Get(ctx, orderID)
}

func (s *Service) shipWithTx(ctx context.Context, order *model.ExchangeOrder, req *dto.ExchangeShipRequest) error {
	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		lockedOrder, err := s.lockExchangeOrderByID(ctx, tx, order.ID)
		if err != nil {
			return err
		}
		if !isShippingExchangeFlow(lockedOrder.FlowType) {
			return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
		}
		if lockedOrder.Status != constants.ExchangeStatusPendingShip {
			return errors.New(errors.CodeExchangeStatusInvalid)
		}

		oldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, lockedOrder.OldAssetType, lockedOrder.OldAssetID)
		if err != nil {
			return err
		}
		newAsset, err := s.resolveAssetByIdentifierWithTx(ctx, tx, "", req.NewIdentifier)
		if err != nil {
			return err
		}
		if newAsset.AssetType != lockedOrder.OldAssetType {
			return errors.New(errors.CodeExchangeAssetTypeMismatch)
		}
		if err = s.validateExchangeAssetsWithTx(ctx, tx, lockedOrder.ID, oldAsset, newAsset); err != nil {
			return err
		}

		now := time.Now()
		result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
			Where("id = ? AND status = ?", lockedOrder.ID, constants.ExchangeStatusPendingShip).
			Updates(map[string]any{
				"new_asset_type":       newAsset.AssetType,
				"new_asset_id":         newAsset.AssetID,
				"new_asset_identifier": newAsset.Identifier,
				"express_company":      req.ExpressCompany,
				"express_no":           req.ExpressNo,
				"migrate_data":         req.MigrateData,
				"shipped_at":           now,
				"status":               constants.ExchangeStatusShipped,
				"updater":              middleware.GetUserIDFromContext(ctx),
				"updated_at":           now,
			})
		if result.Error != nil {
			return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新换货单发货状态失败")
		}
		if result.RowsAffected == 0 {
			return errors.New(errors.CodeExchangeStatusInvalid)
		}
		return nil
	})
}

func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, fromStatus int) error {
	if order.NewAssetID == nil || *order.NewAssetID == 0 || order.NewAssetIdentifier == "" {
		return errors.New(errors.CodeInvalidParam, "新资产信息缺失")
	}

	oldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, order.OldAssetType, order.OldAssetID)
	if err != nil {
		return err
	}
	newAsset, err := s.resolveAssetByIDWithTx(ctx, tx, order.NewAssetType, *order.NewAssetID)
	if err != nil {
		return err
	}
	if err = s.validateExchangeAssetsWithTx(ctx, tx, order.ID, oldAsset, newAsset); err != nil {
		return err
	}
	if err = s.syncNewAssetOwnershipWithTx(ctx, tx, oldAsset, newAsset); err != nil {
		return err
	}
	if err = s.switchCustomerBindingWithTx(ctx, tx, oldAsset, newAsset); err != nil {
		return err
	}
	if err = s.updateAssetStatusesForCompletion(ctx, tx, oldAsset, newAsset); err != nil {
		return err
	}

	var migrationBalance int64
	if order.MigrateData {
		migrationBalance, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
		if err != nil {
			return err
		}
	}

	now := time.Now()
	updates := map[string]any{
		"status":       constants.ExchangeStatusCompleted,
		"completed_at": now,
		"updater":      middleware.GetUserIDFromContext(ctx),
		"updated_at":   now,
	}
	if order.MigrateData {
		updates["migration_completed"] = true
		updates["migration_balance"] = migrationBalance
	}
	result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
		Where("id = ? AND status = ?", order.ID, fromStatus).
		Updates(updates)
	if result.Error != nil {
		return errors.Wrap(errors.CodeDatabaseError, result.Error, "确认换货完成失败")
	}
	if result.RowsAffected == 0 {
		return errors.New(errors.CodeExchangeStatusInvalid)
	}
	return nil
}

func (s *Service) lockExchangeOrderByID(ctx context.Context, tx *gorm.DB, id uint) (*model.ExchangeOrder, error) {
	var order model.ExchangeOrder
	query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id)
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.First(&order).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.CodeExchangeOrderNotFound)
		}
		return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
	}
	return &order, nil
}

func (s *Service) resolveAssetByID(ctx context.Context, assetType string, assetID uint) (*resolvedExchangeAsset, error) {
	if assetType == constants.ExchangeAssetTypeIotCard {
		card, err := s.iotCardStore.GetByID(ctx, assetID)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return nil, errors.New(errors.CodeAssetNotFound)
			}
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
		}
		return newResolvedIotCardAsset(card), nil
	}
	if assetType == constants.ExchangeAssetTypeDevice {
		device, err := s.deviceStore.GetByID(ctx, assetID)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return nil, errors.New(errors.CodeAssetNotFound)
			}
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
		}
		return newResolvedDeviceAsset(device), nil
	}
	return nil, errors.New(errors.CodeInvalidParam, "资产类型不合法")
}

func (s *Service) resolveAssetByIDWithTx(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) (*resolvedExchangeAsset, error) {
	if assetType == constants.ExchangeAssetTypeIotCard {
		var card model.IotCard
		query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", assetID)
		query = middleware.ApplyShopFilter(ctx, query)
		if err := query.First(&card).Error; err != nil {
			if err == gorm.ErrRecordNotFound {
				return nil, errors.New(errors.CodeAssetNotFound)
			}
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
		}
		return newResolvedIotCardAsset(&card), nil
	}
	if assetType == constants.ExchangeAssetTypeDevice {
		var device model.Device
		query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", assetID)
		query = middleware.ApplyShopFilter(ctx, query)
		if err := query.First(&device).Error; err != nil {
			if err == gorm.ErrRecordNotFound {
				return nil, errors.New(errors.CodeAssetNotFound)
			}
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
		}
		return newResolvedDeviceAsset(&device), nil
	}
	return nil, errors.New(errors.CodeInvalidParam, "资产类型不合法")
}

func (s *Service) resolveAssetByIdentifierWithTx(ctx context.Context, tx *gorm.DB, expectedAssetType, identifier string) (*resolvedExchangeAsset, error) {
	if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeDevice {
		var device model.Device
		query := tx.WithContext(ctx).
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("virtual_no = ? OR imei = ? OR sn = ?", identifier, identifier, identifier)
		query = middleware.ApplyShopFilter(ctx, query)
		if err := query.First(&device).Error; err == nil {
			if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeDevice {
				return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
			}
			return newResolvedDeviceAsset(&device), nil
		} else if err != gorm.ErrRecordNotFound {
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
		}
	}
	if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeIotCard {
		var card model.IotCard
		query := tx.WithContext(ctx).
			Clauses(clause.Locking{Strength: "UPDATE"}).
			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 {
			if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeIotCard {
				return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
			}
			return newResolvedIotCardAsset(&card), nil
		} else if err != gorm.ErrRecordNotFound {
			return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
		}
	}
	return nil, errors.New(errors.CodeAssetNotFound)
}

// newResolvedIotCardAsset 将卡的权威 ICCID 固化为换货快照，避免请求标识污染历史记录。
func newResolvedIotCardAsset(card *model.IotCard) *resolvedExchangeAsset {
	return &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, AssetID: card.ID, Identifier: card.ICCID, VirtualNo: card.VirtualNo, AssetStatus: card.AssetStatus, ShopID: card.ShopID, Card: card}
}

// newResolvedDeviceAsset 按虚拟号、IMEI、SN 的稳定优先级生成设备换货快照。
func newResolvedDeviceAsset(device *model.Device) *resolvedExchangeAsset {
	return &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, AssetID: device.ID, Identifier: preferredDeviceIdentifier(device), VirtualNo: device.VirtualNo, AssetStatus: device.AssetStatus, ShopID: device.ShopID, Device: device}
}

func (s *Service) ensureNoActiveExchangeWithTx(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) error {
	var count int64
	query := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
		Where("old_asset_type = ? AND old_asset_id = ?", assetType, assetID).
		Where("status IN ?", []int{constants.ExchangeStatusPendingInfo, constants.ExchangeStatusPendingShip, constants.ExchangeStatusShipped})
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.Count(&count).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
	}
	if count > 0 {
		return errors.New(errors.CodeExchangeInProgress)
	}
	return nil
}

func (s *Service) validateExchangeAssetsWithTx(ctx context.Context, tx *gorm.DB, orderID uint, oldAsset, newAsset *resolvedExchangeAsset) error {
	if oldAsset.AssetType != newAsset.AssetType {
		return errors.New(errors.CodeExchangeAssetTypeMismatch)
	}
	if !isExchangeableAssetStatus(oldAsset.AssetStatus) {
		return oldAssetStatusError(oldAsset.AssetStatus)
	}
	if newAsset.AssetStatus != constants.AssetStatusInStock {
		return errors.New(errors.CodeExchangeNewAssetNotInStock)
	}
	// 平台库存资产没有店铺归属，换货完成时会继承旧资产店铺；
	// 已归属其他店铺的资产仍必须拒绝，避免跨店铺覆盖资产归属。
	if newAsset.ShopID != nil && !sameShopID(oldAsset.ShopID, newAsset.ShopID) {
		return errors.New(errors.CodeForbidden, "新旧资产归属不一致")
	}
	if err := s.ensureNewAssetBindingAvailableWithTx(ctx, tx, oldAsset, newAsset); err != nil {
		return err
	}
	occupied, err := s.hasShippingNewAssetOccupiedWithTx(ctx, tx, newAsset.AssetType, newAsset.AssetID, orderID)
	if err != nil {
		return err
	}
	if occupied {
		return errors.New(errors.CodeExchangeStatusInvalid, "新资产已被其他换货单占用")
	}
	return nil
}

func (s *Service) ensureNewAssetBindingAvailableWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
	newKey := exchangeAssetBindingKey(newAsset)
	// 新资产无虚拟号时不再拦截：Migrate() 已能正确处理无绑定跳过、有绑定迁移到 pci 的情形
	if newKey == "" {
		return nil
	}
	var newBindCount int64
	if err := tx.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
		Where("virtual_no = ? AND status = ?", newKey, constants.StatusEnabled).
		Count(&newBindCount).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "查询新资产客户绑定失败")
	}
	if newBindCount > 0 {
		return errors.New(errors.CodeExchangeStatusInvalid, "新资产已存在有效客户绑定")
	}
	return nil
}

func (s *Service) switchCustomerBindingWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
	return s.customerBinding.Migrate(ctx, tx, oldAsset.AssetType, oldAsset.AssetID, newAsset.AssetType, newAsset.AssetID)
}

// syncNewAssetOwnershipWithTx 将新资产归属同步为旧资产当前归属。
// 归属继承不受 migrate_data 控制，避免平台库存资产换入店铺后仍处于平台租户范围。
func (s *Service) syncNewAssetOwnershipWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
	ownershipStatus := constants.IotCardStatusInStock
	if oldAsset.ShopID != nil {
		ownershipStatus = constants.IotCardStatusDistributed
	}

	modelValue := any(&model.IotCard{})
	if newAsset.AssetType == constants.ExchangeAssetTypeDevice {
		modelValue = &model.Device{}
		ownershipStatus = constants.DeviceStatusInStock
		if oldAsset.ShopID != nil {
			ownershipStatus = constants.DeviceStatusDistributed
		}
	}

	result := tx.WithContext(ctx).Model(modelValue).
		Where("id = ? AND asset_status = ?", newAsset.AssetID, constants.AssetStatusInStock).
		Updates(map[string]any{
			"shop_id":    oldAsset.ShopID,
			"status":     ownershipStatus,
			"updated_at": time.Now(),
		})
	if result.Error != nil {
		return errors.Wrap(errors.CodeDatabaseError, result.Error, "同步新资产店铺归属失败")
	}
	if result.RowsAffected == 0 {
		return errors.New(errors.CodeExchangeNewAssetNotInStock)
	}

	shopIDTag := uint(0)
	if oldAsset.ShopID != nil {
		shopIDTag = *oldAsset.ShopID
	}
	if err := tx.WithContext(ctx).Model(&model.AssetWallet{}).
		Where("resource_type = ? AND resource_id = ?", newAsset.AssetType, newAsset.AssetID).
		Updates(map[string]any{"shop_id_tag": shopIDTag, "updated_at": time.Now()}).Error; err != nil {
		return errors.Wrap(errors.CodeDatabaseError, err, "同步新资产钱包店铺标签失败")
	}

	newAsset.ShopID = cloneShopID(oldAsset.ShopID)
	return nil
}

func cloneShopID(shopID *uint) *uint {
	if shopID == nil {
		return nil
	}
	value := *shopID
	return &value
}

func (s *Service) updateAssetStatusesForCompletion(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
	now := time.Now()
	if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
		result := tx.WithContext(ctx).Model(&model.IotCard{}).
			Where("id = ? AND asset_status IN ?", oldAsset.AssetID, []int{constants.AssetStatusInStock, constants.AssetStatusSold}).
			Updates(map[string]any{"asset_status": constants.AssetStatusExchanged, "updated_at": now})
		if result.Error != nil {
			return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新旧卡状态失败")
		}
		if result.RowsAffected == 0 {
			return errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态已被修改，请重试")
		}
		result = tx.WithContext(ctx).Model(&model.IotCard{}).
			Where("id = ? AND asset_status = ?", newAsset.AssetID, constants.AssetStatusInStock).
			Updates(map[string]any{"asset_status": constants.AssetStatusSold, "updated_at": now})
		if result.Error != nil {
			return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新新卡状态失败")
		}
		if result.RowsAffected == 0 {
			return errors.New(errors.CodeExchangeNewAssetNotInStock)
		}
		return nil
	}
	result := tx.WithContext(ctx).Model(&model.Device{}).
		Where("id = ? AND asset_status IN ?", oldAsset.AssetID, []int{constants.AssetStatusInStock, constants.AssetStatusSold}).
		Updates(map[string]any{"asset_status": constants.AssetStatusExchanged, "updated_at": now})
	if result.Error != nil {
		return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新旧设备状态失败")
	}
	if result.RowsAffected == 0 {
		return errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态已被修改，请重试")
	}
	result = tx.WithContext(ctx).Model(&model.Device{}).
		Where("id = ? AND asset_status = ?", newAsset.AssetID, constants.AssetStatusInStock).
		Updates(map[string]any{"asset_status": constants.AssetStatusSold, "updated_at": now})
	if result.Error != nil {
		return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新新设备状态失败")
	}
	if result.RowsAffected == 0 {
		return errors.New(errors.CodeExchangeNewAssetNotInStock)
	}
	return nil
}

func (s *Service) hasShippingNewAssetOccupiedWithTx(ctx context.Context, tx *gorm.DB, assetType string, assetID uint, excludeOrderID uint) (bool, error) {
	var count int64
	query := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
		Where("new_asset_type = ? AND new_asset_id = ?", assetType, assetID).
		Where("status = ?", constants.ExchangeStatusShipped).
		Where("COALESCE(NULLIF(flow_type, ''), ?) = ?", constants.ExchangeFlowTypeShipping, constants.ExchangeFlowTypeShipping)
	if excludeOrderID > 0 {
		query = query.Where("id <> ?", excludeOrderID)
	}
	query = middleware.ApplyShopFilter(ctx, query)
	if err := query.Count(&count).Error; err != nil {
		return false, errors.Wrap(errors.CodeDatabaseError, err, "查询新资产占用失败")
	}
	return count > 0, nil
}

func (s *Service) customerOwnsAsset(ctx context.Context, asset *resolvedExchangeAsset) bool {
	customerID := middleware.GetCustomerIDFromContext(ctx)
	if customerID == 0 || asset == nil {
		return false
	}
	key := exchangeAssetBindingKey(asset)
	if key == "" {
		return false
	}
	var count int64
	if err := s.db.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
		Where("customer_id = ? AND virtual_no = ? AND status = ?", customerID, key, constants.StatusEnabled).
		Count(&count).Error; err != nil {
		return false
	}
	return count > 0
}

func exchangeAssetBindingKey(asset *resolvedExchangeAsset) string {
	if asset == nil {
		return ""
	}
	if asset.AssetType == constants.ExchangeAssetTypeIotCard {
		if asset.Card != nil {
			return asset.Card.VirtualNo
		}
		return asset.VirtualNo
	}
	if asset.Device == nil {
		return asset.VirtualNo
	}
	if asset.Device.VirtualNo != "" {
		return asset.Device.VirtualNo
	}
	return asset.Device.IMEI
}

func preferredDeviceIdentifier(device *model.Device) string {
	if device == nil {
		return ""
	}
	if device.VirtualNo != "" {
		return device.VirtualNo
	}
	if device.IMEI != "" {
		return device.IMEI
	}
	return device.SN
}

func sameShopID(left, right *uint) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return *left == *right
}

func (s *Service) toExchangeOrderResponse(order *model.ExchangeOrder) *dto.ExchangeOrderResponse {
	if order == nil {
		return nil
	}
	var deletedAt *time.Time
	if order.DeletedAt.Valid {
		deletedAt = &order.DeletedAt.Time
	}
	return &dto.ExchangeOrderResponse{
		ID:                 order.ID,
		ExchangeNo:         order.ExchangeNo,
		FlowType:           effectiveExchangeFlowType(order.FlowType),
		FlowTypeName:       constants.GetExchangeFlowTypeName(order.FlowType),
		OldAssetType:       order.OldAssetType,
		OldAssetID:         order.OldAssetID,
		OldAssetIdentifier: order.OldAssetIdentifier,
		NewAssetType:       order.NewAssetType,
		NewAssetID:         order.NewAssetID,
		NewAssetIdentifier: order.NewAssetIdentifier,
		RecipientName:      order.RecipientName,
		RecipientPhone:     order.RecipientPhone,
		RecipientAddress:   order.RecipientAddress,
		ExpressCompany:     order.ExpressCompany,
		ExpressNo:          order.ExpressNo,
		MigrateData:        order.MigrateData,
		MigrationCompleted: order.MigrationCompleted,
		MigrationBalance:   order.MigrationBalance,
		ShippedAt:          order.ShippedAt,
		CompletedAt:        order.CompletedAt,
		ExchangeReason:     order.ExchangeReason,
		Remark:             order.Remark,
		Status:             order.Status,
		StatusName:         constants.GetExchangeStatusName(order.Status),
		StatusText:         constants.GetExchangeStatusName(order.Status),
		ShopID:             order.ShopID,
		CreatedAt:          order.CreatedAt,
		UpdatedAt:          order.UpdatedAt,
		DeletedAt:          deletedAt,
		SubmitterID:        order.Creator,
		Creator:            order.Creator,
		Updater:            order.Updater,
	}
}

func (s *Service) loadExchangeSubmitterNameBestEffort(ctx context.Context, id uint) string {
	accounts, err := postgres.NewAccountStore(s.db, nil).GetDisplayAccountsByIDs(ctx, []uint{id})
	if err != nil {
		s.logger.Warn("查询换货提交人失败", zap.Uint("submitter_id", id), zap.Error(err))
		return ""
	}
	if len(accounts) == 0 {
		return ""
	}
	return accounts[0].Username
}
