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

package encryption

import (
	"errors"
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/opentofu/opentofu/internal/encryption/config"
	"github.com/opentofu/opentofu/internal/encryption/method"
	"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
	"github.com/opentofu/opentofu/internal/encryption/registry"
	"github.com/zclconf/go-cty/cty"
)

func (e *targetBuilder) setupMethods() hcl.Diagnostics {
	var diags hcl.Diagnostics

	e.methodValues = make(map[string]map[string]cty.Value)
	e.methods = make(map[method.Addr]method.Method)

	for _, m := range e.cfg.MethodConfigs {
		diags = append(diags, e.setupMethod(m)...)
	}

	// Regenerate the context now that the method is loaded
	mMap := make(map[string]cty.Value)
	for name, ms := range e.methodValues {
		mMap[name] = cty.ObjectVal(ms)
	}
	e.ctx.Variables["method"] = cty.ObjectVal(mMap)

	return diags
}

// setupMethod sets up a single method for encryption. It returns a list of diagnostics if the method is invalid.
func (e *targetBuilder) setupMethod(cfg config.MethodConfig) hcl.Diagnostics {
	addr, diags := cfg.Addr()
	if diags.HasErrors() {
		return diags
	}

	// Ensure cfg.Type is in methodValues
	if _, ok := e.methodValues[cfg.Type]; !ok {
		e.methodValues[cfg.Type] = make(map[string]cty.Value)
	}

	// Lookup the definition of the encryption method from the registry
	encryptionMethod, err := e.reg.GetMethodDescriptor(method.ID(cfg.Type))
	if err != nil {

		// Handle if the method was not found
		var notFoundError *registry.MethodNotFoundError
		if errors.Is(err, notFoundError) {
			return append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unknown encryption method type",
				Detail:   fmt.Sprintf("Can not find %q", cfg.Type),
			})
		}

		// Or, we don't know the error type, so we'll just return it as a generic error
		return append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf("Error fetching encryption method %q", cfg.Type),
			Detail:   err.Error(),
		})
	}

	// TODO: we could use varhcl here to provider better error messages
	methodConfig := encryptionMethod.ConfigStruct()
	methodDiags := gohcl.DecodeBody(cfg.Body, e.ctx, methodConfig)
	diags = append(diags, methodDiags...)
	if diags.HasErrors() {
		return diags
	}

	e.methodValues[cfg.Type][cfg.Name] = cty.StringVal(string(addr))
	m, err := methodConfig.Build()
	if err != nil {
		// TODO this error handling could use some work
		return append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Encryption method configuration failed",
			Detail:   err.Error(),
		})
	}
	e.methods[addr] = m

	if unencrypted.Is(m) {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagWarning,
			Summary:  "Unencrypted method configured",
			Detail:   "Method unencrypted is present in configuration. This is a security risk and should only be enabled during migrations.",
		})
	}

	return diags
}
