package terraform

import (
	"fmt"
	"strings"

	"github.com/aquasecurity/trivy/pkg/iac/ignore"
)

type Module struct {
	blocks     Blocks
	blockMap   map[string]Blocks
	rootPath   string
	modulePath string
	ignores    ignore.Rules
	parent     *Module
}

func NewModule(rootPath, modulePath string, blocks Blocks, ignores ignore.Rules) *Module {

	blockMap := make(map[string]Blocks)

	for _, b := range blocks {
		if b.NameLabel() != "" {
			blockMap[b.TypeLabel()] = append(blockMap[b.TypeLabel()], b)
		}
	}

	return &Module{
		blocks:     blocks,
		ignores:    ignores,
		blockMap:   blockMap,
		rootPath:   rootPath,
		modulePath: modulePath,
	}
}

func (c *Module) SetParent(parent *Module) {
	c.parent = parent
}

func (c *Module) RootPath() string {
	return c.rootPath
}

func (c *Module) ModulePath() string {
	return c.modulePath
}

func (c *Module) Parent() *Module {
	return c.parent
}

func (c *Module) Ignores() ignore.Rules {
	return c.ignores
}

func (c *Module) GetBlocks() Blocks {
	return c.blocks
}

func (h *Module) GetBlocksByTypeLabel(typeLabel string) Blocks {
	return h.blockMap[typeLabel]
}

func (c *Module) getBlocksByType(blockType string, labels ...string) Blocks {
	if blockType == "module" {
		return c.getModuleBlocks()
	}
	var results Blocks
	for _, label := range labels {
		for _, block := range c.blockMap[label] {
			if block.Type() == blockType {
				results = append(results, block)
			}
		}
	}
	return results
}

func (c *Module) getModuleBlocks() Blocks {
	var results Blocks
	for _, block := range c.blocks {
		if block.Type() == "module" {
			results = append(results, block)
		}
	}
	return results
}

func (c *Module) GetResourcesByType(labels ...string) Blocks {
	return c.getBlocksByType("resource", labels...)
}

func (c *Module) GetResourcesByIDs(ids ...string) Blocks {
	var blocks Blocks

	for _, id := range ids {
		if block := c.blocks.WithID(id); block != nil {
			blocks = append(blocks, block)
		}
	}
	return blocks
}

func (c *Module) GetDatasByType(label string) Blocks {
	return c.getBlocksByType("data", label)
}

func (c *Module) GetProviderBlocksByProvider(providerName, alias string) Blocks {
	var results Blocks
	for _, block := range c.blocks {
		if block.Type() == "provider" && len(block.Labels()) > 0 && block.TypeLabel() == providerName {
			if alias != "" {
				if block.HasChild("alias") && block.GetAttribute("alias").Equals(strings.ReplaceAll(alias, fmt.Sprintf("%s.", providerName), "")) {
					results = append(results, block)

				}
			} else if block.MissingChild("alias") {
				results = append(results, block)
			}
		}
	}
	return results
}

func (c *Module) GetReferencedBlock(referringAttr *Attribute, parentBlock *Block) (*Block, error) {
	for _, ref := range referringAttr.AllReferences() {
		if ref.TypeLabel() == "each" {
			if forEachAttr := parentBlock.GetAttribute("for_each"); forEachAttr.IsNotNil() {
				if b, err := c.GetReferencedBlock(forEachAttr, parentBlock); err == nil {
					return b, nil
				}
			}
		}
		for _, block := range c.blocks {
			if ref.RefersTo(block.reference) {
				return block, nil
			}
			kref := *ref
			kref.SetKey(parentBlock.reference.RawKey())
			if kref.RefersTo(block.reference) {
				return block, nil
			}
		}
	}
	return nil, fmt.Errorf("no referenced block found in '%s'", referringAttr.Name())
}

func (c *Module) GetBlockByID(id string) (*Block, error) {
	found := c.blocks.WithID(id)
	if found == nil {
		return nil, fmt.Errorf("no block found with id '%s'", id)
	}
	return found, nil
}

func (c *Module) GetReferencingResources(originalBlock *Block, referencingLabel, referencingAttributeName string) Blocks {
	return c.GetReferencingBlocks(originalBlock, "resource", referencingLabel, referencingAttributeName)
}

func (c *Module) GetsModulesBySource(moduleSource string) (Blocks, error) {
	var results Blocks

	modules := c.getModuleBlocks()
	for _, module := range modules {
		if module.HasChild("source") && module.GetAttribute("source").Equals(moduleSource) {
			results = append(results, module)
		}
	}
	return results, nil
}

func (c *Module) GetReferencingBlocks(originalBlock *Block, referencingType, referencingLabel, referencingAttributeName string) Blocks {
	blocks := c.getBlocksByType(referencingType, referencingLabel)
	var results Blocks
	for _, block := range blocks {
		attr := block.GetAttribute(referencingAttributeName)
		if attr == nil {
			continue
		}
		if attr.References(originalBlock.reference) {
			results = append(results, block)
		} else {
			for _, ref := range attr.AllReferences() {
				if ref.TypeLabel() == "each" {
					fe := block.GetAttribute("for_each")
					if fe.References(originalBlock.reference) {
						results = append(results, block)
					}
				}
			}
		}
	}
	return results
}
