// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package dynblock

import (
	"fmt"

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

// expandBody wraps another hcl.Body and expands any "dynamic" blocks found
// inside whenever Content or PartialContent is called.
type expandBody struct {
	original   hcl.Body
	forEachCtx *hcl.EvalContext
	iteration  *iteration // non-nil if we're nested inside another "dynamic" block
	valueMarks cty.ValueMarks

	checkForEach []func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics

	// These are used with PartialContent to produce a "remaining items"
	// body to return. They are nil on all bodies fresh out of the transformer.
	//
	// Note that this is re-implemented here rather than delegating to the
	// existing support required by the underlying body because we need to
	// retain access to the entire original body on subsequent decode operations
	// so we can retain any "dynamic" blocks for types we didn't take consume
	// on the first pass.
	hiddenAttrs  map[string]struct{}
	hiddenBlocks map[string]hcl.BlockHeaderSchema
}

func (b *expandBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
	extSchema := b.extendSchema(schema)
	rawContent, diags := b.original.Content(extSchema)

	blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, false)
	diags = append(diags, blockDiags...)
	attrs := b.prepareAttributes(rawContent.Attributes)

	content := &hcl.BodyContent{
		Attributes:       attrs,
		Blocks:           blocks,
		MissingItemRange: b.original.MissingItemRange(),
	}

	return content, diags
}

func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
	extSchema := b.extendSchema(schema)
	rawContent, _, diags := b.original.PartialContent(extSchema)
	// We discard the "remain" argument above because we're going to construct
	// our own remain that also takes into account remaining "dynamic" blocks.

	blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, true)
	diags = append(diags, blockDiags...)
	attrs := b.prepareAttributes(rawContent.Attributes)

	content := &hcl.BodyContent{
		Attributes:       attrs,
		Blocks:           blocks,
		MissingItemRange: b.original.MissingItemRange(),
	}

	remain := &expandBody{
		original:     b.original,
		forEachCtx:   b.forEachCtx,
		iteration:    b.iteration,
		checkForEach: b.checkForEach,
		hiddenAttrs:  make(map[string]struct{}),
		hiddenBlocks: make(map[string]hcl.BlockHeaderSchema),
	}
	for name := range b.hiddenAttrs {
		remain.hiddenAttrs[name] = struct{}{}
	}
	for typeName, blockS := range b.hiddenBlocks {
		remain.hiddenBlocks[typeName] = blockS
	}
	for _, attrS := range schema.Attributes {
		remain.hiddenAttrs[attrS.Name] = struct{}{}
	}
	for _, blockS := range schema.Blocks {
		remain.hiddenBlocks[blockS.Type] = blockS
	}

	return content, remain, diags
}

func (b *expandBody) extendSchema(schema *hcl.BodySchema) *hcl.BodySchema {
	// We augment the requested schema to also include our special "dynamic"
	// block type, since then we'll get instances of it interleaved with
	// all of the literal child blocks we must also include.
	extSchema := &hcl.BodySchema{
		Attributes: schema.Attributes,
		Blocks:     make([]hcl.BlockHeaderSchema, len(schema.Blocks), len(schema.Blocks)+len(b.hiddenBlocks)+1),
	}
	copy(extSchema.Blocks, schema.Blocks)
	extSchema.Blocks = append(extSchema.Blocks, dynamicBlockHeaderSchema)

	// If we have any hiddenBlocks then we also need to register those here
	// so that a call to "Content" on the underlying body won't fail.
	// (We'll filter these out again once we process the result of either
	// Content or PartialContent.)
	for _, blockS := range b.hiddenBlocks {
		extSchema.Blocks = append(extSchema.Blocks, blockS)
	}

	// If we have any hiddenAttrs then we also need to register these, for
	// the same reason as we deal with hiddenBlocks above.
	if len(b.hiddenAttrs) != 0 {
		newAttrs := make([]hcl.AttributeSchema, len(schema.Attributes), len(schema.Attributes)+len(b.hiddenAttrs))
		copy(newAttrs, extSchema.Attributes)
		for name := range b.hiddenAttrs {
			newAttrs = append(newAttrs, hcl.AttributeSchema{
				Name:     name,
				Required: false,
			})
		}
		extSchema.Attributes = newAttrs
	}

	return extSchema
}

func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) hcl.Attributes {
	if len(b.hiddenAttrs) == 0 && b.iteration == nil && len(b.valueMarks) == 0 {
		// Easy path: just pass through the attrs from the original body verbatim
		return rawAttrs
	}

	// Otherwise we have some work to do: we must filter out any attributes
	// that are hidden (since a previous PartialContent call already saw these)
	// and wrap the expressions of the inner attributes so that they will
	// have access to our iteration variables.
	attrs := make(hcl.Attributes, len(rawAttrs))
	for name, rawAttr := range rawAttrs {
		if _, hidden := b.hiddenAttrs[name]; hidden {
			continue
		}
		if b.iteration != nil {
			attr := *rawAttr // shallow copy so we can mutate it
			attr.Expr = exprWrap{
				Expression:  attr.Expr,
				i:           b.iteration,
				resultMarks: b.valueMarks,
			}
			attrs[name] = &attr
		} else {
			// If we have no active iteration then no wrapping is required
			// unless we have marks to apply.
			if len(b.valueMarks) != 0 {
				attr := *rawAttr // shallow copy so we can mutate it
				attr.Expr = exprWrap{
					Expression:  attr.Expr,
					resultMarks: b.valueMarks,
				}
				attrs[name] = &attr
			} else {
				attrs[name] = rawAttr
			}
		}
	}
	return attrs
}

func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, partial bool) (hcl.Blocks, hcl.Diagnostics) {
	var blocks hcl.Blocks
	var diags hcl.Diagnostics

	for _, rawBlock := range rawBlocks {
		switch rawBlock.Type {
		case "dynamic":
			realBlockType := rawBlock.Labels[0]
			if _, hidden := b.hiddenBlocks[realBlockType]; hidden {
				continue
			}

			var blockS *hcl.BlockHeaderSchema
			for _, candidate := range schema.Blocks {
				if candidate.Type == realBlockType {
					blockS = &candidate
					break
				}
			}
			if blockS == nil {
				// Not a block type that the caller requested.
				if !partial {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Unsupported block type",
						Detail:   fmt.Sprintf("Blocks of type %q are not expected here.", realBlockType),
						Subject:  &rawBlock.LabelRanges[0],
					})
				}
				continue
			}

			spec, specDiags := b.decodeSpec(blockS, rawBlock)
			diags = append(diags, specDiags...)
			if specDiags.HasErrors() {
				continue
			}

			forEachVal, marks := spec.forEachVal.Unmark()
			if forEachVal.IsKnown() {
				for it := forEachVal.ElementIterator(); it.Next(); {
					key, value := it.Element()
					i := b.iteration.MakeChild(spec.iteratorName, key, value)

					block, blockDiags := spec.newBlock(i, b.forEachCtx)
					diags = append(diags, blockDiags...)
					if block != nil {
						// Attach our new iteration context so that attributes
						// and other nested blocks can refer to our iterator.
						block.Body = b.expandChild(block.Body, i, marks)
						blocks = append(blocks, block)
					}
				}
			} else {
				// If our top-level iteration value isn't known then we
				// substitute an unknownBody, which will cause the entire block
				// to evaluate to an unknown value.
				i := b.iteration.MakeChild(spec.iteratorName, cty.DynamicVal, cty.DynamicVal)
				block, blockDiags := spec.newBlock(i, b.forEachCtx)
				diags = append(diags, blockDiags...)
				if block != nil {
					block.Body = unknownBody{
						template:   b.expandChild(block.Body, i, marks),
						valueMarks: marks,
					}
					blocks = append(blocks, block)
				}
			}

		default:
			if _, hidden := b.hiddenBlocks[rawBlock.Type]; !hidden {
				// A static block doesn't create a new iteration context, but
				// it does need to inherit _our own_ iteration context in
				// case it contains expressions that refer to our inherited
				// iterators, or nested "dynamic" blocks.
				expandedBlock := *rawBlock // shallow copy
				expandedBlock.Body = b.expandChild(rawBlock.Body, b.iteration, nil)
				blocks = append(blocks, &expandedBlock)
			}
		}
	}

	return blocks, diags
}

func (b *expandBody) expandChild(child hcl.Body, i *iteration, valueMarks cty.ValueMarks) hcl.Body {
	chiCtx := i.EvalContext(b.forEachCtx)
	ret := Expand(child, chiCtx)
	ret.(*expandBody).iteration = i
	ret.(*expandBody).checkForEach = b.checkForEach
	ret.(*expandBody).valueMarks = valueMarks
	return ret
}

func (b *expandBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
	// blocks aren't allowed in JustAttributes mode and this body can
	// only produce blocks, so we'll just pass straight through to our
	// underlying body here.
	return b.original.JustAttributes()
}

func (b *expandBody) MissingItemRange() hcl.Range {
	return b.original.MissingItemRange()
}

// hcldec.MarkedBody impl
func (b *expandBody) BodyValueMarks() cty.ValueMarks {
	return b.valueMarks
}
