/*
Copyright (c) 2021 VMware, Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package simulator

import (
	"errors"
	"fmt"
	"math/rand"
	"regexp"

	"github.com/vmware/govmomi/eam/types"
	vimobj "github.com/vmware/govmomi/object"
	"github.com/vmware/govmomi/simulator"
	vimmo "github.com/vmware/govmomi/vim25/mo"
	vim "github.com/vmware/govmomi/vim25/types"
)

var (
	// fsOrHTTPRx matches a URI that is either on the local file
	// system or an HTTP endpoint.
	fsOrHTTPRx = regexp.MustCompile(`^(?:\.?/)|(?:http[s]?:)`)
)

var (
	agentVmDatastoreEmptyErr     = errors.New("AgentVmDatastore is empty")
	agentVmNetworkEmptyErr       = errors.New("AgentVmNetwork is empty")
	foldersEmptyErr              = errors.New("Folders is empty")
	scopeComputeResourceEmptyErr = errors.New("Scope.ComputeResource is empty")
	poolRefNilErr                = errors.New("Unable to determine ResourcePool from ComputeResource")
)

func getAgentVMPlacementOptions(
	ctx *simulator.Context,
	reg *simulator.Registry,
	r *rand.Rand, i int,
	baseAgencyConfig types.BaseAgencyConfigInfo) (AgentVMPlacementOptions, error) {

	var opts AgentVMPlacementOptions
	agencyConfig := baseAgencyConfig.GetAgencyConfigInfo()
	if l := len(agencyConfig.AgentVmDatastore); l == 0 {
		return opts, agentVmDatastoreEmptyErr
	} else if l == len(agencyConfig.AgentConfig) {
		opts.datastore = agencyConfig.AgentVmDatastore[i]
	} else {
		opts.datastore = agencyConfig.AgentVmDatastore[r.Intn(l)]
	}

	if l := len(agencyConfig.AgentVmNetwork); l == 0 {
		return opts, agentVmNetworkEmptyErr
	} else if l == len(agencyConfig.AgentConfig) {
		opts.network = agencyConfig.AgentVmNetwork[i]
	} else {
		opts.network = agencyConfig.AgentVmNetwork[r.Intn(l)]
	}

	if l := len(agencyConfig.Folders); l == 0 {
		return opts, foldersEmptyErr
	} else if l == len(agencyConfig.AgentConfig) {
		opts.folder = agencyConfig.Folders[i].FolderId
		opts.datacenter = agencyConfig.Folders[i].DatacenterId
	} else {
		j := r.Intn(l)
		opts.folder = agencyConfig.Folders[j].FolderId
		opts.datacenter = agencyConfig.Folders[j].DatacenterId
	}

	if l := len(agencyConfig.ResourcePools); l == 0 {
		switch tscope := agencyConfig.Scope.(type) {
		case *types.AgencyComputeResourceScope:
			crRefs := tscope.ComputeResource
			if l := len(crRefs); l == 0 {
				return opts, scopeComputeResourceEmptyErr
			} else if l == len(agencyConfig.AgentConfig) {
				opts.computeResource = crRefs[i]
			} else {
				opts.computeResource = crRefs[r.Intn(l)]
			}
			poolRef, err := getPoolFromComputeResource(ctx, reg, opts.computeResource)
			if err != nil {
				return opts, err
			}
			opts.pool = *poolRef
		}
	} else if l == len(agencyConfig.AgentConfig) {
		opts.pool = agencyConfig.ResourcePools[i].ResourcePoolId
		opts.computeResource = agencyConfig.ResourcePools[i].ComputeResourceId
	} else {
		j := r.Intn(l)
		opts.pool = agencyConfig.ResourcePools[j].ResourcePoolId
		opts.computeResource = agencyConfig.ResourcePools[j].ComputeResourceId
	}

	hosts := getHostsFromPool(ctx, reg, opts.pool)
	if l := len(hosts); l == 0 {
		return opts, fmt.Errorf("HostSystems not found for %v", opts.pool)
	} else if l == len(agencyConfig.AgentConfig) {
		opts.host = hosts[i]
	} else {
		opts.host = hosts[r.Intn(l)]
	}

	return opts, nil
}

func getPoolFromComputeResource(
	ctx *simulator.Context,
	reg *simulator.Registry,
	computeResource vim.ManagedObjectReference) (*vim.ManagedObjectReference, error) {

	var poolRef *vim.ManagedObjectReference
	crObj := reg.Get(computeResource)
	if crObj == nil {
		return nil, fmt.Errorf("%v not in registry", computeResource)
	}
	ctx.WithLock(crObj, func() {
		switch cr := crObj.(type) {
		case *vimmo.ComputeResource:
			poolRef = cr.ResourcePool
		case *simulator.ClusterComputeResource:
			poolRef = cr.ResourcePool
		default:
			panic(fmt.Errorf(
				"%v is not a %s or %s",
				crObj,
				"*mo.ComputeResource",
				"*simulator.ClusterComputeResource",
			))
		}
	})
	if poolRef == nil {
		return nil, poolRefNilErr
	}
	return poolRef, nil
}

// getHostsFromPool returns the host(s) for the provided compute resource.
func getHostsFromPool(
	ctx *simulator.Context,
	reg *simulator.Registry,
	poolRef vim.ManagedObjectReference) []vim.ManagedObjectReference {

	pool := reg.Get(poolRef).(vimmo.Entity)
	cr := getEntityComputeResource(reg, pool)

	var hosts []vim.ManagedObjectReference

	ctx.WithLock(cr, func() {
		switch cr := cr.(type) {
		case *vimmo.ComputeResource:
			hosts = cr.Host
		case *simulator.ClusterComputeResource:
			hosts = cr.Host
		}
	})

	return hosts
}

// hostsWithDatastore returns hosts that have access to the given datastore path
func hostsWithDatastore( // nolint:unused nolint:deadcode
	reg *simulator.Registry,
	hosts []vim.ManagedObjectReference, path string) []vim.ManagedObjectReference {

	attached := hosts[:0]
	var p vimobj.DatastorePath
	p.FromString(path)

	for _, host := range hosts {
		h := reg.Get(host).(*simulator.HostSystem)
		if reg.FindByName(p.Datastore, h.Datastore) != nil {
			attached = append(attached, host)
		}
	}

	return attached
}

// getEntityComputeResource returns the ComputeResource parent for the given item.
// A ResourcePool for example may have N Parents of type ResourcePool, but the top
// most Parent pool is always a ComputeResource child.
func getEntityComputeResource(
	reg *simulator.Registry,
	item vimmo.Entity) vimmo.Entity {

	for {
		parent := item.Entity().Parent
		item = reg.Get(*parent).(vimmo.Entity)
		switch item.Reference().Type {
		case "ComputeResource":
			return item
		case "ClusterComputeResource":
			return item
		}
	}
}
