package channelserver

import (
	"database/sql"
	"errors"
	"math/rand"
	"time"

	"erupe-ce/common/byteframe"

	"go.uber.org/zap"
)

// GachaService encapsulates business logic for the gacha lottery system.
type GachaService struct {
	gachaRepo        GachaRepo
	userRepo         UserRepo
	charRepo         CharacterRepo
	logger           *zap.Logger
	maxNetcafePoints int
}

// NewGachaService creates a new GachaService.
func NewGachaService(gr GachaRepo, ur UserRepo, cr CharacterRepo, log *zap.Logger, maxNP int) *GachaService {
	return &GachaService{
		gachaRepo:        gr,
		userRepo:         ur,
		charRepo:         cr,
		logger:           log,
		maxNetcafePoints: maxNP,
	}
}

// GachaReward represents a single gacha reward item with rarity.
type GachaReward struct {
	ItemType uint8
	ItemID   uint16
	Quantity uint16
	Rarity   uint8
}

// GachaPlayResult holds the outcome of a normal or box gacha play.
type GachaPlayResult struct {
	Rewards []GachaReward
}

// StepupPlayResult holds the outcome of a stepup gacha play.
type StepupPlayResult struct {
	RandomRewards     []GachaReward
	GuaranteedRewards []GachaReward
}

// StepupStatus holds the current stepup state for a character on a gacha.
type StepupStatus struct {
	Step uint8
}

// transact processes the cost for a gacha roll, deducting the appropriate currency.
func (svc *GachaService) transact(userID, charID, gachaID uint32, rollID uint8) (int, error) {
	itemType, itemNumber, rolls, err := svc.gachaRepo.GetEntryForTransaction(gachaID, rollID)
	if err != nil {
		return 0, err
	}
	switch itemType {
	case 17:
		svc.deductNetcafePoints(charID, int(itemNumber))
	case 19, 20:
		svc.spendGachaCoin(userID, itemNumber)
	case 21:
		if err := svc.userRepo.DeductFrontierPoints(userID, uint32(itemNumber)); err != nil {
			svc.logger.Error("Failed to deduct frontier points for gacha", zap.Error(err))
		}
	}
	return rolls, nil
}

// deductNetcafePoints removes netcafe points from a character's save data.
func (svc *GachaService) deductNetcafePoints(charID uint32, amount int) {
	points, err := svc.charRepo.ReadInt(charID, "netcafe_points")
	if err != nil {
		svc.logger.Error("Failed to read netcafe points", zap.Error(err))
		return
	}
	points = min(points-amount, svc.maxNetcafePoints)
	if err := svc.charRepo.SaveInt(charID, "netcafe_points", points); err != nil {
		svc.logger.Error("Failed to update netcafe points", zap.Error(err))
	}
}

// spendGachaCoin deducts gacha coins, preferring trial coins over premium.
func (svc *GachaService) spendGachaCoin(userID uint32, quantity uint16) {
	gt, _ := svc.userRepo.GetTrialCoins(userID)
	if quantity <= gt {
		if err := svc.userRepo.DeductTrialCoins(userID, uint32(quantity)); err != nil {
			svc.logger.Error("Failed to deduct gacha trial coins", zap.Error(err))
		}
	} else {
		if err := svc.userRepo.DeductPremiumCoins(userID, uint32(quantity)); err != nil {
			svc.logger.Error("Failed to deduct gacha premium coins", zap.Error(err))
		}
	}
}

// resolveRewards selects random entries and resolves them into rewards.
func (svc *GachaService) resolveRewards(entries []GachaEntry, rolls int, isBox bool) []GachaReward {
	rewardEntries, err := getRandomEntries(entries, rolls, isBox)
	if err != nil {
		svc.logger.Warn("Failed to select gacha entries", zap.Error(err))
		return nil
	}
	var rewards []GachaReward
	for i := range rewardEntries {
		entryItems, err := svc.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
		if err != nil {
			svc.logger.Warn("Gacha entry has no items",
				zap.Uint32("entryID", rewardEntries[i].ID), zap.Error(err))
			continue
		}
		for _, item := range entryItems {
			rewards = append(rewards, GachaReward{
				ItemType: item.ItemType,
				ItemID:   item.ItemID,
				Quantity: item.Quantity,
				Rarity:   rewardEntries[i].Rarity,
			})
		}
	}
	return rewards
}

// saveGachaItems appends reward items to the character's gacha item storage.
func (svc *GachaService) saveGachaItems(charID uint32, items []GachaItem) {
	data, _ := svc.charRepo.LoadColumn(charID, "gacha_items")
	if len(data) > 0 {
		numItems := int(data[0])
		data = data[1:]
		oldItem := byteframe.NewByteFrameFromBytes(data)
		for i := 0; i < numItems; i++ {
			items = append(items, GachaItem{
				ItemType: oldItem.ReadUint8(),
				ItemID:   oldItem.ReadUint16(),
				Quantity: oldItem.ReadUint16(),
			})
		}
	}
	newItem := byteframe.NewByteFrame()
	newItem.WriteUint8(uint8(len(items)))
	for i := range items {
		newItem.WriteUint8(items[i].ItemType)
		newItem.WriteUint16(items[i].ItemID)
		newItem.WriteUint16(items[i].Quantity)
	}
	if err := svc.charRepo.SaveColumn(charID, "gacha_items", newItem.Data()); err != nil {
		svc.logger.Error("Failed to update gacha items", zap.Error(err))
	}
}

// rewardsToItems converts GachaReward slices to GachaItem slices for storage.
func rewardsToItems(rewards []GachaReward) []GachaItem {
	items := make([]GachaItem, len(rewards))
	for i, r := range rewards {
		items[i] = GachaItem{ItemType: r.ItemType, ItemID: r.ItemID, Quantity: r.Quantity}
	}
	return items
}

// PlayNormalGacha processes a normal gacha roll: validates the reward pool,
// deducts cost, selects random rewards, saves items, and returns the result.
func (svc *GachaService) PlayNormalGacha(userID, charID, gachaID uint32, rollType uint8) (*GachaPlayResult, error) {
	entries, err := svc.gachaRepo.GetRewardPool(gachaID)
	if err != nil {
		return nil, err
	}
	if len(entries) == 0 {
		return nil, errors.New("gacha has no valid reward entries")
	}
	rolls, err := svc.transact(userID, charID, gachaID, rollType)
	if err != nil {
		return nil, err
	}
	rewards := svc.resolveRewards(entries, rolls, false)
	svc.saveGachaItems(charID, rewardsToItems(rewards))
	return &GachaPlayResult{Rewards: rewards}, nil
}

// PlayStepupGacha processes a stepup gacha roll: validates the reward pool,
// deducts cost, advances step, awards frontier points, selects random +
// guaranteed rewards, and saves items.
func (svc *GachaService) PlayStepupGacha(userID, charID, gachaID uint32, rollType uint8) (*StepupPlayResult, error) {
	entries, err := svc.gachaRepo.GetRewardPool(gachaID)
	if err != nil {
		return nil, err
	}
	if len(entries) == 0 {
		return nil, errors.New("gacha has no valid reward entries")
	}
	rolls, err := svc.transact(userID, charID, gachaID, rollType)
	if err != nil {
		return nil, err
	}
	if err := svc.userRepo.AddFrontierPointsFromGacha(userID, gachaID, rollType); err != nil {
		svc.logger.Error("Failed to award stepup gacha frontier points", zap.Error(err))
	}
	if err := svc.gachaRepo.DeleteStepup(gachaID, charID); err != nil {
		svc.logger.Error("Failed to delete gacha stepup state", zap.Error(err))
	}
	if err := svc.gachaRepo.InsertStepup(gachaID, rollType+1, charID); err != nil {
		svc.logger.Error("Failed to insert gacha stepup state", zap.Error(err))
	}

	guaranteedItems, _ := svc.gachaRepo.GetGuaranteedItems(rollType, gachaID)
	randomRewards := svc.resolveRewards(entries, rolls, false)

	var guaranteedRewards []GachaReward
	for _, item := range guaranteedItems {
		guaranteedRewards = append(guaranteedRewards, GachaReward{
			ItemType: item.ItemType,
			ItemID:   item.ItemID,
			Quantity: item.Quantity,
			Rarity:   0,
		})
	}

	svc.saveGachaItems(charID, rewardsToItems(randomRewards))
	svc.saveGachaItems(charID, rewardsToItems(guaranteedRewards))
	return &StepupPlayResult{
		RandomRewards:     randomRewards,
		GuaranteedRewards: guaranteedRewards,
	}, nil
}

// PlayBoxGacha processes a box gacha roll: validates the reward pool, deducts
// cost, selects random entries without replacement, records drawn entries,
// saves items, and returns the result.
func (svc *GachaService) PlayBoxGacha(userID, charID, gachaID uint32, rollType uint8) (*GachaPlayResult, error) {
	entries, err := svc.gachaRepo.GetRewardPool(gachaID)
	if err != nil {
		return nil, err
	}
	if len(entries) == 0 {
		return nil, errors.New("gacha has no valid reward entries")
	}
	rolls, err := svc.transact(userID, charID, gachaID, rollType)
	if err != nil {
		return nil, err
	}
	rewardEntries, err := getRandomEntries(entries, rolls, true)
	if err != nil {
		svc.logger.Warn("Failed to select box gacha entries", zap.Error(err))
		return &GachaPlayResult{}, nil
	}
	var rewards []GachaReward
	for i := range rewardEntries {
		entryItems, err := svc.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
		if err != nil {
			svc.logger.Warn("Box gacha entry has no items",
				zap.Uint32("entryID", rewardEntries[i].ID), zap.Error(err))
			continue
		}
		if err := svc.gachaRepo.InsertBoxEntry(gachaID, rewardEntries[i].ID, charID); err != nil {
			svc.logger.Error("Failed to insert gacha box entry", zap.Error(err))
		}
		for _, item := range entryItems {
			rewards = append(rewards, GachaReward{
				ItemType: item.ItemType,
				ItemID:   item.ItemID,
				Quantity: item.Quantity,
				Rarity:   0,
			})
		}
	}
	svc.saveGachaItems(charID, rewardsToItems(rewards))
	return &GachaPlayResult{Rewards: rewards}, nil
}

// GetStepupStatus returns the current stepup step for a character, resetting
// stale progress based on the noon boundary. The now parameter enables
// deterministic testing.
func (svc *GachaService) GetStepupStatus(gachaID, charID uint32, now time.Time) (*StepupStatus, error) {
	// Compute the most recent noon boundary
	y, m, d := now.Date()
	midday := time.Date(y, m, d, 12, 0, 0, 0, now.Location())
	if now.Before(midday) {
		midday = midday.Add(-24 * time.Hour)
	}

	step, createdAt, err := svc.gachaRepo.GetStepupWithTime(gachaID, charID)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		svc.logger.Error("Failed to get gacha stepup state", zap.Error(err))
	}

	if err == nil && createdAt.Before(midday) {
		if err := svc.gachaRepo.DeleteStepup(gachaID, charID); err != nil {
			svc.logger.Error("Failed to reset stale gacha stepup", zap.Error(err))
		}
		step = 0
	} else if err == nil {
		hasEntry, _ := svc.gachaRepo.HasEntryType(gachaID, step)
		if !hasEntry {
			if err := svc.gachaRepo.DeleteStepup(gachaID, charID); err != nil {
				svc.logger.Error("Failed to reset gacha stepup state", zap.Error(err))
			}
			step = 0
		}
	}

	return &StepupStatus{Step: step}, nil
}

// GetBoxInfo returns the entry IDs already drawn for a box gacha.
func (svc *GachaService) GetBoxInfo(gachaID, charID uint32) ([]uint32, error) {
	return svc.gachaRepo.GetBoxEntryIDs(gachaID, charID)
}

// ResetBox clears all drawn entries for a box gacha.
func (svc *GachaService) ResetBox(gachaID, charID uint32) error {
	return svc.gachaRepo.DeleteBoxEntries(gachaID, charID)
}

// getRandomEntries selects random gacha entries. In non-box mode, entries are
// chosen with weighted probability (with replacement). In box mode, entries are
// chosen uniformly without replacement.
func getRandomEntries(entries []GachaEntry, rolls int, isBox bool) ([]GachaEntry, error) {
	if len(entries) == 0 {
		return nil, errors.New("no gacha entries available")
	}
	// Box mode draws without replacement, so clamp rolls to available entries.
	if isBox && rolls > len(entries) {
		rolls = len(entries)
	}
	var chosen []GachaEntry
	var totalWeight float64
	for i := range entries {
		totalWeight += entries[i].Weight
	}
	if !isBox && totalWeight <= 0 {
		return nil, errors.New("gacha entries have zero total weight")
	}
	for rolls != len(chosen) {
		if !isBox {
			result := rand.Float64() * totalWeight
			for _, entry := range entries {
				result -= entry.Weight
				if result < 0 {
					chosen = append(chosen, entry)
					break
				}
			}
		} else {
			result := rand.Intn(len(entries))
			chosen = append(chosen, entries[result])
			entries[result] = entries[len(entries)-1]
			entries = entries[:len(entries)-1]
		}
	}
	return chosen, nil
}
