package bet

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"math/big"
	random "math/rand"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
	"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
	"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
	"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
	"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
	"go.uber.org/zap"
)

var (
	ErrNoEventsAvailable     = errors.New("Not enough events available with the given filters")
	ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
	ErrOutcomesNotCompleted  = errors.New("Some bet outcomes are still pending")
	ErrEventHasBeenRemoved   = errors.New("Event has been removed")
)

type Service struct {
	betStore    BetStore
	eventSvc    event.Service
	prematchSvc odds.ServiceImpl
	walletSvc   wallet.Service
	branchSvc   branch.Service
	logger      *slog.Logger
	mongoLogger *zap.Logger
}

func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service {
	return &Service{
		betStore:    betStore,
		eventSvc:    eventSvc,
		prematchSvc: prematchSvc,
		walletSvc:   walletSvc,
		branchSvc:   branchSvc,
		logger:      logger,
		mongoLogger: mongoLogger,
	}
}

var (
	ErrEventHasNotEnded = errors.New("Event has not ended yet")
	ErrRawOddInvalid    = errors.New("Prematch Raw Odd is Invalid")
	ErrBranchIDRequired = errors.New("Branch ID required for this role")
	ErrOutcomeLimit     = errors.New("Too many outcomes on a single bet")
)

func (s *Service) GenerateCashoutID() (string, error) {
	const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
	const length int = 13
	charLen := big.NewInt(int64(len(chars)))
	result := make([]byte, length)

	for i := 0; i < length; i++ {
		index, err := rand.Int(rand.Reader, charLen)
		if err != nil {
			s.mongoLogger.Error("failed to generate random index for cashout ID",
				zap.Int("position", i),
				zap.Error(err),
			)
			return "", err
		}
		result[i] = chars[index.Int64()]
	}

	return string(result), nil
}

func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
	eventIDStr := strconv.FormatInt(eventID, 10)
	marketIDStr := strconv.FormatInt(marketID, 10)
	oddIDStr := strconv.FormatInt(oddID, 10)

	event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
	if err != nil {
		s.mongoLogger.Error("failed to fetch upcoming event by ID",
			zap.Int64("event_id", eventID),
			zap.Error(err),
		)
		return domain.CreateBetOutcome{}, ErrEventHasBeenRemoved
	}

	currentTime := time.Now()
	if event.StartTime.Before(currentTime) {
		s.mongoLogger.Error("event has already started",
			zap.Int64("event_id", eventID),
			zap.Time("event_start_time", event.StartTime),
			zap.Time("current_time", currentTime),
		)
		return domain.CreateBetOutcome{}, ErrEventHasNotEnded
	}

	odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
	if err != nil {
		s.mongoLogger.Error("failed to get raw odds by market ID",
			zap.Int64("event_id", eventID),
			zap.Int64("market_id", marketID),
			zap.Error(err),
		)
		return domain.CreateBetOutcome{}, err
	}

	type rawOddType struct {
		ID       string
		Name     string
		Odds     string
		Header   string
		Handicap string
	}

	var selectedOdd rawOddType
	var isOddFound bool

	for _, raw := range odds.RawOdds {
		var rawOdd rawOddType
		rawBytes, err := json.Marshal(raw)
		if err != nil {
			s.mongoLogger.Error("failed to marshal raw odd",
				zap.Any("raw", raw),
				zap.Error(err),
			)
			continue
		}
		err = json.Unmarshal(rawBytes, &rawOdd)
		if err != nil {
			s.mongoLogger.Error("failed to unmarshal raw odd",
				zap.ByteString("raw_bytes", rawBytes),
				zap.Error(err),
			)
			continue
		}
		if rawOdd.ID == oddIDStr {
			selectedOdd = rawOdd
			isOddFound = true
			break
		}
	}
	if !isOddFound {
		s.mongoLogger.Error("odd ID not found in raw odds",
			zap.Int64("odd_id", oddID),
			zap.Int64("market_id", marketID),
			zap.Int64("event_id", eventID),
		)
		return domain.CreateBetOutcome{}, ErrRawOddInvalid
	}

	parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
	if err != nil {
		s.mongoLogger.Error("failed to parse selected odd value",
			zap.String("odd", selectedOdd.Odds),
			zap.Int64("odd_id", oddID),
			zap.Error(err),
		)
		return domain.CreateBetOutcome{}, err
	}

	newOutcome := domain.CreateBetOutcome{
		EventID:      eventID,
		OddID:        oddID,
		MarketID:     marketID,
		SportID:      int64(event.SportID),
		HomeTeamName: event.HomeTeam,
		AwayTeamName: event.AwayTeam,
		MarketName:   odds.MarketName,
		Odd:          float32(parsedOdd),
		OddName:      selectedOdd.Name,
		OddHeader:    selectedOdd.Header,
		OddHandicap:  selectedOdd.Handicap,
		Expires:      event.StartTime,
	}

	return newOutcome, nil
}

func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) {
	if len(req.Outcomes) > 30 {
		s.mongoLogger.Error("too many outcomes",
			zap.Int("count", len(req.Outcomes)),
			zap.Int64("user_id", userID),
		)
		return domain.CreateBetRes{}, ErrOutcomeLimit
	}

	var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
	var totalOdds float32 = 1

	for _, outcomeReq := range req.Outcomes {
		fmt.Println("reqq: ", outcomeReq)
		newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
		if err != nil {
			s.mongoLogger.Error("failed to generate outcome",
				zap.Int64("event_id", outcomeReq.EventID),
				zap.Int64("market_id", outcomeReq.MarketID),
				zap.Int64("odd_id", outcomeReq.OddID),
				zap.Int64("user_id", userID),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}
		totalOdds *= float32(newOutcome.Odd)
		outcomes = append(outcomes, newOutcome)
	}

	outcomesHash, err := generateOutcomeHash(outcomes)
	if err != nil {
		s.mongoLogger.Error("failed to generate outcome hash",
			zap.Int64("user_id", userID),
			zap.Error(err),
		)
		return domain.CreateBetRes{}, err
	}

	count, err := s.GetBetCount(ctx, userID, outcomesHash)
	if err != nil {
		return domain.CreateBetRes{}, err
	}
	if count == 2 {
		return domain.CreateBetRes{}, fmt.Errorf("bet already pleaced twice")
	}

	cashoutID, err := s.GenerateCashoutID()
	if err != nil {
		s.mongoLogger.Error("failed to generate cashout ID",
			zap.Int64("user_id", userID),
			zap.Error(err),
		)
		return domain.CreateBetRes{}, err
	}

	newBet := domain.CreateBet{
		Amount:       domain.ToCurrency(req.Amount),
		TotalOdds:    totalOdds,
		Status:       req.Status,
		FullName:     req.FullName,
		PhoneNumber:  req.PhoneNumber,
		CashoutID:    cashoutID,
		OutcomesHash: outcomesHash,
	}

	switch role {
	case domain.RoleCashier:
		branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
		if err != nil {
			s.mongoLogger.Error("failed to get branch by cashier",
				zap.Int64("user_id", userID),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}

		deductedAmount := req.Amount / 10
		err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
		if err != nil {
			s.mongoLogger.Error("failed to deduct from wallet",
				zap.Int64("wallet_id", branch.WalletID),
				zap.Float32("amount", deductedAmount),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}

		newBet.BranchID = domain.ValidInt64{Value: branch.ID, Valid: true}
		newBet.CompanyID = domain.ValidInt64{Value: branch.CompanyID, Valid: true}
		newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
		newBet.IsShopBet = true

	case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
		if req.BranchID == nil {
			s.mongoLogger.Error("branch ID required for admin/manager",
				zap.Int64("user_id", userID),
			)
			return domain.CreateBetRes{}, ErrBranchIDRequired
		}

		branch, err := s.branchSvc.GetBranchByID(ctx, *req.BranchID)
		if err != nil {
			s.mongoLogger.Error("failed to get branch by ID",
				zap.Int64("branch_id", *req.BranchID),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}

		deductedAmount := req.Amount / 10
		err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
		if err != nil {
			s.mongoLogger.Error("wallet deduction failed",
				zap.Int64("wallet_id", branch.WalletID),
				zap.Float32("amount", deductedAmount),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}

		newBet.BranchID = domain.ValidInt64{Value: branch.ID, Valid: true}
		newBet.CompanyID = domain.ValidInt64{Value: branch.CompanyID, Valid: true}
		newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
		newBet.IsShopBet = true

	case domain.RoleCustomer:
		wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
		if err != nil {
			s.mongoLogger.Error("failed to get customer wallets",
				zap.Int64("user_id", userID),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}

		userWallet := wallets[0]
		err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount))
		if err != nil {
			s.mongoLogger.Error("wallet deduction failed for customer",
				zap.Int64("wallet_id", userWallet.ID),
				zap.Float32("amount", req.Amount),
				zap.Error(err),
			)
			return domain.CreateBetRes{}, err
		}

		newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
		newBet.IsShopBet = false

	default:
		s.mongoLogger.Error("unknown role type",
			zap.String("role", string(role)),
			zap.Int64("user_id", userID),
		)
		return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
	}

	fmt.Println("Bet is: ", newBet)
	bet, err := s.CreateBet(ctx, newBet)
	if err != nil {
		s.mongoLogger.Error("failed to create bet",
			zap.Int64("user_id", userID),
			zap.Error(err),
		)
		return domain.CreateBetRes{}, err
	}

	for i := range outcomes {
		outcomes[i].BetID = bet.ID
	}
	rows, err := s.betStore.CreateBetOutcome(ctx, outcomes)
	if err != nil {
		s.mongoLogger.Error("failed to create bet outcomes",
			zap.Int64("bet_id", bet.ID),
			zap.Error(err),
		)
		return domain.CreateBetRes{}, err
	}

	res := domain.ConvertCreateBet(bet, rows)

	return res, nil
}

func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, sportID int32, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) {

	var newOdds []domain.CreateBetOutcome
	var totalOdds float32 = 1

	markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID)
	if err != nil {
		s.logger.Error("failed to get odds for event", "event id", eventID, "error", err)
		s.mongoLogger.Error("failed to get odds for event",
			zap.String("eventID", eventID),
			zap.Int32("sportID", sportID),
			zap.String("homeTeam", HomeTeam),
			zap.String("awayTeam", AwayTeam),
			zap.Error(err))
		return nil, 0, err
	}

	if len(markets) == 0 {
		s.logger.Error("empty odds for event", "event id", eventID)
		s.mongoLogger.Warn("empty odds for event",
			zap.String("eventID", eventID),
			zap.Int32("sportID", sportID),
			zap.String("homeTeam", HomeTeam),
			zap.String("awayTeam", AwayTeam))
		return nil, 0, fmt.Errorf("empty odds or event %v", eventID)
	}

	var selectedMarkets []domain.Odd
	numMarkets = min(numMarkets, len(markets))
	for i := 0; i < numMarkets; i++ {
		randomIndex := random.Intn(len(markets))
		selectedMarkets = append(selectedMarkets, markets[randomIndex])
		markets = append(markets[:randomIndex], markets[randomIndex+1:]...)
	}

	for _, market := range selectedMarkets {

		randomRawOdd := market.RawOdds[random.Intn(len(market.RawOdds))]

		type rawOddType struct {
			ID       string
			Name     string
			Odds     string
			Header   string
			Handicap string
		}

		var selectedOdd rawOddType
		rawBytes, err := json.Marshal(randomRawOdd)
		err = json.Unmarshal(rawBytes, &selectedOdd)

		if err != nil {
			s.logger.Error("Failed to unmarshal raw odd", "error", err)
			s.mongoLogger.Warn("Failed to unmarshal raw odd",
				zap.String("eventID", eventID),
				zap.Int32("sportID", sportID),
				zap.Error(err))
			continue
		}

		parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
		if err != nil {
			s.logger.Error("Failed to parse odd", "error", err)
			s.mongoLogger.Warn("Failed to parse odd",
				zap.String("eventID", eventID),
				zap.String("oddValue", selectedOdd.Odds),
				zap.Error(err))
			continue
		}

		eventIDInt, err := strconv.ParseInt(eventID, 10, 64)
		if err != nil {
			s.logger.Error("Failed to parse eventID", "error", err)
			s.mongoLogger.Warn("Failed to parse eventID",
				zap.String("eventID", eventID),
				zap.Error(err))
			continue
		}

		oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64)
		if err != nil {
			s.logger.Error("Failed to parse oddID", "error", err)
			s.mongoLogger.Warn("Failed to parse oddID",
				zap.String("oddID", selectedOdd.ID),
				zap.Error(err))
			continue
		}

		marketID, err := strconv.ParseInt(market.MarketID, 10, 64)
		if err != nil {
			s.logger.Error("Failed to parse marketID", "error", err)
			s.mongoLogger.Warn("Failed to parse marketID",
				zap.String("marketID", market.MarketID),
				zap.Error(err))
			continue
		}

		marketName := market.MarketName

		newOdds = append(newOdds, domain.CreateBetOutcome{
			EventID:      eventIDInt,
			OddID:        oddID,
			MarketID:     marketID,
			SportID:      int64(sportID),
			HomeTeamName: HomeTeam,
			AwayTeamName: AwayTeam,
			MarketName:   marketName,
			Odd:          float32(parsedOdd),
			OddName:      selectedOdd.Name,
			OddHeader:    selectedOdd.Header,
			OddHandicap:  selectedOdd.Handicap,
			Expires:      StartTime,
		})

		totalOdds *= float32(parsedOdd)
	}

	if len(newOdds) == 0 {
		s.logger.Error("Bet Outcomes is empty for market", "selectedMarkets", len(selectedMarkets))
		s.mongoLogger.Error("Bet Outcomes is empty for market",
			zap.String("eventID", eventID),
			zap.Int32("sportID", sportID),
			zap.String("homeTeam", HomeTeam),
			zap.String("awayTeam", AwayTeam),
			zap.Int("selectedMarkets", len(selectedMarkets)))
		return nil, 0, ErrGenerateRandomOutcome
	}

	// ✅ Final success log (optional)
	s.mongoLogger.Info("Random bet outcomes generated successfully",
		zap.String("eventID", eventID),
		zap.Int32("sportID", sportID),
		zap.Int("numOutcomes", len(newOdds)),
		zap.Float32("totalOdds", totalOdds))

	return newOdds, totalOdds, nil
}

func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) {

	// Get a unexpired event id

	events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx,
		domain.EventFilter{
			SportID:        sportID,
			LeagueID:       leagueID,
			FirstStartTime: firstStartTime,
			LastStartTime:  lastStartTime,
		})

	if err != nil {
		s.mongoLogger.Error("failed to get paginated upcoming events",
			zap.Int64("userID", userID),
			zap.Int64("branchID", branchID),
			zap.Error(err))
		return domain.CreateBetRes{}, err
	}

	if len(events) == 0 {
		s.mongoLogger.Warn("no events available for random bet",
			zap.Int64("userID", userID),
			zap.Int64("branchID", branchID))
		return domain.CreateBetRes{}, ErrNoEventsAvailable
	}

	// TODO: Add the option of passing number of created events
	var selectedUpcomingEvents []domain.UpcomingEvent
	numEventsPerBet := min(random.Intn(4)+1, len(events)) //Eliminate the option of 0

	for i := 0; i < int(numEventsPerBet); i++ {
		randomIndex := random.Intn(len(events))
		selectedUpcomingEvents = append(selectedUpcomingEvents, events[randomIndex])
		events = append(events[:randomIndex], events[randomIndex+1:]...)

	}

	// s.logger.Info("Generating random bet events", "selectedUpcomingEvents", len(selectedUpcomingEvents))

	//  Get market and odds for that
	var randomOdds []domain.CreateBetOutcome
	var totalOdds float32 = 1
	numMarketsPerBet := random.Intn(2) + 1
	for _, event := range selectedUpcomingEvents {

		newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime, numMarketsPerBet)

		if err != nil {
			s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err)
			s.mongoLogger.Error("failed to generate random bet outcome",
				zap.Int64("userID", userID),
				zap.Int64("branchID", branchID),
				zap.String("eventID", event.ID),
				zap.String("error", fmt.Sprintf("%v", err)))
			continue
		}

		randomOdds = append(randomOdds, newOdds...)
		totalOdds = totalOdds * total

	}
	if len(randomOdds) == 0 {
		s.logger.Error("Failed to generate random any outcomes for all events")
		s.mongoLogger.Error("Failed to generate random any outcomes for all events",
			zap.Int64("userID", userID),
			zap.Int64("branchID", branchID))
		return domain.CreateBetRes{}, ErrGenerateRandomOutcome
	}

	// s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds))

	var cashoutID string

	cashoutID, err = s.GenerateCashoutID()
	if err != nil {
		s.mongoLogger.Error("Failed to generate cash out ID",
			zap.Int64("userID", userID),
			zap.Int64("branchID", branchID))
		return domain.CreateBetRes{}, err
	}

	randomNumber := strconv.FormatInt(int64(random.Intn(100000000000)), 10)
	newBet := domain.CreateBet{
		Amount:      domain.ToCurrency(123.5),
		TotalOdds:   totalOdds,
		Status:      domain.OUTCOME_STATUS_PENDING,
		FullName:    "test" + randomNumber,
		PhoneNumber: "0900000000",
		CashoutID:   cashoutID,
		BranchID:    domain.ValidInt64{Valid: true, Value: branchID},
		UserID:      domain.ValidInt64{Valid: true, Value: userID},
	}

	bet, err := s.CreateBet(ctx, newBet)
	if err != nil {
		s.mongoLogger.Error("Failed to create a new random bet",
			zap.Int64("userID", userID),
			zap.Int64("branchID", branchID),
			zap.String("bet", fmt.Sprintf("%+v", newBet)))
		return domain.CreateBetRes{}, err
	}

	for i := range randomOdds {
		randomOdds[i].BetID = bet.ID
	}

	rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds)
	if err != nil {
		s.mongoLogger.Error("Failed to create a new random bet outcome",
			zap.Int64("userID", userID),
			zap.Int64("branchID", branchID),
			zap.String("randomOdds", fmt.Sprintf("%+v", randomOdds)))
		return domain.CreateBetRes{}, err
	}

	res := domain.ConvertCreateBet(bet, rows)

	s.mongoLogger.Info("Random bets placed successfully",
		zap.Int64("userID", userID),
		zap.Int64("branchID", branchID),
		zap.String("response", fmt.Sprintf("%+v", res)))
	return res, nil
}

func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
	return s.betStore.CreateBet(ctx, bet)
}

func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) {
	return s.betStore.CreateBetOutcome(ctx, outcomes)
}

func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
	return s.betStore.GetBetByID(ctx, id)
}
func (s *Service) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) {
	return s.betStore.GetBetByCashoutID(ctx, id)
}
func (s *Service) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) {
	return s.betStore.GetAllBets(ctx, filter)
}

func (s *Service) GetBetByBranchID(ctx context.Context, branchID int64) ([]domain.GetBet, error) {
	return s.betStore.GetBetByBranchID(ctx, branchID)
}

func (s *Service) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) {
	return s.betStore.GetBetByUserID(ctx, UserID)
}

func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
	return s.betStore.GetBetCount(ctx, UserID, outcomesHash)
}

func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
	return s.betStore.UpdateCashOut(ctx, id, cashedOut)
}

func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
	bet, err := s.GetBetByID(ctx, id)
	if err != nil {
		s.mongoLogger.Error("failed to update bet status: invalid bet ID",
			zap.Int64("bet_id", id),
			zap.Error(err),
		)
		return err
	}

	if bet.IsShopBet ||
		status == domain.OUTCOME_STATUS_ERROR ||
		status == domain.OUTCOME_STATUS_PENDING ||
		status == domain.OUTCOME_STATUS_LOSS {
		return s.betStore.UpdateStatus(ctx, id, status)
	}

	customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id)
	if err != nil {
		s.mongoLogger.Error("failed to get customer wallet",
			zap.Int64("bet_id", id),
			zap.Error(err),
		)
		return err
	}

	var amount domain.Currency
	switch status {
	case domain.OUTCOME_STATUS_WIN:
		amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
	case domain.OUTCOME_STATUS_HALF:
		amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
	default:
		amount = bet.Amount
	}

	err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount)
	if err != nil {
		s.mongoLogger.Error("failed to add winnings to wallet",
			zap.Int64("wallet_id", customerWallet.RegularID),
			zap.Float32("amount", float32(amount)),
			zap.Error(err),
		)
		return err
	}

	return s.betStore.UpdateStatus(ctx, id, status)
}

func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
	betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
	if err != nil {
		s.mongoLogger.Error("failed to get bet outcomes",
			zap.Int64("bet_id", betID),
			zap.Error(err),
		)
		return domain.OUTCOME_STATUS_PENDING, err
	}

	status := domain.OUTCOME_STATUS_PENDING

	for _, betOutcome := range betOutcomes {
		if betOutcome.Status == domain.OUTCOME_STATUS_PENDING {
			s.mongoLogger.Info("outcome still pending",
				zap.Int64("bet_id", betID),
			)
			return domain.OUTCOME_STATUS_PENDING, ErrOutcomesNotCompleted
		}
		if betOutcome.Status == domain.OUTCOME_STATUS_ERROR {
			s.mongoLogger.Info("outcome contains error",
				zap.Int64("bet_id", betID),
			)
			return domain.OUTCOME_STATUS_ERROR, nil
		}

		switch status {
		case domain.OUTCOME_STATUS_PENDING:
			status = betOutcome.Status
		case domain.OUTCOME_STATUS_WIN:
			switch betOutcome.Status {
			case domain.OUTCOME_STATUS_LOSS:
				status = domain.OUTCOME_STATUS_LOSS
			case domain.OUTCOME_STATUS_HALF:
				status = domain.OUTCOME_STATUS_HALF
			case domain.OUTCOME_STATUS_VOID:
				status = domain.OUTCOME_STATUS_VOID
			case domain.OUTCOME_STATUS_WIN:
				// remain win
			default:
				status = domain.OUTCOME_STATUS_ERROR
			}
		case domain.OUTCOME_STATUS_LOSS:
			// stay as LOSS regardless of others
		case domain.OUTCOME_STATUS_VOID:
			switch betOutcome.Status {
			case domain.OUTCOME_STATUS_LOSS:
				status = domain.OUTCOME_STATUS_LOSS
			case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF, domain.OUTCOME_STATUS_VOID:
				// remain VOID
			default:
				status = domain.OUTCOME_STATUS_ERROR
			}
		case domain.OUTCOME_STATUS_HALF:
			switch betOutcome.Status {
			case domain.OUTCOME_STATUS_LOSS:
				status = domain.OUTCOME_STATUS_LOSS
			case domain.OUTCOME_STATUS_VOID:
				status = domain.OUTCOME_STATUS_VOID
			case domain.OUTCOME_STATUS_HALF, domain.OUTCOME_STATUS_WIN:
				// remain HALF
			default:
				status = domain.OUTCOME_STATUS_ERROR
			}
		default:
			status = domain.OUTCOME_STATUS_ERROR
		}
	}

	if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR {
		s.mongoLogger.Info("bet status not updated due to status",
			zap.Int64("bet_id", betID),
			zap.String("final_status", string(status)),
		)
	}

	return status, nil
}

func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
	betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
	if err != nil {
		s.mongoLogger.Error("failed to update bet outcome status",
			zap.Int64("betID", id),
			zap.Error(err),
		)
		return domain.BetOutcome{}, err
	}

	return betOutcome, err

}

func (s *Service) DeleteBet(ctx context.Context, id int64) error {
	return s.betStore.DeleteBet(ctx, id)
}

func generateOutcomeHash(outcomes []domain.CreateBetOutcome) (string, error) {
	// should always be in the same order for producing the same hash
	sort.Slice(outcomes, func(i, j int) bool {
		if outcomes[i].EventID != outcomes[j].EventID {
			return outcomes[i].EventID < outcomes[j].EventID
		}
		if outcomes[i].MarketID != outcomes[j].MarketID {
			return outcomes[i].MarketID < outcomes[j].MarketID
		}
		return outcomes[i].OddID < outcomes[j].OddID
	})

	var sb strings.Builder
	for _, o := range outcomes {
		sb.WriteString(fmt.Sprintf("%d-%d-%d;", o.EventID, o.MarketID, o.OddID))
	}

	sum := sha256.Sum256([]byte(sb.String()))
	return hex.EncodeToString(sum[:]), nil
}
