package domain

import (
	"cmp"
	"fmt"
	"math"
	"net/url"
	"slices"
	"strings"
	"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()
}

func (p Player) IsZero() bool {
	return p == Player{}
}

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 {
			return cmp.Or(
				cmp.Compare(a.ServerKey(), serverKey),
				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
	PlayerSortODScoreAttASC
	PlayerSortODScoreAttDESC
	PlayerSortODScoreDefASC
	PlayerSortODScoreDefDESC
	PlayerSortODScoreTotalASC
	PlayerSortODScoreTotalDESC
	PlayerSortPointsASC
	PlayerSortPointsDESC
	PlayerSortDeletedAtASC
	PlayerSortDeletedAtDESC
)

//nolint:gocyclo
func newPlayerSortFromString(s string) (PlayerSort, error) {
	switch strings.ToLower(s) {
	case "odscoreatt:asc":
		return PlayerSortODScoreAttASC, nil
	case "odscoreatt:desc":
		return PlayerSortODScoreAttDESC, nil
	case "odscoredef:asc":
		return PlayerSortODScoreDefASC, nil
	case "odscoredef:desc":
		return PlayerSortODScoreDefDESC, nil
	case "odscoretotal:asc":
		return PlayerSortODScoreTotalASC, nil
	case "odscoretotal:desc":
		return PlayerSortODScoreTotalDESC, nil
	case "points:asc":
		return PlayerSortPointsASC, nil
	case "points:desc":
		return PlayerSortPointsDESC, nil
	case "deletedat:asc":
		return PlayerSortDeletedAtASC, nil
	case "deletedat:desc":
		return PlayerSortDeletedAtDESC, nil
	default:
		return 0, UnsupportedSortStringError{
			Sort: s,
		}
	}
}

type PlayerCursor struct {
	id           int
	serverKey    string
	odScoreAtt   int
	odScoreDef   int
	odScoreTotal int
	points       int
	deletedAt    time.Time
}

const playerCursorModelName = "PlayerCursor"

func NewPlayerCursor(
	id int,
	serverKey string,
	odScoreAtt int,
	odScoreDef int,
	odScoreTotal int,
	points int,
	deletedAt time.Time,
) (PlayerCursor, error) {
	if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
		return PlayerCursor{}, ValidationError{
			Model: playerCursorModelName,
			Field: "id",
			Err:   err,
		}
	}

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

	if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil {
		return PlayerCursor{}, ValidationError{
			Model: playerCursorModelName,
			Field: "odScoreAtt",
			Err:   err,
		}
	}

	if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil {
		return PlayerCursor{}, ValidationError{
			Model: playerCursorModelName,
			Field: "odScoreDef",
			Err:   err,
		}
	}

	if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil {
		return PlayerCursor{}, ValidationError{
			Model: playerCursorModelName,
			Field: "odScoreTotal",
			Err:   err,
		}
	}

	if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
		return PlayerCursor{}, ValidationError{
			Model: playerCursorModelName,
			Field: "points",
			Err:   err,
		}
	}

	return PlayerCursor{
		id:           id,
		serverKey:    serverKey,
		odScoreAtt:   odScoreAtt,
		odScoreDef:   odScoreDef,
		odScoreTotal: odScoreTotal,
		points:       points,
		deletedAt:    deletedAt,
	}, nil
}

//nolint:gocyclo
func decodePlayerCursor(encoded string) (PlayerCursor, error) {
	m, err := decodeCursor(encoded)
	if err != nil {
		return PlayerCursor{}, err
	}

	id, err := m.int("id")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	serverKey, err := m.string("serverKey")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	odScoreAtt, err := m.int("odScoreAtt")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	odScoreDef, err := m.int("odScoreDef")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	odScoreTotal, err := m.int("odScoreTotal")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	points, err := m.int("points")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	deletedAt, err := m.time("deletedAt")
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	pc, err := NewPlayerCursor(
		id,
		serverKey,
		odScoreAtt,
		odScoreDef,
		odScoreTotal,
		points,
		deletedAt,
	)
	if err != nil {
		return PlayerCursor{}, ErrInvalidCursor
	}

	return pc, nil
}

func (pc PlayerCursor) ID() int {
	return pc.id
}

func (pc PlayerCursor) ServerKey() string {
	return pc.serverKey
}

func (pc PlayerCursor) ODScoreAtt() int {
	return pc.odScoreAtt
}

func (pc PlayerCursor) ODScoreDef() int {
	return pc.odScoreDef
}

func (pc PlayerCursor) ODScoreTotal() int {
	return pc.odScoreTotal
}

func (pc PlayerCursor) Points() int {
	return pc.points
}

func (pc PlayerCursor) DeletedAt() time.Time {
	return pc.deletedAt
}

func (pc PlayerCursor) IsZero() bool {
	return pc == PlayerCursor{}
}

func (pc PlayerCursor) Encode() string {
	if pc.IsZero() {
		return ""
	}

	return encodeCursor([]keyValuePair{
		{"id", pc.id},
		{"serverKey", pc.serverKey},
		{"odScoreAtt", pc.odScoreAtt},
		{"odScoreDef", pc.odScoreDef},
		{"odScoreTotal", pc.odScoreTotal},
		{"points", pc.points},
		{"deletedAt", pc.deletedAt},
	})
}

type ListPlayersParams struct {
	ids        []int
	serverKeys []string
	deleted    NullBool
	sort       []PlayerSort
	cursor     PlayerCursor
	limit      int
}

const (
	PlayerListMaxLimit         = 200
	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) 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 = 3
)

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) PrependSortString(sort []string) error {
	if err := validateSliceLen(sort, playerSortMinLength, max(playerSortMaxLength-len(params.sort), 0)); err != nil {
		return ValidationError{
			Model: listPlayersParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

	for i := len(sort) - 1; i >= 0; i-- {
		converted, err := newPlayerSortFromString(sort[i])
		if err != nil {
			return SliceElementValidationError{
				Model: listPlayersParamsModelName,
				Field: "sort",
				Index: i,
				Err:   err,
			}
		}
		params.sort = append([]PlayerSort{converted}, params.sort...)
	}

	return nil
}

func (params *ListPlayersParams) Cursor() PlayerCursor {
	return params.cursor
}

func (params *ListPlayersParams) SetCursor(cursor PlayerCursor) error {
	params.cursor = cursor
	return nil
}

func (params *ListPlayersParams) SetEncodedCursor(encoded string) error {
	decoded, err := decodePlayerCursor(encoded)
	if err != nil {
		return ValidationError{
			Model: listPlayersParamsModelName,
			Field: "cursor",
			Err:   err,
		}
	}

	params.cursor = decoded

	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
}

type ListPlayersResult struct {
	players Players
	self    PlayerCursor
	next    PlayerCursor
}

const listPlayersResultModelName = "ListPlayersResult"

func NewListPlayersResult(players Players, next Player) (ListPlayersResult, error) {
	var err error
	res := ListPlayersResult{
		players: players,
	}

	if len(players) > 0 {
		od := players[0].OD()
		res.self, err = NewPlayerCursor(
			players[0].ID(),
			players[0].ServerKey(),
			od.ScoreAtt(),
			od.ScoreDef(),
			od.ScoreTotal(),
			players[0].Points(),
			players[0].DeletedAt(),
		)
		if err != nil {
			return ListPlayersResult{}, ValidationError{
				Model: listPlayersResultModelName,
				Field: "self",
				Err:   err,
			}
		}
	}

	if !next.IsZero() {
		od := next.OD()
		res.next, err = NewPlayerCursor(
			next.ID(),
			next.ServerKey(),
			od.ScoreAtt(),
			od.ScoreDef(),
			od.ScoreTotal(),
			next.Points(),
			next.DeletedAt(),
		)
		if err != nil {
			return ListPlayersResult{}, ValidationError{
				Model: listPlayersResultModelName,
				Field: "next",
				Err:   err,
			}
		}
	}

	return res, nil
}

func (res ListPlayersResult) Players() Players {
	return res.players
}

func (res ListPlayersResult) Self() PlayerCursor {
	return res.self
}

func (res ListPlayersResult) Next() PlayerCursor {
	return res.next
}
