package domain

import (
	"cmp"
	"fmt"
	"math"
	"net/url"
	"slices"
	"time"
)

const (
	playerNameMinLength = 1
	playerNameMaxLength = 150
)

type Player struct {
	id             int
	serverKey      string
	name           string
	numVillages    int
	points         int
	rank           int
	tribeID        int
	od             OpponentsDefeated
	profileURL     *url.URL
	bestRank       int
	bestRankAt     time.Time
	mostPoints     int
	mostPointsAt   time.Time
	mostVillages   int
	mostVillagesAt time.Time
	lastActivityAt time.Time
	createdAt      time.Time
	deletedAt      time.Time
}

const playerModelName = "Player"

// UnmarshalPlayerFromDatabase unmarshals Player from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalPlayerFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalPlayerFromDatabase(
	id int,
	serverKey string,
	name string,
	numVillages int,
	points int,
	rank int,
	tribeID int,
	od OpponentsDefeated,
	rawProfileURL string,
	bestRank int,
	bestRankAt time.Time,
	mostPoints int,
	mostPointsAt time.Time,
	mostVillages int,
	mostVillagesAt time.Time,
	lastActivityAt time.Time,
	createdAt time.Time,
	deletedAt time.Time,
) (Player, error) {
	if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
		return Player{}, ValidationError{
			Model: playerModelName,
			Field: "id",
			Err:   err,
		}
	}

	if err := validateServerKey(serverKey); err != nil {
		return Player{}, ValidationError{
			Model: playerModelName,
			Field: "serverKey",
			Err:   err,
		}
	}

	profileURL, err := parseURL(rawProfileURL)
	if err != nil {
		return Player{}, ValidationError{
			Model: playerModelName,
			Field: "profileURL",
			Err:   err,
		}
	}

	return Player{
		id:             id,
		serverKey:      serverKey,
		name:           name,
		numVillages:    numVillages,
		points:         points,
		rank:           rank,
		tribeID:        tribeID,
		od:             od,
		profileURL:     profileURL,
		bestRank:       bestRank,
		bestRankAt:     bestRankAt,
		mostPoints:     mostPoints,
		mostPointsAt:   mostPointsAt,
		mostVillages:   mostVillages,
		mostVillagesAt: mostVillagesAt,
		lastActivityAt: lastActivityAt,
		createdAt:      createdAt,
		deletedAt:      deletedAt,
	}, nil
}

func (p Player) ID() int {
	return p.id
}

func (p Player) ServerKey() string {
	return p.serverKey
}

func (p Player) Name() string {
	return p.name
}

func (p Player) NumVillages() int {
	return p.numVillages
}

func (p Player) Points() int {
	return p.points
}

func (p Player) Rank() int {
	return p.rank
}

func (p Player) TribeID() int {
	return p.tribeID
}

func (p Player) OD() OpponentsDefeated {
	return p.od
}

func (p Player) ProfileURL() *url.URL {
	return p.profileURL
}

func (p Player) BestRank() int {
	return p.bestRank
}

func (p Player) BestRankAt() time.Time {
	return p.bestRankAt
}

func (p Player) MostPoints() int {
	return p.mostPoints
}

func (p Player) MostPointsAt() time.Time {
	return p.mostPointsAt
}

func (p Player) MostVillages() int {
	return p.mostVillages
}

func (p Player) MostVillagesAt() time.Time {
	return p.mostVillagesAt
}

func (p Player) LastActivityAt() time.Time {
	return p.lastActivityAt
}

func (p Player) CreatedAt() time.Time {
	return p.createdAt
}

func (p Player) DeletedAt() time.Time {
	return p.deletedAt
}

func (p Player) Base() BasePlayer {
	return BasePlayer{
		id:          p.id,
		name:        p.name,
		numVillages: p.numVillages,
		points:      p.points,
		rank:        p.rank,
		tribeID:     p.tribeID,
		od:          p.od,
		profileURL:  p.profileURL,
	}
}

func (p Player) IsDeleted() bool {
	return !p.deletedAt.IsZero()
}

type Players []Player

// Delete finds all players with the given serverKey that are not in the given slice with active players
// and returns their ids + tribe changes that must be created.
// Both slices must be sorted in ascending order by ID.
// + if ps contains players from different servers. they must be sorted in ascending order by server key.
func (ps Players) Delete(serverKey string, active BasePlayers) ([]int, []CreateTribeChangeParams, error) {
	// players are deleted now and then, there is no point in prereallocating these slices
	//nolint:prealloc
	var toDelete []int
	var params []CreateTribeChangeParams

	for _, p := range ps {
		if p.IsDeleted() || p.ServerKey() != serverKey {
			continue
		}

		_, found := slices.BinarySearchFunc(active, p, func(a BasePlayer, b Player) int {
			return cmp.Compare(a.ID(), b.ID())
		})
		if found {
			continue
		}

		toDelete = append(toDelete, p.ID())

		if p.TribeID() > 0 {
			p, err := NewCreateTribeChangeParams(serverKey, p.ID(), p.TribeID(), 0)
			if err != nil {
				return nil, nil, err
			}

			params = append(params, p)
		}
	}

	return toDelete, params, nil
}

type CreatePlayerParams struct {
	base           BasePlayer
	serverKey      string
	bestRank       int
	bestRankAt     time.Time
	mostPoints     int
	mostPointsAt   time.Time
	mostVillages   int
	mostVillagesAt time.Time
	lastActivityAt time.Time
}

const createPlayerParamsModelName = "CreatePlayerParams"

// NewCreatePlayerParams constructs a slice of CreatePlayerParams based on the given parameters.
// Both slices must be sorted in ascending order by ID
// + if storedPlayers contains players from different servers. they must be sorted in ascending order by server key.
//
//nolint:gocyclo
func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers Players) ([]CreatePlayerParams, error) {
	if err := validateServerKey(serverKey); err != nil {
		return nil, ValidationError{
			Model: createPlayerParamsModelName,
			Field: "serverKey",
			Err:   err,
		}
	}

	params := make([]CreatePlayerParams, 0, len(players))

	for i, player := range players {
		if player.IsZero() {
			return nil, fmt.Errorf("players[%d] is an empty struct", i)
		}

		var old Player
		idx, found := slices.BinarySearchFunc(storedPlayers, player, func(a Player, b BasePlayer) int {
			if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 {
				return res
			}
			return cmp.Compare(a.ID(), b.ID())
		})
		if found {
			old = storedPlayers[idx]
		}

		now := time.Now()

		p := CreatePlayerParams{
			base:           player,
			serverKey:      serverKey,
			bestRank:       player.Rank(),
			bestRankAt:     now,
			mostPoints:     player.Points(),
			mostPointsAt:   now,
			mostVillages:   player.NumVillages(),
			mostVillagesAt: now,
			lastActivityAt: now,
		}

		if old.ID() > 0 {
			if old.MostPoints() >= p.MostPoints() {
				p.mostPoints = old.MostPoints()
				p.mostPointsAt = old.MostPointsAt()
			}

			if old.MostVillages() >= p.MostVillages() {
				p.mostVillages = old.MostVillages()
				p.mostVillagesAt = old.MostVillagesAt()
			}

			if old.BestRank() <= p.BestRank() {
				p.bestRank = old.BestRank()
				p.bestRankAt = old.BestRankAt()
			}

			//nolint:lll
			if player.Points() <= old.Points() && player.NumVillages() <= old.NumVillages() && player.OD().ScoreAtt() <= old.OD().ScoreAtt() {
				p.lastActivityAt = old.LastActivityAt()
			}
		}

		params = append(params, p)
	}

	return params, nil
}

func (params CreatePlayerParams) Base() BasePlayer {
	return params.base
}

func (params CreatePlayerParams) ServerKey() string {
	return params.serverKey
}

func (params CreatePlayerParams) BestRank() int {
	return params.bestRank
}

func (params CreatePlayerParams) BestRankAt() time.Time {
	return params.bestRankAt
}

func (params CreatePlayerParams) MostPoints() int {
	return params.mostPoints
}

func (params CreatePlayerParams) MostPointsAt() time.Time {
	return params.mostPointsAt
}

func (params CreatePlayerParams) MostVillages() int {
	return params.mostVillages
}

func (params CreatePlayerParams) MostVillagesAt() time.Time {
	return params.mostVillagesAt
}

func (params CreatePlayerParams) LastActivityAt() time.Time {
	return params.lastActivityAt
}

type PlayerSort uint8

const (
	PlayerSortIDASC PlayerSort = iota + 1
	PlayerSortIDDESC
	PlayerSortServerKeyASC
	PlayerSortServerKeyDESC
)

const PlayerListMaxLimit = 200

type ListPlayersParams struct {
	ids        []int
	idGT       NullInt
	serverKeys []string
	deleted    NullBool
	sort       []PlayerSort
	limit      int
	offset     int
}

const listPlayersParamsModelName = "ListPlayersParams"

func NewListPlayersParams() ListPlayersParams {
	return ListPlayersParams{
		sort: []PlayerSort{
			PlayerSortServerKeyASC,
			PlayerSortIDASC,
		},
		limit: PlayerListMaxLimit,
	}
}

func (params *ListPlayersParams) IDs() []int {
	return params.ids
}

func (params *ListPlayersParams) SetIDs(ids []int) error {
	for i, id := range ids {
		if err := validateIntInRange(id, 0, math.MaxInt); err != nil {
			return SliceElementValidationError{
				Model: listPlayersParamsModelName,
				Field: "ids",
				Index: i,
				Err:   err,
			}
		}
	}

	params.ids = ids

	return nil
}

func (params *ListPlayersParams) IDGT() NullInt {
	return params.idGT
}

func (params *ListPlayersParams) SetIDGT(idGT NullInt) error {
	if idGT.Valid {
		if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
			return ValidationError{
				Model: listPlayersParamsModelName,
				Field: "idGT",
				Err:   err,
			}
		}
	}

	params.idGT = idGT

	return nil
}

func (params *ListPlayersParams) ServerKeys() []string {
	return params.serverKeys
}

func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
	params.serverKeys = serverKeys
	return nil
}

func (params *ListPlayersParams) Deleted() NullBool {
	return params.deleted
}

func (params *ListPlayersParams) SetDeleted(deleted NullBool) error {
	params.deleted = deleted
	return nil
}

func (params *ListPlayersParams) Sort() []PlayerSort {
	return params.sort
}

const (
	playerSortMinLength = 1
	playerSortMaxLength = 2
)

func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
	if err := validateSliceLen(sort, playerSortMinLength, playerSortMaxLength); err != nil {
		return ValidationError{
			Model: listPlayersParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

	params.sort = sort

	return nil
}

func (params *ListPlayersParams) Limit() int {
	return params.limit
}

func (params *ListPlayersParams) SetLimit(limit int) error {
	if err := validateIntInRange(limit, 1, PlayerListMaxLimit); err != nil {
		return ValidationError{
			Model: listPlayersParamsModelName,
			Field: "limit",
			Err:   err,
		}
	}

	params.limit = limit

	return nil
}

func (params *ListPlayersParams) Offset() int {
	return params.offset
}

func (params *ListPlayersParams) SetOffset(offset int) error {
	if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
		return ValidationError{
			Model: listPlayersParamsModelName,
			Field: "offset",
			Err:   err,
		}
	}

	params.offset = offset

	return nil
}
