package hclparse

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclparse"
)

const (
	// A consistent error message for multiple catalog block in terragrunt config (which is currently not supported)
	multipleBlockDetailFmt = "Terragrunt currently does not support multiple %[1]s blocks in a single config. Consolidate to a single %[1]s block."
)

type File struct {
	*Parser
	*hcl.File
	ConfigPath string
}

func (file *File) Content() string {
	return string(file.Bytes)
}

// Update reparses the file with the new `content`.
func (file *File) Update(content []byte) error {
	// Since `hclparse.Parser` has a cache, we need to recreate(clone) the Parser instance without current file
	// to be able to parse the configuration with the same `configPath`.
	parser := hclparse.NewParser()

	for configPath, copyfile := range file.Files() {
		if configPath != file.ConfigPath {
			parser.AddFile(configPath, copyfile)
		}
	}

	file.Parser.Parser = parser

	// we need to reparse the new updated contents. This is necessarily because the blocks
	// returned by hclparse does not support editing, and so we have to go through hclwrite, which leads to a
	// different AST representation.
	updatedFile, err := file.ParseFromBytes(content, file.ConfigPath)
	if err != nil {
		return err
	}

	file.File = updatedFile.File

	return nil
}

// Decode uses the HCL2 parser to decode the parsed HCL into the struct specified by out.
//
// Note that we take a two pass approach to support parsing include blocks without a label. Ideally we can parse include
// blocks with and without labels in a single pass, but the HCL parser is fairly restrictive when it comes to parsing
// blocks with labels, requiring the exact number of expected labels in the parsing step.  To handle this restriction,
// we first see if there are any include blocks without any labels, and if there is, we modify it in the file object to
// inject the label as "".
func (file *File) Decode(out any, evalContext *hcl.EvalContext) (err error) {
	if file.fileUpdateHandlerFunc != nil {
		if err := file.fileUpdateHandlerFunc(file); err != nil {
			return err
		}
	}

	diags := gohcl.DecodeBody(file.Body, evalContext, out)
	if err := file.HandleDiagnostics(diags); err != nil {
		return err
	}

	return nil
}

// Blocks takes a parsed HCL file and extracts a reference to the `name` block, if there are defined.
func (file *File) Blocks(name string, isMultipleAllowed bool) ([]*Block, error) {
	catalogSchema := &hcl.BodySchema{
		Blocks: []hcl.BlockHeaderSchema{
			{Type: name},
		},
	}
	// We use PartialContent here, because we are only interested in parsing out the catalog block.
	parsed, _, diags := file.Body.PartialContent(catalogSchema)
	if err := file.HandleDiagnostics(diags); err != nil {
		return nil, err
	}

	extractedBlocks := []*Block{}

	for _, block := range parsed.Blocks {
		if block.Type == name {
			extractedBlocks = append(extractedBlocks, &Block{
				File:  file,
				Block: block,
			})
		}
	}

	if len(extractedBlocks) > 1 && !isMultipleAllowed {
		return nil, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf("Multiple %s block", name),
			Detail:   fmt.Sprintf(multipleBlockDetailFmt, name),
		}
	}

	return extractedBlocks, nil
}

func (file *File) JustAttributes() (Attributes, error) {
	hclAttrs, diags := file.Body.JustAttributes()

	if err := file.HandleDiagnostics(diags); err != nil {
		return nil, err
	}

	attrs := NewAttributes(file, hclAttrs)

	if err := attrs.ValidateIdentifier(); err != nil {
		return nil, err
	}

	return attrs, nil
}

func (file *File) HandleDiagnostics(diags hcl.Diagnostics) error {
	return file.handleDiagnostics(file, diags)
}

// Rebind returns a new File wrapper bound to `parser`, sharing the underlying AST.
//
// `parser` must be non-nil; Rebind panics otherwise. In practice all callers
// obtain it from [NewParser], which never returns nil.
//
// The `parser` argument is mutated: the file's AST is registered in its file map.
// This is required because `hcl.NewDiagnosticTextWriter` captures the parser's
// file map by reference at construction time, so a fresh parser cannot render
// code snippets for a file it has not seen parsed.
//
// The original file wrapper is not modified.
func (file *File) Rebind(parser *Parser) *File {
	if parser == nil {
		panic("hclparse: Rebind called with nil parser")
	}

	parser.AddFile(file.ConfigPath, file.File)

	return &File{
		Parser:     parser,
		File:       file.File,
		ConfigPath: file.ConfigPath,
	}
}
