package domain

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

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 {
			if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 {
				return res
			}
			return 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
)

const TribeListMaxLimit = 200

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

const 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) IDGT() NullInt {
	return params.idGT
}

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

	params.idGT = idGT

	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) 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 = 2
)

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) 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
}

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

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

	params.offset = offset

	return nil
}
