// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package configs

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/opentofu/opentofu/internal/lang"
	"github.com/opentofu/opentofu/internal/lang/marks"
	"github.com/zclconf/go-cty/cty"
)

// Backend represents a "backend" block inside a "terraform" block in a module
// or file.
type Backend struct {
	Type   string
	Config hcl.Body
	Eval   *StaticEvaluator

	TypeRange hcl.Range
	DeclRange hcl.Range
}

func decodeBackendBlock(block *hcl.Block) (*Backend, hcl.Diagnostics) {
	return &Backend{
		Type:      block.Labels[0],
		TypeRange: block.LabelRanges[0],
		Config:    block.Body,
		DeclRange: block.DefRange,
	}, nil
}

// Hash produces a hash value for the receiver that covers the type and the
// portions of the config that conform to the given schema.
//
// If the config does not conform to the schema then the result is not
// meaningful for comparison since it will be based on an incomplete result.
//
// As an exception, required attributes in the schema are treated as optional
// for the purpose of hashing, so that an incomplete configuration can still
// be hashed. Other errors, such as extraneous attributes, have no such special
// case.
func (b *Backend) Hash(schema *configschema.Block) (int, hcl.Diagnostics) {
	// Don't fail if required attributes are not set. Instead, we'll just
	// hash them as nulls.
	schema = schema.NoneRequired()

	// This is a bit of an odd workaround, but the decode below intentionally ignores
	// errors.  I don't want to try to change that at this point, but it may be worth doing
	// at some point. For now, I'm just looking to see if there are any references that are
	// not valid that the user should look at, instead of just producing an invalid backend object.
	diags := b.referenceDiagnostics(schema)

	val, _ := b.Decode(schema)
	if val == cty.NilVal {
		val = cty.UnknownVal(schema.ImpliedType())
	}

	if marks.Contains(val, marks.Sensitive) {
		return -1, diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Backend config contains sensitive values",
			Detail:   "The backend configuration is stored in .terraform/terraform.tfstate as well as plan files. It is recommended to instead supply sensitive credentials via backend specific environment variables",
			Subject:  b.DeclRange.Ptr(),
		})
	}

	toHash := cty.TupleVal([]cty.Value{
		cty.StringVal(b.Type),
		val,
	})

	return toHash.Hash(), diags
}

func (b *Backend) Decode(schema *configschema.Block) (cty.Value, hcl.Diagnostics) {
	return b.Eval.DecodeBlock(b.Config, schema.DecoderSpec(), StaticIdentifier{
		Module:    addrs.RootModule,
		Subject:   fmt.Sprintf("backend.%s", b.Type),
		DeclRange: b.DeclRange,
	})
}

// This is a hack that may not be needed, but preserves the idea that invalid backends will show a cryptic error about running init during plan/apply startup.
func (b *Backend) referenceDiagnostics(schema *configschema.Block) hcl.Diagnostics {
	var diags hcl.Diagnostics

	refs, refsDiags := lang.References(addrs.ParseRef, hcldec.Variables(b.Config, schema.DecoderSpec()))
	diags = append(diags, refsDiags.ToHCL()...)
	if diags.HasErrors() {
		return diags
	}

	_, ctxDiags := b.Eval.scope(StaticIdentifier{
		Module:    addrs.RootModule,
		Subject:   fmt.Sprintf("backend.%s", b.Type),
		DeclRange: b.DeclRange,
	}).EvalContext(refs)
	diags = append(diags, ctxDiags.ToHCL()...)

	return diags
}
