package traits

import (
	"fmt"
	"log"
	"sync"
)

// NewMasterTraitList creates a new master trait list.
func NewMasterTraitList() *MasterTraitList {
	return &MasterTraitList{
		traitList: make([]TraitData, 0),
	}
}

// AddTrait adds a trait to the master list.
func (mtl *MasterTraitList) AddTrait(data *TraitData) error {
	if data == nil {
		return ErrInvalidPlayer
	}

	if err := data.Validate(); err != nil {
		return err
	}

	mtl.mutex.Lock()
	defer mtl.mutex.Unlock()

	// Make a copy to avoid external modifications
	traitCopy := *data
	mtl.traitList = append(mtl.traitList, traitCopy)

	return nil
}

// Size returns the total number of traits in the master list.
func (mtl *MasterTraitList) Size() int {
	mtl.mutex.RLock()
	defer mtl.mutex.RUnlock()
	return len(mtl.traitList)
}

// GetTrait retrieves a trait by spell ID.
func (mtl *MasterTraitList) GetTrait(spellID uint32) *TraitData {
	mtl.mutex.RLock()
	defer mtl.mutex.RUnlock()

	for i := range mtl.traitList {
		if mtl.traitList[i].SpellID == spellID {
			// Return a copy to prevent external modifications
			traitCopy := mtl.traitList[i]
			return &traitCopy
		}
	}

	return nil
}

// GetTraitByItemID retrieves a trait by item ID.
func (mtl *MasterTraitList) GetTraitByItemID(itemID uint32) *TraitData {
	mtl.mutex.RLock()
	defer mtl.mutex.RUnlock()

	for i := range mtl.traitList {
		if mtl.traitList[i].ItemID == itemID {
			// Return a copy to prevent external modifications
			traitCopy := mtl.traitList[i]
			return &traitCopy
		}
	}

	return nil
}

// DestroyTraits clears all traits from the master list.
func (mtl *MasterTraitList) DestroyTraits() {
	mtl.mutex.Lock()
	defer mtl.mutex.Unlock()

	mtl.traitList = mtl.traitList[:0]
}

// GenerateTraitLists organizes traits into categorized lists for a specific player.
func (mtl *MasterTraitList) GenerateTraitLists(playerState *PlayerTraitState, maxLevel int16, traitGroup int8) bool {
	if playerState == nil {
		log.Printf("GenerateTraitLists called with nil player state")
		return false
	}

	if mtl.Size() == 0 {
		return false
	}

	mtl.mutex.RLock()
	defer mtl.mutex.RUnlock()

	// Clear existing lists
	playerState.TraitLists.Clear()

	for i := range mtl.traitList {
		trait := &mtl.traitList[i]

		// Skip if level requirement not met
		if maxLevel > 0 && trait.Level > int8(maxLevel) {
			continue
		}

		// Skip if specific group requested and this isn't it
		if traitGroup != UnassignedGroupID && traitGroup != trait.Group {
			continue
		}

		// Categorize the trait
		mtl.categorizeTraitForPlayer(trait, playerState)
	}

	return true
}

// categorizeTraitForPlayer adds a trait to the appropriate category for a player.
func (mtl *MasterTraitList) categorizeTraitForPlayer(trait *TraitData, playerState *PlayerTraitState) {
	// Character Traits (universal traits)
	if trait.IsUniversalTrait() {
		mtl.addToSortedTraitList(trait, playerState.TraitLists.SortedTraitList)
		log.Printf("Added Character Trait: %d Tier %d", trait.SpellID, trait.Tier)
		return
	}

	// Class Training
	if trait.ClassReq == playerState.Class && trait.IsTraining {
		mtl.addToLevelMap(trait, playerState.TraitLists.ClassTraining)
		return
	}

	// Racial Abilities (non-innate)
	if trait.RaceReq == playerState.Race && !trait.IsInnate && !trait.IsTraining {
		mtl.addToGroupMap(trait, playerState.TraitLists.RaceTraits)
		return
	}

	// Innate Racial Abilities
	if trait.RaceReq == playerState.Race && trait.IsInnate {
		mtl.addToGroupMap(trait, playerState.TraitLists.InnateRaceTraits)
		return
	}

	// Focus Effects
	if (trait.ClassReq == playerState.Class || trait.ClassReq == UniversalClassReq) && trait.IsFocusEffect {
		mtl.addToGroupMap(trait, playerState.TraitLists.FocusEffects)
		return
	}
}

// addToSortedTraitList adds a trait to the sorted trait list (group -> level -> traits).
func (mtl *MasterTraitList) addToSortedTraitList(trait *TraitData, sortedList map[int8]map[int8][]*TraitData) {
	// Ensure group map exists
	if sortedList[trait.Group] == nil {
		sortedList[trait.Group] = make(map[int8][]*TraitData)
	}

	// Add to the appropriate level within the group
	sortedList[trait.Group][trait.Level] = append(sortedList[trait.Group][trait.Level], trait)
}

// addToLevelMap adds a trait to a level-indexed map.
func (mtl *MasterTraitList) addToLevelMap(trait *TraitData, levelMap map[int8][]*TraitData) {
	levelMap[trait.Level] = append(levelMap[trait.Level], trait)
}

// addToGroupMap adds a trait to a group-indexed map.
func (mtl *MasterTraitList) addToGroupMap(trait *TraitData, groupMap map[int8][]*TraitData) {
	groupMap[trait.Group] = append(groupMap[trait.Group], trait)
}

// IsPlayerAllowedTrait checks if a player is allowed to select a specific trait.
func (mtl *MasterTraitList) IsPlayerAllowedTrait(playerState *PlayerTraitState, trait *TraitData, config *TraitSystemConfig) bool {
	if playerState == nil || trait == nil || config == nil {
		return false
	}

	// Refresh trait lists if needed
	if playerState.NeedTraitUpdate {
		if !mtl.GenerateTraitLists(playerState, 0, UnassignedGroupID) {
			return false
		}
		playerState.NeedTraitUpdate = false
	}

	// Check trait type and calculate availability
	if trait.IsFocusEffect {
		return mtl.checkFocusEffectAllowed(playerState, config)
	}

	if trait.IsTraining {
		return mtl.checkTrainingAllowed(playerState, config)
	}

	if trait.RaceReq == playerState.Race {
		return mtl.checkRacialTraitAllowed(playerState, config)
	}

	// Character trait
	return mtl.checkCharacterTraitAllowed(playerState, config)
}

// checkFocusEffectAllowed checks if player can select focus effects.
func (mtl *MasterTraitList) checkFocusEffectAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool {
	var numAvailableSelections int16

	if config.FocusSelectLevel > 0 {
		numAvailableSelections = playerState.Level / int16(config.FocusSelectLevel)
	}

	totalUsed := mtl.getSpellCount(playerState, playerState.TraitLists.FocusEffects, false)

	// Check classic table if enabled
	if config.UseClassicLevelTable {
		classicAvail := mtl.getClassicAvailability(PersonalTraitLevelLimits, totalUsed, playerState.Level)
		if classicAvail >= 0 {
			numAvailableSelections = classicAvail
		} else {
			numAvailableSelections = 0
		}
	}

	log.Printf("Player %d FocusEffects used %d, available %d",
		playerState.PlayerID, totalUsed, numAvailableSelections)

	return totalUsed < numAvailableSelections
}

// checkTrainingAllowed checks if player can select training abilities.
func (mtl *MasterTraitList) checkTrainingAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool {
	var numAvailableSelections int16

	if config.TrainingSelectLevel > 0 {
		numAvailableSelections = playerState.Level / int16(config.TrainingSelectLevel)
	}

	totalUsed := mtl.getSpellCount(playerState, playerState.TraitLists.ClassTraining, false)

	// Check classic table if enabled
	if config.UseClassicLevelTable {
		classicAvail := mtl.getClassicAvailability(TrainingTraitLevelLimits, totalUsed, playerState.Level)
		if classicAvail >= 0 {
			numAvailableSelections = classicAvail
		} else {
			numAvailableSelections = 0
		}
	}

	log.Printf("Player %d ClassTraining used %d, available %d",
		playerState.PlayerID, totalUsed, numAvailableSelections)

	return totalUsed < numAvailableSelections
}

// checkRacialTraitAllowed checks if player can select racial traits.
func (mtl *MasterTraitList) checkRacialTraitAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool {
	var numAvailableSelections int16

	if config.RaceSelectLevel > 0 {
		numAvailableSelections = playerState.Level / int16(config.RaceSelectLevel)
	}

	totalUsed := mtl.getSpellCount(playerState, playerState.TraitLists.RaceTraits, false) +
		mtl.getSpellCount(playerState, playerState.TraitLists.InnateRaceTraits, false)

	// Check classic table if enabled
	if config.UseClassicLevelTable {
		classicAvail := mtl.getClassicAvailability(RacialTraitLevelLimits, totalUsed, playerState.Level)
		if classicAvail >= 0 {
			numAvailableSelections = classicAvail
		} else {
			numAvailableSelections = 0
		}
	}

	log.Printf("Player %d RaceTraits used %d, available %d",
		playerState.PlayerID, totalUsed, numAvailableSelections)

	return totalUsed < numAvailableSelections
}

// checkCharacterTraitAllowed checks if player can select character traits.
func (mtl *MasterTraitList) checkCharacterTraitAllowed(playerState *PlayerTraitState, config *TraitSystemConfig) bool {
	var numAvailableSelections int16

	if config.CharacterSelectLevel > 0 {
		numAvailableSelections = playerState.Level / int16(config.CharacterSelectLevel)
	}

	// Count character traits from sorted list
	totalUsed := int16(0)
	for _, levelMap := range playerState.TraitLists.SortedTraitList {
		totalUsed += mtl.getSpellCount(playerState, levelMap, true)
	}

	// Check classic table if enabled
	if config.UseClassicLevelTable {
		classicAvail := mtl.getClassicAvailability(CharacterTraitLevelLimits, totalUsed, playerState.Level)
		if classicAvail >= 0 {
			numAvailableSelections = classicAvail
		} else {
			numAvailableSelections = 0
		}
	}

	log.Printf("Player %d CharacterTraits used %d, available %d",
		playerState.PlayerID, totalUsed, numAvailableSelections)

	return totalUsed < numAvailableSelections
}

// getClassicAvailability calculates availability using classic level tables.
func (mtl *MasterTraitList) getClassicAvailability(levelLimits []int16, totalUsed int16, playerLevel int16) int16 {
	nextIndex := int(totalUsed + 1)
	if nextIndex < len(levelLimits) {
		classicLevelReq := levelLimits[nextIndex]
		if playerLevel >= classicLevelReq {
			return totalUsed + 1
		}
	}
	return -1
}

// getSpellCount counts how many spells from a trait map the player has selected.
func (mtl *MasterTraitList) getSpellCount(playerState *PlayerTraitState, traitMap interface{}, onlyCharTraits bool) int16 {
	count := int16(0)

	switch tm := traitMap.(type) {
	case map[int8][]*TraitData:
		// Level-indexed map (like ClassTraining)
		for _, traits := range tm {
			for _, trait := range traits {
				if playerState.HasTrait(trait.SpellID) {
					if !onlyCharTraits || (onlyCharTraits && trait.IsUniversalTrait()) {
						count++
					}
				}
			}
		}
	case map[int8]map[int8][]*TraitData:
		// Group-level indexed map (like SortedTraitList)
		for _, levelMap := range tm {
			for _, traits := range levelMap {
				for _, trait := range traits {
					if playerState.HasTrait(trait.SpellID) {
						if !onlyCharTraits || (onlyCharTraits && trait.IsUniversalTrait()) {
							count++
						}
					}
				}
			}
		}
	}

	return count
}

// IdentifyNextTrait identifies traits available for selection based on progression rules.
func (mtl *MasterTraitList) IdentifyNextTrait(playerState *PlayerTraitState, traitMap map[int8][]*TraitData, context *TraitSelectionContext, omitFoundMatches bool) bool {
	foundMatch := false

	for _, traits := range traitMap {
		for _, trait := range traits {
			// Handle tiered selection logic
			if context.TieredSelection {
				if context.FoundSpellMatch && trait.Group == context.GroupToApply {
					continue // Skip this group
				} else if trait.Group != context.GroupToApply {
					if context.GroupToApply != UnassignedGroupID && !context.FoundSpellMatch {
						log.Printf("Found match to group id %d", context.GroupToApply)
						foundMatch = true
						break
					} else {
						log.Printf("Try match to group... spell id %d, group id %d", trait.SpellID, trait.Group)
						context.FoundSpellMatch = false
						context.GroupToApply = trait.Group
						if !omitFoundMatches {
							context.TieredTraits = context.TieredTraits[:0]
						}
					}
				}
			}

			// Check if spell was previously matched
			if prevGroup, exists := context.PreviousMatchedSpells[trait.SpellID]; exists && trait.Group > prevGroup {
				continue
			}

			// Check if player is allowed this trait
			config := &TraitSystemConfig{
				TieringSelection:     context.TieredSelection,
				UseClassicLevelTable: true, // TODO: Get from rules
				FocusSelectLevel:     DefaultFocusSelectLevel,
				TrainingSelectLevel:  DefaultTrainingSelectLevel,
				RaceSelectLevel:      DefaultRaceSelectLevel,
				CharacterSelectLevel: DefaultCharacterSelectLevel,
			}

			if !mtl.IsPlayerAllowedTrait(playerState, trait, config) {
				log.Printf("Player not allowed trait: spell id %d, group id %d", trait.SpellID, trait.Group)
				context.FoundSpellMatch = true
			} else if playerState.HasTrait(trait.SpellID) {
				log.Printf("Found existing spell match: spell id %d, group id %d", trait.SpellID, trait.Group)
				if !omitFoundMatches {
					context.FoundSpellMatch = true
				}
				context.PreviousMatchedSpells[trait.SpellID] = trait.Group
			} else {
				context.TieredTraits = append(context.TieredTraits, trait)
				context.CollectTraits = append(context.CollectTraits, trait)
			}
		}

		if foundMatch {
			break
		}
	}

	// Final match check
	if !foundMatch && context.GroupToApply != UnassignedGroupID && !context.FoundSpellMatch {
		foundMatch = true
	} else if !context.TieredSelection && len(context.CollectTraits) > 0 {
		foundMatch = true
	}

	return foundMatch
}

// ChooseNextTrait processes trait selection for a player and returns available choices.
func (mtl *MasterTraitList) ChooseNextTrait(playerState *PlayerTraitState, config *TraitSystemConfig) ([]*TraitData, error) {
	if playerState == nil {
		return nil, ErrInvalidPlayer
	}

	// Generate trait lists
	if !mtl.GenerateTraitLists(playerState, playerState.Level, UnassignedGroupID) {
		return nil, fmt.Errorf("failed to generate trait lists")
	}

	context := NewTraitSelectionContext(config.TieringSelection)
	match := false

	// Check different trait types in priority order
	if !match || !config.TieringSelection {
		match = mtl.IdentifyNextTrait(playerState, playerState.TraitLists.ClassTraining, context, false)
	}

	if !match || !config.TieringSelection {
		match = mtl.IdentifyNextTrait(playerState, playerState.TraitLists.RaceTraits, context, true)

		overrideMatch := mtl.IdentifyNextTrait(playerState, playerState.TraitLists.InnateRaceTraits, context, true)
		if !match && overrideMatch {
			match = true
		}
	}

	if !match || !config.TieringSelection {
		match = mtl.IdentifyNextTrait(playerState, playerState.TraitLists.FocusEffects, context, false)
	}

	// Return appropriate trait list
	if !config.TieringSelection && len(context.CollectTraits) > 0 {
		return context.CollectTraits, nil
	} else if match {
		return context.TieredTraits, nil
	}

	return nil, nil
}

// GetStats returns statistics about the master trait list.
func (mtl *MasterTraitList) GetStats() TraitManagerStats {
	mtl.mutex.RLock()
	defer mtl.mutex.RUnlock()

	stats := TraitManagerStats{
		TotalTraits:   int32(len(mtl.traitList)),
		TraitsByType:  make(map[string]int32),
		TraitsByGroup: make(map[int8]int32),
		TraitsByLevel: make(map[int8]int32),
	}

	for i := range mtl.traitList {
		trait := &mtl.traitList[i]

		// Count by type
		traitType := trait.GetTraitType()
		stats.TraitsByType[traitType]++

		// Count by group
		stats.TraitsByGroup[trait.Group]++

		// Count by level
		stats.TraitsByLevel[trait.Level]++
	}

	return stats
}

// ValidateTraitSelection validates a trait selection request.
func (mtl *MasterTraitList) ValidateTraitSelection(playerState *PlayerTraitState, request *TraitSelectionRequest, config *TraitSystemConfig) *TraitValidationResult {
	result := &TraitValidationResult{
		Allowed: false,
		Reason:  "Unknown validation error",
	}

	if playerState == nil || request == nil {
		result.Reason = "Invalid player or request"
		return result
	}

	// Validate each requested trait
	for _, spellID := range request.TraitSpells {
		trait := mtl.GetTrait(spellID)
		if trait == nil {
			result.Reason = fmt.Sprintf("Trait not found: %d", spellID)
			return result
		}

		if !mtl.IsPlayerAllowedTrait(playerState, trait, config) {
			result.Reason = fmt.Sprintf("Trait not allowed: %d", spellID)
			return result
		}
	}

	result.Allowed = true
	result.Reason = "Valid selection"
	return result
}

// TraitManager manages trait operations and caches player states.
type TraitManager struct {
	masterList   *MasterTraitList
	playerStates map[uint32]*PlayerTraitState
	config       *TraitSystemConfig
	mutex        sync.RWMutex
}

// NewTraitManager creates a new trait manager.
func NewTraitManager(masterList *MasterTraitList, config *TraitSystemConfig) *TraitManager {
	return &TraitManager{
		masterList:   masterList,
		playerStates: make(map[uint32]*PlayerTraitState),
		config:       config,
	}
}

// GetPlayerState gets or creates a player trait state.
func (tm *TraitManager) GetPlayerState(playerID uint32, level int16, classID, raceID int8) *PlayerTraitState {
	tm.mutex.Lock()
	defer tm.mutex.Unlock()

	state, exists := tm.playerStates[playerID]
	if !exists {
		state = NewPlayerTraitState(playerID, level, classID, raceID)
		tm.playerStates[playerID] = state
	} else {
		// Update level if changed
		state.UpdateLevel(level)
	}

	return state
}

// SelectTraits processes a trait selection request.
func (tm *TraitManager) SelectTraits(request *TraitSelectionRequest, playerLevel int16, classID, raceID int8) error {
	playerState := tm.GetPlayerState(request.PlayerID, playerLevel, classID, raceID)

	// Validate the selection
	result := tm.masterList.ValidateTraitSelection(playerState, request, tm.config)
	if !result.Allowed {
		return fmt.Errorf("trait selection not allowed: %s", result.Reason)
	}

	// Apply the selection
	for _, spellID := range request.TraitSpells {
		playerState.SelectTrait(spellID)
	}

	log.Printf("Player %d selected %d traits", request.PlayerID, len(request.TraitSpells))
	return nil
}

// GetAvailableTraits gets traits available for selection by a player.
func (tm *TraitManager) GetAvailableTraits(playerID uint32, level int16, classID, raceID int8) ([]*TraitData, error) {
	playerState := tm.GetPlayerState(playerID, level, classID, raceID)
	return tm.masterList.ChooseNextTrait(playerState, tm.config)
}

// ClearPlayerState removes a player's cached trait state.
func (tm *TraitManager) ClearPlayerState(playerID uint32) {
	tm.mutex.Lock()
	defer tm.mutex.Unlock()

	delete(tm.playerStates, playerID)
}

// GetManagerStats returns statistics about the trait manager.
func (tm *TraitManager) GetManagerStats() TraitManagerStats {
	tm.mutex.RLock()
	playersWithTraits := int32(len(tm.playerStates))
	tm.mutex.RUnlock()

	stats := tm.masterList.GetStats()
	stats.PlayersWithTraits = playersWithTraits

	return stats
}
