package mysql

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

func (r *Repository) ListRedPackets(ctx context.Context, query ledger.RedPacketQuery) ([]ledger.RedPacket, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	query.AppCode = appcode.FromContext(ctx)
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	where, args := redPacketWhereSQL(query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM red_packets `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	args = append(args, int(query.PageSize), int((query.Page-1)*query.PageSize))
	rows, err := r.db.QueryContext(ctx, `
		SELECT 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
		FROM red_packets `+where+`
		ORDER BY created_at_ms DESC, packet_id DESC
		LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	packets := make([]ledger.RedPacket, 0, query.PageSize)
	for rows.Next() {
		packet, err := scanRedPacket(rows)
		if err != nil {
			return nil, 0, err
		}
		packets = append(packets, packet)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	if err := r.attachRefunds(ctx, packets); err != nil {
		return nil, 0, err
	}
	if query.ViewerUserID > 0 {
		if err := r.attachViewerClaims(ctx, packets, query.ViewerUserID); err != nil {
			return nil, 0, err
		}
	}
	return packets, total, nil
}

func (r *Repository) GetRedPacket(ctx context.Context, app string, packetID string, viewerUserID int64, includeClaims bool) (ledger.RedPacket, error) {
	if r == nil || r.db == nil {
		return ledger.RedPacket{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, app)
	packet, err := r.getRedPacket(ctx, r.db, packetID)
	if err != nil {
		return ledger.RedPacket{}, err
	}
	if viewerUserID > 0 {
		packets := []ledger.RedPacket{packet}
		if err := r.attachViewerClaims(ctx, packets, viewerUserID); err != nil {
			return ledger.RedPacket{}, err
		}
		if err := r.attachRefunds(ctx, packets); err != nil {
			return ledger.RedPacket{}, err
		}
		packet = packets[0]
	} else {
		packets := []ledger.RedPacket{packet}
		if err := r.attachRefunds(ctx, packets); err != nil {
			return ledger.RedPacket{}, err
		}
		packet = packets[0]
	}
	if includeClaims {
		claims, err := r.listRedPacketClaims(ctx, packetID)
		if err != nil {
			return ledger.RedPacket{}, err
		}
		packet.Claims = claims
	}
	return packet, nil
}

func (r *Repository) getRedPacketForUpdate(ctx context.Context, tx *sql.Tx, packetID string) (ledger.RedPacket, error) {
	row := tx.QueryRowContext(ctx, redPacketSelectSQL()+`
		WHERE app_code = ? AND packet_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), packetID,
	)
	packet, err := scanRedPacket(row)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return ledger.RedPacket{}, xerr.New(xerr.NotFound, "red packet not found")
		}
		return ledger.RedPacket{}, err
	}
	return packet, nil
}

func (r *Repository) getRedPacket(ctx context.Context, querier sqlRowQuerier, packetID string) (ledger.RedPacket, error) {
	row := querier.QueryRowContext(ctx, redPacketSelectSQL()+`WHERE app_code = ? AND packet_id = ?`, appcode.FromContext(ctx), packetID)
	packet, err := scanRedPacket(row)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return ledger.RedPacket{}, xerr.New(xerr.NotFound, "red packet not found")
		}
		return ledger.RedPacket{}, err
	}
	return packet, nil
}

func redPacketSelectSQL() string {
	return `
		SELECT 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
		FROM red_packets `
}

func scanRedPacket(row redPacketScanner) (ledger.RedPacket, error) {
	var packet ledger.RedPacket
	err := row.Scan(
		&packet.AppCode,
		&packet.PacketID,
		&packet.CommandID,
		&packet.SenderUserID,
		&packet.RoomID,
		&packet.RegionID,
		&packet.PacketType,
		&packet.TotalAmount,
		&packet.PacketCount,
		&packet.RemainingAmount,
		&packet.RemainingCount,
		&packet.Status,
		&packet.OpenAtMS,
		&packet.ExpiresAtMS,
		&packet.CreatedAtMS,
		&packet.UpdatedAtMS,
	)
	return packet, err
}

func (r *Repository) attachViewerClaims(ctx context.Context, packets []ledger.RedPacket, viewerUserID int64) error {
	if len(packets) == 0 || viewerUserID <= 0 {
		return nil
	}
	packetIDs := make([]string, 0, len(packets))
	indexByID := make(map[string]int, len(packets))
	for index := range packets {
		packetIDs = append(packetIDs, packets[index].PacketID)
		indexByID[packets[index].PacketID] = index
	}
	placeholders := strings.TrimRight(strings.Repeat("?,", len(packetIDs)), ",")
	args := []any{appcode.FromContext(ctx), viewerUserID}
	for _, packetID := range packetIDs {
		args = append(args, packetID)
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT packet_id, amount
		FROM red_packet_claims
		WHERE app_code = ? AND user_id = ? AND packet_id IN (`+placeholders+`)`,
		args...,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var packetID string
		var amount int64
		if err := rows.Scan(&packetID, &amount); err != nil {
			return err
		}
		if index, ok := indexByID[packetID]; ok {
			packets[index].ClaimedByViewer = true
			packets[index].ViewerClaimAmount = amount
		}
	}
	return rows.Err()
}

func (r *Repository) attachRefunds(ctx context.Context, packets []ledger.RedPacket) error {
	if len(packets) == 0 {
		return nil
	}
	packetIDs := make([]string, 0, len(packets))
	indexByID := make(map[string]int, len(packets))
	for index := range packets {
		packetIDs = append(packetIDs, packets[index].PacketID)
		indexByID[packets[index].PacketID] = index
	}
	placeholders := strings.TrimRight(strings.Repeat("?,", len(packetIDs)), ",")
	args := []any{appcode.FromContext(ctx)}
	for _, packetID := range packetIDs {
		args = append(args, packetID)
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT packet_id, refund_amount, status, updated_at_ms
		FROM red_packet_refunds
		WHERE app_code = ? AND packet_id IN (`+placeholders+`)`,
		args...,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var packetID string
		var amount int64
		var status string
		var updatedAtMS int64
		if err := rows.Scan(&packetID, &amount, &status, &updatedAtMS); err != nil {
			return err
		}
		if index, ok := indexByID[packetID]; ok {
			packets[index].RefundAmount = amount
			packets[index].RefundStatus = status
			if status == ledger.RedPacketStatusRefunded {
				packets[index].RefundedAtMS = updatedAtMS
			}
		}
	}
	return rows.Err()
}

func (r *Repository) listRedPacketClaims(ctx context.Context, packetID string) ([]ledger.RedPacketClaim, error) {
	rows, err := r.db.QueryContext(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 = ?
		ORDER BY created_at_ms ASC, claim_id ASC`,
		appcode.FromContext(ctx), packetID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	claims := make([]ledger.RedPacketClaim, 0)
	for rows.Next() {
		var claim ledger.RedPacketClaim
		if err := rows.Scan(
			&claim.AppCode,
			&claim.ClaimID,
			&claim.CommandID,
			&claim.PacketID,
			&claim.UserID,
			&claim.Amount,
			&claim.WalletTransactionID,
			&claim.Status,
			&claim.FailureReason,
			&claim.CreatedAtMS,
			&claim.UpdatedAtMS,
		); err != nil {
			return nil, err
		}
		claims = append(claims, claim)
	}
	return claims, rows.Err()
}

func redPacketWhereSQL(query ledger.RedPacketQuery) (string, []any) {
	where := "WHERE app_code = ?"
	args := []any{appcode.Normalize(query.AppCode)}
	if strings.TrimSpace(query.RoomID) != "" {
		where += " AND room_id = ?"
		args = append(args, strings.TrimSpace(query.RoomID))
	}
	if query.SenderUserID > 0 {
		where += " AND sender_user_id = ?"
		args = append(args, query.SenderUserID)
	}
	if query.RegionID > 0 {
		where += " AND region_id = ?"
		args = append(args, query.RegionID)
	}
	if query.PacketType != "" {
		where += " AND packet_type = ?"
		args = append(args, query.PacketType)
	}
	if query.Status != "" {
		where += " AND status = ?"
		args = append(args, query.Status)
	}
	if query.StartAtMS > 0 {
		where += " AND created_at_ms >= ?"
		args = append(args, query.StartAtMS)
	}
	if query.EndAtMS > 0 {
		where += " AND created_at_ms < ?"
		args = append(args, query.EndAtMS)
	}
	return where, args
}
