package proto

import (
	"context"
	"errors"
	"io"
	"net"

	"github.com/edgelesssys/constellation/coordinator/atls"
	"github.com/edgelesssys/constellation/coordinator/attestation/aws"
	"github.com/edgelesssys/constellation/coordinator/attestation/azure"
	"github.com/edgelesssys/constellation/coordinator/attestation/gcp"
	"github.com/edgelesssys/constellation/coordinator/kms"
	"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

// Client wraps a AVPNClient and the connection to it.
// The client offers a method to activate the connected
// AVPNServer as Coordinator.
type Client struct {
	conn       *grpc.ClientConn
	avpn       pubproto.APIClient
	validators []atls.Validator
}

// NewClient creates a Client without a connection.
func NewClient(gcpPCRs map[uint32][]byte) *Client {
	return &Client{
		validators: []atls.Validator{
			aws.NewValidator(aws.NaAdGetVerifiedPayloadAsJson),
			gcp.NewValidator(gcpPCRs),
			gcp.NewNonCVMValidator(map[uint32][]byte{}), // TODO: Remove once we no longer use non cvms
			azure.NewValidator(map[uint32][]byte{}),
		},
	}
}

// Connect connects the client to a given server.
// The connection must be closed using Close(). If connect is
// called on a client that already has a connection, the old
// connection is closed.
func (c *Client) Connect(ip string, port string) error {
	addr := net.JoinHostPort(ip, port)

	tlsConfig, err := atls.CreateAttestationClientTLSConfig(c.validators)
	if err != nil {
		return err
	}

	conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
	if err != nil {
		return err
	}
	if c.conn != nil {
		c.conn.Close()
	}
	c.conn = conn
	c.avpn = pubproto.NewAPIClient(conn)
	return nil
}

// Close closes the grpc connection of the client.
// Close is idempotent and can be called on non connected clients
// without returning an error.
func (c *Client) Close() error {
	if c.conn == nil {
		return nil
	}
	if err := c.conn.Close(); err != nil {
		return err
	}
	c.conn = nil
	return nil
}

// Activate activates the Constellation coordinator via a grpc call.
// The handed endpoints must be the private endpoints of running AWS or GCP instances,
// and the userPublicKey is the VPN key of the users WireGuard interface.
func (c *Client) Activate(ctx context.Context, userPublicKey, masterSecret []byte, endpoints, autoscalingNodeGroups []string, cloudServiceAccountURI string) (ActivationResponseClient, error) {
	if c.avpn == nil {
		return nil, errors.New("client is not connected")
	}
	if len(userPublicKey) == 0 {
		return nil, errors.New("parameter userPublicKey is empty")
	}
	if len(endpoints) == 0 {
		return nil, errors.New("parameter endpoints is empty")
	}

	pubKey, err := wgtypes.ParseKey(string(userPublicKey))
	if err != nil {
		return nil, err
	}

	avpnRequest := &pubproto.ActivateAsCoordinatorRequest{
		AdminVpnPubKey:         pubKey[:],
		NodePublicEndpoints:    endpoints,
		AutoscalingNodeGroups:  autoscalingNodeGroups,
		MasterSecret:           masterSecret,
		KmsUri:                 kms.ClusterKMSURI,
		StorageUri:             kms.NoStoreURI,
		KeyEncryptionKeyId:     "",
		UseExistingKek:         false,
		CloudServiceAccountUri: cloudServiceAccountURI,
	}

	client, err := c.avpn.ActivateAsCoordinator(ctx, avpnRequest)
	if err != nil {
		return nil, err
	}
	return NewActivationRespClient(client), nil
}

// ActivationResponseClient has methods to read messages from a stream of
// ActivateAsCoordinatorResponses.
type ActivationResponseClient interface {
	// NextLog reads responses from the response stream and returns the
	// first received log.
	// If AdminConfig responses are received before the first log response
	// occurs, the state of the client is updated with those configs. An
	// io.EOF error is returned at the end of the stream.
	NextLog() (string, error)

	// WriteLogStream reads responses from the response stream and
	// writes log responses to the handed writer.
	// Occurring AdminConfig responses update the state of the client.
	WriteLogStream(io.Writer) error

	// GetKubeconfig returns the kubeconfig that was received in the
	// latest AdminConfig response or an error if the field is empty.
	GetKubeconfig() (string, error)

	// GetCoordinatorVpnKey returns the Coordinator's VPN key that was
	// received in the latest AdminConfig response or an error if the field
	// is empty.
	GetCoordinatorVpnKey() (string, error)

	// GetClientVpnIp returns the client VPN IP that was received
	// in the latest AdminConfig response or an error if the field is empty.
	GetClientVpnIp() (string, error)

	// GetOwnerID returns the owner identifier, derived from the client's master secret
	// or an error if the field is empty.
	GetOwnerID() (string, error)

	// GetClusterID returns the cluster's unique identifier
	// or an error if the field is empty.
	GetClusterID() (string, error)
}