package json

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

// body is the implementation of "Body" used for files processed with the JSON
// parser.
type body struct {
	val node

	// If non-nil, the keys of this map cause the corresponding attributes to
	// be treated as non-existing. This is used when Body.PartialContent is
	// called, to produce the "remaining content" Body.
	hiddenAttrs map[string]struct{}
}

// expression is the implementation of "Expression" used for files processed
// with the JSON parser.
type expression struct {
	src node
}

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

	hiddenAttrs := newBody.(*body).hiddenAttrs

	var nameSuggestions []string
	for _, attrS := range schema.Attributes {
		if _, ok := hiddenAttrs[attrS.Name]; !ok {
			// Only suggest an attribute name if we didn't use it already.
			nameSuggestions = append(nameSuggestions, attrS.Name)
		}
	}
	for _, blockS := range schema.Blocks {
		// Blocks can appear multiple times, so we'll suggest their type
		// names regardless of whether they've already been used.
		nameSuggestions = append(nameSuggestions, blockS.Type)
	}

	jsonAttrs, attrDiags := b.collectDeepAttrs(b.val, nil)
	diags = append(diags, attrDiags...)

	for _, attr := range jsonAttrs {
		k := attr.Name
		if k == "//" {
			// Ignore "//" keys in objects representing bodies, to allow
			// their use as comments.
			continue
		}

		if _, ok := hiddenAttrs[k]; !ok {
			suggestion := nameSuggestion(k, nameSuggestions)
			if suggestion != "" {
				suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
			}

			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Extraneous JSON object property",
				Detail:   fmt.Sprintf("No argument or block type is named %q.%s", k, suggestion),
				Subject:  &attr.NameRange,
				Context:  attr.Range().Ptr(),
			})
		}
	}

	return content, diags
}

func (b *body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	jsonAttrs, attrDiags := b.collectDeepAttrs(b.val, nil)
	diags = append(diags, attrDiags...)

	usedNames := map[string]struct{}{}
	if b.hiddenAttrs != nil {
		for k := range b.hiddenAttrs {
			usedNames[k] = struct{}{}
		}
	}

	content := &hcl.BodyContent{
		Attributes: map[string]*hcl.Attribute{},
		Blocks:     nil,

		MissingItemRange: b.MissingItemRange(),
	}

	// Create some more convenient data structures for our work below.
	attrSchemas := map[string]hcl.AttributeSchema{}
	blockSchemas := map[string]hcl.BlockHeaderSchema{}
	for _, attrS := range schema.Attributes {
		attrSchemas[attrS.Name] = attrS
	}
	for _, blockS := range schema.Blocks {
		blockSchemas[blockS.Type] = blockS
	}

	for _, jsonAttr := range jsonAttrs {
		attrName := jsonAttr.Name
		if _, used := b.hiddenAttrs[attrName]; used {
			continue
		}

		if attrS, defined := attrSchemas[attrName]; defined {
			if existing, exists := content.Attributes[attrName]; exists {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Duplicate argument",
					Detail:   fmt.Sprintf("The argument %q was already set at %s.", attrName, existing.Range),
					Subject:  &jsonAttr.NameRange,
					Context:  jsonAttr.Range().Ptr(),
				})
				continue
			}

			content.Attributes[attrS.Name] = &hcl.Attribute{
				Name:      attrS.Name,
				Expr:      &expression{src: jsonAttr.Value},
				Range:     hcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()),
				NameRange: jsonAttr.NameRange,
			}
			usedNames[attrName] = struct{}{}

		} else if blockS, defined := blockSchemas[attrName]; defined {
			bv := jsonAttr.Value
			blockDiags := b.unpackBlock(bv, blockS.Type, &jsonAttr.NameRange, blockS.LabelNames, nil, nil, &content.Blocks)
			diags = append(diags, blockDiags...)
			usedNames[attrName] = struct{}{}
		}

		// We ignore anything that isn't defined because that's the
		// PartialContent contract. The Content method will catch leftovers.
	}

	// Make sure we got all the required attributes.
	for _, attrS := range schema.Attributes {
		if !attrS.Required {
			continue
		}
		if _, defined := content.Attributes[attrS.Name]; !defined {
			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(),
			})
		}
	}

	unusedBody := &body{
		val:         b.val,
		hiddenAttrs: usedNames,
	}

	return content, unusedBody, diags
}

// JustAttributes for JSON bodies interprets all properties of the wrapped
// JSON object as attributes and returns them.
func (b *body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	attrs := make(map[string]*hcl.Attribute)

	obj, ok := b.val.(*objectVal)
	if !ok {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Incorrect JSON value type",
			Detail:   "A JSON object is required here, setting the arguments for this block.",
			Subject:  b.val.StartRange().Ptr(),
		})
		return attrs, diags
	}

	for _, jsonAttr := range obj.Attrs {
		name := jsonAttr.Name
		if name == "//" {
			// Ignore "//" keys in objects representing bodies, to allow
			// their use as comments.
			continue
		}

		if _, hidden := b.hiddenAttrs[name]; hidden {
			continue
		}

		if existing, exists := attrs[name]; exists {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Duplicate attribute definition",
				Detail:   fmt.Sprintf("The argument %q was already set at %s.", name, existing.Range),
				Subject:  &jsonAttr.NameRange,
			})
			continue
		}

		attrs[name] = &hcl.Attribute{
			Name:      name,
			Expr:      &expression{src: jsonAttr.Value},
			Range:     hcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()),
			NameRange: jsonAttr.NameRange,
		}
	}

	// No diagnostics possible here, since the parser already took care of
	// finding duplicates and every JSON value can be a valid attribute value.
	return attrs, diags
}

func (b *body) MissingItemRange() hcl.Range {
	switch tv := b.val.(type) {
	case *objectVal:
		return tv.CloseRange
	case *arrayVal:
		return tv.OpenRange
	default:
		// Should not happen in correct operation, but might show up if the
		// input is invalid and we are producing partial results.
		return tv.StartRange()
	}
}

func (b *body) unpackBlock(v node, typeName string, typeRange *hcl.Range, labelsLeft []string, labelsUsed []string, labelRanges []hcl.Range, blocks *hcl.Blocks) (diags hcl.Diagnostics) {
	if len(labelsLeft) > 0 {
		labelName := labelsLeft[0]
		jsonAttrs, attrDiags := b.collectDeepAttrs(v, &labelName)
		diags = append(diags, attrDiags...)

		if len(jsonAttrs) == 0 {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Missing block label",
				Detail:   fmt.Sprintf("At least one object property is required, whose name represents the %s block's %s.", typeName, labelName),
				Subject:  v.StartRange().Ptr(),
			})
			return
		}
		labelsUsed := append(labelsUsed, "")
		labelRanges := append(labelRanges, hcl.Range{})
		for _, p := range jsonAttrs {
			pk := p.Name
			labelsUsed[len(labelsUsed)-1] = pk
			labelRanges[len(labelRanges)-1] = p.NameRange
			diags = append(diags, b.unpackBlock(p.Value, typeName, typeRange, labelsLeft[1:], labelsUsed, labelRanges, blocks)...)
		}
		return
	}

	// By the time we get here, we've peeled off all the labels and we're ready
	// to deal with the block's actual content.

	// need to copy the label slices because their underlying arrays will
	// continue to be mutated after we return.
	labels := make([]string, len(labelsUsed))
	copy(labels, labelsUsed)
	labelR := make([]hcl.Range, len(labelRanges))
	copy(labelR, labelRanges)

	switch tv := v.(type) {
	case *nullVal:
		// There is no block content, e.g the value is null.
		return
	case *objectVal:
		// Single instance of the block
		*blocks = append(*blocks, &hcl.Block{
			Type:   typeName,
			Labels: labels,
			Body: &body{
				val: tv,
			},

			DefRange:    tv.OpenRange,
			TypeRange:   *typeRange,
			LabelRanges: labelR,
		})
	case *arrayVal:
		// Multiple instances of the block
		for _, av := range tv.Values {
			*blocks = append(*blocks, &hcl.Block{
				Type:   typeName,
				Labels: labels,
				Body: &body{
					val: av, // might be mistyped; we'll find out when content is requested for this body
				},

				DefRange:    tv.OpenRange,
				TypeRange:   *typeRange,
				LabelRanges: labelR,
			})
		}
	default:
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Incorrect JSON value type",
			Detail:   fmt.Sprintf("Either a JSON object or a JSON array is required, representing the contents of one or more %q blocks.", typeName),
			Subject:  v.StartRange().Ptr(),
		})
	}
	return
}

// collectDeepAttrs takes either a single object or an array of objects and
// flattens it into a list of object attributes, collecting attributes from
// all of the objects in a given array.
//
// Ordering is preserved, so a list of objects that each have one property
// will result in those properties being returned in the same order as the
// objects appeared in the array.
//
// This is appropriate for use only for objects representing bodies or labels
// within a block.
//
// The labelName argument, if non-null, is used to tailor returned error
// messages to refer to block labels rather than attributes and child blocks.
// It has no other effect.
func (b *body) collectDeepAttrs(v node, labelName *string) ([]*objectAttr, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	var attrs []*objectAttr

	switch tv := v.(type) {
	case *nullVal:
		// If a value is null, then we don't return any attributes or return an error.

	case *objectVal:
		attrs = append(attrs, tv.Attrs...)

	case *arrayVal:
		for _, ev := range tv.Values {
			switch tev := ev.(type) {
			case *objectVal:
				attrs = append(attrs, tev.Attrs...)
			default:
				if labelName != nil {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Incorrect JSON value type",
						Detail:   fmt.Sprintf("A JSON object is required here, to specify %s labels for this block.", *labelName),
						Subject:  ev.StartRange().Ptr(),
					})
				} else {
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Incorrect JSON value type",
						Detail:   "A JSON object is required here, to define arguments and child blocks.",
						Subject:  ev.StartRange().Ptr(),
					})
				}
			}
		}

	default:
		if labelName != nil {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Incorrect JSON value type",
				Detail:   fmt.Sprintf("Either a JSON object or JSON array of objects is required here, to specify %s labels for this block.", *labelName),
				Subject:  v.StartRange().Ptr(),
			})
		} else {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Incorrect JSON value type",
				Detail:   "Either a JSON object or JSON array of objects is required here, to define arguments and child blocks.",
				Subject:  v.StartRange().Ptr(),
			})
		}
	}

	return attrs, diags
}

func (e *expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	switch v := e.src.(type) {
	case *stringVal:
		if ctx != nil {
			// Parse string contents as a HCL native language expression.
			// We only do this if we have a context, so passing a nil context
			// is how the caller specifies that interpolations are not allowed
			// and that the string should just be returned verbatim.
			templateSrc := v.Value
			expr, diags := hclsyntax.ParseTemplate(
				[]byte(templateSrc),
				v.SrcRange.Filename,

				// This won't produce _exactly_ the right result, since
				// the hclsyntax parser can't "see" any escapes we removed
				// while parsing JSON, but it's better than nothing.
				hcl.Pos{
					Line: v.SrcRange.Start.Line,

					// skip over the opening quote mark
					Byte:   v.SrcRange.Start.Byte + 1,
					Column: v.SrcRange.Start.Column + 1,
				},
			)
			if diags.HasErrors() {
				return cty.DynamicVal, diags
			}
			val, evalDiags := expr.Value(ctx)
			diags = append(diags, evalDiags...)
			return val, diags
		}

		return cty.StringVal(v.Value), nil
	case *numberVal:
		return cty.NumberVal(v.Value), nil
	case *booleanVal:
		return cty.BoolVal(v.Value), nil
	case *arrayVal:
		var diags hcl.Diagnostics
		vals := []cty.Value{}
		for _, jsonVal := range v.Values {
			val, valDiags := (&expression{src: jsonVal}).Value(ctx)
			vals = append(vals, val)
			diags = append(diags, valDiags...)
		}
		return cty.TupleVal(vals), diags
	case *objectVal:
		var diags hcl.Diagnostics
		attrs := map[string]cty.Value{}
		attrRanges := map[string]hcl.Range{}
		known := true
		for _, jsonAttr := range v.Attrs {
			// In this one context we allow keys to contain interpolation
			// expressions too, assuming we're evaluating in interpolation
			// mode. This achieves parity with the native syntax where
			// object expressions can have dynamic keys, while block contents
			// may not.
			name, nameDiags := (&expression{src: &stringVal{
				Value:    jsonAttr.Name,
				SrcRange: jsonAttr.NameRange,
			}}).Value(ctx)
			valExpr := &expression{src: jsonAttr.Value}
			val, valDiags := valExpr.Value(ctx)
			diags = append(diags, nameDiags...)
			diags = append(diags, valDiags...)

			var err error
			name, err = convert.Convert(name, cty.String)
			if err != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity:    hcl.DiagError,
					Summary:     "Invalid object key expression",
					Detail:      fmt.Sprintf("Cannot use this expression as an object key: %s.", err),
					Subject:     &jsonAttr.NameRange,
					Expression:  valExpr,
					EvalContext: ctx,
				})
				continue
			}
			if name.IsNull() {
				diags = append(diags, &hcl.Diagnostic{
					Severity:    hcl.DiagError,
					Summary:     "Invalid object key expression",
					Detail:      "Cannot use null value as an object key.",
					Subject:     &jsonAttr.NameRange,
					Expression:  valExpr,
					EvalContext: ctx,
				})
				continue
			}
			if !name.IsKnown() {
				// This is a bit of a weird case, since our usual rules require
				// us to tolerate unknowns and just represent the result as
				// best we can but if we don't know the key then we can't
				// know the type of our object at all, and thus we must turn
				// the whole thing into cty.DynamicVal. This is consistent with
				// how this situation is handled in the native syntax.
				// We'll keep iterating so we can collect other errors in
				// subsequent attributes.
				known = false
				continue
			}
			nameStr := name.AsString()
			if _, defined := attrs[nameStr]; defined {
				diags = append(diags, &hcl.Diagnostic{
					Severity:    hcl.DiagError,
					Summary:     "Duplicate object attribute",
					Detail:      fmt.Sprintf("An attribute named %q was already defined at %s.", nameStr, attrRanges[nameStr]),
					Subject:     &jsonAttr.NameRange,
					Expression:  e,
					EvalContext: ctx,
				})
				continue
			}
			attrs[nameStr] = val
			attrRanges[nameStr] = jsonAttr.NameRange
		}
		if !known {
			// We encountered an unknown key somewhere along the way, so
			// we can't know what our type will eventually be.
			return cty.DynamicVal, diags
		}
		return cty.ObjectVal(attrs), diags
	case *nullVal:
		return cty.NullVal(cty.DynamicPseudoType), nil
	default:
		// Default to DynamicVal so that ASTs containing invalid nodes can
		// still be partially-evaluated.
		return cty.DynamicVal, nil
	}
}

func (e *expression) Variables() []hcl.Traversal {
	var vars []hcl.Traversal

	switch v := e.src.(type) {
	case *stringVal:
		templateSrc := v.Value
		expr, diags := hclsyntax.ParseTemplate(
			[]byte(templateSrc),
			v.SrcRange.Filename,

			// This won't produce _exactly_ the right result, since
			// the hclsyntax parser can't "see" any escapes we removed
			// while parsing JSON, but it's better than nothing.
			hcl.Pos{
				Line: v.SrcRange.Start.Line,

				// skip over the opening quote mark
				Byte:   v.SrcRange.Start.Byte + 1,
				Column: v.SrcRange.Start.Column + 1,
			},
		)
		if diags.HasErrors() {
			return vars
		}
		return expr.Variables()

	case *arrayVal:
		for _, jsonVal := range v.Values {
			vars = append(vars, (&expression{src: jsonVal}).Variables()...)
		}
	case *objectVal:
		for _, jsonAttr := range v.Attrs {
			keyExpr := &stringVal{ // we're going to treat key as an expression in this context
				Value:    jsonAttr.Name,
				SrcRange: jsonAttr.NameRange,
			}
			vars = append(vars, (&expression{src: keyExpr}).Variables()...)
			vars = append(vars, (&expression{src: jsonAttr.Value}).Variables()...)
		}
	}

	return vars
}

func (e *expression) Range() hcl.Range {
	return e.src.Range()
}

func (e *expression) StartRange() hcl.Range {
	return e.src.StartRange()
}

// Implementation for hcl.AbsTraversalForExpr.
func (e *expression) AsTraversal() hcl.Traversal {
	// In JSON-based syntax a traversal is given as a string containing
	// traversal syntax as defined by hclsyntax.ParseTraversalAbs.

	switch v := e.src.(type) {
	case *stringVal:
		traversal, diags := hclsyntax.ParseTraversalAbs([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start)
		if diags.HasErrors() {
			return nil
		}
		return traversal
	default:
		return nil
	}
}

// Implementation for hcl.ExprCall.
func (e *expression) ExprCall() *hcl.StaticCall {
	// In JSON-based syntax a static call is given as a string containing
	// an expression in the native syntax that also supports ExprCall.

	switch v := e.src.(type) {
	case *stringVal:
		expr, diags := hclsyntax.ParseExpression([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start)
		if diags.HasErrors() {
			return nil
		}

		call, diags := hcl.ExprCall(expr)
		if diags.HasErrors() {
			return nil
		}

		return call
	default:
		return nil
	}
}

// Implementation for hcl.ExprList.
func (e *expression) ExprList() []hcl.Expression {
	switch v := e.src.(type) {
	case *arrayVal:
		ret := make([]hcl.Expression, len(v.Values))
		for i, node := range v.Values {
			ret[i] = &expression{src: node}
		}
		return ret
	default:
		return nil
	}
}

// Implementation for hcl.ExprMap.
func (e *expression) ExprMap() []hcl.KeyValuePair {
	switch v := e.src.(type) {
	case *objectVal:
		ret := make([]hcl.KeyValuePair, len(v.Attrs))
		for i, jsonAttr := range v.Attrs {
			ret[i] = hcl.KeyValuePair{
				Key: &expression{src: &stringVal{
					Value:    jsonAttr.Name,
					SrcRange: jsonAttr.NameRange,
				}},
				Value: &expression{src: jsonAttr.Value},
			}
		}
		return ret
	default:
		return nil
	}
}
