package mysql

import (
	"context"
	"fmt"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	"strings"
	"time"
)

// BatchDebitGift 在一个 MySQL 事务内完成多目标送礼；任一目标失败时整批回滚，避免房间命令和账务出现部分成功。
func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.BatchGiftReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	defer func() {

		_ = tx.Rollback()
	}()

	nowMs := time.Now().UnixMilli()
	if command.RegionID < 0 {
		return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "region_id is invalid")
	}
	if command.SenderRegionID < 0 {
		return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "sender_region_id is invalid")
	}
	giftConfig, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	price, err := r.resolveGiftPrice(ctx, tx, command.GiftID, command.PriceVersion, nowMs)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	chargeSource := normalizeGiftChargeSource(command.ChargeSource, command.EntitlementID)
	chargeAssetType := price.ChargeAssetType
	if chargeSource == giftChargeSourceBag {
		chargeAssetType = ledger.AssetBagGift
	}
	chargeAmount, err := checkedMul(price.CoinPrice, int64(command.GiftCount))
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}

	roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}

	heatValue, err := giftDiamondAmount(chargeAmount, roomContributionRatio.PPM)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	giftIncomeRatio, giftIncomeRatioRegionID, err := r.resolveGiftReturnCoinRatio(ctx, tx, command.AppCode, command.RegionID, giftConfig.GiftTypeCode)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	giftIncomeCoinAmount, err := giftDiamondAmount(chargeAmount, giftIncomeRatio.PPM)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	totalChargeAmount, err := checkedMul(chargeAmount, int64(len(command.Targets)))
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}

	existing := make(map[string]ledger.BatchGiftTargetReceipt, len(command.Targets))
	for _, target := range command.Targets {
		targetCommand := debitGiftCommandFromBatchTarget(command, target)
		requestHash := debitRequestHash(targetCommand)
		txRow, exists, err := r.lookupTransaction(ctx, tx, target.CommandID, requestHash, bizTypeGiftDebit)
		if err != nil {
			return ledger.BatchGiftReceipt{}, err
		}
		if !exists {
			continue
		}
		receipt, err := r.receiptForGiftTransaction(ctx, tx, txRow.TransactionID)
		if err != nil {
			return ledger.BatchGiftReceipt{}, err
		}
		existing[target.CommandID] = ledger.BatchGiftTargetReceipt{
			TargetUserID: target.TargetUserID,
			CommandID:    target.CommandID,
			Receipt:      receipt,
		}
	}
	if len(existing) > 0 {
		if len(existing) != len(command.Targets) {

			return ledger.BatchGiftReceipt{}, xerr.New(xerr.IdempotencyConflict, "wallet batch command idempotency conflict")
		}
		targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets))
		for _, target := range command.Targets {
			targetReceipts = append(targetReceipts, existing[target.CommandID])
		}
		aggregate, err := aggregateGiftReceipts(targetReceipts)
		if err != nil {
			return ledger.BatchGiftReceipt{}, err
		}
		return ledger.BatchGiftReceipt{Aggregate: aggregate, Targets: targetReceipts}, nil
	}

	accountAssetType := chargeAssetType
	if chargeSource == giftChargeSourceBag {
		accountAssetType = ledger.AssetCoin
	}
	lockRequests := []walletAccountLockRequest{{
		UserID:          command.SenderUserID,
		AssetType:       accountAssetType,
		CreateIfMissing: totalChargeAmount == 0 || chargeSource == giftChargeSourceBag,
	}}
	if giftIncomeCoinAmount > 0 {
		for _, target := range command.Targets {
			lockRequests = append(lockRequests, walletAccountLockRequest{
				UserID:          target.TargetUserID,
				AssetType:       ledger.AssetCoin,
				CreateIfMissing: true,
			})
		}
	}
	accountStates, err := r.lockAccountsOrdered(ctx, tx, lockRequests, nowMs)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	senderState := accountStates[walletAccountLockKey(command.SenderUserID, accountAssetType)]
	sender := senderState.account
	if chargeSource != giftChargeSourceBag && sender.AvailableAmount < totalChargeAmount {
		return ledger.BatchGiftReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	events := make([]walletOutboxEvent, 0, len(command.Targets)*5+1)
	if chargeSource == giftChargeSourceBag {
		resourceEvent, err := r.consumeGiftEntitlementForSend(ctx, tx, debitGiftCommandFromBatchCommand(command), giftConfig.ResourceID, int64(command.GiftCount)*int64(len(command.Targets)), giftMetadata{
			AppCode:         command.AppCode,
			GiftID:          command.GiftID,
			ResourceID:      giftConfig.ResourceID,
			GiftCount:       command.GiftCount,
			ChargeAssetType: chargeAssetType,
			ChargeSource:    chargeSource,
			EntitlementID:   strings.TrimSpace(command.EntitlementID),
			CoinSpent:       totalChargeAmount,
			SenderUserID:    command.SenderUserID,
			RoomID:          command.RoomID,
		}, nowMs)
		if err != nil {
			return ledger.BatchGiftReceipt{}, err
		}
		events = append(events, resourceEvent)
	}

	anyHostTarget := false
	for _, target := range command.Targets {
		if target.TargetIsHost {
			anyHostTarget = true
			break
		}
	}
	hostRatio := giftDiamondRatioSnapshot{Percent: "0.00", PPM: 0}
	hostRatioRegionID := int64(0)
	if anyHostTarget {
		hostRatio, hostRatioRegionID, err = r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode)
		if err != nil {
			return ledger.BatchGiftReceipt{}, err
		}
	}

	targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets))
	for _, target := range command.Targets {
		hostPeriodDiamondAdded := int64(0)
		hostPeriodCycleKey := ""
		giftDiamondRatioPercent := "0.00"
		giftDiamondRatioRegionID := int64(0)
		if target.TargetIsHost {

			hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, hostRatio.PPM)
			if err != nil {
				return ledger.BatchGiftReceipt{}, err
			}
			giftDiamondRatioPercent = hostRatio.Percent
			giftDiamondRatioRegionID = hostRatioRegionID
			hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs)
		}

		transactionID := transactionID(command.AppCode, target.CommandID)
		senderAfter := senderState.account.AvailableAmount - chargeAmount
		if chargeSource == giftChargeSourceBag {

			senderAfter = senderState.account.AvailableAmount
		}
		giftIncomeBalanceAfter := int64(0)
		targetCoinState := accountStates[walletAccountLockKey(target.TargetUserID, ledger.AssetCoin)]
		if giftIncomeCoinAmount > 0 {
			if targetCoinState == nil {
				return ledger.BatchGiftReceipt{}, xerr.New(xerr.Internal, "gift income account is missing")
			}
			giftIncomeBalanceAfter = targetCoinState.account.AvailableAmount + giftIncomeCoinAmount
			if target.TargetUserID == command.SenderUserID && accountAssetType == ledger.AssetCoin {

				giftIncomeBalanceAfter = senderAfter + giftIncomeCoinAmount
				senderAfter = giftIncomeBalanceAfter
			}
		}
		metadata := giftMetadata{
			AppCode:          command.AppCode,
			GiftID:           command.GiftID,
			GiftName:         giftConfig.Name,
			GiftIconURL:      giftDisplayIconURL(giftConfig.Resource),
			GiftAnimationURL: strings.TrimSpace(giftConfig.Resource.AnimationURL),

			GiftEffectTypes:               normalizeGiftEffectTypes(giftConfig.EffectTypes),
			ResourceID:                    giftConfig.ResourceID,
			ResourceSnapshot:              mustJSON(giftConfig.Resource),
			GiftTypeCode:                  giftConfig.GiftTypeCode,
			CPRelationType:                cpRelationTypeFromPresentationJSON(giftConfig.PresentationJSON),
			PresentationJSON:              giftConfig.PresentationJSON,
			SortOrder:                     giftConfig.SortOrder,
			GiftCount:                     command.GiftCount,
			PriceVersion:                  price.PriceVersion,
			ChargeAssetType:               chargeAssetType,
			ChargeAmount:                  chargeAmount,
			CoinSpent:                     chargeAmount,
			ChargeSource:                  chargeSource,
			EntitlementID:                 strings.TrimSpace(command.EntitlementID),
			GiftPointAdded:                0,
			HeatValue:                     heatValue,
			BalanceAfter:                  senderAfter,
			GiftIncomeCoinAmount:          giftIncomeCoinAmount,
			GiftIncomeRatioPercent:        giftIncomeRatio.Percent,
			GiftIncomeRatioRegionID:       giftIncomeRatioRegionID,
			GiftIncomeBalanceAfter:        giftIncomeBalanceAfter,
			BillingReceipt:                billingReceiptID(command.AppCode, target.CommandID),
			SenderUserID:                  command.SenderUserID,
			SenderRegionID:                command.SenderRegionID,
			TargetUserID:                  target.TargetUserID,
			TargetIsHost:                  target.TargetIsHost,
			TargetHostRegionID:            target.TargetHostRegionID,
			TargetAgencyOwnerUserID:       target.TargetAgencyOwnerUserID,
			HostPeriodDiamondAdded:        hostPeriodDiamondAdded,
			GiftDiamondRatioPercent:       giftDiamondRatioPercent,
			GiftDiamondRatioRegionID:      giftDiamondRatioRegionID,
			HostPeriodCycleKey:            hostPeriodCycleKey,
			RoomID:                        command.RoomID,
			RoomRegionID:                  command.RegionID,
			RoomContributionRatioPercent:  roomContributionRatio.Percent,
			RoomContributionRatioRegionID: roomContributionRatioRegionID,
			CoinPrice:                     price.CoinPrice,
			GiftPointAmount:               0,
			HeatUnitValue:                 price.CoinPrice,
		}
		if err := r.insertTransaction(ctx, tx, transactionID, target.CommandID, bizTypeGiftDebit, debitRequestHash(debitGiftCommandFromBatchTarget(command, target)), fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
			return ledger.BatchGiftReceipt{}, err
		}
		if chargeSource != giftChargeSourceBag {
			debitAccount, err := r.applyTrackedAccountDelta(ctx, tx, senderState, -chargeAmount, 0, nowMs)
			if err != nil {
				return ledger.BatchGiftReceipt{}, err
			}
			if err := r.insertEntry(ctx, tx, walletEntry{
				TransactionID:      transactionID,
				UserID:             command.SenderUserID,
				AssetType:          price.ChargeAssetType,
				AvailableDelta:     -chargeAmount,
				FrozenDelta:        0,
				AvailableAfter:     debitAccount.AvailableAmount,
				FrozenAfter:        debitAccount.FrozenAmount,
				CounterpartyUserID: target.TargetUserID,
				RoomID:             command.RoomID,
				CreatedAtMS:        nowMs,
			}); err != nil {
				return ledger.BatchGiftReceipt{}, err
			}
		}
		if giftIncomeCoinAmount > 0 {
			incomeState := targetCoinState
			if target.TargetUserID == command.SenderUserID && accountAssetType == ledger.AssetCoin {

				incomeState = senderState
			}
			incomeAccount, err := r.applyTrackedAccountDelta(ctx, tx, incomeState, giftIncomeCoinAmount, 0, nowMs)
			if err != nil {
				return ledger.BatchGiftReceipt{}, err
			}
			metadata.GiftIncomeBalanceAfter = incomeAccount.AvailableAmount
			if target.TargetUserID == command.SenderUserID && accountAssetType == ledger.AssetCoin {
				metadata.BalanceAfter = incomeAccount.AvailableAmount
			}
			if err := r.insertEntry(ctx, tx, walletEntry{
				TransactionID:      transactionID,
				UserID:             target.TargetUserID,
				AssetType:          ledger.AssetCoin,
				AvailableDelta:     giftIncomeCoinAmount,
				FrozenDelta:        0,
				AvailableAfter:     incomeAccount.AvailableAmount,
				FrozenAfter:        incomeAccount.FrozenAmount,
				CounterpartyUserID: command.SenderUserID,
				RoomID:             command.RoomID,
				CreatedAtMS:        nowMs,
			}); err != nil {
				return ledger.BatchGiftReceipt{}, err
			}
		}
		if hostPeriodDiamondAdded > 0 {
			hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, target.CommandID, metadata, nowMs)
			if err != nil {
				return ledger.BatchGiftReceipt{}, err
			}
			metadata.HostPeriodDiamondAfter = hostPeriodDiamondAfter
			metadata.HostPeriodDiamondVersion = hostPeriodDiamondVersion
		}

		if chargeSource != giftChargeSourceBag {
			if target.TargetUserID == command.SenderUserID && chargeAssetType == ledger.AssetCoin {
				events = append(events, balanceChangedEvent(transactionID, target.CommandID, command.SenderUserID, ledger.AssetCoin, giftIncomeCoinAmount-chargeAmount, 0, metadata.BalanceAfter, senderState.account.FrozenAmount, senderState.account.Version, metadata, nowMs, bizTypeGiftDebit))
			} else {
				events = append(events, balanceChangedEvent(transactionID, target.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, senderState.account.AvailableAmount, senderState.account.FrozenAmount, senderState.account.Version, metadata, nowMs, bizTypeGiftDebit))
			}
		}
		if giftIncomeCoinAmount > 0 && !(target.TargetUserID == command.SenderUserID && chargeAssetType == ledger.AssetCoin && chargeSource != giftChargeSourceBag) {
			events = append(events, balanceChangedEvent(transactionID, target.CommandID, target.TargetUserID, ledger.AssetCoin, giftIncomeCoinAmount, 0, metadata.GiftIncomeBalanceAfter, targetCoinState.account.FrozenAmount, targetCoinState.account.Version, metadata, nowMs, bizTypeGiftDebit))
		}
		events = append(events, giftDebitedEvent(transactionID, target.CommandID, command.SenderUserID, chargeAssetType, -chargeAmount, 0, metadata, nowMs))
		if giftIncomeCoinAmount > 0 {
			events = append(events, giftIncomeCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
		}
		if hostPeriodDiamondAdded > 0 {
			events = append(events, hostPeriodDiamondCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
		}
		targetReceipts = append(targetReceipts, ledger.BatchGiftTargetReceipt{
			TargetUserID: target.TargetUserID,
			CommandID:    target.CommandID,
			Receipt:      receiptFromGiftMetadata(transactionID, metadata),
		})
	}

	if err := r.insertWalletOutbox(ctx, tx, events); err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.BatchGiftReceipt{}, err
	}

	aggregate, err := aggregateGiftReceipts(targetReceipts)
	if err != nil {
		return ledger.BatchGiftReceipt{}, err
	}
	return ledger.BatchGiftReceipt{Aggregate: aggregate, Targets: targetReceipts}, nil
}

func debitGiftCommandFromBatchTarget(command ledger.BatchDebitGiftCommand, target ledger.DebitGiftTargetCommand) ledger.DebitGiftCommand {

	return ledger.DebitGiftCommand{
		AppCode:                 command.AppCode,
		CommandID:               target.CommandID,
		RoomID:                  command.RoomID,
		SenderUserID:            command.SenderUserID,
		TargetUserID:            target.TargetUserID,
		GiftID:                  command.GiftID,
		GiftCount:               command.GiftCount,
		PriceVersion:            command.PriceVersion,
		RegionID:                command.RegionID,
		SenderRegionID:          command.SenderRegionID,
		TargetIsHost:            target.TargetIsHost,
		TargetHostRegionID:      target.TargetHostRegionID,
		TargetAgencyOwnerUserID: target.TargetAgencyOwnerUserID,
		EntitlementID:           command.EntitlementID,
		ChargeSource:            command.ChargeSource,
	}
}

func debitGiftCommandFromBatchCommand(command ledger.BatchDebitGiftCommand) ledger.DebitGiftCommand {

	return ledger.DebitGiftCommand{
		AppCode:        command.AppCode,
		CommandID:      command.CommandID,
		RoomID:         command.RoomID,
		SenderUserID:   command.SenderUserID,
		GiftID:         command.GiftID,
		GiftCount:      command.GiftCount,
		PriceVersion:   command.PriceVersion,
		RegionID:       command.RegionID,
		SenderRegionID: command.SenderRegionID,
		EntitlementID:  command.EntitlementID,
		ChargeSource:   command.ChargeSource,
	}
}
