package fleeting

import (
	"fmt"
	"os/exec"
	"time"

	"github.com/hashicorp/go-hclog"
	hplugin "github.com/hashicorp/go-plugin"

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

const (
	// DefaultUpdateInterval is the default value used if
	// WithUpdateInterval is unspecified.
	DefaultUpdateInterval = time.Minute

	// DefaultUpdateIntervalWhenExpecting is the default value used if
	// WithUpdateIntervalWhenExpecting is unspecified.
	DefaultUpdateIntervalWhenExpecting = 2 * time.Second

	// DefaultDeletionRetryInterval is the default value used if
	// WithDefaultDeletionRetryInterval is unspecified.
	DefaultDeletionRetryInterval = time.Minute

	// DefaultShutdownDeletionRetries is the default value used if
	// WithShutdownDeletionRetries is unspecified.
	DefaultShutdownDeletionRetries = 3

	// DefaultShutdownDeletionInterval is the default value used if
	// WithShutdownDeletionInterval is unspecified.
	DefaultShutdownDeletionInterval = 10 * time.Second
)

// Option is an option used when creating a provisioner.
type Option func(*options) error

type options struct {
	subscriber                  func([]Instance)
	settings                    provider.Settings
	maxSize                     int
	updateInterval              time.Duration
	updateIntervalWhenExpecting time.Duration
	deletionRetryInterval       time.Duration
	shutdownDeletionRetries     int
	shutdownDeletionInterval    time.Duration
	metricsCollector            metrics.Collector
}

func loadOptions(opts *options, provided []Option) error {
	opts.updateInterval = DefaultUpdateInterval
	opts.updateIntervalWhenExpecting = DefaultUpdateIntervalWhenExpecting
	opts.deletionRetryInterval = DefaultDeletionRetryInterval
	opts.shutdownDeletionRetries = DefaultShutdownDeletionRetries
	opts.shutdownDeletionInterval = DefaultShutdownDeletionInterval

	for _, o := range provided {
		err := o(opts)
		if err != nil {
			return err
		}
	}

	return nil
}

// WithSubscriber sets the subscriber function that will receive updated
// instances.
func WithSubscriber(fn func([]Instance)) Option {
	return func(o *options) error {
		o.subscriber = fn
		return nil
	}
}

// WithInstanceGroupSettings sets the instance groups settings.
func WithInstanceGroupSettings(settings provider.Settings) Option {
	return func(o *options) error {
		o.settings = settings
		return nil
	}
}

// WithMaxSize sets the maximum number of instances supported by the instance
// group.
func WithMaxSize(n int) Option {
	return func(o *options) error {
		o.maxSize = n
		return nil
	}
}

// WithUpdateInterval sets the duration between checking for instance status
// updates.
func WithUpdateInterval(timeout time.Duration) Option {
	return func(o *options) error {
		o.updateInterval = timeout
		return nil
	}
}

// WithUpdateIntervalWhenExpecting sets the duration between checking for
// instance status updates when an instance state change is expected.
//
// If there are pending requests and instances not in the Running state,
// this duration will be used.
func WithUpdateIntervalWhenExpecting(timeout time.Duration) Option {
	return func(o *options) error {
		o.updateIntervalWhenExpecting = timeout
		return nil
	}
}

// WithDeletionRetryInterval sets the duration to retry if a deletion appears
// to have no affect.
func WithDeletionRetryInterval(timeout time.Duration) Option {
	return func(o *options) error {
		o.deletionRetryInterval = timeout
		return nil
	}
}

// WithShutdownDeletionRetries sets how many attempts are made to ensure that
// instances pending deletion complete before shutdown.
func WithShutdownDeletionRetries(attempts int) Option {
	return func(o *options) error {
		o.shutdownDeletionRetries = attempts
		return nil
	}
}

// WithShutdownDeletionInterval sets the duration between removing instances
// and checking their status during shutdown.
func WithShutdownDeletionInterval(timeout time.Duration) Option {
	return func(o *options) error {
		o.shutdownDeletionInterval = timeout
		return nil
	}
}

// WithMetricsCollector sets MetricsCollector instance on the provisioner.
func WithMetricsCollector(mc metrics.Collector) Option {
	return func(o *options) error {
		o.metricsCollector = mc
		return nil
	}
}

// PluginOption is an option used with the RunPlugin function.
type PluginOption func(*pluginOptions) error

type pluginOptions struct {
	logger hclog.Logger
}

// WithPluginLogger sets the logger used with RunPlugin.
func WithPluginLogger(logger hclog.Logger) PluginOption {
	return func(o *pluginOptions) error {
		o.logger = logger
		return nil
	}
}

type Runner struct {
	client *hplugin.Client
	iface  provider.InstanceGroup
	opts   *pluginOptions
}

func RunPlugin(name string, config []byte, opts ...PluginOption) (*Runner, error) {
	r := &Runner{opts: new(pluginOptions)}

	for _, o := range opts {
		err := o(r.opts)
		if err != nil {
			return nil, err
		}
	}

	r.client = hplugin.NewClient(&hplugin.ClientConfig{
		HandshakeConfig: plugin.Handshake,
		VersionedPlugins: map[int]hplugin.PluginSet{
			0: {
				"instancegroup": &plugin.GRPCInstanceGroupPlugin{Config: config},
			},
		},
		Cmd:              exec.Command(name),
		AllowedProtocols: []hplugin.Protocol{hplugin.ProtocolGRPC},
		Logger:           r.opts.logger,
	})

	if err := r.init(); err != nil {
		r.client.Kill()
		return nil, err
	}

	return r, nil
}

func (r *Runner) init() error {
	rpc, err := r.client.Client()
	if err != nil {
		return err
	}

	raw, err := rpc.Dispense("instancegroup")
	if err != nil {
		return err
	}

	r.iface = raw.(provider.InstanceGroup)

	return nil
}

func (r *Runner) InstanceGroup() provider.InstanceGroup {
	return r.iface
}

func (r *Runner) Kill() {
	r.client.Kill()
}

func validateConnectorConfig(c *provider.ConnectorConfig) error {
	if c.Protocol != "" {
		if err := c.Protocol.Valid(); err != nil {
			return err
		}
	}

	if c.Keepalive == 0 {
		c.Keepalive = 30 * time.Second
	}
	if c.Timeout == 0 {
		c.Timeout = 10 * time.Minute
	}

	if c.UseStaticCredentials {
		if c.Username == "" || (c.Password == "" && c.Key == nil) {
			return fmt.Errorf("username and password/key is required when using static credentials")
		}
	}

	return nil
}
