package diff

import (
	caveattypes "github.com/authzed/spicedb/pkg/caveats/types"
	"github.com/authzed/spicedb/pkg/diff/caveats"
	"github.com/authzed/spicedb/pkg/diff/namespace"
	"github.com/authzed/spicedb/pkg/genutil/mapz"
	core "github.com/authzed/spicedb/pkg/proto/core/v1"
	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
)

// DiffableSchema is a schema that can be diffed.
type DiffableSchema struct {
	// ObjectDefinitions holds the object definitions in the schema.
	ObjectDefinitions []*core.NamespaceDefinition

	// CaveatDefinitions holds the caveat definitions in the schema.
	CaveatDefinitions []*core.CaveatDefinition
}

func (ds *DiffableSchema) GetNamespace(namespaceName string) (*core.NamespaceDefinition, bool) {
	for _, ns := range ds.ObjectDefinitions {
		if ns.Name == namespaceName {
			return ns, true
		}
	}

	return nil, false
}

func (ds *DiffableSchema) GetRelation(nsName string, relationName string) (*core.Relation, bool) {
	ns, ok := ds.GetNamespace(nsName)
	if !ok {
		return nil, false
	}

	for _, relation := range ns.Relation {
		if relation.Name == relationName {
			return relation, true
		}
	}

	return nil, false
}

func (ds *DiffableSchema) GetCaveat(caveatName string) (*core.CaveatDefinition, bool) {
	for _, caveat := range ds.CaveatDefinitions {
		if caveat.Name == caveatName {
			return caveat, true
		}
	}

	return nil, false
}

// NewDiffableSchemaFromCompiledSchema creates a new DiffableSchema from a CompiledSchema.
func NewDiffableSchemaFromCompiledSchema(compiled *compiler.CompiledSchema) DiffableSchema {
	return DiffableSchema{
		ObjectDefinitions: compiled.ObjectDefinitions,
		CaveatDefinitions: compiled.CaveatDefinitions,
	}
}

// SchemaDiff holds the diff between two schemas.
type SchemaDiff struct {
	// AddedNamespaces are the namespaces that were added.
	AddedNamespaces []string

	// RemovedNamespaces are the namespaces that were removed.
	RemovedNamespaces []string

	// AddedCaveats are the caveats that were added.
	AddedCaveats []string

	// RemovedCaveats are the caveats that were removed.
	RemovedCaveats []string

	// ChangedNamespaces are the namespaces that were changed.
	ChangedNamespaces map[string]namespace.Diff

	// ChangedCaveats are the caveats that were changed.
	ChangedCaveats map[string]caveats.Diff
}

// DiffSchemas compares two schemas and returns the diff.
func DiffSchemas(existing DiffableSchema, comparison DiffableSchema, caveatTypeSet *caveattypes.TypeSet) (*SchemaDiff, error) {
	existingNamespacesByName := make(map[string]*core.NamespaceDefinition, len(existing.ObjectDefinitions))
	existingNamespaceNames := mapz.NewSet[string]()
	for _, nsDef := range existing.ObjectDefinitions {
		existingNamespacesByName[nsDef.Name] = nsDef
		existingNamespaceNames.Add(nsDef.Name)
	}

	existingCaveatsByName := make(map[string]*core.CaveatDefinition, len(existing.CaveatDefinitions))
	existingCaveatsByNames := mapz.NewSet[string]()
	for _, caveatDef := range existing.CaveatDefinitions {
		existingCaveatsByName[caveatDef.Name] = caveatDef
		existingCaveatsByNames.Add(caveatDef.Name)
	}

	comparisonNamespacesByName := make(map[string]*core.NamespaceDefinition, len(comparison.ObjectDefinitions))
	comparisonNamespaceNames := mapz.NewSet[string]()
	for _, nsDef := range comparison.ObjectDefinitions {
		comparisonNamespacesByName[nsDef.Name] = nsDef
		comparisonNamespaceNames.Add(nsDef.Name)
	}

	comparisonCaveatsByName := make(map[string]*core.CaveatDefinition, len(comparison.CaveatDefinitions))
	comparisonCaveatsByNames := mapz.NewSet[string]()
	for _, caveatDef := range comparison.CaveatDefinitions {
		comparisonCaveatsByName[caveatDef.Name] = caveatDef
		comparisonCaveatsByNames.Add(caveatDef.Name)
	}

	changedNamespaces := make(map[string]namespace.Diff, 0)
	commonNamespaceNames := existingNamespaceNames.Intersect(comparisonNamespaceNames)
	if err := commonNamespaceNames.ForEach(func(name string) error {
		existingNamespace := existingNamespacesByName[name]
		comparisonNamespace := comparisonNamespacesByName[name]

		diff, err := namespace.DiffNamespaces(existingNamespace, comparisonNamespace)
		if err != nil {
			return err
		}

		if len(diff.Deltas()) > 0 {
			changedNamespaces[name] = *diff
		}

		return nil
	}); err != nil {
		return nil, err
	}

	commonCaveatNames := existingCaveatsByNames.Intersect(comparisonCaveatsByNames)
	changedCaveats := make(map[string]caveats.Diff, 0)
	if err := commonCaveatNames.ForEach(func(name string) error {
		existingCaveat := existingCaveatsByName[name]
		comparisonCaveat := comparisonCaveatsByName[name]

		diff, err := caveats.DiffCaveats(existingCaveat, comparisonCaveat, caveatTypeSet)
		if err != nil {
			return err
		}

		if len(diff.Deltas()) > 0 {
			changedCaveats[name] = *diff
		}

		return nil
	}); err != nil {
		return nil, err
	}

	if len(changedNamespaces) == 0 {
		changedNamespaces = nil
	}
	if len(changedCaveats) == 0 {
		changedCaveats = nil
	}

	return &SchemaDiff{
		AddedNamespaces:   comparisonNamespaceNames.Subtract(existingNamespaceNames).AsSlice(),
		RemovedNamespaces: existingNamespaceNames.Subtract(comparisonNamespaceNames).AsSlice(),
		AddedCaveats:      comparisonCaveatsByNames.Subtract(existingCaveatsByNames).AsSlice(),
		RemovedCaveats:    existingCaveatsByNames.Subtract(comparisonCaveatsByNames).AsSlice(),
		ChangedNamespaces: changedNamespaces,
		ChangedCaveats:    changedCaveats,
	}, nil
}
