// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package core

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"github.com/hashicorp/go-argmapper"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-multierror"
	"github.com/pkg/errors"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"

	"github.com/hashicorp/vagrant-plugin-sdk/component"
	vconfig "github.com/hashicorp/vagrant-plugin-sdk/config"
	"github.com/hashicorp/vagrant-plugin-sdk/core"
	"github.com/hashicorp/vagrant-plugin-sdk/datadir"
	"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
	"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
	"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
	"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cleanup"
	"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/dynamic"
	"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/protomappers"
	"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
	"github.com/hashicorp/vagrant-plugin-sdk/terminal"

	"github.com/hashicorp/vagrant/internal/config"
	"github.com/hashicorp/vagrant/internal/plugin"
	"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
	"github.com/hashicorp/vagrant/internal/serverclient"
)

// Basis represents the core basis which may
// include one or more projects.
//
// The Close function should be called when
// finished with the basis to properly clean
// up any open resources.
type Basis struct {
	basis         *vagrant_server.Basis       // stored basis data
	boxCollection *BoxCollection              // box collection for this basis
	cache         cacher.Cache                // local basis cache
	cleaner       cleanup.Cleanup             // cleanup tasks to be run on close
	client        *serverclient.VagrantClient // client to vagrant server
	corePlugins   *CoreManager                // manager for the core plugin types
	ctx           context.Context             // local context
	dir           *datadir.Basis              // data directory for basis
	factory       *Factory                    // scope factory
	index         *TargetIndex                // index of targets within basis
	jobInfo       *component.JobInfo          // jobInfo is the base job info for executed functions
	logger        hclog.Logger                // basis specific logger
	mappers       []*argmapper.Func           // mappers for basis
	plugins       *plugin.Manager             // basis scoped plugin manager
	ready         bool                        // flag that instance is ready
	seedValues    *core.Seeds                 // seed values to be applied when running commands
	statebag      core.StateBag               // statebag to persist values
	ui            terminal.UI                 // basis UI (non-prefixed)
	vagrantfile   *Vagrantfile                // vagrantfile instance for basis

	m sync.Mutex
}

// NewBasis creates a new Basis with the given options.
func NewBasis(ctx context.Context, opts ...BasisOption) (*Basis, error) {
	var err error
	b := &Basis{
		basis: &vagrant_server.Basis{
			Configuration: &vagrant_server.Vagrantfile{
				Unfinalized: &vagrant_plugin_sdk.Args_Hash{},
				Format:      vagrant_server.Vagrantfile_RUBY,
			},
		},
		cache:      cacher.New(),
		cleaner:    cleanup.New(),
		ctx:        ctx,
		logger:     hclog.L(),
		mappers:    []*argmapper.Func{},
		jobInfo:    &component.JobInfo{},
		seedValues: core.NewSeeds(),
		statebag:   NewStateBag(),
	}

	for _, opt := range opts {
		if oerr := opt(b); oerr != nil {
			err = multierror.Append(err, oerr)
		}
	}

	if err != nil {
		return nil, err
	}

	return b, nil
}

func (b *Basis) Init() error {
	var err error

	// If ready then Init was already run
	if b.ready {
		return nil
	}

	// Client is required to be provided
	if b.client == nil {
		return fmt.Errorf("vagrant server client was not provided to basis")
	}

	// If no plugin manager was provided, force an error
	if b.plugins == nil {
		return fmt.Errorf("plugin manager was not provided to basis")
	}

	// Update our plugin manager to be a sub manager so we close
	// it early if needed
	b.plugins = b.plugins.Sub("basis")

	// Configure our logger
	b.logger = b.logger.ResetNamed("vagrant.core.basis")

	// Attempt to reload the basis to populate our
	// data. If the basis is not found, create it.
	err = b.Reload()
	if err != nil {
		stat, ok := status.FromError(err)
		if !ok || stat.Code() != codes.NotFound {
			return err
		}
		// Project doesn't exist so save it to persist
		if err = b.Save(); err != nil {
			return err
		}
	}

	// If our reloaded data does not include any configuration
	// stub in a default value
	if b.basis.Configuration == nil {
		b.basis.Configuration = &vagrant_server.Vagrantfile{
			Unfinalized: &vagrant_plugin_sdk.Args_Hash{},
			Format:      vagrant_server.Vagrantfile_RUBY,
		}
	}

	// If the basis directory is unset, set it
	if b.dir == nil {
		if b.dir, err = datadir.NewBasis(b.basis.Name); err != nil {
			return err
		}
	}

	// If the mappers aren't already set, load known mappers
	if len(b.mappers) == 0 {
		b.mappers, err = argmapper.NewFuncList(protomappers.All,
			argmapper.Logger(dynamic.Logger),
		)

		if err != nil {
			return err
		}

		locals, err := argmapper.NewFuncList(Mappers, argmapper.Logger(dynamic.Logger))
		if err != nil {
			return err
		}

		b.mappers = append(b.mappers, locals...)
	}

	// Create the manager for handling core plugins
	b.corePlugins = NewCoreManager(b.ctx, b.logger)

	// Setup our index
	b.index = &TargetIndex{
		ctx:    b.ctx,
		logger: b.logger,
		client: b.client,
		basis:  b,
	}

	// If no UI was provided, initialize a console UI
	if b.ui == nil {
		b.ui = terminal.ConsoleUI(b.ctx)
	}

	// Create our vagrantfile
	b.vagrantfile = NewVagrantfile(b.factory, b.boxCollection, b.mappers, b.logger)

	// Register configuration plugins when they are loaded
	b.plugins.Initializer(b.configRegistration)

	// Register any configuration plugins already loaded
	cfgs, err := b.plugins.Typed(component.ConfigType)
	if err != nil {
		return err
	}
	for _, cp := range cfgs {
		b.logger.Trace("registering existing config plugin",
			"name", cp,
		)
		p, err := b.plugins.Get(cp, component.ConfigType)
		if err != nil {
			b.logger.Error("failed to get requested plugin",
				"name", cp,
				"error", err,
			)
			return err
		}
		if err = b.configRegistration(p, b.logger); err != nil {
			return err
		}
	}

	// Configure plugins with cache instance
	b.plugins.Configure(b.setPluginCache)

	// Configure plugins to have seeds set
	b.plugins.Configure(b.setPluginSeeds)

	// If we have legacy vagrant loaded, configure managers
	if b.plugins.LegacyEnabled() {
		// Configure plugins to have plugin manager set (used by legacy)
		b.plugins.Configure(b.setPluginManager)

		// Configure plugins to have a core plugin manager set (used by legacy)
		b.plugins.Configure(b.setPluginCoreManager)
	}

	// Load any plugins that may be available
	if err = b.plugins.Discover(b.dir.ConfigDir().Join("plugins")); err != nil {
		b.logger.Error("basis setup failed during plugin discovery",
			"directory", b.dir.ConfigDir().Join("plugins"),
			"error", err,
		)

		return err
	}

	// Set seeds for any plugins that may be used
	b.seed(nil)

	// Register the basis as a Vagrantfile source
	if err = b.vagrantfile.Source(b.basis.Configuration, VAGRANTFILE_BASIS); err != nil {
		return err
	}

	// Initialize the Vagrantfile for the basis
	if err = b.vagrantfile.Init(); err != nil {
		b.logger.Error("basis setup failed to initialize vagrantfile",
			"error", err,
		)
		return err
	}

	// Store our configuration
	sv, err := b.vagrantfile.GetSource(VAGRANTFILE_BASIS)
	if err != nil {
		return err
	}
	b.basis.Configuration = sv

	// Close the plugin manager
	b.Closer(func() error {
		return b.plugins.Close()
	})

	// Close the core manager
	b.Closer(func() error {
		return b.corePlugins.Close()
	})

	// Close the vagrantfile
	b.Closer(func() error {
		return b.vagrantfile.Close()
	})

	// Close the Target Index
	b.Closer(func() error {
		return b.index.Close()
	})

	// Save ourself when closed
	b.Closer(func() error {
		return b.Save()
	})

	// Mark basis as being initialized
	b.ready = true

	// Include this basis information in log lines
	b.logger = b.logger.With("basis", b)
	b.logger.Info("basis initialized")

	return nil
}

// Provide nice output in logger
func (b *Basis) String() string {
	return fmt.Sprintf("core.Basis:[name: %s resource_id: %s address: %p]",
		b.basis.Name, b.basis.ResourceId, b)
}

// Config implements core.Basis
func (b *Basis) Config() (*vconfig.Vagrantfile, error) {
	return nil, fmt.Errorf("not implemented")
	//return vconfig.DecodeVagrantfile(b.basis.Configuration.Finalized)
}

// CWD implements core.Basis
func (p *Basis) CWD() (path path.Path, err error) {
	return paths.VagrantCwd()
}

// Basis UI is the "default" UI with no prefix modifications
func (b *Basis) UI() (terminal.UI, error) {
	return b.ui, nil
}

// Data directory used for this basis
func (b *Basis) DataDir() (*datadir.Basis, error) {
	return b.dir, nil
}

// DefaultPrivateKey implements core.Basis
func (b *Basis) DefaultPrivateKey() (path path.Path, err error) {
	return b.dir.DataDir().Join("insecure_private_key"), nil
}

// DefaultProvider implements core.Basis
// This is a subset of the Project.DefaultProvider() algorithm, just the parts
// that make sense when you don't have a Vagrantfile
func (b *Basis) DefaultProvider() (string, error) {
	logger := b.logger.Named("default-provider")
	logger.Debug("Searching for default provider")

	defaultProvider := os.Getenv("VAGRANT_DEFAULT_PROVIDER")
	if defaultProvider != "" {
		logger.Debug("Using VAGRANT_DEFAULT_PROVIDER", "provider", defaultProvider)
		return defaultProvider, nil
	}

	usableProviders := []*core.NamedPlugin{}
	pluginProviders, err := b.plugins.ListPlugins("provider")
	if err != nil {
		return "", err
	}
	for _, pp := range pluginProviders {
		logger.Debug("considering plugin", "provider", pp.Name)

		plug, err := b.plugins.GetPlugin(pp.Name, pp.Type)
		if err != nil {
			return "", err
		}

		plugOpts := plug.Options.(*component.ProviderOptions)
		logger.Debug("got provider options", "options", fmt.Sprintf("%#v", plugOpts))

		// Skip providers that can't be defaulted.
		if !plugOpts.Defaultable {
			logger.Debug("skipping non-defaultable provider", "provider", pp.Name)
			continue
		}

		// Skip the providers that aren't usable.
		logger.Debug("Checking usable on provider", "provider", pp.Name)
		pluginImpl := plug.Plugin.(core.Provider)
		usable, err := pluginImpl.Usable()
		if err != nil {
			return "", err
		}
		if !usable {
			logger.Debug("Skipping unusable provider", "provider", pp.Name)
			continue
		}

		// If we made it here we have a candidate usable provider
		usableProviders = append(usableProviders, plug)
	}
	logger.Debug("Initial usable provider list", "usableProviders", usableProviders)

	// Sort by plugin priority, higher is first
	sort.SliceStable(usableProviders, func(i, j int) bool {
		iPriority := usableProviders[i].Options.(*component.ProviderOptions).Priority
		jPriority := usableProviders[j].Options.(*component.ProviderOptions).Priority
		return iPriority > jPriority
	})
	logger.Debug("Priority sorted usable provider list", "usableProviders", usableProviders)

	preferredProviders := strings.Split(os.Getenv("VAGRANT_PREFERRED_PROVIDERS"), ",")
	k := 0
	for _, pp := range preferredProviders {
		spp := strings.TrimSpace(pp) // .map { s.strip }
		if spp != "" {               // .select { !s.empty? }
			preferredProviders[k] = spp
			k++
		}
	}
	preferredProviders = preferredProviders[:k]

	for _, pp := range preferredProviders {
		for _, up := range usableProviders {
			if pp == up.Name {
				logger.Debug("Using preffered provider found in usable list",
					"provider", pp)
				return pp, nil
			}
		}
	}

	if len(usableProviders) > 0 {
		logger.Debug("Using the first provider from the usable list",
			"provider", usableProviders[0])
		return usableProviders[0].Name, nil
	}

	return "", errors.New("No default provider.")
}

// Implements core.Basis
// Returns all the registered plugins of the types specified
func (b *Basis) Plugins(types ...string) (plugins []*core.NamedPlugin, err error) {
	plugins = []*core.NamedPlugin{}
	for _, pluginType := range types {
		typ, err := component.FindType(pluginType)
		if err != nil {
			return nil, err
		}
		components, err := b.typeComponents(b.ctx, typ)
		if err != nil {
			return nil, err
		}
		for name, h := range components {
			plugins = append(plugins, &core.NamedPlugin{
				Plugin: h.Value,
				Name:   name,
				Type:   pluginType,
			})
		}
	}
	return
}

// Generic function for providing ref to a scope
func (b *Basis) Ref() interface{} {
	return &vagrant_plugin_sdk.Ref_Basis{
		ResourceId: b.basis.ResourceId,
		Name:       b.Name(),
	}
}

// Custom name defined for this basis
func (b *Basis) Name() string {
	if b.basis == nil {
		return ""
	}

	return b.basis.Name
}

// Resource ID for this basis
func (b *Basis) ResourceId() (string, error) {
	return b.basis.ResourceId, nil
}

// Returns the job info if currently set
func (b *Basis) JobInfo() *component.JobInfo {
	return b.jobInfo
}

// Client connection to the Vagrant server
func (b *Basis) Client() *serverclient.VagrantClient {
	return b.client
}

func (b *Basis) State() *StateBag {
	return b.statebag.(*StateBag)
}

func (b *Basis) Boxes() (bc core.BoxCollection, err error) {
	if b.boxCollection == nil {
		boxesDir := filepath.Join(b.dir.DataDir().String(), "boxes")
		if _, err := os.Stat(boxesDir); os.IsNotExist(err) {
			err := os.MkdirAll(boxesDir, os.ModePerm)
			if err != nil {
				return nil, err
			}
		}
		b.boxCollection, err = NewBoxCollection(b, boxesDir, b.logger)
		if err != nil {
			return nil, err
		}
	}
	return b.boxCollection, nil
}

// Returns the detected host for the current platform
func (b *Basis) Host() (host core.Host, err error) {
	if h := b.cache.Get("host"); h != nil {
		return h.(core.Host), nil
	}

	// TODO(spox): this is for when we have implemented vagrantfile conversions
	// bConfig, err := b.Config()
	// if err != nil {
	// 	b.logger.Error("failed to get configuration for basis")
	// 	return
	// }

	// if bConfig != nil {
	// 	var hostName string
	// 	if bConfig.Vagrant.Host != nil {
	// 		hostName = *bConfig.Vagrant.Host
	// 	}
	// 	if hostName != "" {
	// 		// If a host is set, then just try to detect that
	// 		hostComponent, err := b.component(b.ctx, component.HostType, hostName)
	// 		if err != nil {
	// 			return nil, fmt.Errorf("failed to find requested host plugin")
	// 		}
	// 		b.cache.Register("host", hostComponent.Value.(core.Host))
	// 		b.logger.Info("host detection overridden by local configuration")
	// 		return hostComponent.Value.(core.Host), nil
	// 	}
	// }

	// If a host is not defined in the Vagrantfile, try to detect it
	hosts, err := b.typeComponents(b.ctx, component.HostType)
	if err != nil {
		return nil, err
	}

	var result core.Host
	var result_name string
	var numParents int

	for name, h := range hosts {
		host := h.Value.(core.Host)
		detected, err := host.Detect(b.statebag)
		if err != nil {
			b.logger.Error("host error on detection check",
				"plugin", name,
				"type", "Host",
				"error", err,
			)

			continue
		}
		if result == nil {
			if detected {
				result = host
				result_name = name
				numParents = h.plugin.ParentCount()
			}
			continue
		}

		if detected {
			hp := h.plugin.ParentCount()
			if hp > numParents {
				result = host
				result_name = name
				numParents = hp
			}
		}
	}

	if result == nil {
		return nil, fmt.Errorf("failed to detect host plugin for current platform")
	}

	b.logger.Info("host detection complete",
		"name", result_name)

	b.cache.Register("host", result)

	return result, nil
}

// Initializes the basis for running a command. This will inspect
// all registered components and extract things like custom command
// information before an actual command is run
func (b *Basis) RunInit() (result *vagrant_server.Job_InitResult, err error) {
	b.logger.Debug("running init for basis")
	result = &vagrant_server.Job_InitResult{
		Commands: []*vagrant_plugin_sdk.Command_CommandInfo{},
	}
	ctx := context.Background()

	cmds, err := b.typeComponents(ctx, component.CommandType)
	if err != nil {
		return nil, err
	}

	for _, c := range cmds {
		fn := c.Value.(component.Command).CommandInfoFunc()
		// See core.JobCommandProto
		raw, err := b.callDynamicFunc(ctx, b.logger, fn,
			(*[]*vagrant_plugin_sdk.Command_CommandInfo)(nil),
			argmapper.Typed(b.ctx),
		)
		if err != nil {
			return nil, err
		}

		// Primary comes from plugin options so add that to CommandInfo here
		cinfos := raw.([]*vagrant_plugin_sdk.Command_CommandInfo)
		copts := c.Options.(*component.CommandOptions)
		cinfos[0].Primary = copts.Primary

		result.Commands = append(result.Commands, cinfos...)
	}

	return
}

// Register functions to be called when closing this basis
func (b *Basis) Closer(c func() error) {
	b.cleaner.Do(c)
}

// Close is called to clean up resources allocated by the basis.
// This should be called and blocked on to gracefully stop the basis.
func (b *Basis) Close() (err error) {
	b.logger.Debug("closing basis")

	return b.cleaner.Close()
}

// Reload basis data
func (b *Basis) Reload() (err error) {
	b.m.Lock()
	defer b.m.Unlock()

	if b.basis.ResourceId == "" {
		return status.Error(codes.NotFound, "basis does not exist")
	}

	result, err := b.client.FindBasis(b.ctx,
		&vagrant_server.FindBasisRequest{
			Basis: b.basis,
		},
	)

	if err != nil {
		return
	}

	b.basis = result.Basis
	return
}

// Saves the basis to the db
func (b *Basis) Save() (err error) {
	b.m.Lock()
	defer b.m.Unlock()

	b.logger.Debug("saving basis to db")

	if b.vagrantfile != nil {
		val, err := b.vagrantfile.rootToStore()
		if err != nil {
			b.logger.Warn("failed to convert modified configuration for save",
				"error", err,
			)
		} else {
			b.basis.Configuration.Finalized = val.Data
		}
	}

	result, err := b.Client().UpsertBasis(b.ctx,
		&vagrant_server.UpsertBasisRequest{
			Basis: b.basis})

	if err != nil {
		b.logger.Trace("failed to save basis",
			"error", err)
		return err
	}

	b.basis = result.Basis
	return
}

func (b *Basis) TargetIndex() (core.TargetIndex, error) {
	return b.index, nil
}

func (b *Basis) Vagrantfile() (core.Vagrantfile, error) {
	return b.vagrantfile, nil
}

// Returns the list of all known components
func (b *Basis) Components(ctx context.Context) ([]*Component, error) {
	return b.components(b.ctx)
}

// Runs a specific task via component which matches the task's
// component name. This is the entry point for running commands.
func (b *Basis) Run(ctx context.Context, task *vagrant_server.Job_CommandOp) (err error) {
	b.logger.Debug("running new command",
		"command", task)

	// Build the component to run
	cmd, err := b.component(ctx, component.CommandType, task.Component.Name)
	if err != nil {
		return err
	}

	fn := cmd.Value.(component.Command).ExecuteFunc(
		strings.Split(task.Command, " "))
	result, err := b.callDynamicFunc(ctx, b.logger, fn, (*int32)(nil),
		argmapper.Typed(task.CliArgs, b.jobInfo, b.dir, b.ctx, b.ui),
		argmapper.ConverterFunc(cmd.mappers...),
	)

	if err != nil || result == nil || result.(int32) != 0 {
		b.logger.Error("failed to execute command",
			"type", component.CommandType,
			"name", task.Component.Name,
			"error", err)

		cmdErr := &runError{}
		if err != nil {
			cmdErr.err = err
		}
		if result != nil {
			cmdErr.exitCode = result.(int32)
		}

		return cmdErr
	}

	return
}

// Load a specific component
func (b *Basis) component(
	ctx context.Context, // context for the plugin
	typ component.Type, // type of component
	name string, // name of the component
) (*Component, error) {
	// If this is a command type component, the plugin is registered
	// as only the root command
	if typ == component.CommandType {
		name = strings.Split(name, " ")[0]
	}
	c, err := b.plugins.Find(name, typ)
	if err != nil {
		return nil, err
	}

	// TODO(spox): we need to add hooks

	hooks := map[string][]*config.Hook{}
	return &Component{
		Value: c.Component,
		Info: &vagrant_server.Component{
			Type:       vagrant_server.Component_Type(typ),
			Name:       name,
			ServerAddr: b.Client().ServerTarget(),
		},
		Options: c.Options,
		hooks:   hooks,
		mappers: append(b.mappers, c.Mappers...),
		plugin:  c,
	}, nil
}

// Load all components of a specific type
func (b *Basis) typeComponents(
	ctx context.Context, // context for the plugins,
	typ component.Type, // type of the components,
) (map[string]*Component, error) {
	result := map[string]*Component{}
	plugins, err := b.plugins.Typed(typ)
	if err != nil {
		return nil, err
	}

	b.logger.Info("fetching all typed plugins",
		"type", typ.String(),
	)
	for _, p := range plugins {
		b.logger.Info("fetching typed component",
			"plugin", p,
			"type", typ.String(),
		)
		c, err := b.component(ctx, typ, p)
		if err != nil {
			b.logger.Error("failed to fetch component",
				"plugin", p,
				"type", typ.String(),
			)
			return nil, err
		}
		result[p] = c
	}
	b.logger.Info("fetched all typed plugins",
		"type", typ.String(),
		"count", len(result),
	)
	return result, nil
}

// Load all components
func (b *Basis) components(
	ctx context.Context, // context for the plugins
) ([]*Component, error) {
	result := []*Component{}

	for _, p := range b.plugins.Plugins {
		for _, t := range p.Types {
			c, err := b.component(ctx, t, p.Name)
			if err != nil {
				return nil, err
			}
			result = append(result, c)
		}
	}
	return result, nil
}

// Calls the function provided and converts the
// result to an expected type. If no type conversion
// is required, a `false` value for the expectedType
// will return the raw interface return value.
//
// By default, the basis, provided context, and basis
// UI are added as a typed arguments. The basis is
// also added as a named argument.
func (b *Basis) callDynamicFunc(
	ctx context.Context, // context for function execution
	log hclog.Logger, // logger to provide function execution
	f interface{}, // function to call
	expectedType interface{}, // nil pointer of expected return type
	args ...argmapper.Arg, // list of argmapper arguments
) (interface{}, error) {
	// ensure our UI status is closed after every call since this is
	// the UI we send by default
	defer b.ui.Status().Close()

	// Add seed arguments
	for _, v := range b.seedValues.Typed {
		b.logger.Trace("seeding typed value into dynamic call",
			"fn", hclog.Fmt("%p", f),
			"value", hclog.Fmt("%T", v),
		)

		args = append(args, argmapper.Typed(v))
	}

	for k, v := range b.seedValues.Named {
		b.logger.Trace("seeding named value into dynamic call",
			"fn", hclog.Fmt("%p", f),
			"name", k,
			"value", hclog.Fmt("%T", v),
		)

		args = append(args, argmapper.Named(k, v))
	}

	// Always include a logger within our arguments
	args = append(args, argmapper.Typed(b.logger))
	return dynamic.CallFunc(f, expectedType, b.mappers, args...)
}

func (b *Basis) seed(fn func(*core.Seeds)) {
	s := b.seedValues
	s.AddNamed("basis", b)
	s.AddNamed("basis_ui", b.ui)
	s.AddTyped(b, b.ui, b.corePlugins)
	if fn != nil {
		fn(s)
	}
}

func (b *Basis) configRegistration(p *plugin.Plugin, l hclog.Logger) error {
	if !p.HasType(component.ConfigType) {
		b.logger.Warn("plugin does not implement config component type",
			"name", p.Name,
		)
		return nil
	}

	b.logger.Debug("registering configuration component",
		"name", p.Name,
	)

	i, err := p.Manager().Find(p.Name, component.ConfigType)
	if err != nil {
		b.logger.Error("failed to load configuration component",
			"name", p.Name,
			"error", err,
		)
		return err
	}

	c, ok := i.Component.(core.Config)
	if !ok {
		return fmt.Errorf("component instance is not valid config: %s", p.Name)
	}
	info, err := c.Register()
	if err != nil {
		b.logger.Error("failed to get registration information from plugin",
			"name", p.Name,
			"error", err,
		)
		return err
	}

	b.logger.Info("registering configuration component",
		"plugin", p.Name,
		"info", *info,
	)
	return b.vagrantfile.Register(info, p)
}

func (b *Basis) setPluginCache(i *plugin.Instance, l hclog.Logger) error {
	if c, ok := i.Component.(interface {
		SetCache(cacher.Cache)
	}); ok {
		b.logger.Trace("setting cache on plugin instance",
			"name", i.Name,
			"component", hclog.Fmt("%T", i.Component),
		)
		c.SetCache(b.cache)
	} else {
		b.logger.Warn("cannot set cache on plugin instance",
			"name", i.Name,
			"component", hclog.Fmt("%T", i.Component),
		)
	}

	return nil
}

func (b *Basis) setPluginSeeds(i *plugin.Instance, l hclog.Logger) error {
	if s, ok := i.Component.(core.Seeder); ok {
		if err := s.Seed(b.seedValues); err != nil {
			return err
		}
	}
	return nil
}

func (b *Basis) setPluginManager(i *plugin.Instance, l hclog.Logger) error {
	s, ok := i.Component.(plugin.HasPluginMetadata)
	if !ok {
		l.Warn("plugin does not support metadata, cannot assign plugin manager",
			"component", i.Type.String(),
			"name", i.Name,
		)

		return nil
	}

	srv, err := b.plugins.Servinfo()
	if err != nil {
		l.Warn("failed to get plugin manager information",
			"error", err,
		)

		return nil
	}
	s.SetRequestMetadata("plugin_manager", string(srv))

	return nil
}

func (b *Basis) setPluginCoreManager(i *plugin.Instance, l hclog.Logger) error {
	s, ok := i.Component.(plugin.HasPluginMetadata)
	if !ok {
		l.Warn("plugin does not support metadata, cannot assign plugin manager",
			"component", i.Type.String(),
			"name", i.Name,
		)

		return nil
	}

	srv, err := b.corePlugins.Servinfo(b.plugins.LegacyBroker())
	if err != nil {
		l.Warn("failed to get plugin manager information",
			"error", err,
		)

		return nil
	}
	s.SetRequestMetadata("core_plugin_manager", string(srv))

	return nil
}

func (b *Basis) execHook(
	ctx context.Context,
	log hclog.Logger,
	h *config.Hook,
) error {
	return execHook(ctx, b, log, h)
}

func (b *Basis) doOperation(
	ctx context.Context,
	log hclog.Logger,
	op operation,
) (interface{}, proto.Message, error) {
	return doOperation(ctx, log, b, op)
}

// BasisOption is used to set options for NewBasis.
type BasisOption func(*Basis) error

// WithClient sets the API client to use.
func WithClient(client *serverclient.VagrantClient) BasisOption {
	return func(b *Basis) (err error) {
		b.client = client
		return
	}
}

// WithLogger sets the logger to use with the project. If this option
// is not provided, a default logger will be used (`hclog.L()`).
func WithLogger(log hclog.Logger) BasisOption {
	return func(b *Basis) (err error) {
		b.logger = log
		return
	}
}

func WithPluginManager(m *plugin.Manager) BasisOption {
	return func(b *Basis) (err error) {
		b.plugins = m
		return
	}
}

// WithMappers adds the mappers to the list of mappers.
func WithMappers(m ...*argmapper.Func) BasisOption {
	return func(b *Basis) (err error) {
		b.mappers = append(b.mappers, m...)
		return
	}
}

// WithUI sets the UI to use. If this isn't set, a BasicUI is used.
func WithUI(ui terminal.UI) BasisOption {
	return func(b *Basis) (err error) {
		b.ui = ui
		return
	}
}

// WithJobInfo sets the base job info used for any executed operations.
func WithJobInfo(info *component.JobInfo) BasisOption {
	return func(b *Basis) (err error) {
		b.jobInfo = info
		return
	}
}

// WithBasisDataDir customizes the datadir for the Basis
func WithBasisDataDir(dir *datadir.Basis) BasisOption {
	return func(b *Basis) (err error) {
		b.dir = dir
		return
	}
}

// WithBasisRef is used to load or initialize the basis
func WithBasisRef(r *vagrant_plugin_sdk.Ref_Basis) BasisOption {
	return func(b *Basis) (err error) {
		if r.ResourceId != "" {
			b.basis.ResourceId = r.ResourceId
		}
		if r.Name != "" {
			b.basis.Name = r.Name
		}
		if r.Path != "" {
			b.basis.Path = r.Path
		}

		return
	}
}

func WithBasisResourceId(rid string) BasisOption {
	return func(b *Basis) (err error) {
		result, err := b.client.FindBasis(b.ctx, &vagrant_server.FindBasisRequest{
			Basis: &vagrant_server.Basis{
				ResourceId: rid,
			},
		})
		if err != nil {
			return
		}
		if result == nil {
			b.logger.Error("failed to locate basis during setup",
				"resource-id", rid)

			return fmt.Errorf("requested basis is not found (resource-id: %s", rid)
		}
		b.basis = result.Basis
		return
	}
}

func WithFactory(f *Factory) BasisOption {
	return func(b *Basis) (err error) {
		b.factory = f
		return
	}
}

func FromBasis(basis *Basis) BasisOption {
	return func(b *Basis) (err error) {
		b.logger = basis.logger
		b.plugins = basis.plugins // TODO(spox): we need stacked managers
		b.ctx = basis.ctx
		b.client = basis.client
		b.ui = basis.ui
		return
	}
}

var _ core.Basis = (*Basis)(nil)
var _ Scope = (*Basis)(nil)
