package mysql

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

func (r *Repository) ExpireRedPackets(ctx context.Context, app string, batchSize int32) (ledger.RedPacketExpireResult, error) {
	if r == nil || r.db == nil {
		return ledger.RedPacketExpireResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if batchSize <= 0 {
		batchSize = 50
	}
	if batchSize > 200 {
		batchSize = 200
	}
	nowMS := time.Now().UTC().UnixMilli()
	app = strings.ToLower(strings.TrimSpace(app))
	query := `
		SELECT app_code, packet_id
		FROM red_packets
		WHERE status IN (?, ?) AND expires_at_ms <= ?`
	args := []any{ledger.RedPacketStatusActive, ledger.RedPacketStatusWaitingOpen, nowMS}
	if app != "" {
		query += ` AND app_code = ?`
		args = append(args, appcode.Normalize(app))
	}
	query += ` ORDER BY expires_at_ms ASC, app_code ASC, packet_id ASC LIMIT ?`
	args = append(args, batchSize)
	rows, err := r.db.QueryContext(ctx, query, args...)
	if err != nil {
		return ledger.RedPacketExpireResult{}, err
	}
	type expiryCandidate struct {
		appCode  string
		packetID string
	}
	candidates := make([]expiryCandidate, 0, batchSize)
	for rows.Next() {
		var candidate expiryCandidate
		if err := rows.Scan(&candidate.appCode, &candidate.packetID); err != nil {
			_ = rows.Close()
			return ledger.RedPacketExpireResult{}, err
		}
		candidates = append(candidates, candidate)
	}
	if err := rows.Close(); err != nil {
		return ledger.RedPacketExpireResult{}, err
	}
	var result ledger.RedPacketExpireResult
	for _, candidate := range candidates {
		// 每个红包都用自己的租户 context，保证账户、流水、退款和 outbox 落在同一 app_code 分区。
		candidateCtx := appcode.WithContext(ctx, candidate.appCode)
		refunded, amount, err := r.expireOneRedPacket(candidateCtx, candidate.packetID, nowMS)
		if err != nil {
			return result, err
		}
		if refunded {
			result.ExpiredCount++
			result.RefundedAmount += amount
		}
	}
	return result, nil
}

func (r *Repository) RetryRedPacketRefund(ctx context.Context, app string, packetID string, operatorUserID int64) (ledger.RedPacketRefundReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_ = operatorUserID
	ctx = contextWithCommandApp(ctx, app)
	nowMS := time.Now().UTC().UnixMilli()
	refunded, amount, err := r.expireOneRedPacket(ctx, strings.TrimSpace(packetID), nowMS)
	if err != nil {
		return ledger.RedPacketRefundReceipt{}, err
	}
	packet, err := r.GetRedPacket(ctx, appcode.FromContext(ctx), packetID, 0, false)
	if err != nil {
		return ledger.RedPacketRefundReceipt{}, err
	}
	if !refunded {
		if packet.Status == ledger.RedPacketStatusRefunded {
			return ledger.RedPacketRefundReceipt{Packet: packet, RefundedAmount: packet.RefundAmount}, nil
		}
		return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.RedPacketExpired, "red packet is not expired")
	}
	return ledger.RedPacketRefundReceipt{Packet: packet, RefundedAmount: amount}, nil
}

func (r *Repository) expireOneRedPacket(ctx context.Context, packetID string, nowMS int64) (bool, int64, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return false, 0, err
	}
	defer func() { _ = tx.Rollback() }()

	packet, err := r.getRedPacketForUpdate(ctx, tx, packetID)
	if err != nil {
		return false, 0, err
	}
	if packet.Status != ledger.RedPacketStatusActive && packet.Status != ledger.RedPacketStatusWaitingOpen {
		return false, 0, nil
	}
	if packet.ExpiresAtMS > nowMS {
		return false, 0, nil
	}
	if packet.RemainingAmount <= 0 || packet.RemainingCount <= 0 {
		if _, err := tx.ExecContext(ctx, `
			UPDATE red_packets
			SET status = ?, remaining_amount = 0, remaining_count = 0, updated_at_ms = ?
			WHERE app_code = ? AND packet_id = ?`,
			ledger.RedPacketStatusFinished, nowMS, appcode.FromContext(ctx), packetID,
		); err != nil {
			return false, 0, err
		}
		return true, 0, tx.Commit()
	}
	refundAmount := packet.RemainingAmount
	commandID := "red_packet_refund:" + packetID
	requestHash := stableHash(fmt.Sprintf("red_packet_refund|%s|%s|%d|%d", appcode.FromContext(ctx), packetID, packet.SenderUserID, refundAmount))
	if _, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, commandID, requestHash, bizTypeRedPacketRefund, xerr.RequestConflict); err != nil || exists {
		return false, 0, err
	}
	account, err := r.lockAccount(ctx, tx, packet.SenderUserID, ledger.AssetCoin, true, nowMS)
	if err != nil {
		return false, 0, err
	}
	transactionID := transactionID(appcode.FromContext(ctx), commandID)
	refundID := redPacketRefundID(appcode.FromContext(ctx), packetID)
	balanceAfter := account.AvailableAmount + refundAmount
	metadata := redPacketRefundMetadata{
		AppCode:           appcode.FromContext(ctx),
		RefundID:          refundID,
		CommandID:         commandID,
		PacketID:          packetID,
		SenderUserID:      packet.SenderUserID,
		RoomID:            packet.RoomID,
		RegionID:          packet.RegionID,
		PacketType:        packet.PacketType,
		RefundAmount:      refundAmount,
		PacketStatus:      ledger.RedPacketStatusRefunded,
		BalanceAfter:      balanceAfter,
		WalletTransaction: transactionID,
		RefundedAtMS:      nowMS,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, commandID, bizTypeRedPacketRefund, requestHash, packetID, metadata, nowMS); err != nil {
		return false, 0, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, refundAmount, 0, nowMS); err != nil {
		return false, 0, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         packet.SenderUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: refundAmount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		RoomID:         packet.RoomID,
		CreatedAtMS:    nowMS,
	}); err != nil {
		return false, 0, err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO red_packet_refunds (
			app_code, refund_id, command_id, packet_id, sender_user_id, refund_amount,
			wallet_transaction_id, status, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, 'refunded', ?, ?)`,
		appcode.FromContext(ctx), refundID, commandID, packetID, packet.SenderUserID, refundAmount, transactionID, nowMS, nowMS,
	); err != nil {
		return false, 0, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE red_packet_allocations
		SET status = ?, updated_at_ms = ?
		WHERE app_code = ? AND packet_id = ? AND status = ?`,
		redPacketAllocationExpired, nowMS, appcode.FromContext(ctx), packetID, redPacketAllocationUnclaimed,
	); err != nil {
		return false, 0, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE red_packets
		SET status = ?, remaining_amount = 0, remaining_count = 0, updated_at_ms = ?
		WHERE app_code = ? AND packet_id = ?`,
		ledger.RedPacketStatusRefunded, nowMS, appcode.FromContext(ctx), packetID,
	); err != nil {
		return false, 0, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, commandID, packet.SenderUserID, ledger.AssetCoin, refundAmount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMS),
		redPacketRefundedEvent(transactionID, commandID, metadata, nowMS),
	}); err != nil {
		return false, 0, err
	}
	return true, refundAmount, tx.Commit()
}

func redPacketRefundedEvent(transactionID string, commandID string, metadata redPacketRefundMetadata, nowMS int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, eventTypeRedPacketRefunded, metadata.SenderUserID, ledger.AssetCoin),
		EventType:      eventTypeRedPacketRefunded,
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.SenderUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.RefundAmount,
		FrozenDelta:    0,
		Payload:        metadata,
		CreatedAtMS:    nowMS,
	}
}

func redPacketRefundID(app string, packetID string) string {
	return "rpr_" + stableHash(appcode.Normalize(app)+"|"+packetID)
}
