package domain

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

const (
	tribeNameMinLength = 1
	tribeNameMaxLength = 255
	tribeTagMinLength  = 1
	tribeTagMaxLength  = 10
)

type Tribe struct {
	id             int
	serverKey      string
	name           string
	tag            string
	numMembers     int
	numVillages    int
	points         int
	allPoints      int
	rank           int
	od             OpponentsDefeated
	profileURL     *url.URL
	dominance      float64
	bestRank       int
	bestRankAt     time.Time
	mostPoints     int
	mostPointsAt   time.Time
	mostVillages   int
	mostVillagesAt time.Time
	createdAt      time.Time
	deletedAt      time.Time
}

const tribeModelName = "Tribe"

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

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

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

	return Tribe{
		id:             id,
		serverKey:      serverKey,
		name:           name,
		tag:            tag,
		numMembers:     numMembers,
		numVillages:    numVillages,
		points:         points,
		allPoints:      allPoints,
		rank:           rank,
		od:             od,
		profileURL:     profileURL,
		dominance:      dominance,
		bestRank:       bestRank,
		bestRankAt:     bestRankAt,
		mostPoints:     mostPoints,
		mostPointsAt:   mostPointsAt,
		mostVillages:   mostVillages,
		mostVillagesAt: mostVillagesAt,
		createdAt:      createdAt,
		deletedAt:      deletedAt,
	}, nil
}

func (t Tribe) ID() int {
	return t.id
}

func (t Tribe) ServerKey() string {
	return t.serverKey
}

func (t Tribe) Name() string {
	return t.name
}

func (t Tribe) Tag() string {
	return t.tag
}

func (t Tribe) NumMembers() int {
	return t.numMembers
}

func (t Tribe) NumVillages() int {
	return t.numVillages
}

func (t Tribe) Points() int {
	return t.points
}

func (t Tribe) AllPoints() int {
	return t.allPoints
}

func (t Tribe) Rank() int {
	return t.rank
}

func (t Tribe) OD() OpponentsDefeated {
	return t.od
}

func (t Tribe) ProfileURL() *url.URL {
	return t.profileURL
}

func (t Tribe) Dominance() float64 {
	return t.dominance
}

func (t Tribe) BestRank() int {
	return t.bestRank
}

func (t Tribe) BestRankAt() time.Time {
	return t.bestRankAt
}

func (t Tribe) MostPoints() int {
	return t.mostPoints
}

func (t Tribe) MostPointsAt() time.Time {
	return t.mostPointsAt
}

func (t Tribe) MostVillages() int {
	return t.mostVillages
}

func (t Tribe) MostVillagesAt() time.Time {
	return t.mostVillagesAt
}

func (t Tribe) CreatedAt() time.Time {
	return t.createdAt
}

func (t Tribe) DeletedAt() time.Time {
	return t.deletedAt
}

func (t Tribe) IsDeleted() bool {
	return !t.deletedAt.IsZero()
}

func (t Tribe) Base() BaseTribe {
	return BaseTribe{
		id:          t.id,
		name:        t.name,
		tag:         t.tag,
		numMembers:  t.numMembers,
		numVillages: t.numVillages,
		points:      t.points,
		allPoints:   t.allPoints,
		rank:        t.rank,
		od:          t.od,
		profileURL:  t.profileURL,
	}
}

func (t Tribe) IsZero() bool {
	return t == Tribe{}
}

type Tribes []Tribe

// Delete finds all tribes with the given serverKey that are not in the given slice with active tribes
// and returns their ids. Both slices must be sorted in ascending order by ID
// + if vs contains tribes from different servers. they must be sorted in ascending order by server key.
func (ts Tribes) Delete(serverKey string, active BaseTribes) []int {
	//nolint:prealloc
	var toDelete []int

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

		_, found := slices.BinarySearchFunc(active, t, func(a BaseTribe, b Tribe) int {
			return cmp.Compare(a.ID(), b.ID())
		})
		if found {
			continue
		}

		toDelete = append(toDelete, t.ID())
	}

	return toDelete
}

type CreateTribeParams struct {
	base           BaseTribe
	serverKey      string
	bestRank       int
	bestRankAt     time.Time
	mostPoints     int
	mostPointsAt   time.Time
	mostVillages   int
	mostVillagesAt time.Time
}

const createTribeParamsModelName = "CreateTribeParams"

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

	params := make([]CreateTribeParams, 0, len(tribes))

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

		var old Tribe
		idx, found := slices.BinarySearchFunc(storedTribes, t, func(a Tribe, b BaseTribe) int {
			return cmp.Or(
				cmp.Compare(a.ServerKey(), serverKey),
				cmp.Compare(a.ID(), b.ID()),
			)
		})
		if found {
			old = storedTribes[idx]
		}

		now := time.Now()

		p := CreateTribeParams{
			base:           t,
			serverKey:      serverKey,
			bestRank:       t.Rank(),
			bestRankAt:     now,
			mostPoints:     t.AllPoints(),
			mostPointsAt:   now,
			mostVillages:   t.NumVillages(),
			mostVillagesAt: 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()
			}
		}

		params = append(params, p)
	}

	return params, nil
}

func (params CreateTribeParams) Base() BaseTribe {
	return params.base
}

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

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

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

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

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

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

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

type TribeSort uint8

const (
	TribeSortIDASC TribeSort = iota + 1
	TribeSortIDDESC
	TribeSortServerKeyASC
	TribeSortServerKeyDESC
	TribeSortODScoreAttASC
	TribeSortODScoreAttDESC
	TribeSortODScoreDefASC
	TribeSortODScoreDefDESC
	TribeSortODScoreTotalASC
	TribeSortODScoreTotalDESC
	TribeSortPointsASC
	TribeSortPointsDESC
	TribeSortDominanceASC
	TribeSortDominanceDESC
	TribeSortDeletedAtASC
	TribeSortDeletedAtDESC
)

//nolint:gocyclo
func newTribeSortFromString(s string) (TribeSort, error) {
	switch strings.ToLower(s) {
	case "odscoreatt:asc":
		return TribeSortODScoreAttASC, nil
	case "odscoreatt:desc":
		return TribeSortODScoreAttDESC, nil
	case "odscoredef:asc":
		return TribeSortODScoreDefASC, nil
	case "odscoredef:desc":
		return TribeSortODScoreDefDESC, nil
	case "odscoretotal:asc":
		return TribeSortODScoreTotalASC, nil
	case "odscoretotal:desc":
		return TribeSortODScoreTotalDESC, nil
	case "points:asc":
		return TribeSortPointsASC, nil
	case "points:desc":
		return TribeSortPointsDESC, nil
	case "dominance:asc":
		return TribeSortDominanceASC, nil
	case "dominance:desc":
		return TribeSortDominanceDESC, nil
	case "deletedat:asc":
		return TribeSortDeletedAtASC, nil
	case "deletedat:desc":
		return TribeSortDeletedAtDESC, nil
	default:
		return 0, UnsupportedSortStringError{
			Sort: s,
		}
	}
}

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

const tribeCursorModelName = "TribeCursor"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	dominance, err := m.float64("dominance")
	if err != nil {
		return TribeCursor{}, ErrInvalidCursor
	}

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

	tc, err := NewTribeCursor(
		id,
		serverKey,
		odScoreAtt,
		odScoreDef,
		odScoreTotal,
		points,
		dominance,
		deletedAt,
	)
	if err != nil {
		return TribeCursor{}, ErrInvalidCursor
	}

	return tc, nil
}

func (tc TribeCursor) ID() int {
	return tc.id
}

func (tc TribeCursor) ServerKey() string {
	return tc.serverKey
}

func (tc TribeCursor) ODScoreAtt() int {
	return tc.odScoreAtt
}

func (tc TribeCursor) ODScoreDef() int {
	return tc.odScoreDef
}

func (tc TribeCursor) ODScoreTotal() int {
	return tc.odScoreTotal
}

func (tc TribeCursor) Points() int {
	return tc.points
}

func (tc TribeCursor) Dominance() float64 {
	return tc.dominance
}

func (tc TribeCursor) DeletedAt() time.Time {
	return tc.deletedAt
}

func (tc TribeCursor) IsZero() bool {
	return tc == TribeCursor{}
}

func (tc TribeCursor) Encode() string {
	if tc.IsZero() {
		return ""
	}

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

type ListTribesParams struct {
	ids        []int
	serverKeys []string
	tags       []string
	deleted    NullBool
	sort       []TribeSort
	cursor     TribeCursor
	limit      int
}

const (
	TribeListMaxLimit         = 200
	listTribesParamsModelName = "ListTribesParams"
)

func NewListTribesParams() ListTribesParams {
	return ListTribesParams{
		sort: []TribeSort{
			TribeSortServerKeyASC,
			TribeSortIDASC,
		},
		limit: TribeListMaxLimit,
	}
}

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

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

	params.ids = ids

	return nil
}

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

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

func (params *ListTribesParams) Tags() []string {
	return params.tags
}

const (
	tribeTagsMinLength = 1
	tribeTagsMaxLength = 100
)

func (params *ListTribesParams) SetTags(tags []string) error {
	if err := validateSliceLen(tags, tribeTagsMinLength, tribeTagsMaxLength); err != nil {
		return ValidationError{
			Model: listTribesParamsModelName,
			Field: "tags",
			Err:   err,
		}
	}

	params.tags = tags

	return nil
}

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

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

func (params *ListTribesParams) Sort() []TribeSort {
	return params.sort
}

const (
	tribeSortMinLength = 1
	tribeSortMaxLength = 3
)

func (params *ListTribesParams) SetSort(sort []TribeSort) error {
	if err := validateSliceLen(sort, tribeSortMinLength, tribeSortMaxLength); err != nil {
		return ValidationError{
			Model: listTribesParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

	params.sort = sort

	return nil
}

func (params *ListTribesParams) PrependSortString(sort []string) error {
	if err := validateSliceLen(sort, tribeSortMinLength, max(tribeSortMaxLength-len(params.sort), 0)); err != nil {
		return ValidationError{
			Model: listTribesParamsModelName,
			Field: "sort",
			Err:   err,
		}
	}

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

	return nil
}

func (params *ListTribesParams) Cursor() TribeCursor {
	return params.cursor
}

func (params *ListTribesParams) SetCursor(cursor TribeCursor) error {
	params.cursor = cursor
	return nil
}

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

	params.cursor = decoded

	return nil
}

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

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

	params.limit = limit

	return nil
}

type ListTribesResult struct {
	tribes Tribes
	self   TribeCursor
	next   TribeCursor
}

const listTribesResultModelName = "ListTribesResult"

func NewListTribesResult(tribes Tribes, next Tribe) (ListTribesResult, error) {
	var err error
	res := ListTribesResult{
		tribes: tribes,
	}

	if len(tribes) > 0 {
		od := tribes[0].OD()
		res.self, err = NewTribeCursor(
			tribes[0].ID(),
			tribes[0].ServerKey(),
			od.ScoreAtt(),
			od.ScoreDef(),
			od.ScoreTotal(),
			tribes[0].Points(),
			tribes[0].Dominance(),
			tribes[0].DeletedAt(),
		)
		if err != nil {
			return ListTribesResult{}, ValidationError{
				Model: listTribesResultModelName,
				Field: "self",
				Err:   err,
			}
		}
	}

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

	return res, nil
}

func (res ListTribesResult) Tribes() Tribes {
	return res.tribes
}

func (res ListTribesResult) Self() TribeCursor {
	return res.self
}

func (res ListTribesResult) Next() TribeCursor {
	return res.next
}

type TribeNotFoundError struct {
	ID        int
	ServerKey string
}

var _ ErrorWithParams = TribeNotFoundError{}

func (e TribeNotFoundError) Error() string {
	return fmt.Sprintf("tribe with id %d and server key %s not found", e.ID, e.ServerKey)
}

func (e TribeNotFoundError) Type() ErrorType {
	return ErrorTypeNotFound
}

func (e TribeNotFoundError) Code() string {
	return "tribe-not-found"
}

func (e TribeNotFoundError) Params() map[string]any {
	return map[string]any{
		"ID":        e.ID,
		"ServerKey": e.ServerKey,
	}
}
