//                           _       _
// __      _____  __ ___   ___  __ _| |_ ___
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
//
//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
//
//  CONTACT: hello@weaviate.io
//

package validation

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/go-openapi/strfmt"
	"github.com/weaviate/weaviate/entities/additional"
	"github.com/weaviate/weaviate/entities/models"
	"github.com/weaviate/weaviate/entities/schema/crossref"
	"github.com/weaviate/weaviate/usecases/config"
)

type exists func(_ context.Context, class string, _ strfmt.UUID, _ *additional.ReplicationProperties, _ string) (bool, error)

const (
	// ErrorMissingActionObjects message
	ErrorMissingActionObjects string = "no objects, object and subject, are added. Add 'objects' by using the 'objects' key in the root of the JSON"
	// ErrorMissingActionObjectsObject message
	ErrorMissingActionObjectsObject string = "no object-thing is added. Add the 'object' inside the 'objects' part of the JSON"
	// ErrorMissingActionObjectsSubject message
	ErrorMissingActionObjectsSubject string = "no subject-thing is added. Add the 'subject' inside the 'objects' part of the JSON"
	// ErrorMissingActionObjectsObjectLocation message
	ErrorMissingActionObjectsObjectLocation string = "no 'locationURL' is found in the object-thing. Add the 'locationURL' inside the 'object-thing' part of the JSON"
	// ErrorMissingActionObjectsObjectType message
	ErrorMissingActionObjectsObjectType string = "no 'type' is found in the object-thing. Add the 'type' inside the 'object-thing' part of the JSON"
	// ErrorInvalidActionObjectsObjectType message
	ErrorInvalidActionObjectsObjectType string = "object-thing requires one of the following values in 'type': '%s', '%s' or '%s'"
	// ErrorMissingActionObjectsSubjectLocation message
	ErrorMissingActionObjectsSubjectLocation string = "no 'locationURL' is found in the subject-thing. Add the 'locationURL' inside the 'subject-thing' part of the JSON"
	// ErrorMissingActionObjectsSubjectType message
	ErrorMissingActionObjectsSubjectType string = "no 'type' is found in the subject-thing. Add the 'type' inside the 'subject-thing' part of the JSON"
	// ErrorInvalidActionObjectsSubjectType message
	ErrorInvalidActionObjectsSubjectType string = "subject-thing requires one of the following values in 'type': '%s', '%s' or '%s'"
	// ErrorMisingObject message
	ErrorMissingObject = "the given object is empty"
	// ErrorMissingClass message
	ErrorMissingClass string = "the given class is empty"
	// ErrorMissingContext message
	ErrorMissingContext string = "the given context is empty"
	// ErrorNoExternalCredentials message
	ErrorNoExternalCredentials string = "no credentials available for the Weaviate instance for %s given in the %s"
	// ErrorExternalNotFound message
	ErrorExternalNotFound string = "given statuscode of '%s' is '%d', but 200 was expected for LocationURL given in the %s"
	// ErrorInvalidCRefType message
	ErrorInvalidCRefType string = "'cref' type '%s' does not exists"
	// ErrorNotFoundInDatabase message
	ErrorNotFoundInDatabase string = "%s: no object with id %s found"
	// ErrorInvalidProperties message
	ErrorInvalidProperties string = "properties of object %v must be of type map[string]interface"
)

type Validator struct {
	exists           exists
	config           *config.WeaviateConfig
	replicationProps *additional.ReplicationProperties
}

func New(exists exists, config *config.WeaviateConfig,
	repl *additional.ReplicationProperties,
) *Validator {
	return &Validator{
		exists:           exists,
		config:           config,
		replicationProps: repl,
	}
}

func (v *Validator) Object(ctx context.Context, class *models.Class,
	incoming *models.Object, existing *models.Object,
) error {
	if incoming.Class == "" {
		return errors.New(ErrorMissingClass)
	}

	if err := v.vector(ctx, class, incoming); err != nil {
		return err
	}

	return v.properties(ctx, class, incoming, existing)
}

// ValidateSingleRef validates a single ref based on location URL and existence of the object in the database
func (v *Validator) ValidateSingleRef(cref *models.SingleRef) (*crossref.Ref, error) {
	ref, err := crossref.ParseSingleRef(cref)
	if err != nil {
		return nil, fmt.Errorf("invalid reference: %w", err)
	}

	// target id must be lowercase
	ref.TargetID = strfmt.UUID(strings.ToLower(ref.TargetID.String()))

	if !ref.Local {
		return nil, fmt.Errorf("unrecognized cross-ref ref format")
	}

	return ref, nil
}

func (v *Validator) ValidateExistence(ctx context.Context, ref *crossref.Ref, errorVal string, tenant string) error {
	// locally check for object existence
	ok, err := v.exists(ctx, ref.Class, ref.TargetID, v.replicationProps, tenant)
	if err != nil {
		if tenant == "" {
			return err
		}
		// since refs can be created to non-MT classes, check again if non-MT object exists
		// (use empty tenant, if previously was given)
		ok2, err2 := v.exists(ctx, ref.Class, ref.TargetID, v.replicationProps, "")
		if err2 != nil {
			// return orig error
			return err
		}
		ok = ok2
	}
	if !ok {
		return fmt.Errorf(ErrorNotFoundInDatabase, errorVal, ref.TargetID)
	}

	return nil
}

func (v *Validator) ValidateMultipleRef(ctx context.Context, refs models.MultipleRef,
	errorVal string, tenant string,
) ([]*crossref.Ref, error) {
	parsedRefs := make([]*crossref.Ref, len(refs))

	if refs == nil {
		return parsedRefs, nil
	}

	for i, ref := range refs {
		parsedRef, err := v.ValidateSingleRef(ref)
		if err != nil {
			return nil, err
		}
		parsedRefs[i] = parsedRef

	}
	return parsedRefs, nil
}
