package mysql

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/json"
	"fmt"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	"math/big"
	"strings"
	"time"
)

// CreateRedPacket 在同一事务内校验配置、扣 sender 金币、写随机份额和钱包 outbox。
func (r *Repository) CreateRedPacket(ctx context.Context, command ledger.RedPacketCreateCommand) (ledger.RedPacketCreateReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.RedPacketCreateReceipt{}, 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.RedPacketCreateReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := redPacketCreateRequestHash(command)
	if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypeRedPacketCreate, xerr.RequestConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.RedPacketCreateReceipt{}, err
		}
		return r.redPacketCreateReceiptForTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMS := time.Now().UTC().UnixMilli()
	config, err := r.getRedPacketConfig(ctx, tx)
	if err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if !config.Enabled {
		return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.RedPacketDisabled, "red packet is disabled")
	}
	if !int64InSet(command.TotalAmount, config.AmountTiers) {
		return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.RedPacketInvalidAmountTier, "red packet amount tier is invalid")
	}
	if !int32InSet(command.PacketCount, config.CountTiers) {
		return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.RedPacketInvalidCountTier, "red packet count tier is invalid")
	}
	if command.TotalAmount < int64(command.PacketCount) {
		return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.RedPacketAmountTooSmall, "red packet amount is too small")
	}
	if ledger.RedPacketExpireSeconds <= 0 || (command.PacketType == ledger.RedPacketTypeDelayed && ledger.RedPacketExpireSeconds <= config.DelayedOpenSeconds) {
		return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.InvalidArgument, "red packet config time range is invalid")
	}
	if err := r.incrementRedPacketDailyCounter(ctx, tx, command.SenderUserID, config.DailySendLimit, nowMS); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}

	sender, err := r.lockAccount(ctx, tx, command.SenderUserID, ledger.AssetCoin, false, nowMS)
	if err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if sender.AvailableAmount < command.TotalAmount {
		return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	allocations, err := splitRedPacketAmounts(command.TotalAmount, command.PacketCount)
	if err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}

	packetID := redPacketID(command.AppCode, command.CommandID)
	transactionID := transactionID(command.AppCode, command.CommandID)
	openAtMS := nowMS
	status := ledger.RedPacketStatusActive
	if command.PacketType == ledger.RedPacketTypeDelayed {
		openAtMS = nowMS + int64(config.DelayedOpenSeconds)*1000
		status = ledger.RedPacketStatusWaitingOpen
	}
	expiresAtMS := nowMS + int64(ledger.RedPacketExpireSeconds)*1000
	balanceAfter := sender.AvailableAmount - command.TotalAmount
	metadata := redPacketMetadata{
		AppCode:           command.AppCode,
		PacketID:          packetID,
		CommandID:         command.CommandID,
		SenderUserID:      command.SenderUserID,
		RoomID:            command.RoomID,
		RegionID:          command.RegionID,
		RoomLocked:        command.RoomLocked,
		OwnerUserID:       command.OwnerUserID,
		PacketType:        command.PacketType,
		TotalAmount:       command.TotalAmount,
		PacketCount:       command.PacketCount,
		RemainingAmount:   command.TotalAmount,
		RemainingCount:    command.PacketCount,
		Status:            status,
		OpenAtMS:          openAtMS,
		ExpiresAtMS:       expiresAtMS,
		BalanceAfter:      balanceAfter,
		WalletTransaction: transactionID,
		CreatedAtMS:       nowMS,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeRedPacketCreate, requestHash, packetID, metadata, nowMS); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, sender, -command.TotalAmount, 0, nowMS); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.SenderUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -command.TotalAmount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    sender.FrozenAmount,
		RoomID:         command.RoomID,
		CreatedAtMS:    nowMS,
	}); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if err := r.insertRedPacket(ctx, tx, metadata); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if err := r.insertRedPacketAllocations(ctx, tx, packetID, allocations, nowMS); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.SenderUserID, ledger.AssetCoin, -command.TotalAmount, 0, balanceAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMS, bizTypeRedPacketCreate),
		redPacketCreatedEvent(transactionID, command.CommandID, metadata, nowMS),
	}); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	return ledger.RedPacketCreateReceipt{Packet: redPacketFromMetadata(metadata), BalanceAfter: balanceAfter}, nil
}

func (r *Repository) insertRedPacket(ctx context.Context, tx *sql.Tx, metadata redPacketMetadata) error {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO red_packets (
			app_code, packet_id, command_id, sender_user_id, room_id, region_id, packet_type,
			total_amount, packet_count, remaining_amount, remaining_count, status,
			open_at_ms, expires_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), metadata.PacketID, metadata.CommandID, metadata.SenderUserID, metadata.RoomID, metadata.RegionID,
		metadata.PacketType, metadata.TotalAmount, metadata.PacketCount, metadata.RemainingAmount, metadata.RemainingCount,
		metadata.Status, metadata.OpenAtMS, metadata.ExpiresAtMS, metadata.CreatedAtMS, metadata.CreatedAtMS,
	)
	return err
}

func (r *Repository) insertRedPacketAllocations(ctx context.Context, tx *sql.Tx, packetID string, allocations []int64, nowMS int64) error {
	for index, amount := range allocations {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO red_packet_allocations (
				app_code, packet_id, allocation_no, amount, status, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?)`,
			appcode.FromContext(ctx), packetID, index+1, amount, redPacketAllocationUnclaimed, nowMS, nowMS,
		); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) redPacketCreateReceiptForTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.RedPacketCreateReceipt, error) {
	var raw string
	if err := tx.QueryRowContext(ctx, `
		SELECT COALESCE(CAST(metadata_json AS CHAR), '{}')
		FROM wallet_transactions
		WHERE app_code = ? AND transaction_id = ?`,
		appcode.FromContext(ctx), transactionID,
	).Scan(&raw); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	var metadata redPacketMetadata
	if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
		return ledger.RedPacketCreateReceipt{}, err
	}
	packet, err := r.getRedPacket(ctx, tx, metadata.PacketID)
	if err != nil {
		packet = redPacketFromMetadata(metadata)
	}
	return ledger.RedPacketCreateReceipt{Packet: packet, BalanceAfter: metadata.BalanceAfter}, nil
}

func redPacketFromMetadata(metadata redPacketMetadata) ledger.RedPacket {
	return ledger.RedPacket{
		AppCode:         metadata.AppCode,
		PacketID:        metadata.PacketID,
		CommandID:       metadata.CommandID,
		SenderUserID:    metadata.SenderUserID,
		RoomID:          metadata.RoomID,
		RegionID:        metadata.RegionID,
		PacketType:      metadata.PacketType,
		TotalAmount:     metadata.TotalAmount,
		PacketCount:     metadata.PacketCount,
		RemainingAmount: metadata.RemainingAmount,
		RemainingCount:  metadata.RemainingCount,
		Status:          metadata.Status,
		OpenAtMS:        metadata.OpenAtMS,
		ExpiresAtMS:     metadata.ExpiresAtMS,
		CreatedAtMS:     metadata.CreatedAtMS,
		UpdatedAtMS:     metadata.CreatedAtMS,
	}
}

func redPacketCreatedEvent(transactionID string, commandID string, metadata redPacketMetadata, nowMS int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, eventTypeRedPacketCreated, metadata.SenderUserID, ledger.AssetCoin),
		EventType:      eventTypeRedPacketCreated,
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.SenderUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -metadata.TotalAmount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":   transactionID,
			"command_id":       commandID,
			"packet_id":        metadata.PacketID,
			"packet_type":      metadata.PacketType,
			"sender_user_id":   metadata.SenderUserID,
			"room_id":          metadata.RoomID,
			"region_id":        metadata.RegionID,
			"room_locked":      metadata.RoomLocked,
			"owner_user_id":    metadata.OwnerUserID,
			"total_amount":     metadata.TotalAmount,
			"packet_count":     metadata.PacketCount,
			"remaining_amount": metadata.RemainingAmount,
			"remaining_count":  metadata.RemainingCount,
			"status":           metadata.Status,
			"open_at_ms":       metadata.OpenAtMS,
			"expires_at_ms":    metadata.ExpiresAtMS,
			"created_at_ms":    metadata.CreatedAtMS,
		},
		CreatedAtMS: nowMS,
	}
}

func redPacketCreateRequestHash(command ledger.RedPacketCreateCommand) string {
	// room_locked/owner_user_id 都是非资金展示快照，不进入幂等冲突哈希：未知结果重试必须返回首笔已提交事实，不能因展示字段变化制造账务冲突。
	return stableHash(fmt.Sprintf("red_packet_create|%s|%s|%d|%s|%d|%s|%d|%d",
		appcode.Normalize(command.AppCode),
		command.CommandID,
		command.SenderUserID,
		strings.TrimSpace(command.RoomID),
		command.RegionID,
		command.PacketType,
		command.TotalAmount,
		command.PacketCount,
	))
}

func redPacketID(app string, commandID string) string {
	return "rp_" + stableHash(appcode.Normalize(app)+"|"+commandID)
}

func splitRedPacketAmounts(total int64, count int32) ([]int64, error) {
	if total < int64(count) || count <= 0 {
		return nil, xerr.New(xerr.RedPacketAmountTooSmall, "red packet amount is too small")
	}
	remainingAmount := total
	remainingCount := int64(count)
	amounts := make([]int64, 0, count)
	for remainingCount > 0 {
		if remainingCount == 1 {
			amounts = append(amounts, remainingAmount)
			break
		}
		maxAmount := remainingAmount - (remainingCount - 1)
		avg := remainingAmount / remainingCount
		drawMax := avg * 2
		if drawMax < 1 {
			drawMax = 1
		}
		if drawMax > maxAmount {
			drawMax = maxAmount
		}
		drawn, err := secureRandomInt64(1, drawMax)
		if err != nil {
			return nil, err
		}
		amounts = append(amounts, drawn)
		remainingAmount -= drawn
		remainingCount--
	}
	if err := shuffleInt64(amounts); err != nil {
		return nil, err
	}
	return amounts, nil
}

func secureRandomInt64(min int64, max int64) (int64, error) {
	if min > max {
		return 0, xerr.New(xerr.InvalidArgument, "random range is invalid")
	}
	span := big.NewInt(max - min + 1)
	value, err := rand.Int(rand.Reader, span)
	if err != nil {
		return 0, err
	}
	return min + value.Int64(), nil
}

func shuffleInt64(values []int64) error {
	for i := len(values) - 1; i > 0; i-- {
		j, err := secureRandomInt64(0, int64(i))
		if err != nil {
			return err
		}
		values[i], values[int(j)] = values[int(j)], values[i]
	}
	return nil
}
