// © Broadcom. All Rights Reserved.
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
// SPDX-License-Identifier: Apache-2.0

package mo

import (
	"context"
	"reflect"

	"github.com/vmware/govmomi/vim25/methods"
	"github.com/vmware/govmomi/vim25/soap"
	"github.com/vmware/govmomi/vim25/types"
)

func ignoreMissingProperty(ref types.ManagedObjectReference, p types.MissingProperty) bool {
	switch ref.Type {
	case "VirtualMachine":
		switch p.Path {
		case "environmentBrowser":
			// See https://github.com/vmware/govmomi/pull/242
			return true
		case "alarmActionsEnabled":
			// Seen with vApp child VM
			return true
		}
	case "ResourcePool":
		switch p.Path {
		case "resourceConfigSpecDetailed":
			return true
		}
	}

	return false
}

// ObjectContentToType loads an ObjectContent value into the value it
// represents. If the ObjectContent value has a non-empty 'MissingSet' field,
// it returns the first fault it finds there as error. If the 'MissingSet'
// field is empty, it returns a pointer to a reflect.Value. It handles contain
// nested properties, such as 'guest.ipAddress' or 'config.hardware'.
func ObjectContentToType(o types.ObjectContent, ptr ...bool) (any, error) {
	// Expect no properties in the missing set
	for _, p := range o.MissingSet {
		if ignoreMissingProperty(o.Obj, p) {
			continue
		}

		return nil, soap.WrapVimFault(p.Fault.Fault)
	}

	ti := typeInfoForType(o.Obj.Type)
	v, err := ti.LoadFromObjectContent(o)
	if err != nil {
		return nil, err
	}

	if len(ptr) == 1 && ptr[0] {
		return v.Interface(), nil
	}
	return v.Elem().Interface(), nil
}

// ApplyPropertyChange converts the response of a call to WaitForUpdates
// and applies it to the given managed object.
func ApplyPropertyChange(obj Reference, changes []types.PropertyChange) {
	t := typeInfoForType(obj.Reference().Type)
	v := reflect.ValueOf(obj)

	for _, p := range changes {
		var field Field
		if !field.FromString(p.Name) {
			panic(p.Name + ": invalid property path")
		}

		rv, ok := t.props[field.Path]
		if !ok {
			panic(field.Path + ": property not found")
		}

		if field.Key == nil { // Key is only used for notifications
			assignValue(v, rv, reflect.ValueOf(p.Val))
		}
	}
}

// LoadObjectContent converts the response of a call to
// RetrieveProperties{Ex} to one or more managed objects.
func LoadObjectContent(content []types.ObjectContent, dst any) error {
	rt := reflect.TypeOf(dst)
	if rt == nil || rt.Kind() != reflect.Ptr {
		panic("need pointer")
	}

	rv := reflect.ValueOf(dst).Elem()
	if !rv.CanSet() {
		panic("cannot set dst")
	}

	isSlice := false
	switch rt.Elem().Kind() {
	case reflect.Struct:
	case reflect.Slice:
		isSlice = true
	default:
		panic("unexpected type")
	}

	if isSlice {
		for _, p := range content {
			v, err := ObjectContentToType(p)
			if err != nil {
				return err
			}

			vt := reflect.TypeOf(v)

			if !rv.Type().AssignableTo(vt) {
				// For example: dst is []ManagedEntity, res is []HostSystem
				if field, ok := vt.FieldByName(rt.Elem().Elem().Name()); ok && field.Anonymous {
					rv.Set(reflect.Append(rv, reflect.ValueOf(v).FieldByIndex(field.Index)))
					continue
				}
			}

			rv.Set(reflect.Append(rv, reflect.ValueOf(v)))
		}
	} else {
		switch len(content) {
		case 0:
		case 1:
			v, err := ObjectContentToType(content[0])
			if err != nil {
				return err
			}

			vt := reflect.TypeOf(v)

			if !rv.Type().AssignableTo(vt) {
				// For example: dst is ComputeResource, res is ClusterComputeResource
				if field, ok := vt.FieldByName(rt.Elem().Name()); ok && field.Anonymous {
					rv.Set(reflect.ValueOf(v).FieldByIndex(field.Index))
					return nil
				}
			}

			rv.Set(reflect.ValueOf(v))
		default:
			// If dst is not a slice, expect to receive 0 or 1 results
			panic("more than 1 result")
		}
	}

	return nil
}

// RetrievePropertiesEx wraps RetrievePropertiesEx and ContinueRetrievePropertiesEx to collect properties in batches.
func RetrievePropertiesEx(ctx context.Context, r soap.RoundTripper, req types.RetrievePropertiesEx) ([]types.ObjectContent, error) {
	rx, err := methods.RetrievePropertiesEx(ctx, r, &req)
	if err != nil {
		return nil, err
	}

	if rx.Returnval == nil {
		return nil, nil
	}

	objects := rx.Returnval.Objects
	token := rx.Returnval.Token

	for token != "" {
		cx, err := methods.ContinueRetrievePropertiesEx(ctx, r, &types.ContinueRetrievePropertiesEx{
			This:  req.This,
			Token: token,
		})
		if err != nil {
			return nil, err
		}

		token = cx.Returnval.Token
		objects = append(objects, cx.Returnval.Objects...)
	}

	return objects, nil
}

// RetrievePropertiesForRequest calls the RetrieveProperties method with the
// specified request and decodes the response struct into the value pointed to
// by dst.
func RetrievePropertiesForRequest(ctx context.Context, r soap.RoundTripper, req types.RetrieveProperties, dst any) error {
	objects, err := RetrievePropertiesEx(ctx, r, types.RetrievePropertiesEx{
		This:    req.This,
		SpecSet: req.SpecSet,
	})
	if err != nil {
		return err
	}

	return LoadObjectContent(objects, dst)
}

// RetrieveProperties retrieves the properties of the managed object specified
// as obj and decodes the response struct into the value pointed to by dst.
func RetrieveProperties(ctx context.Context, r soap.RoundTripper, pc, obj types.ManagedObjectReference, dst any) error {
	req := types.RetrieveProperties{
		This: pc,
		SpecSet: []types.PropertyFilterSpec{
			{
				ObjectSet: []types.ObjectSpec{
					{
						Obj:  obj,
						Skip: types.NewBool(false),
					},
				},
				PropSet: []types.PropertySpec{
					{
						All:  types.NewBool(true),
						Type: obj.Type,
					},
				},
			},
		},
	}

	return RetrievePropertiesForRequest(ctx, r, req, dst)
}

var morType = reflect.TypeOf((*types.ManagedObjectReference)(nil)).Elem()

// References returns all non-nil moref field values in the given struct.
// Only Anonymous struct fields are followed by default. The optional follow
// param will follow any struct fields when true.
func References(s any, follow ...bool) []types.ManagedObjectReference {
	var refs []types.ManagedObjectReference
	rval := reflect.ValueOf(s)
	rtype := rval.Type()

	if rval.Kind() == reflect.Ptr {
		rval = rval.Elem()
		rtype = rval.Type()
	}

	for i := 0; i < rval.NumField(); i++ {
		val := rval.Field(i)
		finfo := rtype.Field(i)

		if finfo.Anonymous {
			refs = append(refs, References(val.Interface(), follow...)...)
			continue
		}
		if finfo.Name == "Self" {
			continue
		}

		ftype := val.Type()

		if ftype.Kind() == reflect.Slice {
			if ftype.Elem() == morType {
				s := val.Interface().([]types.ManagedObjectReference)
				for i := range s {
					refs = append(refs, s[i])
				}
			}
			continue
		}

		if ftype.Kind() == reflect.Ptr {
			if val.IsNil() {
				continue
			}
			val = val.Elem()
			ftype = val.Type()
		}

		if ftype == morType {
			refs = append(refs, val.Interface().(types.ManagedObjectReference))
			continue
		}

		if len(follow) != 0 && follow[0] {
			if ftype.Kind() == reflect.Struct && val.CanSet() {
				refs = append(refs, References(val.Interface(), follow...)...)
			}
		}
	}

	return refs
}
