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

package hfresh

import (
	"errors"
	"fmt"

	schemaConfig "github.com/weaviate/weaviate/entities/schema/config"
	vectorIndexCommon "github.com/weaviate/weaviate/entities/vectorindex/common"
)

const (
	DefaultMaxPostingSizeKB = 48
	MaxPostingSizeKBFloor   = 8
	DefaultReplicas         = 4
	DefaultRNGFactor        = 10.0
	DefaultSearchProbe      = 64
	DefaultRescoreLimit     = 350
)

// UserConfig defines the configuration options for the HFresh index.
// Will be populated once we decide what should be exposed.
type UserConfig struct {
	MaxPostingSizeKB uint32  `json:"maxPostingSizeKB"`
	Replicas         uint32  `json:"replicas"`
	RNGFactor        float32 `json:"rngFactor"`
	SearchProbe      uint32  `json:"searchProbe"`
	Distance         string  `json:"distance"`
	RescoreLimit     uint32  `json:"rescoreLimit"`
}

// IndexType returns the type of the underlying vector index, thus making sure
// the schema.VectorIndexConfig interface is implemented
func (u UserConfig) IndexType() string {
	return "hfresh"
}

func (u UserConfig) DistanceName() string {
	return u.Distance
}

func (u UserConfig) IsMultiVector() bool {
	return false
}

// SetDefaults in the user-specifyable part of the config
func (u *UserConfig) SetDefaults() {
	u.MaxPostingSizeKB = DefaultMaxPostingSizeKB
	u.Replicas = DefaultReplicas
	u.RNGFactor = DefaultRNGFactor
	u.SearchProbe = DefaultSearchProbe
	u.Distance = vectorIndexCommon.DefaultDistanceMetric
	u.RescoreLimit = DefaultRescoreLimit
	// TODO: add quantization config
}

func NewDefaultUserConfig() UserConfig {
	var uc UserConfig
	uc.SetDefaults()
	return uc
}

func (u *UserConfig) validate() error {
	var errs []error

	if u.Distance != vectorIndexCommon.DistanceCosine && u.Distance != vectorIndexCommon.DistanceL2Squared {
		errs = append(errs, fmt.Errorf(
			"unsupported distance type '%s', HFresh only supports 'cosine' or 'l2-squared' for the distance metric",
			u.Distance,
		))
	}

	if u.MaxPostingSizeKB < MaxPostingSizeKBFloor {
		errs = append(errs, fmt.Errorf(
			"maxPostingSizeKB is '%d' but must be at least %d",
			u.MaxPostingSizeKB,
			MaxPostingSizeKBFloor,
		))
	}

	if len(errs) > 0 {
		return fmt.Errorf("invalid hnsw config: %w", errors.Join(errs...))
	}

	return nil
}

// ParseAndValidateConfig from an unknown input value, as this is not further
// specified in the API to allow of exchanging the index type
func ParseAndValidateConfig(input interface{}, isMultiVector bool) (schemaConfig.VectorIndexConfig, error) {
	uc := UserConfig{}
	uc.SetDefaults()

	if input == nil {
		return uc, nil
	}

	asMap, ok := input.(map[string]interface{})
	if !ok || asMap == nil {
		return uc, fmt.Errorf("input must be a non-nil map")
	}

	if err := vectorIndexCommon.OptionalIntFromMap(asMap, "maxPostingSizeKB", func(v int) {
		uc.MaxPostingSizeKB = uint32(v)
	}); err != nil {
		return uc, err
	}

	if err := vectorIndexCommon.OptionalIntFromMap(asMap, "replicas", func(v int) {
		uc.Replicas = uint32(v)
	}); err != nil {
		return uc, err
	}

	if err := vectorIndexCommon.OptionalIntFromMap(asMap, "rngFactor", func(v int) {
		uc.RNGFactor = float32(v)
	}); err != nil {
		return uc, err
	}

	if err := vectorIndexCommon.OptionalIntFromMap(asMap, "searchProbe", func(v int) {
		uc.SearchProbe = uint32(v)
	}); err != nil {
		return uc, err
	}

	if err := vectorIndexCommon.OptionalIntFromMap(asMap, "rescoreLimit", func(v int) {
		if v >= 0 {
			uc.RescoreLimit = uint32(v)
		}
	}); err != nil {
		return uc, err
	}

	if err := vectorIndexCommon.OptionalStringFromMap(asMap, "distance", func(v string) {
		uc.Distance = v
	}); err != nil {
		return uc, err
	}

	// TODO: add quantization config

	return uc, uc.validate()
}
