package plugin

import (
	"context"
	"io"
	"time"

	"github.com/hashicorp/go-hclog"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"gitlab.com/gitlab-org/fleeting/fleeting/internal/plugin/proto"
	"gitlab.com/gitlab-org/fleeting/fleeting/provider"
)

var (
	_     provider.InstanceGroup = (*instanceGroupGRPCClient)(nil)
	empty                        = &proto.Empty{}
)

type instanceGroupGRPCClient struct {
	client proto.InstanceGroupClient
	config []byte
}

func (c *instanceGroupGRPCClient) Init(ctx context.Context, log hclog.Logger, connectorConfig provider.Settings) (provider.ProviderInfo, error) {
	resp, err := c.client.Init(ctx, &proto.InitRequest{
		Config: c.config,
		Settings: &proto.Settings{
			ConnectorConfig: &proto.ConnectorConfig{
				Os:                   connectorConfig.OS,
				Arch:                 connectorConfig.Arch,
				Protocol:             string(connectorConfig.Protocol),
				Username:             connectorConfig.Username,
				Password:             connectorConfig.Password,
				Key:                  connectorConfig.Key,
				UseStaticCredentials: connectorConfig.UseStaticCredentials,
				Keepalive:            int64(connectorConfig.Keepalive),
				Timeout:              int64(connectorConfig.Timeout),
			},
		},
	})
	if err != nil {
		return provider.ProviderInfo{}, err
	}
	c.config = nil

	return provider.ProviderInfo{
		ID:        resp.Id,
		MaxSize:   int(resp.MaxSize),
		Version:   resp.Version,
		BuildInfo: resp.BuildInfo,
	}, nil
}

func (c *instanceGroupGRPCClient) Update(ctx context.Context, fn func(instance string, state provider.State)) error {
	updater, err := c.client.Update(ctx, empty)
	if err != nil {
		return err
	}

	resp := new(proto.UpdateResponse)
	for {
		err := updater.RecvMsg(resp)
		switch {
		case err == io.EOF:
			return nil
		case err != nil:
			return err
		}

		fn(resp.Instance, provider.State(resp.State.String()))
	}
}

func (c *instanceGroupGRPCClient) Increase(ctx context.Context, n int) (int, error) {
	resp, err := c.client.Increase(ctx, &proto.IncreaseRequest{N: uint32(n)})
	if resp != nil {
		return int(resp.Succeeded), err
	}

	return 0, err
}

func (c *instanceGroupGRPCClient) Decrease(ctx context.Context, instances []string) ([]string, error) {
	resp, err := c.client.Decrease(ctx, &proto.DecreaseRequest{Instances: instances})
	if resp != nil {
		return resp.Succeeded, err
	}

	return nil, err
}

func (c *instanceGroupGRPCClient) ConnectInfo(ctx context.Context, instance string) (provider.ConnectInfo, error) {
	resp, err := c.client.ConnectInfo(ctx, &proto.ConnectInfoRequest{Instance: instance})
	if err != nil {
		return provider.ConnectInfo{}, err
	}

	return provider.ConnectInfo{
		ConnectorConfig: provider.ConnectorConfig{
			OS:                   resp.Connector.Os,
			Arch:                 resp.Connector.Arch,
			Protocol:             provider.Protocol(resp.Connector.Protocol),
			Username:             resp.Connector.Username,
			Password:             resp.Connector.Password,
			Key:                  resp.Connector.Key,
			UseStaticCredentials: resp.Connector.UseStaticCredentials,
			Keepalive:            time.Duration(resp.Connector.Keepalive),
			Timeout:              time.Duration(resp.Connector.Timeout),
		},
		ID:           instance,
		ExternalAddr: resp.ExternalAddr,
		InternalAddr: resp.InternalAddr,
		Expires: func() *time.Time {
			if resp.Expires > 0 {
				t := time.Unix(resp.Expires, 0).UTC()
				return &t
			}
			return nil
		}(),
	}, nil
}

func (c *instanceGroupGRPCClient) Shutdown(ctx context.Context) error {
	_, err := c.client.Shutdown(ctx, empty)
	if err == nil {
		return nil
	}

	// an unimplemented Shutdown() is a no-op
	s, ok := status.FromError(err)
	if ok && s.Code() == codes.Unimplemented {
		return nil
	}

	return err
}
