package hclsyntax

import (
	"fmt"
	"strings"

	"github.com/hashicorp/hcl2/hcl"
)

// AsHCLBlock returns the block data expressed as a *hcl.Block.
func (b *Block) AsHCLBlock() *hcl.Block {
	if b == nil {
		return nil
	}

	lastHeaderRange := b.TypeRange
	if len(b.LabelRanges) > 0 {
		lastHeaderRange = b.LabelRanges[len(b.LabelRanges)-1]
	}

	return &hcl.Block{
		Type:   b.Type,
		Labels: b.Labels,
		Body:   b.Body,

		DefRange:    hcl.RangeBetween(b.TypeRange, lastHeaderRange),
		TypeRange:   b.TypeRange,
		LabelRanges: b.LabelRanges,
	}
}

// Body is the implementation of hcl.Body for the HCL native syntax.
type Body struct {
	Attributes Attributes
	Blocks     Blocks

	// These are used with PartialContent to produce a "remaining items"
	// body to return. They are nil on all bodies fresh out of the parser.
	hiddenAttrs  map[string]struct{}
	hiddenBlocks map[string]struct{}

	SrcRange hcl.Range
	EndRange hcl.Range // Final token of the body, for reporting missing items
}

// Assert that *Body implements hcl.Body
var assertBodyImplBody hcl.Body = &Body{}

func (b *Body) walkChildNodes(w internalWalkFunc) {
	w(b.Attributes)
	w(b.Blocks)
}

func (b *Body) Range() hcl.Range {
	return b.SrcRange
}

func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
	content, remainHCL, diags := b.PartialContent(schema)

	// No we'll see if anything actually remains, to produce errors about
	// extraneous items.
	remain := remainHCL.(*Body)

	for name, attr := range b.Attributes {
		if _, hidden := remain.hiddenAttrs[name]; !hidden {
			var suggestions []string
			for _, attrS := range schema.Attributes {
				if _, defined := content.Attributes[attrS.Name]; defined {
					continue
				}
				suggestions = append(suggestions, attrS.Name)
			}
			suggestion := nameSuggestion(name, suggestions)
			if suggestion != "" {
				suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
			} else {
				// Is there a block of the same name?
				for _, blockS := range schema.Blocks {
					if blockS.Type == name {
						suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name)
						break
					}
				}
			}

			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unsupported argument",
				Detail:   fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
				Subject:  &attr.NameRange,
			})
		}
	}

	for _, block := range b.Blocks {
		blockTy := block.Type
		if _, hidden := remain.hiddenBlocks[blockTy]; !hidden {
			var suggestions []string
			for _, blockS := range schema.Blocks {
				suggestions = append(suggestions, blockS.Type)
			}
			suggestion := nameSuggestion(blockTy, suggestions)
			if suggestion != "" {
				suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
			} else {
				// Is there an attribute of the same name?
				for _, attrS := range schema.Attributes {
					if attrS.Name == blockTy {
						suggestion = fmt.Sprintf(" Did you mean to define argument %q? If so, use the equals sign to assign it a value.", blockTy)
						break
					}
				}
			}

			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unsupported block type",
				Detail:   fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion),
				Subject:  &block.TypeRange,
			})
		}
	}

	return content, diags
}

func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
	attrs := make(hcl.Attributes)
	var blocks hcl.Blocks
	var diags hcl.Diagnostics
	hiddenAttrs := make(map[string]struct{})
	hiddenBlocks := make(map[string]struct{})

	if b.hiddenAttrs != nil {
		for k, v := range b.hiddenAttrs {
			hiddenAttrs[k] = v
		}
	}
	if b.hiddenBlocks != nil {
		for k, v := range b.hiddenBlocks {
			hiddenBlocks[k] = v
		}
	}

	for _, attrS := range schema.Attributes {
		name := attrS.Name
		attr, exists := b.Attributes[name]
		_, hidden := hiddenAttrs[name]
		if hidden || !exists {
			if attrS.Required {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Missing required argument",
					Detail:   fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
					Subject:  b.MissingItemRange().Ptr(),
				})
			}
			continue
		}

		hiddenAttrs[name] = struct{}{}
		attrs[name] = attr.AsHCLAttribute()
	}

	blocksWanted := make(map[string]hcl.BlockHeaderSchema)
	for _, blockS := range schema.Blocks {
		blocksWanted[blockS.Type] = blockS
	}

	for _, block := range b.Blocks {
		if _, hidden := hiddenBlocks[block.Type]; hidden {
			continue
		}
		blockS, wanted := blocksWanted[block.Type]
		if !wanted {
			continue
		}

		if len(block.Labels) > len(blockS.LabelNames) {
			name := block.Type
			if len(blockS.LabelNames) == 0 {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  fmt.Sprintf("Extraneous label for %s", name),
					Detail: fmt.Sprintf(
						"No labels are expected for %s blocks.", name,
					),
					Subject: block.LabelRanges[0].Ptr(),
					Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(),
				})
			} else {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  fmt.Sprintf("Extraneous label for %s", name),
					Detail: fmt.Sprintf(
						"Only %d labels (%s) are expected for %s blocks.",
						len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "), name,
					),
					Subject: block.LabelRanges[len(blockS.LabelNames)].Ptr(),
					Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(),
				})
			}
			continue
		}

		if len(block.Labels) < len(blockS.LabelNames) {
			name := block.Type
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  fmt.Sprintf("Missing %s for %s", blockS.LabelNames[len(block.Labels)], name),
				Detail: fmt.Sprintf(
					"All %s blocks must have %d labels (%s).",
					name, len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "),
				),
				Subject: &block.OpenBraceRange,
				Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(),
			})
			continue
		}

		blocks = append(blocks, block.AsHCLBlock())
	}

	// We hide blocks only after we've processed all of them, since otherwise
	// we can't process more than one of the same type.
	for _, blockS := range schema.Blocks {
		hiddenBlocks[blockS.Type] = struct{}{}
	}

	remain := &Body{
		Attributes: b.Attributes,
		Blocks:     b.Blocks,

		hiddenAttrs:  hiddenAttrs,
		hiddenBlocks: hiddenBlocks,

		SrcRange: b.SrcRange,
		EndRange: b.EndRange,
	}

	return &hcl.BodyContent{
		Attributes: attrs,
		Blocks:     blocks,

		MissingItemRange: b.MissingItemRange(),
	}, remain, diags
}

func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
	attrs := make(hcl.Attributes)
	var diags hcl.Diagnostics

	if len(b.Blocks) > 0 {
		example := b.Blocks[0]
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf("Unexpected %q block", example.Type),
			Detail:   "Blocks are not allowed here.",
			Subject:  &example.TypeRange,
		})
		// we will continue processing anyway, and return the attributes
		// we are able to find so that certain analyses can still be done
		// in the face of errors.
	}

	if b.Attributes == nil {
		return attrs, diags
	}

	for name, attr := range b.Attributes {
		if _, hidden := b.hiddenAttrs[name]; hidden {
			continue
		}
		attrs[name] = attr.AsHCLAttribute()
	}

	return attrs, diags
}

func (b *Body) MissingItemRange() hcl.Range {
	return hcl.Range{
		Filename: b.SrcRange.Filename,
		Start:    b.SrcRange.Start,
		End:      b.SrcRange.Start,
	}
}

// Attributes is the collection of attribute definitions within a body.
type Attributes map[string]*Attribute

func (a Attributes) walkChildNodes(w internalWalkFunc) {
	for _, attr := range a {
		w(attr)
	}
}

// Range returns the range of some arbitrary point within the set of
// attributes, or an invalid range if there are no attributes.
//
// This is provided only to complete the Node interface, but has no practical
// use.
func (a Attributes) Range() hcl.Range {
	// An attributes doesn't really have a useful range to report, since
	// it's just a grouping construct. So we'll arbitrarily take the
	// range of one of the attributes, or produce an invalid range if we have
	// none. In practice, there's little reason to ask for the range of
	// an Attributes.
	for _, attr := range a {
		return attr.Range()
	}
	return hcl.Range{
		Filename: "<unknown>",
	}
}

// Attribute represents a single attribute definition within a body.
type Attribute struct {
	Name string
	Expr Expression

	SrcRange    hcl.Range
	NameRange   hcl.Range
	EqualsRange hcl.Range
}

func (a *Attribute) walkChildNodes(w internalWalkFunc) {
	w(a.Expr)
}

func (a *Attribute) Range() hcl.Range {
	return a.SrcRange
}

// AsHCLAttribute returns the block data expressed as a *hcl.Attribute.
func (a *Attribute) AsHCLAttribute() *hcl.Attribute {
	if a == nil {
		return nil
	}
	return &hcl.Attribute{
		Name: a.Name,
		Expr: a.Expr,

		Range:     a.SrcRange,
		NameRange: a.NameRange,
	}
}

// Blocks is the list of nested blocks within a body.
type Blocks []*Block

func (bs Blocks) walkChildNodes(w internalWalkFunc) {
	for _, block := range bs {
		w(block)
	}
}

// Range returns the range of some arbitrary point within the list of
// blocks, or an invalid range if there are no blocks.
//
// This is provided only to complete the Node interface, but has no practical
// use.
func (bs Blocks) Range() hcl.Range {
	if len(bs) > 0 {
		return bs[0].Range()
	}
	return hcl.Range{
		Filename: "<unknown>",
	}
}

// Block represents a nested block structure
type Block struct {
	Type   string
	Labels []string
	Body   *Body

	TypeRange       hcl.Range
	LabelRanges     []hcl.Range
	OpenBraceRange  hcl.Range
	CloseBraceRange hcl.Range
}

func (b *Block) walkChildNodes(w internalWalkFunc) {
	w(b.Body)
}

func (b *Block) Range() hcl.Range {
	return hcl.RangeBetween(b.TypeRange, b.CloseBraceRange)
}

func (b *Block) DefRange() hcl.Range {
	return hcl.RangeBetween(b.TypeRange, b.OpenBraceRange)
}
