package domain

import (
	"fmt"
	"math"
	"time"
)

const PlayerSnapshotRetentionForClosedServers = 180 * 24 * time.Hour

type PlayerSnapshot struct {
	id          int
	playerID    int
	serverKey   string
	numVillages int
	points      int
	rank        int
	tribeID     int
	od          OpponentsDefeated
	date        time.Time
	createdAt   time.Time
}

const playerSnapshotModelName = "PlayerSnapshot"

// UnmarshalPlayerSnapshotFromDatabase unmarshals PlayerSnapshot from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalPlayerSnapshotFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalPlayerSnapshotFromDatabase(
	id int,
	playerID int,
	serverKey string,
	numVillages int,
	points int,
	rank int,
	tribeID int,
	od OpponentsDefeated,
	date time.Time,
	createdAt time.Time,
) (PlayerSnapshot, error) {
	if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
		return PlayerSnapshot{}, ValidationError{
			Model: playerSnapshotModelName,
			Field: "id",
			Err:   err,
		}
	}

	if err := validateIntInRange(playerID, 1, math.MaxInt); err != nil {
		return PlayerSnapshot{}, ValidationError{
			Model: playerSnapshotModelName,
			Field: "playerID",
			Err:   err,
		}
	}

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

	return PlayerSnapshot{
		id:          id,
		playerID:    playerID,
		serverKey:   serverKey,
		numVillages: numVillages,
		points:      points,
		rank:        rank,
		tribeID:     tribeID,
		od:          od,
		date:        date,
		createdAt:   createdAt,
	}, nil
}

func (ps PlayerSnapshot) ID() int {
	return ps.id
}

func (ps PlayerSnapshot) PlayerID() int {
	return ps.playerID
}

func (ps PlayerSnapshot) ServerKey() string {
	return ps.serverKey
}

func (ps PlayerSnapshot) NumVillages() int {
	return ps.numVillages
}

func (ps PlayerSnapshot) Points() int {
	return ps.points
}

func (ps PlayerSnapshot) Rank() int {
	return ps.rank
}

func (ps PlayerSnapshot) TribeID() int {
	return ps.tribeID
}

func (ps PlayerSnapshot) OD() OpponentsDefeated {
	return ps.od
}

func (ps PlayerSnapshot) Date() time.Time {
	return ps.date
}

func (ps PlayerSnapshot) CreatedAt() time.Time {
	return ps.createdAt
}

func (ps PlayerSnapshot) WithRelations(player PlayerMeta, tribe NullTribeMeta) PlayerSnapshotWithRelations {
	return PlayerSnapshotWithRelations{
		snapshot: ps,
		player:   player,
		tribe:    tribe,
	}
}

func (ps PlayerSnapshot) ToCursor() (PlayerSnapshotCursor, error) {
	return NewPlayerSnapshotCursor(ps.id, ps.serverKey, ps.date)
}

func (ps PlayerSnapshot) IsZero() bool {
	return ps == PlayerSnapshot{}
}

type PlayerSnapshots []PlayerSnapshot

type PlayerSnapshotWithRelations struct {
	snapshot PlayerSnapshot
	player   PlayerMeta
	tribe    NullTribeMeta
}

func (ps PlayerSnapshotWithRelations) PlayerSnapshot() PlayerSnapshot {
	return ps.snapshot
}

func (ps PlayerSnapshotWithRelations) Player() PlayerMeta {
	return ps.player
}

func (ps PlayerSnapshotWithRelations) Tribe() NullTribeMeta {
	return ps.tribe
}

func (ps PlayerSnapshotWithRelations) IsZero() bool {
	return ps.snapshot.IsZero()
}

type PlayerSnapshotsWithRelations []PlayerSnapshotWithRelations

type CreatePlayerSnapshotParams struct {
	playerID    int
	serverKey   string
	numVillages int
	points      int
	rank        int
	tribeID     int
	od          OpponentsDefeated
	date        time.Time
}

func NewCreatePlayerSnapshotParams(players Players, date time.Time) ([]CreatePlayerSnapshotParams, error) {
	params := make([]CreatePlayerSnapshotParams, 0, len(players))

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

		if p.IsDeleted() {
			continue
		}

		params = append(params, CreatePlayerSnapshotParams{
			playerID:    p.ID(),
			serverKey:   p.ServerKey(),
			numVillages: p.NumVillages(),
			points:      p.Points(),
			rank:        p.Rank(),
			tribeID:     p.TribeID(),
			od:          p.OD(),
			date:        date,
		})
	}

	return params, nil
}

func (params CreatePlayerSnapshotParams) PlayerID() int {
	return params.playerID
}

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

func (params CreatePlayerSnapshotParams) NumVillages() int {
	return params.numVillages
}

func (params CreatePlayerSnapshotParams) Points() int {
	return params.points
}

func (params CreatePlayerSnapshotParams) Rank() int {
	return params.rank
}

func (params CreatePlayerSnapshotParams) TribeID() int {
	return params.tribeID
}

func (params CreatePlayerSnapshotParams) OD() OpponentsDefeated {
	return params.od
}

func (params CreatePlayerSnapshotParams) Date() time.Time {
	return params.date
}

type PlayerSnapshotSort uint8

const (
	PlayerSnapshotSortDateASC PlayerSnapshotSort = iota + 1
	PlayerSnapshotSortDateDESC
	PlayerSnapshotSortIDASC
	PlayerSnapshotSortIDDESC
	PlayerSnapshotSortServerKeyASC
	PlayerSnapshotSortServerKeyDESC
)

// IsInConflict returns true if two sorts can't be used together
// (e.g. PlayerSnapshotSortIDASC and PlayerSnapshotSortIDDESC).
func (s PlayerSnapshotSort) IsInConflict(s2 PlayerSnapshotSort) bool {
	return isSortInConflict(s, s2)
}

//nolint:gocyclo
func (s PlayerSnapshotSort) String() string {
	switch s {
	case PlayerSnapshotSortDateASC:
		return "date:ASC"
	case PlayerSnapshotSortDateDESC:
		return "date:DESC"
	case PlayerSnapshotSortIDASC:
		return "id:ASC"
	case PlayerSnapshotSortIDDESC:
		return "id:DESC"
	case PlayerSnapshotSortServerKeyASC:
		return "serverKey:ASC"
	case PlayerSnapshotSortServerKeyDESC:
		return "serverKey:DESC"
	default:
		return "unknown player snapshot sort"
	}
}

type PlayerSnapshotCursor struct {
	id        int
	serverKey string
	date      time.Time
}

const playerSnapshotCursorModelName = "PlayerSnapshotCursor"

func NewPlayerSnapshotCursor(id int, serverKey string, date time.Time) (PlayerSnapshotCursor, error) {
	if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
		return PlayerSnapshotCursor{}, ValidationError{
			Model: playerSnapshotCursorModelName,
			Field: "id",
			Err:   err,
		}
	}

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

	return PlayerSnapshotCursor{
		id:        id,
		serverKey: serverKey,
		date:      date,
	}, nil
}

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

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

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

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

	psc, err := NewPlayerSnapshotCursor(
		id,
		serverKey,
		date,
	)
	if err != nil {
		return PlayerSnapshotCursor{}, ErrInvalidCursor
	}

	return psc, nil
}

func (psc PlayerSnapshotCursor) ID() int {
	return psc.id
}

func (psc PlayerSnapshotCursor) ServerKey() string {
	return psc.serverKey
}

func (psc PlayerSnapshotCursor) Date() time.Time {
	return psc.date
}

func (psc PlayerSnapshotCursor) IsZero() bool {
	return psc == PlayerSnapshotCursor{}
}

func (psc PlayerSnapshotCursor) Encode() string {
	if psc.IsZero() {
		return ""
	}

	return encodeCursor([]keyValuePair{
		{"id", psc.id},
		{"serverKey", psc.serverKey},
		{"date", psc.date},
	})
}

type ListPlayerSnapshotsParams struct {
	serverKeys []string
	playerIDs  []int
	sort       []PlayerSnapshotSort
	cursor     PlayerSnapshotCursor
	limit      int
}

const (
	PlayerSnapshotListMaxLimit         = 500
	listPlayerSnapshotsParamsModelName = "ListPlayerSnapshotsParams"
)

func NewListPlayerSnapshotsParams() ListPlayerSnapshotsParams {
	return ListPlayerSnapshotsParams{
		sort: []PlayerSnapshotSort{
			PlayerSnapshotSortServerKeyASC,
			PlayerSnapshotSortDateASC,
			PlayerSnapshotSortIDASC,
		},
		limit: PlayerSnapshotListMaxLimit,
	}
}

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

func (params *ListPlayerSnapshotsParams) SetServerKeys(serverKeys []string) error {
	for i, sk := range serverKeys {
		if err := validateServerKey(sk); err != nil {
			return SliceElementValidationError{
				Model: listPlayerSnapshotsParamsModelName,
				Field: "serverKeys",
				Index: i,
				Err:   err,
			}
		}
	}

	params.serverKeys = serverKeys

	return nil
}

func (params *ListPlayerSnapshotsParams) PlayerIDs() []int {
	return params.playerIDs
}

func (params *ListPlayerSnapshotsParams) SetPlayerIDs(playerIDs []int) error {
	for i, id := range playerIDs {
		if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
			return SliceElementValidationError{
				Model: listPlayerSnapshotsParamsModelName,
				Field: "playerIDs",
				Index: i,
				Err:   err,
			}
		}
	}

	params.playerIDs = playerIDs

	return nil
}

func (params *ListPlayerSnapshotsParams) Sort() []PlayerSnapshotSort {
	return params.sort
}

const (
	playerSnapshotSortMinLength = 0
	playerSnapshotSortMaxLength = 3
)

func (params *ListPlayerSnapshotsParams) SetSort(sort []PlayerSnapshotSort) error {
	if err := validateSort(sort, playerSnapshotSortMinLength, playerSnapshotSortMaxLength); err != nil {
		return ValidationError{
			Model: listPlayerSnapshotsParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

	params.sort = sort

	return nil
}

func (params *ListPlayerSnapshotsParams) PrependSort(sort []PlayerSnapshotSort) error {
	if len(sort) == 0 {
		return nil
	}

	if err := validateSliceLen(sort, 0, max(playerSnapshotSortMaxLength-len(params.sort), 0)); err != nil {
		return ValidationError{
			Model: listPlayerSnapshotsParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

	return params.SetSort(append(sort, params.sort...))
}

func (params *ListPlayerSnapshotsParams) PrependSortString(
	sort []string,
	allowed []PlayerSnapshotSort,
	maxLength int,
) error {
	if len(sort) == 0 {
		return nil
	}

	if err := validateSliceLen(sort, 0, max(min(playerSnapshotSortMaxLength-len(params.sort), maxLength), 0)); err != nil {
		return ValidationError{
			Model: listPlayerSnapshotsParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

	toPrepend := make([]PlayerSnapshotSort, 0, len(sort))

	for i, s := range sort {
		converted, err := newSortFromString(s, allowed...)
		if err != nil {
			return SliceElementValidationError{
				Model: listPlayerSnapshotsParamsModelName,
				Field: "sort",
				Index: i,
				Err:   err,
			}
		}
		toPrepend = append(toPrepend, converted)
	}

	return params.SetSort(append(toPrepend, params.sort...))
}

func (params *ListPlayerSnapshotsParams) Cursor() PlayerSnapshotCursor {
	return params.cursor
}

func (params *ListPlayerSnapshotsParams) SetCursor(cursor PlayerSnapshotCursor) error {
	params.cursor = cursor
	return nil
}

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

	params.cursor = decoded

	return nil
}

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

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

	params.limit = limit

	return nil
}

type ListPlayerSnapshotsResult struct {
	snapshots PlayerSnapshots
	self      PlayerSnapshotCursor
	next      PlayerSnapshotCursor
}

const listPlayerSnapshotsResultModelName = "ListPlayerSnapshotsResult"

func NewListPlayerSnapshotsResult(
	snapshots PlayerSnapshots,
	next PlayerSnapshot,
) (ListPlayerSnapshotsResult, error) {
	var err error
	res := ListPlayerSnapshotsResult{
		snapshots: snapshots,
	}

	if len(snapshots) > 0 {
		res.self, err = snapshots[0].ToCursor()
		if err != nil {
			return ListPlayerSnapshotsResult{}, ValidationError{
				Model: listPlayerSnapshotsResultModelName,
				Field: "self",
				Err:   err,
			}
		}
	}

	if !next.IsZero() {
		res.next, err = next.ToCursor()
		if err != nil {
			return ListPlayerSnapshotsResult{}, ValidationError{
				Model: listPlayerSnapshotsResultModelName,
				Field: "next",
				Err:   err,
			}
		}
	}

	return res, nil
}

func (res ListPlayerSnapshotsResult) PlayerSnapshots() PlayerSnapshots {
	return res.snapshots
}

func (res ListPlayerSnapshotsResult) Self() PlayerSnapshotCursor {
	return res.self
}

func (res ListPlayerSnapshotsResult) Next() PlayerSnapshotCursor {
	return res.next
}

type ListPlayerSnapshotsWithRelationsResult struct {
	snapshots PlayerSnapshotsWithRelations
	self      PlayerSnapshotCursor
	next      PlayerSnapshotCursor
}

const listPlayerSnapshotsWithRelationsResultModelName = "ListPlayerSnapshotsWithRelationsResult"

func NewListPlayerSnapshotsWithRelationsResult(
	snapshots PlayerSnapshotsWithRelations,
	next PlayerSnapshotWithRelations,
) (ListPlayerSnapshotsWithRelationsResult, error) {
	var err error
	res := ListPlayerSnapshotsWithRelationsResult{
		snapshots: snapshots,
	}

	if len(snapshots) > 0 {
		res.self, err = snapshots[0].PlayerSnapshot().ToCursor()
		if err != nil {
			return ListPlayerSnapshotsWithRelationsResult{}, ValidationError{
				Model: listPlayerSnapshotsWithRelationsResultModelName,
				Field: "self",
				Err:   err,
			}
		}
	}

	if !next.IsZero() {
		res.next, err = next.PlayerSnapshot().ToCursor()
		if err != nil {
			return ListPlayerSnapshotsWithRelationsResult{}, ValidationError{
				Model: listPlayerSnapshotsWithRelationsResultModelName,
				Field: "next",
				Err:   err,
			}
		}
	}

	return res, nil
}

func (res ListPlayerSnapshotsWithRelationsResult) PlayerSnapshots() PlayerSnapshotsWithRelations {
	return res.snapshots
}

func (res ListPlayerSnapshotsWithRelationsResult) Self() PlayerSnapshotCursor {
	return res.self
}

func (res ListPlayerSnapshotsWithRelationsResult) Next() PlayerSnapshotCursor {
	return res.next
}
