// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package consul

import (
	"net"
	"strings"
	"time"

	consulapi "github.com/hashicorp/consul/api"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/backend"
	"github.com/hashicorp/terraform/internal/backend/backendbase"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// New creates a new backend for Consul remote state.
func New() backend.Backend {
	return &Backend{
		Base: backendbase.Base{
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"path": {
						Type:        cty.String,
						Required:    true,
						Description: "Path to store state in Consul",
					},
					"access_token": {
						Type:        cty.String,
						Optional:    true,
						Description: "Access token for a Consul ACL",
					},
					"address": {
						Type:        cty.String,
						Optional:    true,
						Description: "Address to the Consul Cluster",
					},
					"scheme": {
						Type:        cty.String,
						Optional:    true,
						Description: "Scheme to communicate to Consul with",
					},
					"datacenter": {
						Type:        cty.String,
						Optional:    true,
						Description: "Datacenter to communicate with",
					},
					"http_auth": {
						Type:        cty.String,
						Optional:    true,
						Description: "HTTP Auth in the format of 'username:password'",
					},
					"gzip": {
						Type:        cty.Bool,
						Optional:    true,
						Description: "Compress the state data using gzip",
					},
					"lock": {
						Type:        cty.Bool,
						Optional:    true,
						Description: "Lock state access",
					},
					"ca_file": {
						Type:        cty.String,
						Optional:    true,
						Description: "A path to a PEM-encoded certificate authority used to verify the remote agent's certificate",
					},
					"cert_file": {
						Type:        cty.String,
						Optional:    true,
						Description: "A path to a PEM-encoded certificate provided to the remote agent; requires use of key_file",
					},
					"key_file": {
						Type:        cty.String,
						Optional:    true,
						Description: "A path to a PEM-encoded private key, required if cert_file is specified",
					},
				},
			},
		},
	}
}

type Backend struct {
	backendbase.Base

	// The fields below are set from configure
	client *consulapi.Client
	path   string
	gzip   bool
	lock   bool
}

func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics {
	b.path = configVal.GetAttr("path").AsString()
	b.gzip = backendbase.MustBoolValue(
		backendbase.GetAttrDefault(configVal, "gzip", cty.False),
	)
	b.lock = backendbase.MustBoolValue(
		backendbase.GetAttrDefault(configVal, "lock", cty.True),
	)

	// Configure the client
	config := consulapi.DefaultConfig()

	// replace the default Transport Dialer to reduce the KeepAlive
	config.Transport.DialContext = dialContext

	empty := cty.StringVal("")
	if v := backendbase.GetAttrDefault(configVal, "access_token", empty); v != empty {
		config.Token = v.AsString()
	}
	if v := backendbase.GetAttrDefault(configVal, "address", empty); v != empty {
		config.Address = v.AsString()
	}
	if v := backendbase.GetAttrDefault(configVal, "scheme", empty); v != empty {
		config.Scheme = v.AsString()
	}
	if v := backendbase.GetAttrDefault(configVal, "datacenter", empty); v != empty {
		config.Datacenter = v.AsString()
	}

	if v := backendbase.GetAttrEnvDefaultFallback(configVal, "ca_file", "CONSUL_CACERT", empty); v != empty {
		config.TLSConfig.CAFile = v.AsString()
	}
	if v := backendbase.GetAttrEnvDefaultFallback(configVal, "cert_file", "CONSUL_CLIENT_CERT", empty); v != empty {
		config.TLSConfig.CertFile = v.AsString()
	}
	if v := backendbase.GetAttrEnvDefaultFallback(configVal, "key_file", "CONSUL_CLIENT_KEY", empty); v != empty {
		config.TLSConfig.KeyFile = v.AsString()
	}

	if v := backendbase.GetAttrDefault(configVal, "http_auth", empty); v != empty {
		auth := v.AsString()

		var username, password string
		if strings.Contains(auth, ":") {
			split := strings.SplitN(auth, ":", 2)
			username = split[0]
			password = split[1]
		} else {
			username = auth
		}

		config.HttpAuth = &consulapi.HttpBasicAuth{
			Username: username,
			Password: password,
		}
	}

	client, err := consulapi.NewClient(config)
	if err != nil {
		return backendbase.ErrorAsDiagnostics(err)
	}

	b.client = client
	return nil
}

// dialContext is the DialContext function for the consul client transport.
// This is stored in a package var to inject a different dialer for tests.
var dialContext = (&net.Dialer{
	Timeout:   30 * time.Second,
	KeepAlive: 17 * time.Second,
}).DialContext
