package datastore

import (
	"context"
	"fmt"
	"iter"
	"slices"
	"sort"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"google.golang.org/protobuf/types/known/structpb"

	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"

	"github.com/authzed/spicedb/pkg/datastore/options"
	core "github.com/authzed/spicedb/pkg/proto/core/v1"
	"github.com/authzed/spicedb/pkg/tuple"
)

var Engines []string

// SortedEngineIDs returns the full set of engine IDs, sorted.
func SortedEngineIDs() []string {
	engines := append([]string{}, Engines...)
	sort.Strings(engines)
	return engines
}

// EngineOptions returns the full set of engine IDs, sorted and quoted into a string.
func EngineOptions() string {
	ids := SortedEngineIDs()
	quoted := make([]string, 0, len(ids))
	for _, id := range ids {
		quoted = append(quoted, `"`+id+`"`)
	}
	return strings.Join(quoted, ", ")
}

// Ellipsis is a special relation that is assumed to be valid on the right
// hand side of a tuple.
const Ellipsis = "..."

// RevisionChanges represents the changes in a single revision.
type RevisionChanges struct {
	Revision Revision

	// RelationshipChanges are any relationships that were changed at this revision.
	RelationshipChanges []tuple.RelationshipUpdate

	// ChangedDefinitions are any definitions that were added or changed at this revision.
	ChangedDefinitions []SchemaDefinition

	// DeletedNamespaces are any namespaces that were deleted.
	DeletedNamespaces []string

	// DeletedCaveats are any caveats that were deleted.
	DeletedCaveats []string

	// IsCheckpoint, if true, indicates that the datastore has reported all changes
	// up until and including the Revision and that no additional schema updates can
	// have occurred before this point.
	IsCheckpoint bool

	// Metadatas is the list of metadata associated with the revision, if any.
	Metadatas []*structpb.Struct
}

func (rc RevisionChanges) DebugString() string {
	if rc.IsCheckpoint {
		return "[checkpoint]"
	}

	debugString := ""

	for _, relChange := range rc.RelationshipChanges {
		debugString += relChange.DebugString() + "\n"
	}

	for _, def := range rc.ChangedDefinitions {
		debugString += fmt.Sprintf("Definition: %T:%s\n", def, def.GetName())
	}

	for _, ns := range rc.DeletedNamespaces {
		debugString += fmt.Sprintf("DeletedNamespace: %s\n", ns)
	}

	for _, caveat := range rc.DeletedCaveats {
		debugString += fmt.Sprintf("DeletedCaveat: %s\n", caveat)
	}

	return debugString
}

func (rc RevisionChanges) MarshalZerologObject(e *zerolog.Event) {
	e.Str("revision", rc.Revision.String())
	e.Bool("is-checkpoint", rc.IsCheckpoint)
	e.Array("deleted-namespaces", strArray(rc.DeletedNamespaces))
	e.Array("deleted-caveats", strArray(rc.DeletedCaveats))

	changedNames := make([]string, 0, len(rc.ChangedDefinitions))
	for _, cd := range rc.ChangedDefinitions {
		changedNames = append(changedNames, fmt.Sprintf("%T:%s", cd, cd.GetName()))
	}

	e.Array("changed-definitions", strArray(changedNames))
	e.Int("num-changed-relationships", len(rc.RelationshipChanges))
}

// ExpirationFilterOption is the filter option for the expiration field on relationships.
type ExpirationFilterOption int

const (
	// ExpirationFilterOptionNone indicates that the expiration filter should not be used:
	// relationships both with and without expiration will be returned.
	ExpirationFilterOptionNone ExpirationFilterOption = iota

	// ExpirationFilterOptionHasExpiration indicates that the expiration filter should only
	// return relationships with an expiration.
	ExpirationFilterOptionHasExpiration

	// ExpirationFilterOptionNoExpiration indicates that the expiration filter should only
	// return relationships without an expiration.
	ExpirationFilterOptionNoExpiration
)

// CaveatFilterOption is the filter option for the caveat name field on relationships.
type CaveatFilterOption int

const (
	// CaveatFilterOptionNone indicates that the caveat filter should not be used:
	// relationships both with and without caveats will be returned.
	CaveatFilterOptionNone CaveatFilterOption = iota

	// CaveatFilterOptionHasMatchingCaveat indicates that the caveat filter should only
	// return relationships with the matching caveat.
	CaveatFilterOptionHasMatchingCaveat

	// CaveatFilterOptionNoCaveat indicates that the caveat filter should only
	// return relationships without a caveat.
	CaveatFilterOptionNoCaveat
)

// RelationshipsFilter is a filter for relationships.
type RelationshipsFilter struct {
	// OptionalResourceType is the namespace/type for the resources to be found.
	OptionalResourceType string

	// OptionalResourceIds are the IDs of the resources to find. If nil empty, any resource ID will be allowed.
	// Cannot be used with OptionalResourceIDPrefix.
	OptionalResourceIds []string

	// OptionalResourceIDPrefix is the prefix to use for resource IDs. If empty, any prefix is allowed.
	// Cannot be used with OptionalResourceIds.
	OptionalResourceIDPrefix string

	// OptionalResourceRelation is the relation of the resource to find. If empty, any relation is allowed.
	OptionalResourceRelation string

	// OptionalSubjectsSelectors is the selectors to use for subjects of the relationship. If nil, all subjects are allowed.
	// If specified, relationships matching *any* selector will be returned.
	OptionalSubjectsSelectors []SubjectsSelector

	// OptionalCaveatNameFilter is the filter to use for caveated relationships, filtering by a specific caveat name.
	// By default, no caveat filtered is done (one direction or the other).
	OptionalCaveatNameFilter CaveatNameFilter

	// OptionalExpirationOption is the filter to use for relationships with or without an expiration.
	OptionalExpirationOption ExpirationFilterOption
}

// Test returns true iff the given relationship is matched by this filter.
func (rf RelationshipsFilter) Test(relationship tuple.Relationship) bool {
	if rf.OptionalResourceType != "" && rf.OptionalResourceType != relationship.Resource.ObjectType {
		return false
	}

	if len(rf.OptionalResourceIds) > 0 && !slices.Contains(rf.OptionalResourceIds, relationship.Resource.ObjectID) {
		return false
	}

	if rf.OptionalResourceIDPrefix != "" && !strings.HasPrefix(relationship.Resource.ObjectID, rf.OptionalResourceIDPrefix) {
		return false
	}

	if rf.OptionalResourceRelation != "" && rf.OptionalResourceRelation != relationship.Resource.Relation {
		return false
	}

	if len(rf.OptionalSubjectsSelectors) > 0 {
		for _, selector := range rf.OptionalSubjectsSelectors {
			if selector.Test(relationship.Subject) {
				return true
			}
		}
		return false
	}

	switch rf.OptionalCaveatNameFilter.Option {
	case CaveatFilterOptionNone:
		// No caveat filter, so no need to check.

	case CaveatFilterOptionHasMatchingCaveat:
		if relationship.OptionalCaveat == nil || relationship.OptionalCaveat.CaveatName != rf.OptionalCaveatNameFilter.CaveatName {
			return false
		}

	case CaveatFilterOptionNoCaveat:
		if relationship.OptionalCaveat != nil && relationship.OptionalCaveat.CaveatName != "" {
			return false
		}
	}

	if rf.OptionalExpirationOption == ExpirationFilterOptionHasExpiration && relationship.OptionalExpiration == nil {
		return false
	}

	if rf.OptionalExpirationOption == ExpirationFilterOptionNoExpiration && relationship.OptionalExpiration != nil {
		return false
	}

	return true
}

// CaveatNameFilter is a filter for caveat names.
type CaveatNameFilter struct {
	// Option is the filter option to use for the caveat name.
	Option CaveatFilterOption

	// CaveatName is the name of the caveat to filter by. Must be specified if option is
	// CaveatFilterOptionHasCaveat.
	CaveatName string
}

func WithCaveatName(caveatName string) CaveatNameFilter {
	return CaveatNameFilter{
		Option:     CaveatFilterOptionHasMatchingCaveat,
		CaveatName: caveatName,
	}
}

func WithNoCaveat() CaveatNameFilter {
	return CaveatNameFilter{
		Option: CaveatFilterOptionNoCaveat,
	}
}

// CoreFilterFromRelationshipFilter constructs a core RelationshipFilter from a V1 RelationshipsFilter.
func CoreFilterFromRelationshipFilter(filter *v1.RelationshipFilter) *core.RelationshipFilter {
	return &core.RelationshipFilter{
		ResourceType:             filter.ResourceType,
		OptionalResourceId:       filter.OptionalResourceId,
		OptionalResourceIdPrefix: filter.OptionalResourceIdPrefix,
		OptionalRelation:         filter.OptionalRelation,
		OptionalSubjectFilter:    coreFilterFromSubjectsFilter(filter.OptionalSubjectFilter),
	}
}

func coreFilterFromSubjectsFilter(filter *v1.SubjectFilter) *core.SubjectFilter {
	if filter == nil {
		return nil
	}

	return &core.SubjectFilter{
		SubjectType:       filter.SubjectType,
		OptionalSubjectId: filter.OptionalSubjectId,
		OptionalRelation:  coreFilterFromSubjectRelationFilter(filter.OptionalRelation),
	}
}

func coreFilterFromSubjectRelationFilter(filter *v1.SubjectFilter_RelationFilter) *core.SubjectFilter_RelationFilter {
	if filter == nil {
		return nil
	}

	return &core.SubjectFilter_RelationFilter{
		Relation: filter.Relation,
	}
}

// RelationshipsFilterFromCoreFilter constructs a datastore RelationshipsFilter from a core RelationshipFilter.
func RelationshipsFilterFromCoreFilter(filter *core.RelationshipFilter) (RelationshipsFilter, error) {
	var resourceIds []string
	if filter.OptionalResourceId != "" {
		resourceIds = []string{filter.OptionalResourceId}
	}

	var subjectsSelectors []SubjectsSelector
	if filter.OptionalSubjectFilter != nil {
		var subjectIds []string
		if filter.OptionalSubjectFilter.OptionalSubjectId != "" {
			subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId}
		}

		relationFilter := SubjectRelationFilter{}

		if filter.OptionalSubjectFilter.OptionalRelation != nil {
			relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation()
			if relation != "" {
				relationFilter = relationFilter.WithNonEllipsisRelation(relation)
			} else {
				relationFilter = relationFilter.WithEllipsisRelation()
			}
		}

		subjectsSelectors = append(subjectsSelectors, SubjectsSelector{
			OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType,
			OptionalSubjectIds:  subjectIds,
			RelationFilter:      relationFilter,
		})
	}

	if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" {
		return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix")
	}

	if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 {
		return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set")
	}

	return RelationshipsFilter{
		OptionalResourceType:      filter.ResourceType,
		OptionalResourceIds:       resourceIds,
		OptionalResourceIDPrefix:  filter.OptionalResourceIdPrefix,
		OptionalResourceRelation:  filter.OptionalRelation,
		OptionalSubjectsSelectors: subjectsSelectors,
	}, nil
}

// RelationshipsFilterFromPublicFilter constructs a datastore RelationshipsFilter from an API-defined RelationshipFilter.
func RelationshipsFilterFromPublicFilter(filter *v1.RelationshipFilter) (RelationshipsFilter, error) {
	var resourceIds []string
	if filter.OptionalResourceId != "" {
		resourceIds = []string{filter.OptionalResourceId}
	}

	var subjectsSelectors []SubjectsSelector
	if filter.OptionalSubjectFilter != nil {
		var subjectIds []string
		if filter.OptionalSubjectFilter.OptionalSubjectId != "" {
			subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId}
		}

		relationFilter := SubjectRelationFilter{}

		if filter.OptionalSubjectFilter.OptionalRelation != nil {
			relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation()
			if relation != "" {
				relationFilter = relationFilter.WithNonEllipsisRelation(relation)
			} else {
				relationFilter = relationFilter.WithEllipsisRelation()
			}
		}

		subjectsSelectors = append(subjectsSelectors, SubjectsSelector{
			OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType,
			OptionalSubjectIds:  subjectIds,
			RelationFilter:      relationFilter,
		})
	}

	if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" {
		return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix")
	}

	if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 {
		return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set")
	}

	return RelationshipsFilter{
		OptionalResourceType:      filter.ResourceType,
		OptionalResourceIds:       resourceIds,
		OptionalResourceIDPrefix:  filter.OptionalResourceIdPrefix,
		OptionalResourceRelation:  filter.OptionalRelation,
		OptionalSubjectsSelectors: subjectsSelectors,
	}, nil
}

// SubjectsSelector is a selector for subjects.
type SubjectsSelector struct {
	// OptionalSubjectType is the namespace/type for the subjects to be found, if any.
	OptionalSubjectType string

	// OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed.
	OptionalSubjectIds []string

	// RelationFilter is the filter to use for the relation(s) of the subjects. If neither field
	// is set, any relation is allowed.
	RelationFilter SubjectRelationFilter
}

// Test returns true iff the given subject is matched by this filter.
func (ss SubjectsSelector) Test(subject tuple.ObjectAndRelation) bool {
	if ss.OptionalSubjectType != "" && ss.OptionalSubjectType != subject.ObjectType {
		return false
	}

	if len(ss.OptionalSubjectIds) > 0 && !slices.Contains(ss.OptionalSubjectIds, subject.ObjectID) {
		return false
	}

	if !ss.RelationFilter.IsEmpty() {
		if ss.RelationFilter.IncludeEllipsisRelation && subject.Relation == tuple.Ellipsis {
			return true
		}

		if ss.RelationFilter.NonEllipsisRelation != "" && ss.RelationFilter.NonEllipsisRelation != subject.Relation {
			return false
		}

		if ss.RelationFilter.OnlyNonEllipsisRelations && subject.Relation == tuple.Ellipsis {
			return false
		}
	}

	return true
}

// SubjectRelationFilter is the filter to use for relation(s) of subjects being queried.
type SubjectRelationFilter struct {
	// NonEllipsisRelation is the relation of the subject type to find. If empty,
	// IncludeEllipsisRelation must be true.
	NonEllipsisRelation string

	// IncludeEllipsisRelation, if true, indicates that the ellipsis relation
	// should be included as an option.
	IncludeEllipsisRelation bool

	// OnlyNonEllipsisRelations, if true, indicates that only non-ellipsis relations
	// should be included.
	OnlyNonEllipsisRelations bool
}

// WithOnlyNonEllipsisRelations indicates that only non-ellipsis relations should be included.
func (sf SubjectRelationFilter) WithOnlyNonEllipsisRelations() SubjectRelationFilter {
	sf.OnlyNonEllipsisRelations = true
	sf.NonEllipsisRelation = ""
	sf.IncludeEllipsisRelation = false
	return sf
}

// WithEllipsisRelation indicates that the subject filter should include the ellipsis relation
// as an option for the subjects' relation.
func (sf SubjectRelationFilter) WithEllipsisRelation() SubjectRelationFilter {
	sf.IncludeEllipsisRelation = true
	sf.OnlyNonEllipsisRelations = false
	return sf
}

// WithNonEllipsisRelation indicates that the specified non-ellipsis relation should be included as an
// option for the subjects' relation.
func (sf SubjectRelationFilter) WithNonEllipsisRelation(relation string) SubjectRelationFilter {
	sf.NonEllipsisRelation = relation
	sf.OnlyNonEllipsisRelations = false
	return sf
}

// WithRelation indicates that the specified relation should be included as an
// option for the subjects' relation.
func (sf SubjectRelationFilter) WithRelation(relation string) SubjectRelationFilter {
	if relation == tuple.Ellipsis {
		return sf.WithEllipsisRelation()
	}
	return sf.WithNonEllipsisRelation(relation)
}

// IsEmpty returns true if the subject relation filter is empty.
func (sf SubjectRelationFilter) IsEmpty() bool {
	return !sf.IncludeEllipsisRelation && sf.NonEllipsisRelation == "" && !sf.OnlyNonEllipsisRelations
}

// SubjectsFilter is a filter for subjects.
type SubjectsFilter struct {
	// SubjectType is the namespace/type for the subjects to be found.
	SubjectType string

	// OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed.
	OptionalSubjectIds []string

	// RelationFilter is the filter to use for the relation(s) of the subjects. If neither field
	// is set, any relation is allowed.
	RelationFilter SubjectRelationFilter
}

func (sf SubjectsFilter) AsSelector() SubjectsSelector {
	return SubjectsSelector{
		OptionalSubjectType: sf.SubjectType,
		OptionalSubjectIds:  sf.OptionalSubjectIds,
		RelationFilter:      sf.RelationFilter,
	}
}

// SchemaDefinition represents a namespace or caveat definition under a schema.
type SchemaDefinition interface {
	GetName() string
	SizeVT() int
}

// RevisionedDefinition holds a schema definition and its last updated revision.
type RevisionedDefinition[T SchemaDefinition] struct {
	// Definition is the namespace or caveat definition.
	Definition T

	// LastWrittenRevision is the revision at which the namespace or caveat was last updated.
	LastWrittenRevision Revision
}

func (rd RevisionedDefinition[T]) GetLastWrittenRevision() Revision {
	return rd.LastWrittenRevision
}

// RevisionedNamespace is a revisioned version of a namespace definition.
type RevisionedNamespace = RevisionedDefinition[*core.NamespaceDefinition]

// Reader is an interface for reading relationships from the datastore.
type Reader interface {
	CaveatReader
	CounterReader

	// QueryRelationships reads relationships, starting from the resource side.
	QueryRelationships(
		ctx context.Context,
		filter RelationshipsFilter,
		options ...options.QueryOptionsOption,
	) (RelationshipIterator, error)

	// ReverseQueryRelationships reads relationships, starting from the subject.
	ReverseQueryRelationships(
		ctx context.Context,
		subjectsFilter SubjectsFilter,
		options ...options.ReverseQueryOptionsOption,
	) (RelationshipIterator, error)

	// ReadNamespaceByName reads a namespace definition and the revision at which it was created or
	// last written. It returns an instance of NamespaceNotFoundError if not found.
	ReadNamespaceByName(ctx context.Context, nsName string) (ns *core.NamespaceDefinition, lastWritten Revision, err error)

	// ListAllNamespaces lists all namespaces defined.
	ListAllNamespaces(ctx context.Context) ([]RevisionedNamespace, error)

	// LookupNamespacesWithNames finds all namespaces with the matching names.
	LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]RevisionedNamespace, error)
}

type ReadWriteTransaction interface {
	Reader
	CaveatStorer
	CounterRegisterer

	// WriteRelationships takes a list of tuple mutations and applies them to the datastore.
	WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error

	// DeleteRelationships deletes relationships that match the provided filter, with
	// the optional limit. Returns the number of deleted relationships. If a limit
	// is provided and reached, the method will return true as the second return value.
	// Otherwise, the boolean can be ignored.
	DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter,
		options ...options.DeleteOptionsOption,
	) (uint64, bool, error)

	// WriteNamespaces takes proto namespace definitions and persists them.
	WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error

	// DeleteNamespaces deletes namespaces including associated relationships.
	DeleteNamespaces(ctx context.Context, nsNames ...string) error

	// BulkLoad takes a relationship source iterator, and writes all of the
	// relationships to the backing datastore in an optimized fashion. This
	// method can and will omit checks and otherwise cut corners in the
	// interest of performance, and should not be relied upon for OLTP-style
	// workloads.
	BulkLoad(ctx context.Context, iter BulkWriteRelationshipSource) (uint64, error)
}

// TxUserFunc is a type for the function that users supply when they invoke a read-write transaction.
type TxUserFunc func(context.Context, ReadWriteTransaction) error

// ReadyState represents the ready state of the datastore.
type ReadyState struct {
	// Message is a human-readable status message for the current state.
	Message string

	// IsReady indicates whether the datastore is ready.
	IsReady bool
}

// BulkWriteRelationshipSource is an interface for transferring relationships
// to a backing datastore with a zero-copy methodology.
type BulkWriteRelationshipSource interface {
	// Next Returns a pointer to a relation tuple if one is available, or nil if
	// there are no more or there was an error.
	//
	// Note: sources may re-use the same memory address for every tuple, data
	// may change on every call to next even if the pointer has not changed.
	Next(ctx context.Context) (*tuple.Relationship, error)
}

type WatchContent int

const (
	WatchRelationships WatchContent = 1 << 0
	WatchSchema        WatchContent = 1 << 1
	WatchCheckpoints   WatchContent = 1 << 2
)

// WatchOptions are options for a Watch call.
type WatchOptions struct {
	// Content is the content to watch.
	Content WatchContent

	// CheckpointInterval is the interval to use for checkpointing in the watch.
	// If given the zero value, the datastore's default will be used. If smaller
	// than the datastore's minimum, the minimum will be used.
	CheckpointInterval time.Duration

	// WatchBufferLength is the length of the buffer for the watch channel. If
	// given the zero value, the datastore's default will be used.
	WatchBufferLength uint16

	// WatchBufferWriteTimeout is the timeout for writing to the watch channel.
	// If given the zero value, the datastore's default will be used.
	WatchBufferWriteTimeout time.Duration

	// WatchConnectTimeout is the timeout for connecting to the watch channel.
	// If given the zero value, the datastore's default will be used.
	// May not be supported by the datastore.
	WatchConnectTimeout time.Duration

	// MaximumBufferedChangesByteSize is the maximum byte size of the buffered changes struct.
	// If unspecified, no maximum will be enforced. If the maximum is reached before
	// the changes can be sent, the watch will be closed with an error.
	MaximumBufferedChangesByteSize uint64

	// EmissionStrategy defines when are changes streamed to the client. If unspecified, changes will be buffered until
	// they can be checkpointed, which is the default behavior.
	EmissionStrategy EmissionStrategy
}

// EmissionStrategy describes when changes are emitted to the client.
type EmissionStrategy int

const (
	// EmitWhenCheckpointedStrategy will buffer changes until a checkpoint is reached. This also means that
	// changes will be deduplicated and revisions will be sorted before emission as soon as they can be checkpointed.
	EmitWhenCheckpointedStrategy = iota

	// EmitImmediatelyStrategy emits changes as soon as they are available. This means changes will not be buffered,
	// and thus will be emitted as soon as they are available, but clients are responsible for buffering, deduplication,
	// and sorting revisions. In practical terms that can only happens if Checkpoints have been requested, so enabling
	// EmitImmediatelyStrategy without Checkpoints will return an error.
	EmitImmediatelyStrategy
)

// WatchJustRelationships returns watch options for just relationships.
func WatchJustRelationships() WatchOptions {
	return WatchOptions{
		Content: WatchRelationships,
	}
}

// WatchJustSchema returns watch options for just schema.
func WatchJustSchema() WatchOptions {
	return WatchOptions{
		Content: WatchSchema,
	}
}

// WithCheckpointInterval sets the checkpoint interval on a watch options, returning
// an updated options struct.
func (wo WatchOptions) WithCheckpointInterval(interval time.Duration) WatchOptions {
	return WatchOptions{
		Content:            wo.Content,
		CheckpointInterval: interval,
	}
}

// ReadOnlyDatastore is an interface for reading relationships from the datastore.
type ReadOnlyDatastore interface {
	// MetricsID returns an identifier for the datastore for use in metrics.
	// This identifier is typically the hostname of the datastore (where applicable)
	// and may not be unique; callers should not rely on uniqueness.
	MetricsID() (string, error)

	// UniqueID returns a unique identifier for the datastore. This identifier
	// must be stable across restarts of the datastore if the datastore is
	// persistent.
	UniqueID(context.Context) (string, error)

	// SnapshotReader creates a read-only handle that reads the datastore at the specified revision.
	// Any errors establishing the reader will be returned by subsequent calls.
	SnapshotReader(Revision) Reader

	// OptimizedRevision gets a revision that will likely already be replicated
	// and will likely be shared amongst many queries.
	OptimizedRevision(ctx context.Context) (Revision, error)

	// HeadRevision gets a revision that is guaranteed to be at least as fresh as
	// right now.
	HeadRevision(ctx context.Context) (Revision, error)

	// CheckRevision checks the specified revision to make sure it's valid and
	// hasn't been garbage collected.
	CheckRevision(ctx context.Context, revision Revision) error

	// RevisionFromString will parse the revision text and return the specific type of Revision
	// used by the specific datastore implementation.
	RevisionFromString(serialized string) (Revision, error)

	// Watch notifies the caller about changes to the datastore, based on the specified options.
	//
	// All events following afterRevision will be sent to the caller.
	//
	// Errors returned will fall into a few classes:
	// - WatchDisconnectedError - the watch has fallen too far behind and has been disconnected.
	// - WatchCanceledError     - the watch was canceled by the caller.
	// - WatchDisabledError     - the watch is disabled by being unsupported by the datastore.
	// - WatchRetryableError    - the watch is retryable, and the caller may retry after some backoff time.
	// - InvalidRevisionError   - the revision specified has passed the datastore's watch history window and
	//                            the watch cannot be retried.
	// - Other errors 			- the watch should not be retried due to a fatal error.
	Watch(ctx context.Context, afterRevision Revision, options WatchOptions) (<-chan RevisionChanges, <-chan error)

	// ReadyState returns a state indicating whether the datastore is ready to accept data.
	// Datastores that require database schema creation will return not-ready until the migrations
	// have been run to create the necessary tables.
	ReadyState(ctx context.Context) (ReadyState, error)

	// Features returns an object representing what features this
	// datastore can support. Can make calls to the database, so should
	// only be used when a connection is allowed.
	Features(ctx context.Context) (*Features, error)

	// OfflineFeatures returns an object representing what features this
	// datastore supports code-wise, without making any calls to the database.
	OfflineFeatures() (*Features, error)

	// Statistics returns relevant values about the data contained in this cluster.
	Statistics(ctx context.Context) (Stats, error)

	// Close closes the data store.
	Close() error
}

// Datastore represents tuple access for a single namespace.
type Datastore interface {
	ReadOnlyDatastore

	// ReadWriteTx starts a read/write transaction, which will be committed if no error is
	// returned and rolled back if an error is returned.
	ReadWriteTx(context.Context, TxUserFunc, ...options.RWTOptionsOption) (Revision, error)
}

// ParsedExplain represents the parsed output of an EXPLAIN statement.
type ParsedExplain struct {
	// IndexesUsed is the list of indexes used in the query.
	IndexesUsed []string
}

// Explainable is an interface for datastores that support EXPLAIN statements.
type Explainable interface {
	// BuildExplainQuery builds an EXPLAIN statement for the given SQL and arguments.
	BuildExplainQuery(sql string, args []any) (string, []any, error)

	// ParseExplain parses the output of an EXPLAIN statement.
	ParseExplain(explain string) (ParsedExplain, error)

	// PreExplainStatements returns any statements that should be run before the EXPLAIN statement.
	PreExplainStatements() []string
}

// SQLDatastore is an interface for datastores that support SQL-based operations.
type SQLDatastore interface {
	Datastore
	Explainable
}

// StrictReadDatastore is an interface for datastores that support strict read mode.
type StrictReadDatastore interface {
	Datastore

	// IsStrictReadModeEnabled returns whether the datastore is in strict read mode.
	IsStrictReadModeEnabled() bool
}

type strArray []string

// MarshalZerologArray implements zerolog array marshalling.
func (strs strArray) MarshalZerologArray(a *zerolog.Array) {
	for _, val := range strs {
		a.Str(val)
	}
}

// StartableDatastore is an optional extension to the datastore interface that, when implemented,
// provides the ability for callers to start background operations on the datastore.
type StartableDatastore interface {
	Datastore

	// Start starts any background operations on the datastore. The context provided, if canceled, will
	// also cancel the background operation(s) on the datastore.
	Start(ctx context.Context) error
}

// RepairOperation represents a single kind of repair operation that can be run in a repairable
// datastore.
type RepairOperation struct {
	// Name is the command-line name for the repair operation.
	Name string

	// Description is the human-readable description for the repair operation.
	Description string
}

// RepairableDatastore is an optional extension to the datastore interface that, when implemented,
// provides the ability for callers to repair the datastore's data in some fashion.
type RepairableDatastore interface {
	Datastore

	// Repair runs the repair operation on the datastore.
	Repair(ctx context.Context, operationName string, outputProgress bool) error

	// RepairOperations returns the available repair operations for the datastore.
	RepairOperations() []RepairOperation
}

// UnwrappableDatastore represents a datastore that can be unwrapped into the underlying
// datastore.
type UnwrappableDatastore interface {
	// Unwrap returns the wrapped datastore.
	Unwrap() Datastore
}

// UnwrapAs recursively attempts to unwrap the datastore into the specified type
// In none of the layers of the datastore implement the specified type, nil is returned.
func UnwrapAs[T any](datastore Datastore) T {
	var ds T
	uwds := datastore

	for {
		var ok bool
		ds, ok = uwds.(T)
		if ok {
			break
		}

		wds, ok := uwds.(UnwrappableDatastore)
		if !ok {
			break
		}

		uwds = wds.Unwrap()
	}

	return ds
}

// FeatureStatus are the possible statuses for a feature in the datastore.
type FeatureStatus int

const (
	// FeatureStatusUnknown indicates that the status of the feature is unknown.
	// This can be returned, for example, when a call is made to OfflineFeatures
	// but the feature requires a call to the database to determine its status.
	FeatureStatusUnknown FeatureStatus = iota

	// FeatureSupported indicates that the feature is supported by the datastore.
	FeatureSupported

	// FeatureUnsupported indicates that the feature is not supported by the datastore.
	FeatureUnsupported
)

// Feature represents a capability that a datastore can support, plus an
// optional message explaining the feature is available (or not).
type Feature struct {
	Status FeatureStatus
	Reason string
}

// Features holds values that represent what features a database can support.
type Features struct {
	// Watch is enabled if the underlying datastore can support the Watch api.
	Watch Feature

	// ContinuousCheckpointing is enabled if the underlying datastore supports continuous checkpointing
	// via the Watch API. If not supported, clients of the Watch API may expect checkpoints only when
	// new transactions are committed.
	ContinuousCheckpointing Feature

	// WatchEmitsImmediately indicates if the datastore supports the EmitImmediatelyStrategy EmissionStrategy.
	// If not supported, clients of the Watch API will receive an error when calling Watch API with
	// EmitImmediatelyStrategy option.
	WatchEmitsImmediately Feature

	// IntegrityData is enabled if the underlying datastore supports retrieving and storing
	// integrity information.
	IntegrityData Feature
}

// ObjectTypeStat represents statistics for a single object type (namespace).
type ObjectTypeStat struct {
	// NumRelations is the number of relations defined in a single object type.
	NumRelations uint32

	// NumPermissions is the number of permissions defined in a single object type.
	NumPermissions uint32
}

// Stats represents statistics for the entire datastore.
type Stats struct {
	// UniqueID is a unique string for a single datastore.
	UniqueID string

	// EstimatedRelationshipCount is a best-guess estimate of the number of relationships
	// in the datastore. Computing it should use a lightweight method such as reading
	// table statistics.
	EstimatedRelationshipCount uint64

	// ObjectTypeStatistics returns a slice element for each object type (namespace)
	// stored in the datastore.
	ObjectTypeStatistics []ObjectTypeStat
}

// RelationshipIterator is an iterator over matched tuples. It is a single use
// iterator.
type RelationshipIterator iter.Seq2[tuple.Relationship, error]

func IteratorToSlice(iter RelationshipIterator) ([]tuple.Relationship, error) {
	results := make([]tuple.Relationship, 0)
	for rel, err := range iter {
		if err != nil {
			return nil, err
		}
		results = append(results, rel)
	}
	return results, nil
}

// FirstRelationshipIn returns the first relationship found via the iterator, if any.
func FirstRelationshipIn(iter RelationshipIterator) (tuple.Relationship, bool, error) {
	for rel, err := range iter {
		if err != nil {
			return tuple.Relationship{}, false, err
		}

		return rel, true, nil
	}

	return tuple.Relationship{}, false, nil
}

// Revision is an interface for a comparable revision type that can be different for
// each datastore implementation.
type Revision interface {
	fmt.Stringer

	// Equal returns whether the revisions should be considered equal.
	Equal(Revision) bool

	// GreaterThan returns whether the receiver is probably greater than the right hand side.
	GreaterThan(Revision) bool

	// LessThan returns whether the receiver is probably less than the right hand side.
	LessThan(Revision) bool

	// ByteSortable returns true if the string representation of the Revision is byte sortable, false otherwise.
	ByteSortable() bool
}

type nilRevision struct{}

func (nilRevision) ByteSortable() bool {
	return false
}

func (nilRevision) Equal(rhs Revision) bool {
	return rhs == NoRevision
}

func (nilRevision) GreaterThan(_ Revision) bool {
	return false
}

func (nilRevision) LessThan(_ Revision) bool {
	return true
}

func (nilRevision) String() string {
	return "nil"
}

// NoRevision is a zero type for the revision that will make changing the
// revision type in the future a bit easier if necessary. Implementations
// should use any time they want to signal an empty/error revision.
var NoRevision Revision = nilRevision{}
