package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
	"strings"
	"time"
)

// PurchaseVip 在一个事务内完成金币扣费、VIP 状态更新、订单历史和资源组发放。
func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.PurchaseVipReceipt{}, 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.PurchaseVipReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := stableHash(fmt.Sprintf("vip_purchase|%s|%d|%d", command.AppCode, command.UserID, command.Level))
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeVIPPurchase); err != nil || exists {
		if err != nil || !exists {
			return ledger.PurchaseVipReceipt{}, err
		}
		return r.vipPurchaseReceiptForTransaction(ctx, tx, txRow.TransactionID, command.UserID)
	}

	nowMs := time.Now().UnixMilli()
	level, err := r.getVipLevelForUpdate(ctx, tx, command.Level)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if level.Status != ledger.VipStatusActive {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPLevelDisabled, "vip level is disabled")
	}
	current, err := r.ensureUserVipForUpdate(ctx, tx, command.UserID, nowMs)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	previousLevel := int32(0)
	baseExpiresAt := nowMs
	if current.Active {
		previousLevel = current.Level
		baseExpiresAt = current.ExpiresAtMS
		if current.Level > command.Level {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed")
		}
	}
	if level.RechargeGateRequired {
		totalRechargeCoin, err := r.userRechargeCoinAmount(ctx, tx, command.UserID)
		if err != nil {
			return ledger.PurchaseVipReceipt{}, err
		}
		if totalRechargeCoin < level.RequiredRechargeCoinAmount {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPRechargeRequired, "vip recharge requirement is not met")
		}
	}
	newExpiresAt := baseExpiresAt + level.DurationMS
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if account.AvailableAmount < level.PriceCoin {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}

	transactionID := transactionID(command.AppCode, command.CommandID)
	orderID := "vip_order_" + stableHash(command.AppCode+"|"+command.CommandID)
	coinBalanceAfter := account.AvailableAmount - level.PriceCoin
	metadata := map[string]any{
		"app_code":                      command.AppCode,
		"order_id":                      orderID,
		"user_id":                       command.UserID,
		"target_level":                  command.Level,
		"previous_level":                previousLevel,
		"price_coin":                    level.PriceCoin,
		"expires_at_ms":                 newExpiresAt,
		"balance_after":                 coinBalanceAfter,
		"reward_group_id":               level.RewardResourceGroupID,
		"duration_ms":                   level.DurationMS,
		"required_recharge_coin_amount": level.RequiredRechargeCoinAmount,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPPurchase, requestHash, orderID, metadata, nowMs); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, -level.PriceCoin, 0, nowMs); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -level.PriceCoin,
		FrozenDelta:    0,
		AvailableAfter: coinBalanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	activation, err := r.activateUserVip(ctx, tx, vipActivationCommand{
		AppCode:        command.AppCode,
		CommandID:      command.CommandID,
		UserID:         command.UserID,
		Source:         ledger.VipGrantSourcePurchase,
		Reason:         "vip purchase reward",
		OperatorUserID: command.UserID,
		TransactionID:  transactionID,
		RecordID:       orderID,
		Level:          level,
		Current:        current,
		NowMS:          nowMs,
	})
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO vip_purchase_orders (
		   app_code, order_id, command_id, user_id, target_level, previous_level, price_coin,
		   status, wallet_transaction_id, resource_grant_id, request_hash, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, 'succeeded', ?, ?, ?, ?, ?)`,
		command.AppCode, orderID, command.CommandID, command.UserID, command.Level, previousLevel,
		level.PriceCoin, transactionID, activation.ResourceGrantID, requestHash, nowMs, nowMs,
	); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	vipEventType := "VipPurchased"
	if activation.Action == "renew" {
		vipEventType = "VipRenewed"
	} else if activation.Action == "upgrade" {
		vipEventType = "VipUpgraded"
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -level.PriceCoin, 0, coinBalanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs, bizTypeVIPPurchase),
		{
			EventID:        eventID(transactionID, vipEventType, command.UserID, "VIP"),
			EventType:      vipEventType,
			TransactionID:  transactionID,
			CommandID:      command.CommandID,
			UserID:         command.UserID,
			AssetType:      "VIP",
			AvailableDelta: 0,
			FrozenDelta:    0,
			Payload:        metadata,
			CreatedAtMS:    nowMs,
		},
		vipActivatedEvent(transactionID, command.CommandID, command.UserID, ledger.VipGrantSourcePurchase, activation.Action, activation.PreviousLevel, activation.VIP, level.RewardResourceGroupID, activation.ResourceGrantID, activation.RewardItems, nowMs),
	}); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}

	return ledger.PurchaseVipReceipt{
		OrderID:          orderID,
		TransactionID:    transactionID,
		Vip:              activation.VIP,
		CoinSpent:        level.PriceCoin,
		CoinBalanceAfter: coinBalanceAfter,
		RewardItems:      activation.RewardItems,
	}, nil
}

func (r *Repository) activateUserVip(ctx context.Context, tx *sql.Tx, command vipActivationCommand) (vipActivationResult, error) {
	previousLevel := int32(0)
	baseExpiresAt := command.NowMS
	startedAt := command.NowMS
	if command.Current.Active {
		previousLevel = command.Current.Level
		baseExpiresAt = command.Current.ExpiresAtMS
		startedAt = command.Current.StartedAtMS
		if command.Current.Level > command.Level.Level {
			return vipActivationResult{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed")
		}
	}
	expiresAt := baseExpiresAt + command.Level.DurationMS
	rewardItems, resourceGrantID, err := r.applyVipRewardGroup(ctx, tx, vipRewardGrantCommand{
		AppCode:        command.AppCode,
		CommandID:      command.CommandID,
		UserID:         command.UserID,
		Source:         command.Source,
		Reason:         command.Reason,
		OperatorUserID: command.OperatorUserID,
	}, command.Level, expiresAt, command.NowMS)
	if err != nil {
		return vipActivationResult{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE user_vip_memberships
		SET level = ?, name = ?, status = ?, started_at_ms = ?, expires_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?`,
		command.Level.Level, command.Level.Name, ledger.VipStatusActive, startedAt, expiresAt, command.NowMS, command.AppCode, command.UserID,
	); err != nil {
		return vipActivationResult{}, err
	}
	action := vipActivationAction(command.Source, previousLevel, command.Level.Level)
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO user_vip_history (
		   app_code, user_id, action, previous_level, new_level, order_id, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
		command.AppCode, command.UserID, action, previousLevel, command.Level.Level, command.RecordID, command.NowMS,
	); err != nil {
		return vipActivationResult{}, err
	}
	return vipActivationResult{
		VIP: ledger.UserVip{
			UserID:      command.UserID,
			Level:       command.Level.Level,
			Name:        command.Level.Name,
			Active:      true,
			StartedAtMS: startedAt,
			ExpiresAtMS: expiresAt,
			UpdatedAtMS: command.NowMS,
		},
		PreviousLevel:   previousLevel,
		Action:          action,
		ResourceGrantID: resourceGrantID,
		RewardItems:     rewardItems,
	}, nil
}

func vipActivationAction(source string, previousLevel int32, newLevel int32) string {
	if source != ledger.VipGrantSourcePurchase {
		return source
	}
	if previousLevel == newLevel {
		return "renew"
	}
	if previousLevel > 0 && previousLevel < newLevel {
		return "upgrade"
	}
	return "purchase"
}

func (r *Repository) getVipLevelForUpdate(ctx context.Context, tx *sql.Tx, level int32) (ledger.VipLevel, error) {
	item, err := r.scanVipLevel(tx.QueryRowContext(ctx, `
		SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id,
		       required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
		FROM vip_levels
		WHERE app_code = ? AND level = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), level,
	))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.VipLevel{}, xerr.New(xerr.VIPLevelNotFound, "vip level not found")
	}
	return item, err
}

func (r *Repository) applyVipRewardGroup(ctx context.Context, tx *sql.Tx, command vipRewardGrantCommand, level ledger.VipLevel, expiresAtMS int64, nowMs int64) ([]ledger.VipRewardItem, string, error) {
	if level.RewardResourceGroupID <= 0 {
		return nil, "", nil
	}
	group, err := r.getResourceGroup(ctx, tx, level.RewardResourceGroupID, true)
	if err != nil {
		return nil, "", err
	}
	if group.Status != resourcedomain.StatusActive {
		return nil, "", xerr.New(xerr.Conflict, "vip reward group is disabled")
	}
	group.Items, err = r.listResourceGroupItemsTx(ctx, tx, level.RewardResourceGroupID, true)
	if err != nil {
		return nil, "", err
	}
	groupSnapshot, err := json.Marshal(group)
	if err != nil {
		return nil, "", err
	}
	rewardCommandID := "vip_reward:" + command.CommandID
	grantID := resourceGrantID(command.AppCode, rewardCommandID)
	source := ledger.NormalizeVipGrantSource(command.Source)
	if source == "" {
		source = ledger.VipGrantSourcePurchase
	}
	reason := strings.TrimSpace(command.Reason)
	if reason == "" {
		reason = source
	}
	operatorUserID := command.OperatorUserID
	if operatorUserID <= 0 {
		operatorUserID = command.UserID
	}
	if err := r.insertResourceGrant(ctx, tx, grantID, rewardCommandID, command.UserID, source, resourcedomain.GrantSubjectGroup, fmt.Sprintf("%d", level.RewardResourceGroupID), stableHash(string(groupSnapshot)), string(groupSnapshot), reason, operatorUserID, nowMs); err != nil {
		return nil, "", err
	}
	durationMS := expiresAtMS - nowMs
	rewards := make([]ledger.VipRewardItem, 0, len(group.Items))
	for _, item := range group.Items {
		if resourcedomain.NormalizeGroupItemType(item.ItemType) == resourcedomain.GroupItemTypeWalletAsset {
			inserted, err := r.applyWalletAssetGrantItem(ctx, tx, grantID, rewardCommandID, command.UserID, item, nowMs)
			if err != nil {
				return nil, "", err
			}
			rewards = append(rewards, ledger.VipRewardItem{Quantity: inserted.Quantity, ExpiresAtMS: 0})
			continue
		}
		if err := validateActiveResourceGroupResource(item.Resource); err != nil {
			return nil, "", err
		}
		inserted, err := r.applyGrantItem(ctx, tx, grantID, rewardCommandID, command.UserID, item.Resource, item.Quantity, durationMS, nowMs)
		if err != nil {
			return nil, "", err
		}
		if inserted.EntitlementID != "" {
			if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, expiresAtMS, nowMs); err != nil {
				return nil, "", err
			}
		}
		rewards = append(rewards, ledger.VipRewardItem{
			ResourceID:   item.Resource.ResourceID,
			ResourceCode: item.Resource.ResourceCode,
			ResourceType: item.Resource.ResourceType,
			Name:         item.Resource.Name,
			Quantity:     inserted.Quantity,
			ExpiresAtMS:  expiresAtMS,
			AssetURL:     item.Resource.AssetURL,
			PreviewURL:   item.Resource.PreviewURL,
			AnimationURL: item.Resource.AnimationURL,
		})
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupGranted", rewardCommandID, command.UserID, level.RewardResourceGroupID, map[string]any{"grant_id": grantID, "source": source}, nowMs),
	}); err != nil {
		return nil, "", err
	}
	return rewards, grantID, nil
}

func vipActivatedEvent(transactionID string, commandID string, userID int64, source string, action string, previousLevel int32, vip ledger.UserVip, rewardGroupID int64, resourceGrantID string, rewardItems []ledger.VipRewardItem, nowMs int64) walletOutboxEvent {
	payload := map[string]any{
		"event_type":               "VipActivated",
		"transaction_id":           transactionID,
		"command_id":               commandID,
		"user_id":                  userID,
		"source":                   source,
		"action":                   action,
		"previous_level":           previousLevel,
		"level":                    vip.Level,
		"vip_name":                 vip.Name,
		"started_at_ms":            vip.StartedAtMS,
		"expires_at_ms":            vip.ExpiresAtMS,
		"reward_resource_group_id": rewardGroupID,
		"resource_grant_id":        resourceGrantID,
		"reward_items":             vipRewardItemsNoticePayload(rewardItems),
		"created_at_ms":            nowMs,
	}
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "VipActivated", userID, vipOutboxAsset),
		EventType:      "VipActivated",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         userID,
		AssetType:      vipOutboxAsset,
		AvailableDelta: 0,
		FrozenDelta:    0,
		Payload:        payload,
		CreatedAtMS:    nowMs,
	}
}

func vipRewardItemsNoticePayload(items []ledger.VipRewardItem) []map[string]any {
	payload := make([]map[string]any, 0, len(items))
	for _, item := range items {
		payload = append(payload, map[string]any{
			"resource_id":   item.ResourceID,
			"resource_code": item.ResourceCode,
			"resource_type": item.ResourceType,
			"name":          item.Name,
			"quantity":      item.Quantity,
			"expires_at_ms": item.ExpiresAtMS,
			"asset_url":     item.AssetURL,
			"preview_url":   item.PreviewURL,
			"animation_url": item.AnimationURL,
		})
	}
	return payload
}

func (r *Repository) vipPurchaseReceiptForTransaction(ctx context.Context, tx *sql.Tx, transactionID string, userID int64) (ledger.PurchaseVipReceipt, error) {
	var orderID string
	var level int32
	var priceCoin int64
	var grantID string
	err := tx.QueryRowContext(ctx, `
		SELECT order_id, target_level, price_coin, resource_grant_id
		FROM vip_purchase_orders
		WHERE app_code = ? AND wallet_transaction_id = ?`,
		appcode.FromContext(ctx), transactionID,
	).Scan(&orderID, &level, &priceCoin, &grantID)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	vip, err := r.queryUserVip(ctx, tx, userID, time.Now().UnixMilli(), false)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	balance, err := r.balanceAfterTransaction(ctx, tx, transactionID, userID, ledger.AssetCoin)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	rewards, err := r.rewardItemsByGrant(ctx, tx, grantID, vip.ExpiresAtMS)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	return ledger.PurchaseVipReceipt{
		OrderID:          orderID,
		TransactionID:    transactionID,
		Vip:              vip,
		CoinSpent:        priceCoin,
		CoinBalanceAfter: balance.AvailableAmount,
		RewardItems:      rewards,
	}, nil
}
