package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
)

func normalizeGiftChargeSource(source string, entitlementID string) string {
	normalized := strings.ToLower(strings.TrimSpace(source))
	if normalized == giftChargeSourceBag || strings.TrimSpace(entitlementID) != "" {
		return giftChargeSourceBag
	}
	return giftChargeSourceCoin
}

type resolvedGiftDebitSource struct {
	GiftConfig  resourcedomain.GiftConfig
	Price       giftPrice
	Entitlement *resourcedomain.UserResourceEntitlement
}

// requireVIPGiftLevel 在任何扣币或背包消耗前校验 Wallet owner 的 effective VIP。
// paid 与已佩戴体验卡的覆盖顺序必须和 GetVipState 一致；错误 metadata 是客户端国际化提示的稳定契约，
// 不能把服务端英文 message 当作展示文案。查询复用 (app_code,user_id) 主键和现有体验卡索引，
// 单次送礼固定为一条 program 主键查询及一条 paid/trial 合并查询，不随礼物数量增长。
func (r *Repository) requireVIPGiftLevel(ctx context.Context, tx *sql.Tx, gift resourcedomain.GiftConfig, senderUserID int64, nowMS int64) error {
	if !resourcedomain.IsVIPGiftType(gift.GiftTypeCode) || gift.VIPLevel <= 0 {
		return nil
	}
	currentLevel := int32(0)
	program, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return err
	}
	if program.Status == ledger.VipStatusActive {
		levels, queryErr := r.batchVipEffectiveLevelsFromSnapshot(ctx, tx, []int64{senderUserID}, nowMS)
		if queryErr != nil {
			return queryErr
		}
		access := levels[senderUserID]
		currentLevel = access.Paid
		// 已佩戴体验卡是显式身份选择，即使等级低于付费 VIP，也必须保持现有 effective VIP 语义。
		if program.TrialCardEnabled && access.Trial > 0 {
			currentLevel = access.Trial
		}
	}
	if currentLevel >= gift.VIPLevel {
		return nil
	}
	return xerr.NewWithMetadata(xerr.VIPGiftLevelRequired, "vip level is required for this gift", map[string]string{
		"gift_id":                 strings.TrimSpace(gift.GiftID),
		"required_level":          fmt.Sprintf("%d", gift.VIPLevel),
		"current_effective_level": fmt.Sprintf("%d", currentLevel),
	})
}

// resolveGiftDebitSource is the only price authority for send-gift. Coin/legacy bag gifts use the live catalog;
// pinned bag gifts use the wallet-owned entitlement bytes and deliberately ignore caller price_version.
func (r *Repository) resolveGiftDebitSource(ctx context.Context, tx *sql.Tx, command ledger.DebitGiftCommand, quantity int64, nowMS int64) (resolvedGiftDebitSource, error) {
	chargeSource := normalizeGiftChargeSource(command.ChargeSource, command.EntitlementID)
	if chargeSource != giftChargeSourceBag {
		config, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
		if err != nil {
			return resolvedGiftDebitSource{}, err
		}
		price, err := r.resolveGiftPrice(ctx, tx, command.GiftID, command.PriceVersion, nowMS)
		return resolvedGiftDebitSource{GiftConfig: config, Price: price}, err
	}

	entitlement, err := r.lockGiftEntitlementForSend(ctx, tx, command, quantity, nowMS)
	if err != nil {
		return resolvedGiftDebitSource{}, err
	}
	if entitlement.SourceSnapshotID != "" {
		configs, err := decodePinnedGiftConfigs(entitlement)
		if err != nil {
			return resolvedGiftDebitSource{}, xerr.New(xerr.Internal, err.Error())
		}
		for _, config := range configs {
			if config.GiftID != strings.TrimSpace(command.GiftID) {
				continue
			}
			if !pinnedGiftAvailableInRegion(config, command.RegionID) {
				return resolvedGiftDebitSource{}, xerr.New(xerr.NotFound, "pinned bag gift is unavailable in region")
			}
			price := giftPrice{
				GiftID: config.GiftID, PriceVersion: config.PriceVersion,
				ChargeAssetType: config.ChargeAssetType, CoinPrice: config.CoinPrice,
				GiftPointAmount: 0, HeatValue: config.HeatValue,
			}
			return resolvedGiftDebitSource{GiftConfig: config, Price: price, Entitlement: &entitlement}, nil
		}
		return resolvedGiftDebitSource{}, xerr.New(xerr.InvalidArgument, "gift_id does not belong to pinned bag entitlement")
	}

	// Legacy grants did not persist gift semantics. They remain bound to the current active catalog and current
	// resource status, preserving their historical behaviour while new activity rewards are immutable.
	if entitlement.Resource.Status != resourcedomain.StatusActive {
		return resolvedGiftDebitSource{}, xerr.New(xerr.Conflict, "bag gift entitlement resource is not active")
	}
	config, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
	if err != nil {
		return resolvedGiftDebitSource{}, err
	}
	if config.ResourceID != entitlement.ResourceID {
		return resolvedGiftDebitSource{}, xerr.New(xerr.InvalidArgument, "bag gift entitlement resource mismatch")
	}
	// Legacy bag inventory also uses the latest server price. A caller-selected historical active version could
	// otherwise spoof room contribution even though no coin is debited from the sender.
	price, err := r.resolveGiftPrice(ctx, tx, command.GiftID, "", nowMS)
	if err != nil {
		return resolvedGiftDebitSource{}, err
	}
	return resolvedGiftDebitSource{GiftConfig: config, Price: price, Entitlement: &entitlement}, nil
}

func (r *Repository) lockGiftEntitlementForSend(ctx context.Context, tx *sql.Tx, command ledger.DebitGiftCommand, quantity int64, nowMS int64) (resourcedomain.UserResourceEntitlement, error) {
	entitlementID := strings.TrimSpace(command.EntitlementID)
	if entitlementID == "" {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "entitlement_id is required")
	}
	if command.SenderUserID <= 0 || quantity <= 0 {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "bag gift entitlement command is invalid")
	}
	row := tx.QueryRowContext(ctx, userResourceSelectSQL()+`
		WHERE e.app_code = ? AND e.entitlement_id = ?
		FOR UPDATE`, appcode.FromContext(ctx), entitlementID)
	entitlement, err := scanUserResourceEntitlement(row)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "bag gift entitlement not found")
	}
	if err != nil {
		return resourcedomain.UserResourceEntitlement{}, err
	}
	if entitlement.UserID != command.SenderUserID {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.PermissionDenied, "bag gift entitlement does not belong to sender")
	}
	if entitlement.Status != resourcedomain.StatusActive {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Conflict, "bag gift entitlement is not active")
	}
	if resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType) != resourcedomain.TypeGift {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "bag entitlement resource_type must be gift")
	}
	if entitlement.EffectiveAtMS > nowMS || (entitlement.ExpiresAtMS > 0 && entitlement.ExpiresAtMS <= nowMS) {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Conflict, "bag gift entitlement is expired")
	}
	if entitlement.RemainingQuantity < quantity {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InsufficientBalance, "bag gift inventory is insufficient")
	}
	return entitlement, nil
}

func (r *Repository) consumeGiftEntitlementForSend(ctx context.Context, tx *sql.Tx, command ledger.DebitGiftCommand, entitlement resourcedomain.UserResourceEntitlement, quantity int64, metadata giftMetadata, nowMS int64) (walletOutboxEvent, error) {
	if entitlement.EntitlementID == "" || entitlement.UserID != command.SenderUserID || quantity <= 0 || entitlement.RemainingQuantity < quantity {
		return walletOutboxEvent{}, xerr.New(xerr.InsufficientBalance, "bag gift inventory is insufficient")
	}
	result, err := tx.ExecContext(ctx, `
		UPDATE user_resource_entitlements
		SET remaining_quantity = remaining_quantity - ?, updated_at_ms = ?
		WHERE app_code = ? AND entitlement_id = ? AND user_id = ? AND status = 'active' AND remaining_quantity >= ?`,
		quantity, nowMS, appcode.FromContext(ctx), entitlement.EntitlementID, command.SenderUserID, quantity)
	if err != nil {
		return walletOutboxEvent{}, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return walletOutboxEvent{}, err
	}
	if affected != 1 {
		return walletOutboxEvent{}, xerr.New(xerr.InsufficientBalance, "bag gift inventory is insufficient")
	}
	entitlement.RemainingQuantity -= quantity
	entitlement.UpdatedAtMS = nowMS
	return resourceOutboxEvent("UserResourceChanged", command.CommandID, command.SenderUserID, entitlement.ResourceID, map[string]any{
		"action":             "consume_gift_send",
		"app_code":           appcode.FromContext(ctx),
		"user_id":            command.SenderUserID,
		"resource_id":        entitlement.ResourceID,
		"resource_type":      entitlement.Resource.ResourceType,
		"entitlement_id":     entitlement.EntitlementID,
		"consumed_quantity":  quantity,
		"remaining_quantity": entitlement.RemainingQuantity,
		"room_id":            command.RoomID,
		"gift_id":            command.GiftID,
		"charge_source":      giftChargeSourceBag,
		"gift_value_coins":   metadata.CoinSpent,
		"updated_at_ms":      nowMS,
		"entitlement":        entitlement,
	}, nowMS), nil
}
