package mysql

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

// ClaimRedPacket 领取一个未分配份额，并把金额原子入账到领取者 COIN 余额。
func (r *Repository) ClaimRedPacket(ctx context.Context, command ledger.RedPacketClaimCommand) (ledger.RedPacketClaimReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.RedPacketClaimReceipt{}, 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.RedPacketClaimReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := redPacketClaimRequestHash(command)
	if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypeRedPacketClaim, xerr.RequestConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.RedPacketClaimReceipt{}, err
		}
		return r.redPacketClaimReceiptForTransaction(ctx, tx, txRow.TransactionID)
	}
	nowMS := time.Now().UTC().UnixMilli()
	packet, err := r.getRedPacketForUpdate(ctx, tx, command.PacketID)
	if err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if packet.Status == ledger.RedPacketStatusFinished || packet.RemainingCount <= 0 || packet.RemainingAmount <= 0 {
		return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.RedPacketSoldOut, "red packet is sold out")
	}
	if packet.Status == ledger.RedPacketStatusRefunded || nowMS >= packet.ExpiresAtMS {
		return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.RedPacketExpired, "red packet is expired")
	}
	if nowMS < packet.OpenAtMS {
		return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.RedPacketNotOpen, "red packet is not open")
	}
	if existingClaim, claimed, err := r.getUserRedPacketClaim(ctx, tx, command.PacketID, command.UserID); err != nil || claimed {
		if err != nil {
			return ledger.RedPacketClaimReceipt{}, err
		}
		return ledger.RedPacketClaimReceipt{Claim: existingClaim, Packet: packet}, nil
	}
	allocationNo, amount, err := r.lockOneRedPacketAllocation(ctx, tx, command.PacketID)
	if err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if amount <= 0 || amount > packet.RemainingAmount {
		return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.LedgerConflict, "red packet allocation is invalid")
	}

	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, true, nowMS)
	if err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	claimID := redPacketClaimID(command.AppCode, command.CommandID)
	balanceAfter := account.AvailableAmount + amount
	metadata := redPacketClaimMetadata{
		AppCode:           command.AppCode,
		ClaimID:           claimID,
		CommandID:         command.CommandID,
		PacketID:          command.PacketID,
		UserID:            command.UserID,
		Amount:            amount,
		RoomID:            packet.RoomID,
		RegionID:          packet.RegionID,
		PacketType:        packet.PacketType,
		RemainingAmount:   packet.RemainingAmount - amount,
		RemainingCount:    packet.RemainingCount - 1,
		BalanceAfter:      balanceAfter,
		WalletTransaction: transactionID,
		ClaimedAtMS:       nowMS,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeRedPacketClaim, requestHash, command.PacketID, metadata, nowMS); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, amount, 0, nowMS); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.UserID,
		AssetType:          ledger.AssetCoin,
		AvailableDelta:     amount,
		FrozenDelta:        0,
		AvailableAfter:     balanceAfter,
		FrozenAfter:        account.FrozenAmount,
		CounterpartyUserID: packet.SenderUserID,
		RoomID:             packet.RoomID,
		CreatedAtMS:        nowMS,
	}); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if err := r.insertRedPacketClaim(ctx, tx, metadata); err != nil {
		if isMySQLDuplicateError(err) {
			existingClaim, claimed, lookupErr := r.getUserRedPacketClaim(ctx, tx, command.PacketID, command.UserID)
			if lookupErr != nil {
				return ledger.RedPacketClaimReceipt{}, lookupErr
			}
			if claimed {
				return r.redPacketClaimReceiptForTransaction(ctx, tx, existingClaim.WalletTransactionID)
			}
			return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.LedgerConflict, "red packet claim already exists")
		}
		return ledger.RedPacketClaimReceipt{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE red_packet_allocations
		SET status = ?, claimed_by_user_id = ?, claim_id = ?, claimed_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND packet_id = ? AND allocation_no = ? AND status = ?`,
		redPacketAllocationClaimed, command.UserID, claimID, nowMS, nowMS,
		appcode.FromContext(ctx), command.PacketID, allocationNo, redPacketAllocationUnclaimed,
	); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	newRemainingAmount := packet.RemainingAmount - amount
	newRemainingCount := packet.RemainingCount - 1
	newStatus := ledger.RedPacketStatusActive
	if newRemainingCount == 0 {
		newStatus = ledger.RedPacketStatusFinished
	}
	metadata.PacketStatus = newStatus
	if _, err := tx.ExecContext(ctx, `
		UPDATE red_packets
		SET remaining_amount = ?, remaining_count = ?, status = ?, updated_at_ms = ?
		WHERE app_code = ? AND packet_id = ?`,
		newRemainingAmount, newRemainingCount, newStatus, nowMS, appcode.FromContext(ctx), command.PacketID,
	); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, amount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMS),
		redPacketClaimedEvent(transactionID, command.CommandID, metadata, nowMS),
	}); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	packet.RemainingAmount = newRemainingAmount
	packet.RemainingCount = newRemainingCount
	packet.Status = newStatus
	packet.UpdatedAtMS = nowMS
	return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), Packet: packet, BalanceAfter: balanceAfter}, nil
}

func (r *Repository) insertRedPacketClaim(ctx context.Context, tx *sql.Tx, metadata redPacketClaimMetadata) error {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO red_packet_claims (
			app_code, claim_id, command_id, packet_id, user_id, amount, wallet_transaction_id,
			status, failure_reason, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`,
		appcode.FromContext(ctx), metadata.ClaimID, metadata.CommandID, metadata.PacketID, metadata.UserID,
		metadata.Amount, metadata.WalletTransaction, ledger.RedPacketClaimStatusClaimed, metadata.ClaimedAtMS, metadata.ClaimedAtMS,
	)
	return err
}

func (r *Repository) getUserRedPacketClaim(ctx context.Context, tx *sql.Tx, packetID string, userID int64) (ledger.RedPacketClaim, bool, error) {
	var claim ledger.RedPacketClaim
	err := tx.QueryRowContext(ctx, `
		SELECT app_code, claim_id, command_id, packet_id, user_id, amount, wallet_transaction_id,
		       status, failure_reason, created_at_ms, updated_at_ms
		FROM red_packet_claims
		WHERE app_code = ? AND packet_id = ? AND user_id = ?
		LIMIT 1`,
		appcode.FromContext(ctx), packetID, userID,
	).Scan(
		&claim.AppCode,
		&claim.ClaimID,
		&claim.CommandID,
		&claim.PacketID,
		&claim.UserID,
		&claim.Amount,
		&claim.WalletTransactionID,
		&claim.Status,
		&claim.FailureReason,
		&claim.CreatedAtMS,
		&claim.UpdatedAtMS,
	)
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.RedPacketClaim{}, false, nil
	}
	return claim, err == nil, err
}

func (r *Repository) lockOneRedPacketAllocation(ctx context.Context, tx *sql.Tx, packetID string) (int32, int64, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT allocation_no, amount
		FROM red_packet_allocations
		WHERE app_code = ? AND packet_id = ? AND status = ?
		ORDER BY allocation_no ASC
		LIMIT 1
		FOR UPDATE`,
		appcode.FromContext(ctx), packetID, redPacketAllocationUnclaimed,
	)
	var allocationNo int32
	var amount int64
	if err := row.Scan(&allocationNo, &amount); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return 0, 0, xerr.New(xerr.RedPacketSoldOut, "red packet is sold out")
		}
		return 0, 0, err
	}
	return allocationNo, amount, nil
}

func (r *Repository) redPacketClaimReceiptForTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.RedPacketClaimReceipt, 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.RedPacketClaimReceipt{}, err
	}
	var metadata redPacketClaimMetadata
	if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
		return ledger.RedPacketClaimReceipt{}, err
	}
	packet, err := r.getRedPacket(ctx, tx, metadata.PacketID)
	if err != nil {
		return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), BalanceAfter: metadata.BalanceAfter}, nil
	}
	return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), Packet: packet, BalanceAfter: metadata.BalanceAfter}, nil
}

func redPacketClaimFromMetadata(metadata redPacketClaimMetadata) ledger.RedPacketClaim {
	return ledger.RedPacketClaim{
		AppCode:             metadata.AppCode,
		ClaimID:             metadata.ClaimID,
		CommandID:           metadata.CommandID,
		PacketID:            metadata.PacketID,
		UserID:              metadata.UserID,
		Amount:              metadata.Amount,
		WalletTransactionID: metadata.WalletTransaction,
		Status:              ledger.RedPacketClaimStatusClaimed,
		CreatedAtMS:         metadata.ClaimedAtMS,
		UpdatedAtMS:         metadata.ClaimedAtMS,
	}
}

func redPacketClaimedEvent(transactionID string, commandID string, metadata redPacketClaimMetadata, nowMS int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, eventTypeRedPacketClaimed, metadata.UserID, ledger.AssetCoin),
		EventType:      eventTypeRedPacketClaimed,
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload:        metadata,
		CreatedAtMS:    nowMS,
	}
}

func redPacketClaimRequestHash(command ledger.RedPacketClaimCommand) string {
	return stableHash(fmt.Sprintf("red_packet_claim|%s|%s|%d",
		appcode.Normalize(command.AppCode),
		strings.TrimSpace(command.PacketID),
		command.UserID,
	))
}

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