package terraform

import (
	"fmt"

	"github.com/hashicorp/terraform/config"
	"github.com/mitchellh/mapstructure"
)

// EvalValidateError is the error structure returned if there were
// validation errors.
type EvalValidateError struct {
	Warnings []string
	Errors   []error
}

func (e *EvalValidateError) Error() string {
	return fmt.Sprintf("Warnings: %s. Errors: %s", e.Warnings, e.Errors)
}

// EvalValidateCount is an EvalNode implementation that validates
// the count of a resource.
type EvalValidateCount struct {
	Resource *config.Resource
}

// TODO: test
func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) {
	var count int
	var errs []error
	var err error
	if _, err := ctx.Interpolate(n.Resource.RawCount, nil); err != nil {
		errs = append(errs, fmt.Errorf(
			"Failed to interpolate count: %s", err))
		goto RETURN
	}

	count, err = n.Resource.Count()
	if err != nil {
		// If we can't get the count during validation, then
		// just replace it with the number 1.
		c := n.Resource.RawCount.Config()
		c[n.Resource.RawCount.Key] = "1"
		count = 1
	}
	err = nil

	if count < 0 {
		errs = append(errs, fmt.Errorf(
			"Count is less than zero: %d", count))
	}

RETURN:
	if len(errs) != 0 {
		err = &EvalValidateError{
			Errors: errs,
		}
	}
	return nil, err
}

// EvalValidateProvider is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateProvider struct {
	Provider *ResourceProvider
	Config   **ResourceConfig
}

func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) {
	provider := *n.Provider
	config := *n.Config

	warns, errs := provider.Validate(config)
	if len(warns) == 0 && len(errs) == 0 {
		return nil, nil
	}

	return nil, &EvalValidateError{
		Warnings: warns,
		Errors:   errs,
	}
}

// EvalValidateProvisioner is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateProvisioner struct {
	Provisioner *ResourceProvisioner
	Config      **ResourceConfig
	ConnConfig  **ResourceConfig
}

func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) {
	provisioner := *n.Provisioner
	config := *n.Config
	var warns []string
	var errs []error

	{
		// Validate the provisioner's own config first
		w, e := provisioner.Validate(config)
		warns = append(warns, w...)
		errs = append(errs, e...)
	}

	{
		// Now validate the connection config, which might either be from
		// the provisioner block itself or inherited from the resource's
		// shared connection info.
		w, e := n.validateConnConfig(*n.ConnConfig)
		warns = append(warns, w...)
		errs = append(errs, e...)
	}

	if len(warns) == 0 && len(errs) == 0 {
		return nil, nil
	}

	return nil, &EvalValidateError{
		Warnings: warns,
		Errors:   errs,
	}
}

func (n *EvalValidateProvisioner) validateConnConfig(connConfig *ResourceConfig) (warns []string, errs []error) {
	// We can't comprehensively validate the connection config since its
	// final structure is decided by the communicator and we can't instantiate
	// that until we have a complete instance state. However, we *can* catch
	// configuration keys that are not valid for *any* communicator, catching
	// typos early rather than waiting until we actually try to run one of
	// the resource's provisioners.

	type connConfigSuperset struct {
		// All attribute types are interface{} here because at this point we
		// may still have unresolved interpolation expressions, which will
		// appear as strings regardless of the final goal type.

		Type       interface{} `mapstructure:"type"`
		User       interface{} `mapstructure:"user"`
		Password   interface{} `mapstructure:"password"`
		Host       interface{} `mapstructure:"host"`
		Port       interface{} `mapstructure:"port"`
		Timeout    interface{} `mapstructure:"timeout"`
		ScriptPath interface{} `mapstructure:"script_path"`

		// For type=ssh only (enforced in ssh communicator)
		PrivateKey        interface{} `mapstructure:"private_key"`
		Agent             interface{} `mapstructure:"agent"`
		BastionHost       interface{} `mapstructure:"bastion_host"`
		BastionPort       interface{} `mapstructure:"bastion_port"`
		BastionUser       interface{} `mapstructure:"bastion_user"`
		BastionPassword   interface{} `mapstructure:"bastion_password"`
		BastionPrivateKey interface{} `mapstructure:"bastion_private_key"`

		// For type=winrm only (enforced in winrm communicator)
		HTTPS    interface{} `mapstructure:"https"`
		Insecure interface{} `mapstructure:"insecure"`
		CACert   interface{} `mapstructure:"cacert"`
	}

	var metadata mapstructure.Metadata
	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		Metadata: &metadata,
		Result:   &connConfigSuperset{}, // result is disregarded; we only care about unused keys
	})
	if err != nil {
		// should never happen
		errs = append(errs, err)
		return
	}

	if err := decoder.Decode(connConfig.Config); err != nil {
		errs = append(errs, err)
		return
	}

	for _, attrName := range metadata.Unused {
		errs = append(errs, fmt.Errorf("unknown 'connection' argument %q", attrName))
	}
	return
}

// EvalValidateResource is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateResource struct {
	Provider     *ResourceProvider
	Config       **ResourceConfig
	ResourceName string
	ResourceType string
	ResourceMode config.ResourceMode

	// IgnoreWarnings means that warnings will not be passed through. This allows
	// "just-in-time" passes of validation to continue execution through warnings.
	IgnoreWarnings bool
}

func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
	provider := *n.Provider
	cfg := *n.Config
	var warns []string
	var errs []error
	// Provider entry point varies depending on resource mode, because
	// managed resources and data resources are two distinct concepts
	// in the provider abstraction.
	switch n.ResourceMode {
	case config.ManagedResourceMode:
		warns, errs = provider.ValidateResource(n.ResourceType, cfg)
	case config.DataResourceMode:
		warns, errs = provider.ValidateDataSource(n.ResourceType, cfg)
	}

	// If the resource name doesn't match the name regular
	// expression, show an error.
	if !config.NameRegexp.Match([]byte(n.ResourceName)) {
		errs = append(errs, fmt.Errorf(
			"%s: resource name can only contain letters, numbers, "+
				"dashes, and underscores.", n.ResourceName))
	}

	if (len(warns) == 0 || n.IgnoreWarnings) && len(errs) == 0 {
		return nil, nil
	}

	return nil, &EvalValidateError{
		Warnings: warns,
		Errors:   errs,
	}
}
