package terraform

import (
	"errors"
	"fmt"
	"io/fs"
	"strconv"
	"strings"

	"github.com/google/uuid"
	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/gocty"

	"github.com/aquasecurity/trivy/pkg/iac/terraform/context"
	iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
)

type Block struct {
	id           string
	hclBlock     *hcl.Block
	context      *context.Context
	moduleBlock  *Block
	parentBlock  *Block
	expanded     bool
	cloneIndex   int
	childBlocks  []*Block
	attributes   []*Attribute
	metadata     iacTypes.Metadata
	moduleSource string
	moduleFS     fs.FS
	reference    Reference
}

func NewBlock(hclBlock *hcl.Block, ctx *context.Context, moduleBlock *Block, parentBlock *Block, moduleSource string,
	moduleFS fs.FS, index ...cty.Value,
) *Block {
	if ctx == nil {
		ctx = context.NewContext(&hcl.EvalContext{}, nil)
	}

	var r hcl.Range
	switch body := hclBlock.Body.(type) {
	case *hclsyntax.Body:
		r = body.SrcRange
	default:
		r = hclBlock.DefRange
		r.End = hclBlock.Body.MissingItemRange().End
	}
	moduleName := "root"
	if moduleBlock != nil {
		moduleName = moduleBlock.FullName()
	}
	rng := iacTypes.NewRange(
		r.Filename,
		r.Start.Line,
		r.End.Line,
		moduleSource,
		moduleFS,
	)

	var parts []string
	// if there are no labels then use the block type
	// this is for the case where "special" keywords like "resource" are used
	// as normal block names in top level blocks - see issue tfsec#1528 for an example
	if hclBlock.Type != "resource" || len(hclBlock.Labels) == 0 {
		parts = append(parts, hclBlock.Type)
	}
	parts = append(parts, hclBlock.Labels...)

	var parent string
	if moduleBlock != nil {
		parent = moduleBlock.FullName()
	}
	ref, _ := newReference(parts, parent)
	if len(index) > 0 {
		key := index[0]
		ref.SetKey(key)
	}

	metadata := iacTypes.NewMetadata(rng, ref.String())

	if parentBlock != nil {
		metadata = metadata.WithParent(parentBlock.metadata)
	} else if moduleBlock != nil {
		metadata = metadata.WithParent(moduleBlock.GetMetadata())
	}

	b := Block{
		id:           uuid.NewString(),
		context:      ctx,
		hclBlock:     hclBlock,
		moduleBlock:  moduleBlock,
		moduleSource: moduleSource,
		moduleFS:     moduleFS,
		parentBlock:  parentBlock,
		metadata:     metadata,
		reference:    *ref,
	}

	var children Blocks
	switch body := hclBlock.Body.(type) {
	case *hclsyntax.Body:
		for _, b2 := range body.Blocks {
			children = append(children, NewBlock(b2.AsHCLBlock(), ctx, moduleBlock, &b, moduleSource, moduleFS))
		}
	default:
		content, _, diag := hclBlock.Body.PartialContent(Schema)
		if diag == nil {
			for _, hb := range content.Blocks {
				children = append(children, NewBlock(hb, ctx, moduleBlock, &b, moduleSource, moduleFS))
			}
		}
	}

	b.childBlocks = children

	for _, attr := range b.createAttributes() {
		b.attributes = append(b.attributes, NewAttribute(attr, ctx, moduleName, metadata, *ref, moduleSource, moduleFS))
	}

	return &b
}

func (b *Block) HCLBlock() *hcl.Block {
	return b.hclBlock
}

func (b *Block) ID() string {
	return b.id
}

func (b *Block) Reference() Reference {
	return b.reference
}

func (b *Block) GetMetadata() iacTypes.Metadata {
	return b.metadata
}

func (b *Block) GetRawValue() any {
	return nil
}

func (b *Block) injectBlock(block *Block) {
	for attrName, attr := range block.Attributes() {
		path := fmt.Sprintf("%s.%s.%s", b.reference.String(), block.hclBlock.Type, attrName)
		b.context.Root().SetByDot(attr.Value(), path)
	}
	b.childBlocks = append(b.childBlocks, block)
}

func (b *Block) markExpanded() {
	b.expanded = true
}

func (b *Block) IsExpanded() bool {
	return b.expanded
}

func (b *Block) inherit(ctx *context.Context, index ...cty.Value) *Block {
	return NewBlock(b.copyBlock(), ctx, b.moduleBlock, b.parentBlock, b.moduleSource, b.moduleFS, index...)
}

func (b *Block) copyBlock() *hcl.Block {
	hclBlock := *b.hclBlock
	return &hclBlock
}

func (b *Block) childContext() *context.Context {
	if b.context == nil {
		return context.NewContext(&hcl.EvalContext{}, nil)
	}
	return b.context.NewChild()
}

func (b *Block) Clone(index cty.Value) *Block {
	childCtx := b.childContext()
	clone := b.inherit(childCtx, index)

	if len(clone.hclBlock.Labels) > 0 {
		position := len(clone.hclBlock.Labels) - 1
		labels := make([]string, len(clone.hclBlock.Labels))
		for i := range labels {
			labels[i] = clone.hclBlock.Labels[i]
		}
		if index.IsKnown() && !index.IsNull() {
			switch index.Type() {
			case cty.Number:
				f, _ := index.AsBigFloat().Float64()
				labels[position] = fmt.Sprintf("%s[%d]", clone.hclBlock.Labels[position], int(f))
			case cty.String:
				labels[position] = fmt.Sprintf("%s[%q]", clone.hclBlock.Labels[position], index.AsString())
			default:
				labels[position] = fmt.Sprintf("%s[%#v]", clone.hclBlock.Labels[position], index)
			}
		} else {
			labels[position] = fmt.Sprintf("%s[%d]", clone.hclBlock.Labels[position], b.cloneIndex)
		}
		clone.hclBlock.Labels = labels
	}
	indexVal, _ := gocty.ToCtyValue(index, cty.Number)
	clone.context.SetByDot(indexVal, "count.index")
	clone.markExpanded()
	b.cloneIndex++
	return clone
}

func (b *Block) Context() *context.Context {
	return b.context
}

func (b *Block) OverrideContext(ctx *context.Context) {
	b.context = ctx
	for _, block := range b.childBlocks {
		block.OverrideContext(ctx.NewChild())
	}
	for _, attr := range b.attributes {
		attr.ctx = ctx
	}
}

func (b *Block) Type() string {
	return b.hclBlock.Type
}

func (b *Block) Labels() []string {
	return b.hclBlock.Labels
}

func (b *Block) GetFirstMatchingBlock(names ...string) *Block {
	var returnBlock *Block
	for _, name := range names {
		childBlock := b.GetBlock(name)
		if childBlock.IsNotNil() {
			return childBlock
		}
	}
	return returnBlock
}

func (b *Block) createAttributes() hcl.Attributes {
	switch body := b.hclBlock.Body.(type) {
	case *hclsyntax.Body:
		attributes := make(hcl.Attributes)
		for _, a := range body.Attributes {
			attributes[a.Name] = a.AsHCLAttribute()
		}
		return attributes
	default:
		_, body, diag := b.hclBlock.Body.PartialContent(Schema)
		if diag != nil {
			return nil
		}
		attrs, diag := body.JustAttributes()
		if diag != nil {
			return nil
		}
		return attrs
	}
}

func (b *Block) GetBlock(name string) *Block {
	var returnBlock *Block
	if b == nil || b.hclBlock == nil {
		return returnBlock
	}
	for _, child := range b.childBlocks {
		if child.Type() == name {
			return child
		}
	}
	return returnBlock
}

func (b *Block) AllBlocks() Blocks {
	if b == nil || b.hclBlock == nil {
		return nil
	}
	return b.childBlocks
}

func (b *Block) GetBlocks(name string) Blocks {
	if b == nil || b.hclBlock == nil {
		return nil
	}
	var results []*Block
	for _, child := range b.childBlocks {
		if child.Type() == name {
			results = append(results, child)
		}
	}
	return results
}

func (b *Block) GetAttributes() []*Attribute {
	if b == nil {
		return nil
	}
	return b.attributes
}

func (b *Block) GetAttribute(name string) *Attribute {
	if b == nil || b.hclBlock == nil {
		return nil
	}
	for _, attr := range b.attributes {
		if attr.Name() == name {
			return attr
		}
	}
	return nil
}

// GetValueByPath returns the value of the attribute located at the given path.
// Supports special paths like "count.index," "each.key," and "each.value."
// The path may contain indices, keys and dots (used as separators).
func (b *Block) GetValueByPath(path string) cty.Value {
	if path == "count.index" || path == "each.key" || path == "each.value" {
		return b.Context().GetByDot(path)
	}

	if restPath, ok := strings.CutPrefix(path, "each.value."); ok {
		if restPath == "" {
			return cty.NilVal
		}

		val := b.Context().GetByDot("each.value")
		res, err := getValueByPath(val, strings.Split(restPath, "."))
		if err != nil {
			return cty.NilVal
		}
		return res
	}

	attr, restPath := b.getAttributeByPath(path)

	if attr == nil {
		return cty.NilVal
	}

	if !attr.IsIterable() || len(restPath) == 0 {
		return attr.Value()
	}

	res, err := getValueByPath(attr.Value(), restPath)
	if err != nil {
		return cty.NilVal
	}
	return res
}

func (b *Block) getAttributeByPath(path string) (*Attribute, []string) {
	steps := strings.Split(path, ".")

	if len(steps) == 1 {
		return b.GetAttribute(steps[0]), nil
	}

	var (
		attribute *Attribute
		stepIndex int
	)

	for currentBlock := b; currentBlock != nil && stepIndex < len(steps); {
		blocks := currentBlock.GetBlocks(steps[stepIndex])
		var nextBlock *Block
		if !hasIndex(steps, stepIndex+1) && len(blocks) > 0 {
			// if index is not provided then return the first block for backwards compatibility
			nextBlock = blocks[0]
		} else if len(blocks) > 1 && stepIndex < len(steps)-2 {
			// handling the case when there are multiple blocks with the same name,
			// e.g. when using a `dynamic` block
			indexVal, err := strconv.Atoi(steps[stepIndex+1])
			if err == nil && indexVal >= 0 && indexVal < len(blocks) {
				nextBlock = blocks[indexVal]
				stepIndex++
			}
		}

		if nextBlock == nil {
			attribute = currentBlock.GetAttribute(steps[stepIndex])
		}

		currentBlock = nextBlock
		stepIndex++
	}

	return attribute, steps[stepIndex:]
}

func hasIndex(steps []string, idx int) bool {
	if idx < 0 || idx >= len(steps) {
		return false
	}
	_, err := strconv.Atoi(steps[idx])
	return err == nil
}

func getValueByPath(val cty.Value, path []string) (cty.Value, error) {
	var err error
	for _, step := range path {
		switch valType := val.Type(); {
		case valType.IsMapType():
			val, err = cty.IndexStringPath(step).Apply(val)
		case valType.IsObjectType():
			val, err = cty.GetAttrPath(step).Apply(val)
		case valType.IsListType() || valType.IsTupleType():
			var idx int
			idx, err = strconv.Atoi(step)
			if err != nil {
				return cty.NilVal, fmt.Errorf("index %q is not a number", step)
			}
			val, err = cty.IndexIntPath(idx).Apply(val)
		default:
			return cty.NilVal, fmt.Errorf(
				"unexpected value type %s for path step %q",
				valType.FriendlyName(), step,
			)
		}
		if err != nil {
			return cty.NilVal, err
		}
	}
	return val, nil
}

func (b *Block) GetNestedAttribute(name string) (*Attribute, *Block) {
	parts := strings.Split(name, ".")
	blocks := parts[:len(parts)-1]
	attrName := parts[len(parts)-1]

	working := b
	for _, subBlock := range blocks {
		checkBlock := working.GetBlock(subBlock)
		if checkBlock == nil {
			return nil, working
		}
		working = checkBlock
	}

	if working != nil {
		if attr := working.GetAttribute(attrName); attr != nil {
			return attr, working
		}
	}

	return nil, b
}

func MapNestedAttribute[T any](block *Block, path string, f func(attr *Attribute, parent *Block) T) T {
	return f(block.GetNestedAttribute(path))
}

// LocalName is the name relative to the current module
func (b *Block) LocalName() string {
	return b.reference.String()
}

func (b *Block) FullLocalName() string {
	if b.parentBlock != nil {
		return fmt.Sprintf(
			"%s.%s",
			b.parentBlock.FullLocalName(),
			b.LocalName(),
		)
	}
	return b.LocalName()
}

func (b *Block) FullName() string {
	if b.moduleBlock != nil {
		return fmt.Sprintf(
			"%s.%s",
			b.moduleBlock.FullName(),
			b.LocalName(),
		)
	}

	return b.LocalName()
}

func (b *Block) ModuleBlock() *Block {
	return b.moduleBlock
}

func (b *Block) ModuleKey() string {
	name := b.Reference().NameLabel()
	if b.moduleBlock == nil {
		return name
	}
	return fmt.Sprintf("%s.%s", b.moduleBlock.ModuleKey(), name)
}

func (b *Block) UniqueName() string {
	if b.moduleBlock != nil {
		return fmt.Sprintf("%s:%s:%s", b.FullName(), b.metadata.Range().GetFilename(), b.moduleBlock.UniqueName())
	}
	return fmt.Sprintf("%s:%s", b.FullName(), b.metadata.Range().GetFilename())
}

func (b *Block) TypeLabel() string {
	if len(b.Labels()) > 0 {
		return b.Labels()[0]
	}
	return ""
}

func (b *Block) NameLabel() string {
	if len(b.Labels()) > 1 {
		return b.Labels()[1]
	}
	return ""
}

func (b *Block) InModule() bool {
	if b == nil {
		return false
	}
	return b.moduleBlock != nil
}

func (b *Block) Label() string {
	return strings.Join(b.hclBlock.Labels, ".")
}

func (b *Block) IsResourceType(resourceType string) bool {
	return b.TypeLabel() == resourceType
}

func (b *Block) IsEmpty() bool {
	return len(b.AllBlocks()) == 0 && len(b.GetAttributes()) == 0
}

func (b *Block) Attributes() map[string]*Attribute {
	attributes := make(map[string]*Attribute)
	for _, attr := range b.GetAttributes() {
		attributes[attr.Name()] = attr
	}
	return attributes
}

func (b *Block) NullableValues() cty.Value {
	return b.values(true)
}

func (b *Block) Values() cty.Value {
	return b.values(false)
}

func (b *Block) values(allowNull bool) cty.Value {
	values := createPresetValues(b)
	for _, attribute := range b.GetAttributes() {
		if attribute.Name() == "for_each" {
			continue
		}
		if allowNull {
			values[attribute.Name()] = attribute.NullableValue()
		} else {
			values[attribute.Name()] = attribute.Value()
		}
	}
	return cty.ObjectVal(postProcessValues(b, values))
}

func (b *Block) IsNil() bool {
	return b == nil
}

func (b *Block) IsNotNil() bool {
	return !b.IsNil()
}

func (b *Block) ExpandBlock() error {
	var (
		expanded []*Block
		errs     error
	)

	for _, child := range b.childBlocks {
		if child.Type() == "dynamic" {
			blocks, err := child.expandDynamic()
			if err != nil {
				errs = multierror.Append(errs, fmt.Errorf("block %q: %w", child.TypeLabel(), err))
				continue
			}
			expanded = append(expanded, blocks...)
		}
	}

	for _, block := range expanded {
		b.injectBlock(block)
	}

	return errs
}

func (b *Block) expandDynamic() ([]*Block, error) {
	if b.IsExpanded() || b.Type() != "dynamic" {
		return nil, nil
	}

	realBlockType := b.TypeLabel()
	if realBlockType == "" {
		return nil, errors.New("dynamic block must have 1 label")
	}

	forEachVal, err := b.validateForEach()
	if err != nil {
		return nil, fmt.Errorf("invalid for-each in %s block: %w", b.FullLocalName(), err)
	}

	if !forEachVal.IsKnown() {
		return nil, errors.New("for-each must be known")
	}

	var (
		expanded []*Block
		errs     error
	)

	forEachVal.ForEachElement(func(key, val cty.Value) (stop bool) {
		if val.IsNull() || !val.IsKnown() {
			return
		}

		iteratorName, err := b.iteratorName(realBlockType)
		if err != nil {
			errs = multierror.Append(errs, err)
			return
		}

		forEachCtx := b.childContext()
		obj := cty.ObjectVal(map[string]cty.Value{
			"key":   key,
			"value": val,
		})
		forEachCtx.Set(obj, iteratorName)

		if content := b.GetBlock("content"); content != nil {
			inherited := content.inherit(forEachCtx)
			inherited.hclBlock.Labels = []string{}
			inherited.hclBlock.Type = realBlockType
			if err := inherited.ExpandBlock(); err != nil {
				errs = multierror.Append(errs, err)
				return
			}
			expanded = append(expanded, inherited)
		}
		return
	})

	if len(expanded) > 0 {
		b.markExpanded()
	}

	return expanded, errs
}

func (b *Block) validateForEach() (cty.Value, error) {
	forEachAttr := b.GetAttribute("for_each")
	if forEachAttr == nil {
		return cty.NilVal, errors.New("for_each attribute required")
	}

	forEachVal := forEachAttr.Value()

	if !forEachVal.CanIterateElements() {
		return cty.NilVal, fmt.Errorf("cannot use a %s value in for_each. An iterable collection is required", forEachVal.GoString())
	}

	return forEachVal, nil
}

func (b *Block) iteratorName(blockType string) (string, error) {
	iteratorAttr := b.GetAttribute("iterator")
	if iteratorAttr == nil {
		return blockType, nil
	}

	traversal, diags := hcl.AbsTraversalForExpr(iteratorAttr.hclAttribute.Expr)
	if diags.HasErrors() {
		return "", diags
	}

	if len(traversal) != 1 {
		return "", errors.New("dynamic iterator must be a single variable name")
	}

	return traversal.RootName(), nil
}
