package performer

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/stashapp/stash/pkg/models"
)

var (
	ErrNameMissing = errors.New("performer name must not be blank")
)

type NotFoundError struct {
	id int
}

func (e *NotFoundError) Error() string {
	return fmt.Sprintf("performer with id %d not found", e.id)
}

type NameExistsError struct {
	Name           string
	Disambiguation string
}

func (e *NameExistsError) Error() string {
	if e.Disambiguation != "" {
		return fmt.Sprintf("performer with name '%s' and disambiguation '%s' already exists", e.Name, e.Disambiguation)
	}
	return fmt.Sprintf("performer with name '%s' already exists", e.Name)
}

type DuplicateAliasError struct {
	Alias string
}

func (e *DuplicateAliasError) Error() string {
	return fmt.Sprintf("performer contains duplicate alias '%s'", e.Alias)
}

type DeathDateError struct {
	Birthdate models.Date
	DeathDate models.Date
}

func (e *DeathDateError) Error() string {
	return fmt.Sprintf("death date %s should be after birthdate %s", e.DeathDate, e.Birthdate)
}

func ValidateCreate(ctx context.Context, performer models.Performer, qb models.PerformerReader) error {
	if err := ValidateName(ctx, performer.Name, performer.Disambiguation, qb); err != nil {
		return err
	}

	if err := ValidateAliases(performer.Name, performer.Aliases); err != nil {
		return err
	}

	if err := ValidateDeathDate(performer.Birthdate, performer.DeathDate); err != nil {
		return err
	}

	return nil
}

func ValidateUpdate(ctx context.Context, id int, partial models.PerformerPartial, qb models.PerformerReader) error {
	existing, err := qb.Find(ctx, id)
	if err != nil {
		return err
	}

	if existing == nil {
		return &NotFoundError{id}
	}

	if err := ValidateUpdateName(ctx, *existing, partial.Name, partial.Disambiguation, qb); err != nil {
		return err
	}

	if err := existing.LoadAliases(ctx, qb); err != nil {
		return err
	}
	if err := ValidateUpdateAliases(*existing, partial.Name, partial.Aliases); err != nil {
		return err
	}

	if err := ValidateUpdateDeathDate(*existing, partial.Birthdate, partial.DeathDate); err != nil {
		return err
	}

	return nil
}

func validateName(ctx context.Context, name string, disambig string, existingID *int, qb models.PerformerQueryer) error {
	performerFilter := models.PerformerFilterType{
		Name: &models.StringCriterionInput{
			Value:    name,
			Modifier: models.CriterionModifierEquals,
		},
	}

	if disambig != "" {
		performerFilter.Disambiguation = &models.StringCriterionInput{
			Value:    disambig,
			Modifier: models.CriterionModifierEquals,
		}
	}

	if existingID == nil {
		// creating: error if any existing performer matches

		pp := 1
		findFilter := models.FindFilterType{
			PerPage: &pp,
		}

		count, err := qb.QueryCount(ctx, &performerFilter, &findFilter)
		if err != nil {
			return err
		}

		if count > 0 {
			return &NameExistsError{
				Name:           name,
				Disambiguation: disambig,
			}
		}

		return nil
	} else {
		// updating: check for matches, but ignore self

		pp := 2
		findFilter := models.FindFilterType{
			PerPage: &pp,
		}

		conflicts, _, err := qb.Query(ctx, &performerFilter, &findFilter)
		if err != nil {
			return err
		}

		if len(conflicts) > 0 {
			// valid if the only conflict is the existing performer
			if len(conflicts) > 1 || conflicts[0].ID != *existingID {
				return &NameExistsError{
					Name:           name,
					Disambiguation: disambig,
				}
			}
		}

		return nil
	}
}

// ValidateName returns an error if the performer name and disambiguation provided is used by another performer.
func ValidateName(ctx context.Context, name string, disambig string, qb models.PerformerQueryer) error {
	if name == "" {
		return ErrNameMissing
	}

	return validateName(ctx, name, disambig, nil, qb)
}

// ValidateUpdateName performs the same check as ValidateName, but is used when modifying an existing performer.
func ValidateUpdateName(ctx context.Context, existing models.Performer, name models.OptionalString, disambig models.OptionalString, qb models.PerformerQueryer) error {
	// if neither name nor disambig is set, don't check anything
	if !name.Set && !disambig.Set {
		return nil
	}

	newName := existing.Name
	if name.Set {
		newName = name.Value
	}

	if newName == "" {
		return ErrNameMissing
	}

	newDisambig := existing.Disambiguation
	if disambig.Set {
		newDisambig = disambig.Value
	}

	return validateName(ctx, newName, newDisambig, &existing.ID, qb)
}

func ValidateAliases(name string, aliases models.RelatedStrings) error {
	if !aliases.Loaded() {
		return nil
	}

	m := make(map[string]bool)
	nameL := strings.ToLower(name)
	m[nameL] = true

	for _, alias := range aliases.List() {
		aliasL := strings.ToLower(alias)
		if m[aliasL] {
			return &DuplicateAliasError{alias}
		}
		m[aliasL] = true
	}

	return nil
}

func ValidateUpdateAliases(existing models.Performer, name models.OptionalString, aliases *models.UpdateStrings) error {
	// if neither name nor aliases is set, don't check anything
	if !name.Set && aliases == nil {
		return nil
	}

	newName := existing.Name
	if name.Set {
		newName = name.Value
	}

	newAliases := aliases.Apply(existing.Aliases.List())

	return ValidateAliases(newName, models.NewRelatedStrings(newAliases))
}

// ValidateDeathDate returns an error if the birthdate is after the death date.
func ValidateDeathDate(birthdate *models.Date, deathDate *models.Date) error {
	if birthdate == nil || deathDate == nil {
		return nil
	}

	if birthdate.After(*deathDate) {
		return &DeathDateError{Birthdate: *birthdate, DeathDate: *deathDate}
	}

	return nil
}

// ValidateUpdateDeathDate performs the same check as ValidateDeathDate, but is used when modifying an existing performer.
func ValidateUpdateDeathDate(existing models.Performer, birthdate models.OptionalDate, deathDate models.OptionalDate) error {
	// if neither birthdate nor deathDate is set, don't check anything
	if !birthdate.Set && !deathDate.Set {
		return nil
	}

	newBirthdate := existing.Birthdate
	if birthdate.Set {
		newBirthdate = birthdate.Ptr()
	}

	newDeathDate := existing.DeathDate
	if deathDate.Set {
		newDeathDate = deathDate.Ptr()
	}

	return ValidateDeathDate(newBirthdate, newDeathDate)
}
