package config

import (
	"fmt"
	"sort"
	"strings"

	hcl2 "github.com/hashicorp/hcl/v2"
	gohcl2 "github.com/hashicorp/hcl/v2/gohcl"
	hcl2parse "github.com/hashicorp/hcl/v2/hclparse"
	"github.com/hashicorp/terraform/configs/hcl2shim"
	"github.com/zclconf/go-cty/cty"
)

// hcl2Configurable is an implementation of configurable that knows
// how to turn a HCL Body into a *Config object.
type hcl2Configurable struct {
	SourceFilename string
	Body           hcl2.Body
}

// hcl2Loader is a wrapper around a HCL parser that provides a fileLoaderFunc.
type hcl2Loader struct {
	Parser *hcl2parse.Parser
}

// For the moment we'll just have a global loader since we don't have anywhere
// better to stash this.
// TODO: refactor the loader API so that it uses some sort of object we can
// stash the parser inside.
var globalHCL2Loader = newHCL2Loader()

// newHCL2Loader creates a new hcl2Loader containing a new HCL Parser.
//
// HCL parsers retain information about files that are loaded to aid in
// producing diagnostic messages, so all files within a single configuration
// should be loaded with the same parser to ensure the availability of
// full diagnostic information.
func newHCL2Loader() hcl2Loader {
	return hcl2Loader{
		Parser: hcl2parse.NewParser(),
	}
}

// loadFile is a fileLoaderFunc that knows how to read a HCL2 file and turn it
// into a hcl2Configurable.
func (l hcl2Loader) loadFile(filename string) (configurable, []string, error) {
	var f *hcl2.File
	var diags hcl2.Diagnostics
	if strings.HasSuffix(filename, ".json") {
		f, diags = l.Parser.ParseJSONFile(filename)
	} else {
		f, diags = l.Parser.ParseHCLFile(filename)
	}
	if diags.HasErrors() {
		// Return diagnostics as an error; callers may type-assert this to
		// recover the original diagnostics, if it doesn't end up wrapped
		// in another error.
		return nil, nil, diags
	}

	return &hcl2Configurable{
		SourceFilename: filename,
		Body:           f.Body,
	}, nil, nil
}

func (t *hcl2Configurable) Config() (*Config, error) {
	config := &Config{}

	// these structs are used only for the initial shallow decoding; we'll
	// expand this into the main, public-facing config structs afterwards.
	type atlas struct {
		Name    string    `hcl:"name"`
		Include *[]string `hcl:"include"`
		Exclude *[]string `hcl:"exclude"`
	}
	type provider struct {
		Name    string    `hcl:"name,label"`
		Alias   *string   `hcl:"alias,attr"`
		Version *string   `hcl:"version,attr"`
		Config  hcl2.Body `hcl:",remain"`
	}
	type module struct {
		Name      string             `hcl:"name,label"`
		Source    string             `hcl:"source,attr"`
		Version   *string            `hcl:"version,attr"`
		Providers *map[string]string `hcl:"providers,attr"`
		Config    hcl2.Body          `hcl:",remain"`
	}
	type resourceLifecycle struct {
		CreateBeforeDestroy *bool     `hcl:"create_before_destroy,attr"`
		PreventDestroy      *bool     `hcl:"prevent_destroy,attr"`
		IgnoreChanges       *[]string `hcl:"ignore_changes,attr"`
	}
	type connection struct {
		Config hcl2.Body `hcl:",remain"`
	}
	type provisioner struct {
		Type string `hcl:"type,label"`

		When      *string `hcl:"when,attr"`
		OnFailure *string `hcl:"on_failure,attr"`

		Connection *connection `hcl:"connection,block"`
		Config     hcl2.Body   `hcl:",remain"`
	}
	type managedResource struct {
		Type string `hcl:"type,label"`
		Name string `hcl:"name,label"`

		CountExpr hcl2.Expression `hcl:"count,attr"`
		Provider  *string         `hcl:"provider,attr"`
		DependsOn *[]string       `hcl:"depends_on,attr"`

		Lifecycle    *resourceLifecycle `hcl:"lifecycle,block"`
		Provisioners []provisioner      `hcl:"provisioner,block"`
		Connection   *connection        `hcl:"connection,block"`

		Config hcl2.Body `hcl:",remain"`
	}
	type dataResource struct {
		Type string `hcl:"type,label"`
		Name string `hcl:"name,label"`

		CountExpr hcl2.Expression `hcl:"count,attr"`
		Provider  *string         `hcl:"provider,attr"`
		DependsOn *[]string       `hcl:"depends_on,attr"`

		Config hcl2.Body `hcl:",remain"`
	}
	type variable struct {
		Name string `hcl:"name,label"`

		DeclaredType *string    `hcl:"type,attr"`
		Default      *cty.Value `hcl:"default,attr"`
		Description  *string    `hcl:"description,attr"`
		Sensitive    *bool      `hcl:"sensitive,attr"`
	}
	type output struct {
		Name string `hcl:"name,label"`

		ValueExpr   hcl2.Expression `hcl:"value,attr"`
		DependsOn   *[]string       `hcl:"depends_on,attr"`
		Description *string         `hcl:"description,attr"`
		Sensitive   *bool           `hcl:"sensitive,attr"`
	}
	type locals struct {
		Definitions hcl2.Attributes `hcl:",remain"`
	}
	type backend struct {
		Type   string    `hcl:"type,label"`
		Config hcl2.Body `hcl:",remain"`
	}
	type terraform struct {
		RequiredVersion *string  `hcl:"required_version,attr"`
		Backend         *backend `hcl:"backend,block"`
	}
	type topLevel struct {
		Atlas     *atlas            `hcl:"atlas,block"`
		Datas     []dataResource    `hcl:"data,block"`
		Modules   []module          `hcl:"module,block"`
		Outputs   []output          `hcl:"output,block"`
		Providers []provider        `hcl:"provider,block"`
		Resources []managedResource `hcl:"resource,block"`
		Terraform *terraform        `hcl:"terraform,block"`
		Variables []variable        `hcl:"variable,block"`
		Locals    []*locals         `hcl:"locals,block"`
	}

	var raw topLevel
	diags := gohcl2.DecodeBody(t.Body, nil, &raw)
	if diags.HasErrors() {
		// Do some minimal decoding to see if we can at least get the
		// required Terraform version, which might help explain why we
		// couldn't parse the rest.
		if raw.Terraform != nil && raw.Terraform.RequiredVersion != nil {
			config.Terraform = &Terraform{
				RequiredVersion: *raw.Terraform.RequiredVersion,
			}
		}

		// We return the diags as an implementation of error, which the
		// caller than then type-assert if desired to recover the individual
		// diagnostics.
		// FIXME: The current API gives us no way to return warnings in the
		// absence of any errors.
		return config, diags
	}

	if raw.Terraform != nil {
		var reqdVersion string
		var backend *Backend

		if raw.Terraform.RequiredVersion != nil {
			reqdVersion = *raw.Terraform.RequiredVersion
		}
		if raw.Terraform.Backend != nil {
			backend = new(Backend)
			backend.Type = raw.Terraform.Backend.Type

			// We don't permit interpolations or nested blocks inside the
			// backend config, so we can decode the config early here and
			// get direct access to the values, which is important for the
			// config hashing to work as expected.
			var config map[string]string
			configDiags := gohcl2.DecodeBody(raw.Terraform.Backend.Config, nil, &config)
			diags = append(diags, configDiags...)

			raw := make(map[string]interface{}, len(config))
			for k, v := range config {
				raw[k] = v
			}

			var err error
			backend.RawConfig, err = NewRawConfig(raw)
			if err != nil {
				diags = append(diags, &hcl2.Diagnostic{
					Severity: hcl2.DiagError,
					Summary:  "Invalid backend configuration",
					Detail:   fmt.Sprintf("Error in backend configuration: %s", err),
				})
			}
		}

		config.Terraform = &Terraform{
			RequiredVersion: reqdVersion,
			Backend:         backend,
		}
	}

	if raw.Atlas != nil {
		var include, exclude []string
		if raw.Atlas.Include != nil {
			include = *raw.Atlas.Include
		}
		if raw.Atlas.Exclude != nil {
			exclude = *raw.Atlas.Exclude
		}
		config.Atlas = &AtlasConfig{
			Name:    raw.Atlas.Name,
			Include: include,
			Exclude: exclude,
		}
	}

	for _, rawM := range raw.Modules {
		m := &Module{
			Name:      rawM.Name,
			Source:    rawM.Source,
			RawConfig: NewRawConfigHCL2(rawM.Config),
		}

		if rawM.Version != nil {
			m.Version = *rawM.Version
		}

		if rawM.Providers != nil {
			m.Providers = *rawM.Providers
		}

		config.Modules = append(config.Modules, m)
	}

	for _, rawV := range raw.Variables {
		v := &Variable{
			Name: rawV.Name,
		}
		if rawV.DeclaredType != nil {
			v.DeclaredType = *rawV.DeclaredType
		}
		if rawV.Default != nil {
			v.Default = hcl2shim.ConfigValueFromHCL2(*rawV.Default)
		}
		if rawV.Description != nil {
			v.Description = *rawV.Description
		}

		config.Variables = append(config.Variables, v)
	}

	for _, rawO := range raw.Outputs {
		o := &Output{
			Name: rawO.Name,
		}

		if rawO.Description != nil {
			o.Description = *rawO.Description
		}
		if rawO.DependsOn != nil {
			o.DependsOn = *rawO.DependsOn
		}
		if rawO.Sensitive != nil {
			o.Sensitive = *rawO.Sensitive
		}

		// The result is expected to be a map like map[string]interface{}{"value": something},
		// so we'll fake that with our hcl2shim.SingleAttrBody shim.
		o.RawConfig = NewRawConfigHCL2(hcl2shim.SingleAttrBody{
			Name: "value",
			Expr: rawO.ValueExpr,
		})

		config.Outputs = append(config.Outputs, o)
	}

	for _, rawR := range raw.Resources {
		r := &Resource{
			Mode: ManagedResourceMode,
			Type: rawR.Type,
			Name: rawR.Name,
		}
		if rawR.Lifecycle != nil {
			var l ResourceLifecycle
			if rawR.Lifecycle.CreateBeforeDestroy != nil {
				l.CreateBeforeDestroy = *rawR.Lifecycle.CreateBeforeDestroy
			}
			if rawR.Lifecycle.PreventDestroy != nil {
				l.PreventDestroy = *rawR.Lifecycle.PreventDestroy
			}
			if rawR.Lifecycle.IgnoreChanges != nil {
				l.IgnoreChanges = *rawR.Lifecycle.IgnoreChanges
			}
			r.Lifecycle = l
		}
		if rawR.Provider != nil {
			r.Provider = *rawR.Provider
		}
		if rawR.DependsOn != nil {
			r.DependsOn = *rawR.DependsOn
		}

		var defaultConnInfo *RawConfig
		if rawR.Connection != nil {
			defaultConnInfo = NewRawConfigHCL2(rawR.Connection.Config)
		}

		for _, rawP := range rawR.Provisioners {
			p := &Provisioner{
				Type: rawP.Type,
			}

			switch {
			case rawP.When == nil:
				p.When = ProvisionerWhenCreate
			case *rawP.When == "create":
				p.When = ProvisionerWhenCreate
			case *rawP.When == "destroy":
				p.When = ProvisionerWhenDestroy
			default:
				p.When = ProvisionerWhenInvalid
			}

			switch {
			case rawP.OnFailure == nil:
				p.OnFailure = ProvisionerOnFailureFail
			case *rawP.When == "fail":
				p.OnFailure = ProvisionerOnFailureFail
			case *rawP.When == "continue":
				p.OnFailure = ProvisionerOnFailureContinue
			default:
				p.OnFailure = ProvisionerOnFailureInvalid
			}

			if rawP.Connection != nil {
				p.ConnInfo = NewRawConfigHCL2(rawP.Connection.Config)
			} else {
				p.ConnInfo = defaultConnInfo
			}

			p.RawConfig = NewRawConfigHCL2(rawP.Config)

			r.Provisioners = append(r.Provisioners, p)
		}

		// The old loader records the count expression as a weird RawConfig with
		// a single-element map inside. Since the rest of the world is assuming
		// that, we'll mimic it here.
		{
			countBody := hcl2shim.SingleAttrBody{
				Name: "count",
				Expr: rawR.CountExpr,
			}

			r.RawCount = NewRawConfigHCL2(countBody)
			r.RawCount.Key = "count"
		}

		r.RawConfig = NewRawConfigHCL2(rawR.Config)

		config.Resources = append(config.Resources, r)

	}

	for _, rawR := range raw.Datas {
		r := &Resource{
			Mode: DataResourceMode,
			Type: rawR.Type,
			Name: rawR.Name,
		}

		if rawR.Provider != nil {
			r.Provider = *rawR.Provider
		}
		if rawR.DependsOn != nil {
			r.DependsOn = *rawR.DependsOn
		}

		// The old loader records the count expression as a weird RawConfig with
		// a single-element map inside. Since the rest of the world is assuming
		// that, we'll mimic it here.
		{
			countBody := hcl2shim.SingleAttrBody{
				Name: "count",
				Expr: rawR.CountExpr,
			}

			r.RawCount = NewRawConfigHCL2(countBody)
			r.RawCount.Key = "count"
		}

		r.RawConfig = NewRawConfigHCL2(rawR.Config)

		config.Resources = append(config.Resources, r)
	}

	for _, rawP := range raw.Providers {
		p := &ProviderConfig{
			Name: rawP.Name,
		}

		if rawP.Alias != nil {
			p.Alias = *rawP.Alias
		}
		if rawP.Version != nil {
			p.Version = *rawP.Version
		}

		// The result is expected to be a map like map[string]interface{}{"value": something},
		// so we'll fake that with our hcl2shim.SingleAttrBody shim.
		p.RawConfig = NewRawConfigHCL2(rawP.Config)

		config.ProviderConfigs = append(config.ProviderConfigs, p)
	}

	for _, rawL := range raw.Locals {
		names := make([]string, 0, len(rawL.Definitions))
		for n := range rawL.Definitions {
			names = append(names, n)
		}
		sort.Strings(names)
		for _, n := range names {
			attr := rawL.Definitions[n]
			l := &Local{
				Name: n,
				RawConfig: NewRawConfigHCL2(hcl2shim.SingleAttrBody{
					Name: "value",
					Expr: attr.Expr,
				}),
			}
			config.Locals = append(config.Locals, l)
		}
	}

	// FIXME: The current API gives us no way to return warnings in the
	// absence of any errors.
	var err error
	if diags.HasErrors() {
		err = diags
	}

	return config, err
}
