package tuple

import (
	"errors"
	"fmt"
	"time"

	"google.golang.org/protobuf/types/known/timestamppb"

	core "github.com/authzed/spicedb/pkg/proto/core/v1"
	"github.com/authzed/spicedb/pkg/spiceerrors"
)

const (
	// Ellipsis is the Ellipsis relation in v0 style subjects.
	Ellipsis = "..."

	// PublicWildcard is the wildcard value for subject object IDs that indicates public access
	// for the subject type.
	PublicWildcard = "*"
)

// ObjectAndRelation represents an object and its relation.
type ObjectAndRelation struct {
	ObjectID   string
	ObjectType string
	Relation   string
}

const onrStructSize = 48 /* size of the struct itself */

func (onr ObjectAndRelation) SizeVT() int {
	return len(onr.ObjectID) + len(onr.ObjectType) + len(onr.Relation) + onrStructSize
}

// WithRelation returns a copy of the object and relation with the given relation.
func (onr ObjectAndRelation) WithRelation(relation string) ObjectAndRelation {
	onr.Relation = relation
	return onr
}

// RelationReference returns a RelationReference for the object and relation.
func (onr ObjectAndRelation) RelationReference() RelationReference {
	return RelationReference{
		ObjectType: onr.ObjectType,
		Relation:   onr.Relation,
	}
}

// ToCoreONR converts the ObjectAndRelation to a core.ObjectAndRelation.
func (onr ObjectAndRelation) ToCoreONR() *core.ObjectAndRelation {
	return &core.ObjectAndRelation{
		Namespace: onr.ObjectType,
		ObjectId:  onr.ObjectID,
		Relation:  onr.Relation,
	}
}

func (onr ObjectAndRelation) String() string {
	return onr.ObjectType + ":" + onr.ObjectID + "#" + onr.Relation
}

// RelationshipReference represents a reference to a relationship, i.e. those portions
// of a relationship that are not the integrity or caveat and thus form the unique
// identifier of the relationship.
type RelationshipReference struct {
	Resource ObjectAndRelation
	Subject  ObjectAndRelation
}

func (rr RelationshipReference) String() string {
	return rr.Resource.String() + "@" + rr.Subject.String()
}

// Relationship represents a relationship between two objects.
type Relationship struct {
	OptionalCaveat     *core.ContextualizedCaveat
	OptionalExpiration *time.Time
	OptionalIntegrity  *core.RelationshipIntegrity

	RelationshipReference
}

// ToCoreTuple converts the Relationship to a core.RelationTuple.
func (r Relationship) ToCoreTuple() *core.RelationTuple {
	var expirationTime *timestamppb.Timestamp
	if r.OptionalExpiration != nil {
		expirationTime = timestamppb.New(*r.OptionalExpiration)
	}

	return &core.RelationTuple{
		ResourceAndRelation:    r.Resource.ToCoreONR(),
		Subject:                r.Subject.ToCoreONR(),
		Caveat:                 r.OptionalCaveat,
		Integrity:              r.OptionalIntegrity,
		OptionalExpirationTime: expirationTime,
	}
}

const relStructSize = 120 /* size of the struct itself */

func (r Relationship) SizeVT() int {
	size := r.Resource.SizeVT() + r.Subject.SizeVT() + relStructSize
	if r.OptionalCaveat != nil {
		size += r.OptionalCaveat.SizeVT()
	}
	return size
}

// ValidateNotEmpty returns true if the relationship is not empty.
func (r Relationship) ValidateNotEmpty() bool {
	return r.Resource.ObjectType != "" && r.Resource.ObjectID != "" && r.Subject.ObjectType != "" && r.Subject.ObjectID != "" && r.Resource.Relation != "" && r.Subject.Relation != ""
}

// Validate returns an error if the relationship is invalid.
func (r Relationship) Validate() error {
	if !r.ValidateNotEmpty() {
		return errors.New("object and relation must not be empty")
	}

	if r.Resource.ObjectID == PublicWildcard {
		return errors.New("invalid resource id")
	}

	return nil
}

// WithoutIntegrity returns a copy of the relationship without its integrity.
func (r Relationship) WithoutIntegrity() Relationship {
	r.OptionalIntegrity = nil
	return r
}

// WithCaveat returns a copy of the relationship with the given caveat.
func (r Relationship) WithCaveat(caveat *core.ContextualizedCaveat) Relationship {
	r.OptionalCaveat = caveat
	return r
}

// UpdateOperation represents the type of update to a relationship.
type UpdateOperation int

const (
	UpdateOperationTouch UpdateOperation = iota
	UpdateOperationCreate
	UpdateOperationDelete
)

// RelationshipUpdate represents an update to a relationship.
type RelationshipUpdate struct {
	Relationship Relationship
	Operation    UpdateOperation
}

func (ru RelationshipUpdate) OperationString() string {
	switch ru.Operation {
	case UpdateOperationTouch:
		return "TOUCH"
	case UpdateOperationCreate:
		return "CREATE"
	case UpdateOperationDelete:
		return "DELETE"
	default:
		return "unknown"
	}
}

func (ru RelationshipUpdate) DebugString() string {
	return fmt.Sprintf("%s(%s)", ru.OperationString(), StringWithoutCaveatOrExpiration(ru.Relationship))
}

// RelationReference represents a reference to a relation.
type RelationReference struct {
	ObjectType string
	Relation   string
}

// ToCoreRR converts the RelationReference to a core.RelationReference.
func (rr RelationReference) ToCoreRR() *core.RelationReference {
	return &core.RelationReference{
		Namespace: rr.ObjectType,
		Relation:  rr.Relation,
	}
}

func (rr RelationReference) RefString() string {
	return JoinRelRef(rr.ObjectType, rr.Relation)
}

func (rr RelationReference) String() string {
	return rr.RefString()
}

// ONR creates an ObjectAndRelation.
func ONR(namespace, objectID, relation string) ObjectAndRelation {
	spiceerrors.DebugAssertf(func() bool {
		return namespace != "" && objectID != "" && relation != ""
	}, "invalid ONR: %s %s %s", namespace, objectID, relation)

	return ObjectAndRelation{
		ObjectType: namespace,
		ObjectID:   objectID,
		Relation:   relation,
	}
}

// ONRRef creates an ObjectAndRelation reference.
func ONRRef(namespace, objectID, relation string) *ObjectAndRelation {
	onr := ONR(namespace, objectID, relation)
	return &onr
}

// RR creates a RelationReference.
func RR(namespace, relation string) RelationReference {
	spiceerrors.DebugAssertf(func() bool {
		return namespace != "" && relation != ""
	}, "invalid RR: %s %s", namespace, relation)

	return RelationReference{
		ObjectType: namespace,
		Relation:   relation,
	}
}
