package graph

import (
	"sort"
	"sync"

	"github.com/authzed/spicedb/pkg/genutil/mapz"
	core "github.com/authzed/spicedb/pkg/proto/core/v1"
	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
	"github.com/authzed/spicedb/pkg/spiceerrors"
	"github.com/authzed/spicedb/pkg/tuple"
)

type syncONRSet struct {
	sync.Mutex
	items map[string]struct{} // GUARDED_BY(Mutex)
}

func (s *syncONRSet) Add(onr *core.ObjectAndRelation) bool {
	key := tuple.StringONR(tuple.FromCoreObjectAndRelation(onr))
	s.Lock()
	_, existed := s.items[key]
	if !existed {
		s.items[key] = struct{}{}
	}
	s.Unlock()
	return !existed
}

func NewSyncONRSet() *syncONRSet {
	return &syncONRSet{items: make(map[string]struct{})}
}

// resourcesSubjectMap2 is a multimap which tracks mappings from found resource IDs
// to the subject IDs (may be more than one) for each, as well as whether the mapping
// is conditional due to the use of a caveat on the relationship which formed the mapping.
type resourcesSubjectMap2 struct {
	resourceType         *core.RelationReference
	resourcesAndSubjects *mapz.MultiMap[string, subjectInfo2]
}

// subjectInfo2 is the information about a subject contained in a resourcesSubjectMap2.
type subjectInfo2 struct {
	subjectID                string
	missingContextParameters []string
}

func newResourcesSubjectMap2(resourceType *core.RelationReference) resourcesSubjectMap2 {
	return resourcesSubjectMap2{
		resourceType:         resourceType,
		resourcesAndSubjects: mapz.NewMultiMap[string, subjectInfo2](),
	}
}

func newResourcesSubjectMap2WithCapacity(resourceType *core.RelationReference, capacity uint32) resourcesSubjectMap2 {
	return resourcesSubjectMap2{
		resourceType:         resourceType,
		resourcesAndSubjects: mapz.NewMultiMapWithCap[string, subjectInfo2](capacity),
	}
}

func subjectIDsToResourcesMap2(resourceType *core.RelationReference, subjectIDs []string) resourcesSubjectMap2 {
	rsm := newResourcesSubjectMap2(resourceType)
	for _, subjectID := range subjectIDs {
		rsm.addSubjectIDAsFoundResourceID(subjectID)
	}
	return rsm
}

// addRelationship adds the relationship to the resource subject map, recording a mapping from
// the resource of the relationship to the subject, as well as whether the relationship was caveated.
func (rsm resourcesSubjectMap2) addRelationship(rel tuple.Relationship, missingContextParameters []string) error {
	spiceerrors.DebugAssertf(func() bool {
		return rel.Resource.ObjectType == rsm.resourceType.Namespace && rel.Resource.Relation == rsm.resourceType.Relation
	}, "invalid relationship for addRelationship. expected: %v, found: %v", rsm.resourceType, rel.Resource)

	spiceerrors.DebugAssertf(func() bool {
		return len(missingContextParameters) == 0 || rel.OptionalCaveat != nil
	}, "missing context parameters must be empty if there is no caveat")

	rsm.resourcesAndSubjects.Add(rel.Resource.ObjectID, subjectInfo2{rel.Subject.ObjectID, missingContextParameters})
	return nil
}

// withAdditionalMissingContextForDispatchedResourceID adds additional missing context parameters
// to the existing missing context parameters for the dispatched resource ID.
func (rsm resourcesSubjectMap2) withAdditionalMissingContextForDispatchedResourceID(
	resourceID string,
	additionalMissingContext []string,
) {
	if len(additionalMissingContext) == 0 {
		return
	}

	subjectInfo2s, _ := rsm.resourcesAndSubjects.Get(resourceID)
	updatedInfos := make([]subjectInfo2, 0, len(subjectInfo2s))
	for _, info := range subjectInfo2s {
		info.missingContextParameters = append(info.missingContextParameters, additionalMissingContext...)
		updatedInfos = append(updatedInfos, info)
	}
	rsm.resourcesAndSubjects.Set(resourceID, updatedInfos)
}

// addSubjectIDAsFoundResourceID adds a subject ID directly as a found subject for itself as the resource,
// with no associated caveat.
func (rsm resourcesSubjectMap2) addSubjectIDAsFoundResourceID(subjectID string) {
	rsm.resourcesAndSubjects.Add(subjectID, subjectInfo2{subjectID, nil})
}

// asReadOnly returns a read-only dispatchableResourcesSubjectMap2 for dispatching for the
// resources in this map (if any).
func (rsm resourcesSubjectMap2) asReadOnly() dispatchableResourcesSubjectMap2 {
	return dispatchableResourcesSubjectMap2{rsm}
}

func (rsm resourcesSubjectMap2) len() int {
	return rsm.resourcesAndSubjects.Len()
}

// dispatchableResourcesSubjectMap2 is a read-only, frozen version of the resourcesSubjectMap2 that
// can be used for mapping conditionals once calls have been dispatched. This is read-only due to
// its use by concurrent callers.
type dispatchableResourcesSubjectMap2 struct {
	resourcesSubjectMap2
}

func (rsm dispatchableResourcesSubjectMap2) len() int {
	return rsm.resourcesAndSubjects.Len()
}

func (rsm dispatchableResourcesSubjectMap2) isEmpty() bool {
	return rsm.resourcesAndSubjects.IsEmpty()
}

func (rsm dispatchableResourcesSubjectMap2) resourceIDs() []string {
	return rsm.resourcesAndSubjects.Keys()
}

// filterSubjectIDsToDispatch returns the set of subject IDs that have not yet been
// dispatched, by adding them to the dispatched set.
func (rsm dispatchableResourcesSubjectMap2) filterSubjectIDsToDispatch(dispatched *syncONRSet, dispatchSubjectType *core.RelationReference) []string {
	resourceIDs := rsm.resourceIDs()
	filtered := make([]string, 0, len(resourceIDs))
	for _, resourceID := range resourceIDs {
		if dispatched.Add(&core.ObjectAndRelation{
			Namespace: dispatchSubjectType.Namespace,
			ObjectId:  resourceID,
			Relation:  dispatchSubjectType.Relation,
		}) {
			filtered = append(filtered, resourceID)
		}
	}

	return filtered
}

// cloneAsMutable returns a mutable clone of this dispatchableResourcesSubjectMap2.
func (rsm dispatchableResourcesSubjectMap2) cloneAsMutable() resourcesSubjectMap2 {
	return resourcesSubjectMap2{
		resourceType:         rsm.resourceType,
		resourcesAndSubjects: rsm.resourcesAndSubjects.Clone(),
	}
}

func (rsm dispatchableResourcesSubjectMap2) asPossibleResources() []*v1.PossibleResource {
	resources := make([]*v1.PossibleResource, 0, rsm.resourcesAndSubjects.Len())

	// Sort for stability.
	sortedResourceIds := rsm.resourcesAndSubjects.Keys()
	sort.Strings(sortedResourceIds)

	for _, resourceID := range sortedResourceIds {
		subjectInfo2s, _ := rsm.resourcesAndSubjects.Get(resourceID)
		subjectIDs := make([]string, 0, len(subjectInfo2s))
		allCaveated := true
		nonCaveatedSubjectIDs := make([]string, 0, len(subjectInfo2s))
		missingContextParameters := mapz.NewSet[string]()

		for _, info := range subjectInfo2s {
			subjectIDs = append(subjectIDs, info.subjectID)
			if len(info.missingContextParameters) == 0 {
				allCaveated = false
				nonCaveatedSubjectIDs = append(nonCaveatedSubjectIDs, info.subjectID)
			} else {
				missingContextParameters.Extend(info.missingContextParameters)
			}
		}

		// Sort for stability.
		sort.Strings(subjectIDs)

		// If all the incoming edges are caveated, then the entire status has to be marked as a check
		// is required. Otherwise, if there is at least *one* non-caveated incoming edge, then we can
		// return the existing status as a short-circuit for those non-caveated found subjects.
		if allCaveated {
			resources = append(resources, &v1.PossibleResource{
				ResourceId:           resourceID,
				ForSubjectIds:        subjectIDs,
				MissingContextParams: missingContextParameters.AsSlice(),
			})
		} else {
			resources = append(resources, &v1.PossibleResource{
				ResourceId:    resourceID,
				ForSubjectIds: nonCaveatedSubjectIDs,
			})
		}
	}
	return resources
}

func (rsm dispatchableResourcesSubjectMap2) mapPossibleResource(foundResource *v1.PossibleResource) (*v1.PossibleResource, error) {
	forSubjectIDs := mapz.NewSet[string]()
	nonCaveatedSubjectIDs := mapz.NewSet[string]()
	missingContextParameters := mapz.NewSet[string]()

	for _, forSubjectID := range foundResource.ForSubjectIds {
		// Map from the incoming subject ID to the subject ID(s) that caused the dispatch.
		infos, ok := rsm.resourcesAndSubjects.Get(forSubjectID)
		if !ok {
			return nil, spiceerrors.MustBugf("missing for subject ID")
		}

		for _, info := range infos {
			forSubjectIDs.Insert(info.subjectID)
			if len(info.missingContextParameters) == 0 {
				nonCaveatedSubjectIDs.Insert(info.subjectID)
			} else {
				missingContextParameters.Extend(info.missingContextParameters)
			}
		}
	}

	// If there are some non-caveated IDs, return those and mark as the parent status.
	if nonCaveatedSubjectIDs.Len() > 0 {
		return &v1.PossibleResource{
			ResourceId:    foundResource.ResourceId,
			ForSubjectIds: nonCaveatedSubjectIDs.AsSlice(),
		}, nil
	}

	// Otherwise, everything is caveated, so return the full set of subject IDs and mark
	// as a check is required.
	return &v1.PossibleResource{
		ResourceId:           foundResource.ResourceId,
		ForSubjectIds:        forSubjectIDs.AsSlice(),
		MissingContextParams: missingContextParameters.AsSlice(),
	}, nil
}
