package models

import (
	"fmt"
	"io"
	"strconv"

	"github.com/stashapp/stash/pkg/sliceutil"
	"github.com/stashapp/stash/pkg/sliceutil/intslice"
)

type RelationshipUpdateMode string

const (
	RelationshipUpdateModeSet    RelationshipUpdateMode = "SET"
	RelationshipUpdateModeAdd    RelationshipUpdateMode = "ADD"
	RelationshipUpdateModeRemove RelationshipUpdateMode = "REMOVE"
)

var AllRelationshipUpdateMode = []RelationshipUpdateMode{
	RelationshipUpdateModeSet,
	RelationshipUpdateModeAdd,
	RelationshipUpdateModeRemove,
}

func (e RelationshipUpdateMode) IsValid() bool {
	switch e {
	case RelationshipUpdateModeSet, RelationshipUpdateModeAdd, RelationshipUpdateModeRemove:
		return true
	}
	return false
}

func (e RelationshipUpdateMode) String() string {
	return string(e)
}

func (e *RelationshipUpdateMode) UnmarshalGQL(v interface{}) error {
	str, ok := v.(string)
	if !ok {
		return fmt.Errorf("enums must be strings")
	}

	*e = RelationshipUpdateMode(str)
	if !e.IsValid() {
		return fmt.Errorf("%s is not a valid RelationshipUpdateMode", str)
	}
	return nil
}

func (e RelationshipUpdateMode) MarshalGQL(w io.Writer) {
	fmt.Fprint(w, strconv.Quote(e.String()))
}

type UpdateIDs struct {
	IDs  []int                  `json:"ids"`
	Mode RelationshipUpdateMode `json:"mode"`
}

func (u *UpdateIDs) IDStrings() []string {
	if u == nil {
		return nil
	}

	return intslice.IntSliceToStringSlice(u.IDs)
}

// GetImpactedIDs returns the IDs that will be impacted by the update.
// If the update is to add IDs, then the impacted IDs are the IDs being added.
// If the update is to remove IDs, then the impacted IDs are the IDs being removed.
// If the update is to set IDs, then the impacted IDs are the IDs being removed and the IDs being added.
// Any IDs that are already present and are being added are not returned.
// Likewise, any IDs that are not present that are being removed are not returned.
func (u *UpdateIDs) ImpactedIDs(existing []int) []int {
	if u == nil {
		return nil
	}

	switch u.Mode {
	case RelationshipUpdateModeAdd:
		return sliceutil.Exclude(u.IDs, existing)
	case RelationshipUpdateModeRemove:
		return sliceutil.Intersect(existing, u.IDs)
	case RelationshipUpdateModeSet:
		// get the difference between the two lists
		return sliceutil.NotIntersect(existing, u.IDs)
	}

	return nil
}

// Apply applies the update to a list of existing ids, returning the result.
func (u *UpdateIDs) Apply(existing []int) []int {
	if u == nil {
		return existing
	}

	return applyUpdate(u.IDs, u.Mode, existing)
}

type UpdateStrings struct {
	Values []string               `json:"values"`
	Mode   RelationshipUpdateMode `json:"mode"`
}

func (u *UpdateStrings) Strings() []string {
	if u == nil {
		return nil
	}

	return u.Values
}

// Apply applies the update to a list of existing strings, returning the result.
func (u *UpdateStrings) Apply(existing []string) []string {
	if u == nil {
		return existing
	}

	return applyUpdate(u.Values, u.Mode, existing)
}

// applyUpdate applies values to existing, using the update mode specified.
func applyUpdate[T comparable](values []T, mode RelationshipUpdateMode, existing []T) []T {
	switch mode {
	case RelationshipUpdateModeAdd:
		return sliceutil.AppendUniques(existing, values)
	case RelationshipUpdateModeRemove:
		return sliceutil.Exclude(existing, values)
	case RelationshipUpdateModeSet:
		return values
	}

	return nil
}
