package hclsyntax

import (
    "fmt"
    "strings"

    "Havoc/pkg/profile/yaotl"
)

// 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)
}
