package configs

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

func decodeMockProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, config, moreDiags := block.Body.PartialContent(mockProviderSchema)
	diags = append(diags, moreDiags...)

	name := block.Labels[0]
	nameDiags := checkProviderNameNormalized(name, block.DefRange)
	diags = append(diags, nameDiags...)
	if nameDiags.HasErrors() {
		// If the name is invalid then we mustn't produce a result because
		// downstream could try to use it as a provider type and then crash.
		return nil, diags
	}

	provider := &Provider{
		Name:      name,
		NameRange: block.LabelRanges[0],
		DeclRange: block.DefRange,

		// Mock providers shouldn't need any additional data.
		Config: hcl.EmptyBody(),

		// Mark this provider as being mocked.
		Mock: true,
	}

	if attr, exists := content.Attributes["alias"]; exists {
		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias)
		diags = append(diags, valDiags...)
		provider.AliasRange = attr.Expr.Range().Ptr()

		if !hclsyntax.ValidIdentifier(provider.Alias) {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid provider configuration alias",
				Detail:   fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail),
			})
		}
	}

	var dataDiags hcl.Diagnostics
	provider.MockData, dataDiags = decodeMockDataBody(config, MockProviderOverrideSource)
	diags = append(diags, dataDiags...)

	if attr, exists := content.Attributes["source"]; exists {
		sourceDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.MockDataExternalSource)
		diags = append(diags, sourceDiags...)
	}

	return provider, diags
}

// MockData packages up all the available mock and override data available to
// a mocked provider.
type MockData struct {
	MockResources   map[string]*MockResource
	MockDataSources map[string]*MockResource
	Overrides       addrs.Map[addrs.Targetable, *Override]
}

// Merge will merge the target MockData object into the current MockData.
//
// If skipCollisions is true, then Merge will simply ignore any entries within
// other that clash with entries already in data. If skipCollisions is false,
// then we will create diagnostics for each duplicate resource.
func (data *MockData) Merge(other *MockData, skipCollisions bool) (diags hcl.Diagnostics) {
	if other == nil {
		return diags
	}

	for name, resource := range other.MockResources {
		current, exists := data.MockResources[name]
		if !exists {
			data.MockResources[name] = resource
			continue
		}

		if skipCollisions {
			continue
		}

		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate mock resource block",
			Detail:   fmt.Sprintf("A mock_resource %q block already exists at %s.", name, current.Range),
			Subject:  resource.TypeRange.Ptr(),
		})
	}
	for name, datasource := range other.MockDataSources {
		current, exists := data.MockDataSources[name]
		if !exists {
			data.MockDataSources[name] = datasource
			continue
		}

		if skipCollisions {
			continue
		}

		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate mock resource block",
			Detail:   fmt.Sprintf("A mock_data %q block already exists at %s.", name, current.Range),
			Subject:  datasource.TypeRange.Ptr(),
		})
	}
	for _, elem := range other.Overrides.Elems {
		target, override := elem.Key, elem.Value

		current, exists := data.Overrides.GetOk(target)
		if !exists {
			data.Overrides.Put(target, override)
			continue
		}

		if skipCollisions {
			continue
		}

		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate override block",
			Detail:   fmt.Sprintf("An override block for %s already exists at %s.", target, current.Range),
			Subject:  override.Range.Ptr(),
		})
	}
	return diags
}

// MockResource maps a resource or data source type and name to a set of values
// for that resource.
type MockResource struct {
	Mode addrs.ResourceMode
	Type string

	Defaults cty.Value

	Range         hcl.Range
	TypeRange     hcl.Range
	DefaultsRange hcl.Range
}

type OverrideSource int

const (
	UnknownOverrideSource OverrideSource = iota
	RunBlockOverrideSource
	TestFileOverrideSource
	MockProviderOverrideSource
	MockDataFileOverrideSource
)

// Override targets a specific module, resource or data source with a set of
// replacement values that should be used in place of whatever the underlying
// provider would normally do.
type Override struct {
	Target *addrs.Target
	Values cty.Value

	// Source tells us where this Override was defined.
	Source OverrideSource

	Range       hcl.Range
	TypeRange   hcl.Range
	TargetRange hcl.Range
	ValuesRange hcl.Range
}

func decodeMockDataBody(body hcl.Body, source OverrideSource) (*MockData, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := body.Content(mockDataSchema)
	diags = append(diags, contentDiags...)

	data := &MockData{
		MockResources:   make(map[string]*MockResource),
		MockDataSources: make(map[string]*MockResource),
		Overrides:       addrs.MakeMap[addrs.Targetable, *Override](),
	}

	for _, block := range content.Blocks {
		switch block.Type {
		case "mock_resource", "mock_data":
			resource, resourceDiags := decodeMockResourceBlock(block)
			diags = append(diags, resourceDiags...)

			if resource != nil {
				switch resource.Mode {
				case addrs.ManagedResourceMode:
					if previous, ok := data.MockResources[resource.Type]; ok {
						diags = append(diags, &hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Duplicate mock_resource block",
							Detail:   fmt.Sprintf("A mock_resource block for %s has already been defined at %s.", resource.Type, previous.Range),
							Subject:  resource.TypeRange.Ptr(),
						})
						continue
					}
					data.MockResources[resource.Type] = resource
				case addrs.DataResourceMode:
					if previous, ok := data.MockDataSources[resource.Type]; ok {
						diags = append(diags, &hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Duplicate mock_data block",
							Detail:   fmt.Sprintf("A mock_data block for %s has already been defined at %s.", resource.Type, previous.Range),
							Subject:  resource.TypeRange.Ptr(),
						})
						continue
					}
					data.MockDataSources[resource.Type] = resource
				}
			}
		case "override_resource":
			override, overrideDiags := decodeOverrideResourceBlock(block, source)
			diags = append(diags, overrideDiags...)

			if override != nil && override.Target != nil {
				subject := override.Target.Subject
				if previous, ok := data.Overrides.GetOk(subject); ok {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Duplicate override_resource block",
						Detail:   fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range),
						Subject:  override.Range.Ptr(),
					})
					continue
				}
				data.Overrides.Put(subject, override)
			}
		case "override_data":
			override, overrideDiags := decodeOverrideDataBlock(block, source)
			diags = append(diags, overrideDiags...)

			if override != nil && override.Target != nil {
				subject := override.Target.Subject
				if previous, ok := data.Overrides.GetOk(subject); ok {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Duplicate override_data block",
						Detail:   fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range),
						Subject:  override.Range.Ptr(),
					})
					continue
				}
				data.Overrides.Put(subject, override)
			}
		}
	}

	return data, diags
}

func decodeMockResourceBlock(block *hcl.Block) (*MockResource, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := block.Body.Content(mockResourceSchema)
	diags = append(diags, contentDiags...)

	resource := &MockResource{
		Type:      block.Labels[0],
		Range:     block.DefRange,
		TypeRange: block.LabelRanges[0],
	}

	switch block.Type {
	case "mock_resource":
		resource.Mode = addrs.ManagedResourceMode
	case "mock_data":
		resource.Mode = addrs.DataResourceMode
	}

	if defaults, exists := content.Attributes["defaults"]; exists {
		var defaultDiags hcl.Diagnostics
		resource.DefaultsRange = defaults.Range
		resource.Defaults, defaultDiags = defaults.Expr.Value(nil)
		diags = append(diags, defaultDiags...)
	} else {
		// It's fine if we don't have any defaults, just means we'll generate
		// values for everything ourselves.
		resource.Defaults = cty.NilVal
	}

	return resource, diags
}

func decodeOverrideModuleBlock(block *hcl.Block, source OverrideSource) (*Override, hcl.Diagnostics) {
	override, diags := decodeOverrideBlock(block, "outputs", "override_module", source)

	if override.Target != nil {
		switch override.Target.Subject.AddrType() {
		case addrs.ModuleAddrType, addrs.ModuleInstanceAddrType:
			// Do nothing, we're good here.
		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid override target",
				Detail:   fmt.Sprintf("You can only target modules from override_module blocks, not %s.", override.Target.Subject),
				Subject:  override.TargetRange.Ptr(),
			})
			return nil, diags
		}
	}

	return override, diags
}

func decodeOverrideResourceBlock(block *hcl.Block, source OverrideSource) (*Override, hcl.Diagnostics) {
	override, diags := decodeOverrideBlock(block, "values", "override_resource", source)

	if override.Target != nil {
		var mode addrs.ResourceMode

		switch override.Target.Subject.AddrType() {
		case addrs.AbsResourceInstanceAddrType:
			subject := override.Target.Subject.(addrs.AbsResourceInstance)
			mode = subject.Resource.Resource.Mode
		case addrs.AbsResourceAddrType:
			subject := override.Target.Subject.(addrs.AbsResource)
			mode = subject.Resource.Mode
		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid override target",
				Detail:   fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject),
				Subject:  override.TargetRange.Ptr(),
			})
			return nil, diags
		}

		if mode != addrs.ManagedResourceMode {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid override target",
				Detail:   fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject),
				Subject:  override.TargetRange.Ptr(),
			})
			return nil, diags
		}
	}

	return override, diags
}

func decodeOverrideDataBlock(block *hcl.Block, source OverrideSource) (*Override, hcl.Diagnostics) {
	override, diags := decodeOverrideBlock(block, "values", "override_data", source)

	if override.Target != nil {
		var mode addrs.ResourceMode

		switch override.Target.Subject.AddrType() {
		case addrs.AbsResourceInstanceAddrType:
			subject := override.Target.Subject.(addrs.AbsResourceInstance)
			mode = subject.Resource.Resource.Mode
		case addrs.AbsResourceAddrType:
			subject := override.Target.Subject.(addrs.AbsResource)
			mode = subject.Resource.Mode
		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid override target",
				Detail:   fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject),
				Subject:  override.TargetRange.Ptr(),
			})
			return nil, diags
		}

		if mode != addrs.DataResourceMode {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid override target",
				Detail:   fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject),
				Subject:  override.TargetRange.Ptr(),
			})
			return nil, diags
		}
	}

	return override, diags
}

func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName string, source OverrideSource) (*Override, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := block.Body.Content(&hcl.BodySchema{
		Attributes: []hcl.AttributeSchema{
			{Name: "target"},
			{Name: attributeName},
		},
	})
	diags = append(diags, contentDiags...)

	override := &Override{
		Source:    source,
		Range:     block.DefRange,
		TypeRange: block.TypeRange,
	}

	if target, exists := content.Attributes["target"]; exists {
		override.TargetRange = target.Range
		traversal, traversalDiags := hcl.AbsTraversalForExpr(target.Expr)
		diags = append(diags, traversalDiags...)
		if traversal != nil {
			var targetDiags tfdiags.Diagnostics
			override.Target, targetDiags = addrs.ParseTarget(traversal)
			diags = append(diags, targetDiags.ToHCL()...)
		}
	} else {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Missing target attribute",
			Detail:   fmt.Sprintf("%s blocks must specify a target address.", blockName),
			Subject:  override.Range.Ptr(),
		})
	}

	if attribute, exists := content.Attributes[attributeName]; exists {
		var valueDiags hcl.Diagnostics
		override.ValuesRange = attribute.Range
		override.Values, valueDiags = attribute.Expr.Value(nil)
		diags = append(diags, valueDiags...)
	} else {
		// It's fine if we don't have any values, just means we'll generate
		// values for everything ourselves. We set this to an empty object so
		// it's equivalent to `values = {}` which makes later processing easier.
		override.Values = cty.EmptyObjectVal
	}

	if !override.Values.Type().IsObjectType() {

		var attributePreposition string
		switch attributeName {
		case "outputs":
			attributePreposition = "an"
		default:
			attributePreposition = "a"
		}

		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf("Invalid %s attribute", attributeName),
			Detail:   fmt.Sprintf("%s blocks must specify %s %s attribute that is an object.", blockName, attributePreposition, attributeName),
			Subject:  override.ValuesRange.Ptr(),
		})
	}

	return override, diags
}

var mockProviderSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name: "alias",
		},
		{
			Name: "source",
		},
	},
}

var mockDataSchema = &hcl.BodySchema{
	Blocks: []hcl.BlockHeaderSchema{
		{Type: "mock_resource", LabelNames: []string{"type"}},
		{Type: "mock_data", LabelNames: []string{"type"}},
		{Type: "override_resource"},
		{Type: "override_data"},
	},
}

var mockResourceSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{Name: "defaults"},
	},
}
