package tuple

import (
	"fmt"
	"time"

	"github.com/jzelinskie/stringz"
	"google.golang.org/protobuf/types/known/timestamppb"

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

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

// ParseV1Rel parses a string representation of a relationship into a v1.Relationship object.
func ParseV1Rel(relString string) (*v1.Relationship, error) {
	parsed, err := Parse(relString)
	if err != nil {
		return nil, err
	}

	return ToV1Relationship(parsed), nil
}

// Same as above, but panics if it cannot be parsed. Should only be used in tests.
func MustParseV1Rel(relString string) *v1.Relationship {
	parsed, err := Parse(relString)
	if err != nil {
		panic(fmt.Sprintf("could not parse relationship string: %s %s", relString, err))
	}

	return ToV1Relationship(parsed)
}

// MustV1RelString converts a relationship into a string.  Will panic if
// the Relationship does not validate.
func MustV1RelString(rel *v1.Relationship) string {
	if err := rel.Validate(); err != nil {
		panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err))
	}
	return MustV1StringRelationship(rel)
}

// StringObjectRef marshals a *v1.ObjectReference into a string.
//
// This function assumes that the provided values have already been validated.
func V1StringObjectRef(ref *v1.ObjectReference) string {
	return JoinObjectRef(ref.ObjectType, ref.ObjectId)
}

// StringSubjectRef marshals a *v1.SubjectReference into a string.
//
// This function assumes that the provided values have already been validated.
func V1StringSubjectRef(ref *v1.SubjectReference) string {
	if ref.OptionalRelation == "" {
		return V1StringObjectRef(ref.Object)
	}
	return JoinRelRef(V1StringObjectRef(ref.Object), ref.OptionalRelation)
}

// MustV1StringRelationship converts a v1.Relationship to a string.
func MustV1StringRelationship(rel *v1.Relationship) string {
	relString, err := V1StringRelationship(rel)
	if err != nil {
		panic(err)
	}
	return relString
}

// V1StringRelationship converts a v1.Relationship to a string.
func V1StringRelationship(rel *v1.Relationship) (string, error) {
	if rel == nil || rel.Resource == nil || rel.Subject == nil {
		return "", nil
	}

	caveatString, err := V1StringCaveatRef(rel.OptionalCaveat)
	if err != nil {
		return "", err
	}

	expirationString, err := V1StringExpiration(rel.OptionalExpiresAt)
	if err != nil {
		return "", err
	}

	return V1StringRelationshipWithoutCaveatOrExpiration(rel) + caveatString + expirationString, nil
}

func V1StringExpiration(expiration *timestamppb.Timestamp) (string, error) {
	if expiration == nil {
		return "", nil
	}

	return "[expiration:" + expiration.AsTime().Format(expirationFormat) + "]", nil
}

// V1StringRelationshipWithoutCaveatOrExpiration converts a v1.Relationship to a string, excluding any caveat.
func V1StringRelationshipWithoutCaveatOrExpiration(rel *v1.Relationship) string {
	if rel == nil || rel.Resource == nil || rel.Subject == nil {
		return ""
	}

	return V1StringObjectRef(rel.Resource) + "#" + rel.Relation + "@" + V1StringSubjectRef(rel.Subject)
}

// V1StringCaveatRef converts a v1.ContextualizedCaveat to a string.
func V1StringCaveatRef(caveat *v1.ContextualizedCaveat) (string, error) {
	if caveat == nil || caveat.CaveatName == "" {
		return "", nil
	}

	contextString, err := StringCaveatContext(caveat.Context)
	if err != nil {
		return "", err
	}

	if len(contextString) > 0 {
		contextString = ":" + contextString
	}

	return "[" + caveat.CaveatName + contextString + "]", nil
}

// UpdateToV1RelationshipUpdate converts a RelationshipUpdate into a
// v1.RelationshipUpdate.
func UpdateToV1RelationshipUpdate(update RelationshipUpdate) (*v1.RelationshipUpdate, error) {
	var op v1.RelationshipUpdate_Operation
	switch update.Operation {
	case UpdateOperationCreate:
		op = v1.RelationshipUpdate_OPERATION_CREATE
	case UpdateOperationDelete:
		op = v1.RelationshipUpdate_OPERATION_DELETE
	case UpdateOperationTouch:
		op = v1.RelationshipUpdate_OPERATION_TOUCH
	default:
		return nil, spiceerrors.MustBugf("unknown update operation: %v", update.Operation)
	}

	return &v1.RelationshipUpdate{
		Operation:    op,
		Relationship: ToV1Relationship(update.Relationship),
	}, nil
}

// MustUpdateToV1RelationshipUpdate converts a RelationshipUpdate into a
// v1.RelationshipUpdate. Panics on error.
func MustUpdateToV1RelationshipUpdate(update RelationshipUpdate) *v1.RelationshipUpdate {
	v1rel, err := UpdateToV1RelationshipUpdate(update)
	if err != nil {
		panic(err)
	}

	return v1rel
}

// UpdateFromV1RelationshipUpdate converts a RelationshipUpdate into a
// RelationTupleUpdate.
func UpdateFromV1RelationshipUpdate(update *v1.RelationshipUpdate) (RelationshipUpdate, error) {
	var op UpdateOperation
	switch update.Operation {
	case v1.RelationshipUpdate_OPERATION_CREATE:
		op = UpdateOperationCreate
	case v1.RelationshipUpdate_OPERATION_DELETE:
		op = UpdateOperationDelete
	case v1.RelationshipUpdate_OPERATION_TOUCH:
		op = UpdateOperationTouch
	default:
		return RelationshipUpdate{}, spiceerrors.MustBugf("unknown update operation: %v", update.Operation)
	}

	return RelationshipUpdate{
		Operation:    op,
		Relationship: FromV1Relationship(update.Relationship),
	}, nil
}

// FromV1Relationship converts a v1.Relationship into a Relationship.
func FromV1Relationship(rel *v1.Relationship) Relationship {
	var caveat *core.ContextualizedCaveat
	if rel.OptionalCaveat != nil {
		caveat = &core.ContextualizedCaveat{
			CaveatName: rel.OptionalCaveat.CaveatName,
			Context:    rel.OptionalCaveat.Context,
		}
	}

	var expiration *time.Time
	if rel.OptionalExpiresAt != nil {
		t := rel.OptionalExpiresAt.AsTime()
		expiration = &t
	}

	return Relationship{
		RelationshipReference: RelationshipReference{
			Resource: ObjectAndRelation{
				ObjectID:   rel.Resource.ObjectId,
				ObjectType: rel.Resource.ObjectType,
				Relation:   rel.Relation,
			},
			Subject: ObjectAndRelation{
				ObjectID:   rel.Subject.Object.ObjectId,
				ObjectType: rel.Subject.Object.ObjectType,
				Relation:   stringz.Default(rel.Subject.OptionalRelation, Ellipsis, ""),
			},
		},
		OptionalCaveat:     caveat,
		OptionalExpiration: expiration,
	}
}

// ToV1Relationship converts a Relationship into a v1.Relationship.
func ToV1Relationship(rel Relationship) *v1.Relationship {
	var caveat *v1.ContextualizedCaveat
	if rel.OptionalCaveat != nil {
		caveat = &v1.ContextualizedCaveat{
			CaveatName: rel.OptionalCaveat.CaveatName,
			Context:    rel.OptionalCaveat.Context,
		}
	}

	var expiration *timestamppb.Timestamp
	if rel.OptionalExpiration != nil {
		expiration = timestamppb.New(*rel.OptionalExpiration)
	}

	return &v1.Relationship{
		Resource: &v1.ObjectReference{
			ObjectType: rel.Resource.ObjectType,
			ObjectId:   rel.Resource.ObjectID,
		},
		Relation: rel.Resource.Relation,
		Subject: &v1.SubjectReference{
			Object: &v1.ObjectReference{
				ObjectType: rel.Subject.ObjectType,
				ObjectId:   rel.Subject.ObjectID,
			},
			OptionalRelation: stringz.Default(rel.Subject.Relation, "", Ellipsis),
		},
		OptionalCaveat:    caveat,
		OptionalExpiresAt: expiration,
	}
}

// CopyToV1Relationship copies a Relationship into a v1.Relationship.
func CopyToV1Relationship(rel Relationship, v1rel *v1.Relationship) {
	v1rel.Resource.ObjectType = rel.Resource.ObjectType
	v1rel.Resource.ObjectId = rel.Resource.ObjectID
	v1rel.Relation = rel.Resource.Relation
	v1rel.Subject.Object.ObjectType = rel.Subject.ObjectType
	v1rel.Subject.Object.ObjectId = rel.Subject.ObjectID
	v1rel.Subject.OptionalRelation = stringz.Default(rel.Subject.Relation, "", Ellipsis)

	if rel.OptionalCaveat != nil {
		if v1rel.OptionalCaveat == nil {
			v1rel.OptionalCaveat = &v1.ContextualizedCaveat{}
		}

		v1rel.OptionalCaveat.CaveatName = rel.OptionalCaveat.CaveatName
		v1rel.OptionalCaveat.Context = rel.OptionalCaveat.Context
	} else {
		v1rel.OptionalCaveat = nil
	}

	if rel.OptionalExpiration != nil {
		v1rel.OptionalExpiresAt = timestamppb.New(*rel.OptionalExpiration)
	} else {
		v1rel.OptionalExpiresAt = nil
	}
}

// UpdatesToV1RelationshipUpdates converts a slice of RelationshipUpdate into a
// slice of v1.RelationshipUpdate.
func UpdatesToV1RelationshipUpdates(updates []RelationshipUpdate) ([]*v1.RelationshipUpdate, error) {
	relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates))

	for _, update := range updates {
		converted, err := UpdateToV1RelationshipUpdate(update)
		if err != nil {
			return nil, err
		}

		relationshipUpdates = append(relationshipUpdates, converted)
	}

	return relationshipUpdates, nil
}

// UpdatesFromV1RelationshipUpdates converts a slice of v1.RelationshipUpdate into a
// slice of RelationshipUpdate.
func UpdatesFromV1RelationshipUpdates(updates []*v1.RelationshipUpdate) ([]RelationshipUpdate, error) {
	relationshipUpdates := make([]RelationshipUpdate, 0, len(updates))

	for _, update := range updates {
		converted, err := UpdateFromV1RelationshipUpdate(update)
		if err != nil {
			return nil, err
		}

		relationshipUpdates = append(relationshipUpdates, converted)
	}

	return relationshipUpdates, nil
}

// ToV1Filter converts a RelationTuple into a RelationshipFilter.
func ToV1Filter(rel Relationship) *v1.RelationshipFilter {
	return &v1.RelationshipFilter{
		ResourceType:          rel.Resource.ObjectType,
		OptionalResourceId:    rel.Resource.ObjectID,
		OptionalRelation:      rel.Resource.Relation,
		OptionalSubjectFilter: SubjectONRToSubjectFilter(rel.Subject),
	}
}

// SubjectONRToSubjectFilter converts a userset to the equivalent exact SubjectFilter.
func SubjectONRToSubjectFilter(subject ObjectAndRelation) *v1.SubjectFilter {
	return &v1.SubjectFilter{
		SubjectType:       subject.ObjectType,
		OptionalSubjectId: subject.ObjectID,
		OptionalRelation: &v1.SubjectFilter_RelationFilter{
			Relation: stringz.Default(subject.Relation, "", Ellipsis),
		},
	}
}

// RelToFilter converts a Relationship into a RelationshipFilter.
func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter {
	return &v1.RelationshipFilter{
		ResourceType:       rel.Resource.ObjectType,
		OptionalResourceId: rel.Resource.ObjectId,
		OptionalRelation:   rel.Relation,
		OptionalSubjectFilter: &v1.SubjectFilter{
			SubjectType:       rel.Subject.Object.ObjectType,
			OptionalSubjectId: rel.Subject.Object.ObjectId,
			OptionalRelation: &v1.SubjectFilter_RelationFilter{
				Relation: rel.Subject.OptionalRelation,
			},
		},
	}
}
