// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package internal // import "go.opentelemetry.io/collector/confmap/internal"

import (
	"reflect"

	"github.com/go-viper/mapstructure/v2"

	encoder "go.opentelemetry.io/collector/confmap/internal/mapstructure"
)

func Encode(rawVal any, set MarshalOptions) (any, error) {
	enc := encoder.New(EncoderConfig(rawVal, set))
	data, err := enc.Encode(rawVal)
	if err != nil {
		return nil, err
	}
	return data, nil
}

// EncoderConfig returns a default encoder.EncoderConfig that includes
// an EncodeHook that handles both TextMarshaler and Marshaler
// interfaces.
func EncoderConfig(rawVal any, opts MarshalOptions) *encoder.EncoderConfig {
	hooks := []mapstructure.DecodeHookFunc{
		encoder.YamlMarshalerHookFunc(),
	}

	if opts.OpaqueUnredacted {
		hooks = append(hooks, encoder.StringTextUnredactedHookFunc())
	}

	hooks = append(hooks,
		encoder.TextMarshalerHookFunc(),
		// This must come before unmarshalerHookFunc; the two may both want to trigger
		// their corresponding interface for structs implementing both, and the scalar
		// interfaces are the ones that will sometimes defer to the non-scalar interfaces.
		scalarMarshalerHookFunc(),
		marshalerHookFunc(rawVal),
	)

	return &encoder.EncoderConfig{
		EncodeHook: mapstructure.ComposeDecodeHookFunc(hooks...),
	}
}

// marshalerHookFunc returns a DecodeHookFuncValue that checks structs that aren't
// the original to see if they implement the Marshaler interface.
func marshalerHookFunc(orig any) mapstructure.DecodeHookFuncValue {
	origType := reflect.TypeOf(orig)
	return safeWrapDecodeHookFunc(func(from, _ reflect.Value) (any, error) {
		if from.Kind() != reflect.Struct {
			return from.Interface(), nil
		}

		// ignore original to avoid infinite loop.
		if from.Type() == origType && reflect.DeepEqual(from.Interface(), orig) {
			return from.Interface(), nil
		}
		marshaler, ok := from.Interface().(Marshaler)
		if !ok {
			return from.Interface(), nil
		}
		conf := NewFromStringMap(nil)
		if err := marshaler.Marshal(conf); err != nil {
			return nil, err
		}

		stringMap := conf.ToStringMap()
		if stringMap == nil {
			// If conf is still nil after marshaling, we want to encode it as an untyped nil
			// instead of a map-typed nil. This ensures the value is a proper null value
			// in the final marshaled output instead of an empty map. We hit this case
			// when marshaling wrapper structs that have no direct representation
			// in the marshaled output that aren't tagged with "squash" on the fields
			// they're used on.
			return nil, nil
		}
		return stringMap, nil
	})
}
