package relationships

import (
	"fmt"
	"maps"
	"sort"
	"strings"

	"github.com/lithammer/fuzzysearch/fuzzy"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

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

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

// InvalidSubjectTypeError indicates that a write was attempted with a subject type which is not
// allowed on relation.
type InvalidSubjectTypeError struct {
	error
	relationship      tuple.Relationship
	relationType      *core.AllowedRelation
	additionalDetails map[string]string
}

// NewInvalidSubjectTypeError constructs a new error for attempting to write an invalid subject type.
func NewInvalidSubjectTypeError(
	relationship tuple.Relationship,
	relationType *core.AllowedRelation,
	definition *schema.Definition,
) error {
	allowedTypes, err := definition.AllowedDirectRelationsAndWildcards(relationship.Resource.Relation)
	if err != nil {
		return err
	}

	// Special case: if the subject is uncaveated but only a caveated version is allowed, return
	// a more descriptive error.
	if relationship.OptionalCaveat == nil {
		allowedCaveatsForSubject := mapz.NewSet[string]()

		for _, allowedType := range allowedTypes {
			if allowedType.RequiredCaveat != nil &&
				allowedType.RequiredCaveat.CaveatName != "" &&
				allowedType.Namespace == relationship.Subject.ObjectType &&
				allowedType.GetRelation() == relationship.Subject.Relation &&
				(allowedType.RequiredExpiration != nil) == (relationship.OptionalExpiration != nil) {
				allowedCaveatsForSubject.Add(allowedType.RequiredCaveat.CaveatName)
			}
		}

		if !allowedCaveatsForSubject.IsEmpty() {
			return InvalidSubjectTypeError{
				error: fmt.Errorf(
					"subjects of type `%s` are not allowed on relation `%s#%s` without one of the following caveats: %s",
					schema.SourceForAllowedRelation(relationType),
					relationship.Resource.ObjectType,
					relationship.Resource.Relation,
					strings.Join(allowedCaveatsForSubject.AsSlice(), ","),
				),
				relationship: relationship,
				relationType: relationType,
				additionalDetails: map[string]string{
					"allowed_caveats": strings.Join(allowedCaveatsForSubject.AsSlice(), ","),
				},
			}
		}
	}

	allowedTypeStrings := make([]string, 0, len(allowedTypes))
	for _, allowedType := range allowedTypes {
		allowedTypeStrings = append(allowedTypeStrings, schema.SourceForAllowedRelation(allowedType))
	}

	matches := fuzzy.RankFind(schema.SourceForAllowedRelation(relationType), allowedTypeStrings)
	sort.Sort(matches)
	if len(matches) > 0 {
		return InvalidSubjectTypeError{
			error: fmt.Errorf(
				"subjects of type `%s` are not allowed on relation `%s#%s`; did you mean `%s`?",
				schema.SourceForAllowedRelation(relationType),
				relationship.Resource.ObjectType,
				relationship.Resource.Relation,
				matches[0].Target,
			),
			relationship:      relationship,
			relationType:      relationType,
			additionalDetails: nil,
		}
	}

	return InvalidSubjectTypeError{
		error: fmt.Errorf(
			"subjects of type `%s` are not allowed on relation `%s#%s`",
			schema.SourceForAllowedRelation(relationType),
			relationship.Resource.ObjectType,
			relationship.Resource.Relation,
		),
		relationship:      relationship,
		relationType:      relationType,
		additionalDetails: nil,
	}
}

// GRPCStatus implements retrieving the gRPC status for the error.
func (err InvalidSubjectTypeError) GRPCStatus() *status.Status {
	details := map[string]string{
		"definition_name": err.relationship.Resource.ObjectType,
		"relation_name":   err.relationship.Resource.Relation,
		"subject_type":    schema.SourceForAllowedRelation(err.relationType),
	}

	if err.additionalDetails != nil {
		maps.Copy(details, err.additionalDetails)
	}

	return spiceerrors.WithCodeAndDetails(
		err,
		codes.InvalidArgument,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_INVALID_SUBJECT_TYPE,
			details,
		),
	)
}

// CannotWriteToPermissionError indicates that a write was attempted on a permission.
type CannotWriteToPermissionError struct {
	error
	rel tuple.Relationship
}

// NewCannotWriteToPermissionError constructs a new error for attempting to write to a permission.
func NewCannotWriteToPermissionError(rel tuple.Relationship) CannotWriteToPermissionError {
	return CannotWriteToPermissionError{
		error: fmt.Errorf(
			"cannot write a relationship to permission `%s` under definition `%s`",
			rel.Resource.Relation,
			rel.Resource.ObjectType,
		),
		rel: rel,
	}
}

// GRPCStatus implements retrieving the gRPC status for the error.
func (err CannotWriteToPermissionError) GRPCStatus() *status.Status {
	return spiceerrors.WithCodeAndDetails(
		err,
		codes.InvalidArgument,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_CANNOT_UPDATE_PERMISSION,
			map[string]string{
				"definition_name": err.rel.Resource.ObjectType,
				"permission_name": err.rel.Resource.Relation,
			},
		),
	)
}

// CaveatNotFoundError indicates that a caveat referenced in a relationship update was not found.
type CaveatNotFoundError struct {
	error
	relationship tuple.Relationship
}

// NewCaveatNotFoundError constructs a new caveat not found error.
func NewCaveatNotFoundError(relationship tuple.Relationship) CaveatNotFoundError {
	return CaveatNotFoundError{
		error: fmt.Errorf(
			"the caveat `%s` was not found for relationship `%s`",
			relationship.OptionalCaveat.CaveatName,
			tuple.MustString(relationship),
		),
		relationship: relationship,
	}
}

// GRPCStatus implements retrieving the gRPC status for the error.
func (err CaveatNotFoundError) GRPCStatus() *status.Status {
	return spiceerrors.WithCodeAndDetails(
		err,
		codes.FailedPrecondition,
		spiceerrors.ForReason(
			v1.ErrorReason_ERROR_REASON_UNKNOWN_CAVEAT,
			map[string]string{
				"caveat_name": err.relationship.OptionalCaveat.CaveatName,
			},
		),
	)
}
