package blocktoattr

import (
	"log"

	"github.com/camptocamp/terraboard/internal/terraform/configs/configschema"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/zclconf/go-cty/cty"
)

// FixUpBlockAttrs takes a raw HCL body and adds some additional normalization
// functionality to allow attributes that are specified as having list or set
// type in the schema to be written with HCL block syntax as multiple nested
// blocks with the attribute name as the block type.
//
// The fixup is only applied in the absence of structural attribute types. The
// presence of these types indicate the use of a provider which does not
// support mapping blocks to attributes.
//
// This partially restores some of the block/attribute confusion from HCL 1
// so that existing patterns that depended on that confusion can continue to
// be used in the short term while we settle on a longer-term strategy.
//
// Most of the fixup work is actually done when the returned body is
// subsequently decoded, so while FixUpBlockAttrs always succeeds, the eventual
// decode of the body might not, if the content of the body is so ambiguous
// that there's no safe way to map it to the schema.
func FixUpBlockAttrs(body hcl.Body, schema *configschema.Block) hcl.Body {
	// The schema should never be nil, but in practice it seems to be sometimes
	// in the presence of poorly-configured test mocks, so we'll be robust
	// by synthesizing an empty one.
	if schema == nil {
		schema = &configschema.Block{}
	}

	if skipFixup(schema) {
		// we don't have any context for the resource name or type, but
		// hopefully this could help locate the evaluation in the logs if there
		// were a problem
		log.Println("[DEBUG] skipping FixUpBlockAttrs")
		return body
	}

	return &fixupBody{
		original: body,
		schema:   schema,
		names:    ambiguousNames(schema),
	}
}

// skipFixup detects any use of Attribute.NestedType, or Types which could not
// be generate by the legacy SDK when taking SchemaConfigModeAttr into account.
func skipFixup(schema *configschema.Block) bool {
	for _, attr := range schema.Attributes {
		if attr.NestedType != nil {
			return true
		}
		ty := attr.Type

		// Lists and sets of objects could be generated by
		// SchemaConfigModeAttr, but some other combinations can be ruled out.

		// Tuples and objects could not be generated at all.
		if ty.IsTupleType() || ty.IsObjectType() {
			return true
		}

		// A map of objects was not possible.
		if ty.IsMapType() && ty.ElementType().IsObjectType() {
			return true
		}

		// Nested collections were not really supported, but could be generated
		// with string types (though we conservatively limit this to primitive types)
		if ty.IsCollectionType() {
			ety := ty.ElementType()
			if ety.IsCollectionType() && !ety.ElementType().IsPrimitiveType() {
				return true
			}
		}
	}

	for _, block := range schema.BlockTypes {
		if skipFixup(&block.Block) {
			return true
		}
	}

	return false
}

type fixupBody struct {
	original hcl.Body
	schema   *configschema.Block
	names    map[string]struct{}
}

type unknownBlock interface {
	Unknown() bool
}

func (b *fixupBody) Unknown() bool {
	if u, ok := b.original.(unknownBlock); ok {
		return u.Unknown()
	}
	return false
}

// Content decodes content from the body. The given schema must be the lower-level
// representation of the same schema that was previously passed to FixUpBlockAttrs,
// or else the result is undefined.
func (b *fixupBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
	schema = b.effectiveSchema(schema)
	content, diags := b.original.Content(schema)
	return b.fixupContent(content), diags
}

func (b *fixupBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
	schema = b.effectiveSchema(schema)
	content, remain, diags := b.original.PartialContent(schema)
	remain = &fixupBody{
		original: remain,
		schema:   b.schema,
		names:    b.names,
	}
	return b.fixupContent(content), remain, diags
}

func (b *fixupBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
	// FixUpBlockAttrs is not intended to be used in situations where we'd use
	// JustAttributes, so we just pass this through verbatim to complete our
	// implementation of hcl.Body.
	return b.original.JustAttributes()
}

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

// effectiveSchema produces a derived *hcl.BodySchema by sniffing the body's
// content to determine whether the author has used attribute or block syntax
// for each of the ambigious attributes where both are permitted.
//
// The resulting schema will always contain all of the same names that are
// in the given schema, but some attribute schemas may instead be replaced by
// block header schemas.
func (b *fixupBody) effectiveSchema(given *hcl.BodySchema) *hcl.BodySchema {
	return effectiveSchema(given, b.original, b.names, true)
}

func (b *fixupBody) fixupContent(content *hcl.BodyContent) *hcl.BodyContent {
	var ret hcl.BodyContent
	ret.Attributes = make(hcl.Attributes)
	for name, attr := range content.Attributes {
		ret.Attributes[name] = attr
	}
	blockAttrVals := make(map[string][]*hcl.Block)
	for _, block := range content.Blocks {
		if _, exists := b.names[block.Type]; exists {
			// If we get here then we've found a block type whose instances need
			// to be re-interpreted as a list-of-objects attribute. We'll gather
			// those up and fix them up below.
			blockAttrVals[block.Type] = append(blockAttrVals[block.Type], block)
			continue
		}

		// We need to now re-wrap our inner body so it will be subject to the
		// same attribute-as-block fixup when recursively decoded.
		retBlock := *block // shallow copy
		if blockS, ok := b.schema.BlockTypes[block.Type]; ok {
			// Would be weird if not ok, but we'll allow it for robustness; body just won't be fixed up, then
			retBlock.Body = FixUpBlockAttrs(retBlock.Body, &blockS.Block)
		}

		ret.Blocks = append(ret.Blocks, &retBlock)
	}
	// No we'll install synthetic attributes for each of our fixups. We can't
	// do this exactly because HCL's information model expects an attribute
	// to be a single decl but we have multiple separate blocks. We'll
	// approximate things, then, by using only our first block for the source
	// location information. (We are guaranteed at least one by the above logic.)
	for name, blocks := range blockAttrVals {
		ret.Attributes[name] = &hcl.Attribute{
			Name: name,
			Expr: &fixupBlocksExpr{
				blocks: blocks,
				ety:    b.schema.Attributes[name].Type.ElementType(),
			},

			Range:     blocks[0].DefRange,
			NameRange: blocks[0].TypeRange,
		}
	}

	ret.MissingItemRange = b.MissingItemRange()
	return &ret
}

type fixupBlocksExpr struct {
	blocks hcl.Blocks
	ety    cty.Type
}

func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	// In order to produce a suitable value for our expression we need to
	// now decode the whole descendent block structure under each of our block
	// bodies.
	//
	// That requires us to do something rather strange: we must construct a
	// synthetic block type schema derived from the element type of the
	// attribute, thus inverting our usual direction of lowering a schema
	// into an implied type. Because a type is less detailed than a schema,
	// the result is imprecise and in particular will just consider all
	// the attributes to be optional and let the provider eventually decide
	// whether to return errors if they turn out to be null when required.
	schema := SchemaForCtyElementType(e.ety) // this schema's ImpliedType will match e.ety
	spec := schema.DecoderSpec()

	vals := make([]cty.Value, len(e.blocks))
	var diags hcl.Diagnostics
	for i, block := range e.blocks {
		body := FixUpBlockAttrs(block.Body, schema)
		val, blockDiags := hcldec.Decode(body, spec, ctx)
		diags = append(diags, blockDiags...)
		if val == cty.NilVal {
			val = cty.UnknownVal(e.ety)
		}
		vals[i] = val
	}
	if len(vals) == 0 {
		return cty.ListValEmpty(e.ety), diags
	}
	return cty.ListVal(vals), diags
}

func (e *fixupBlocksExpr) Variables() []hcl.Traversal {
	var ret []hcl.Traversal
	schema := SchemaForCtyElementType(e.ety)
	spec := schema.DecoderSpec()
	for _, block := range e.blocks {
		ret = append(ret, hcldec.Variables(block.Body, spec)...)
	}
	return ret
}

func (e *fixupBlocksExpr) Range() hcl.Range {
	// This is not really an appropriate range for the expression but it's
	// the best we can do from here.
	return e.blocks[0].DefRange
}

func (e *fixupBlocksExpr) StartRange() hcl.Range {
	return e.blocks[0].DefRange
}
