package autoconf

import (
	"context"
	"fmt"
	"net"
	"strings"

	"github.com/hashicorp/consul/agent/structs"
)

func (ac *AutoConfig) autoEncryptInitialCerts(ctx context.Context) (*structs.SignedResponse, error) {
	// generate a CSR
	csr, key, err := ac.generateCSR()
	if err != nil {
		return nil, err
	}

	// this resets the failures so that we will perform immediate request
	wait := ac.acConfig.Waiter.Success()
	for {
		select {
		case <-wait:
			if resp, err := ac.autoEncryptInitialCertsOnce(ctx, csr, key); err == nil && resp != nil {
				return resp, nil
			} else if err != nil {
				ac.logger.Error(err.Error())
			} else {
				ac.logger.Error("No error returned when fetching certificates from the servers but no response was either")
			}

			wait = ac.acConfig.Waiter.Failed()
		case <-ctx.Done():
			ac.logger.Info("interrupted during retrieval of auto-encrypt certificates", "err", ctx.Err())
			return nil, ctx.Err()
		}
	}
}

func (ac *AutoConfig) autoEncryptInitialCertsOnce(ctx context.Context, csr, key string) (*structs.SignedResponse, error) {
	request := structs.CASignRequest{
		WriteRequest: structs.WriteRequest{Token: ac.acConfig.Tokens.AgentToken()},
		Datacenter:   ac.config.Datacenter,
		CSR:          csr,
	}
	var resp structs.SignedResponse

	servers, err := ac.autoEncryptHosts()
	if err != nil {
		return nil, err
	}

	for _, s := range servers {
		// try each IP to see if we can successfully make the request
		for _, addr := range ac.resolveHost(s) {
			if ctx.Err() != nil {
				return nil, ctx.Err()
			}

			ac.logger.Debug("making AutoEncrypt.Sign RPC", "addr", addr.String())
			err = ac.acConfig.DirectRPC.RPC(ac.config.Datacenter, ac.config.NodeName, &addr, "AutoEncrypt.Sign", &request, &resp)
			if err != nil {
				ac.logger.Error("AutoEncrypt.Sign RPC failed", "addr", addr.String(), "error", err)
				continue
			}

			resp.IssuedCert.PrivateKeyPEM = key
			return &resp, nil
		}
	}
	return nil, fmt.Errorf("No servers successfully responded to the auto-encrypt request")
}

func (ac *AutoConfig) autoEncryptHosts() ([]string, error) {
	// use servers known to gossip if there are any
	if ac.acConfig.ServerProvider != nil {
		if srv := ac.acConfig.ServerProvider.FindLANServer(); srv != nil {
			return []string{srv.Addr.String()}, nil
		}
	}

	hosts, err := ac.discoverServers(ac.config.RetryJoinLAN)
	if err != nil {
		return nil, err
	}

	var addrs []string

	// The addresses we use for auto-encrypt are the retry join and start join
	// addresses. These are for joining serf and therefore we cannot rely on the
	// ports for these. This loop strips any port that may have been specified and
	// will let subsequent resolveAddr calls add on the default RPC port.
	for _, addr := range append(ac.config.StartJoinAddrsLAN, hosts...) {
		host, _, err := net.SplitHostPort(addr)
		if err != nil {
			if strings.Contains(err.Error(), "missing port in address") {
				host = addr
			} else {
				ac.logger.Warn("error splitting host address into IP and port", "address", addr, "error", err)
				continue
			}
		}
		addrs = append(addrs, host)
	}

	if len(addrs) == 0 {
		return nil, fmt.Errorf("no auto-encrypt server addresses available for use")
	}

	return addrs, nil
}
