package fleeting

import (
	"context"
	"sync"
	"time"

	"gitlab.com/gitlab-org/fleeting/fleeting/provider"
)

// Instance represents a single instance from an instance group.
type Instance interface {
	// ID is the instance's unique ID.
	ID() string

	// State is the instances state: creating, running, deleting.
	State() provider.State

	// Delete marks the instance as ready to be deleted.
	Delete()

	// Returns the reason an instance exists:
	// - Preexisted: it was available from when the provisioner started.
	// - Requested:  the instance was requested and fulfilled by Provision().
	// - Unexpected: the instance was provisioned either by a canceled
	//               Provision() call or it was created out-of-band.
	Cause() Cause

	// ReadyWait waits for the instance to be ready. This can be canceled with
	// context provided. It returns true if the instance is ready.
	ReadyWait(context.Context) bool

	// ConnectInfo returns additional information about an instance, useful
	// for creating a connection. ConnectInfo can perform non-idempotent
	// setup on the instance and should ideally only ever be called once.
	//
	// It is up to the caller to cache the info returned.
	ConnectInfo(context.Context) (provider.ConnectInfo, error)

	RequestedAt() time.Time
	ProvisionedAt() time.Time
	UpdatedAt() time.Time
	DeletedAt() time.Time
}

type Cause string

const (
	CausePreexisted Cause = "preexisted"
	CauseRequested  Cause = "requested"
	CauseUnexpected Cause = "unexpected"
)

type instance struct {
	id            string
	provisioner   *Provisioner
	state         provider.State
	cause         Cause
	missedUpdates int64
	removing      bool

	requestedAt   time.Time
	provisionedAt time.Time
	updatedAt     time.Time
	deletedAt     time.Time

	readyCloseOnce sync.Once
	ready          chan struct{}
}

func (i *instance) ID() string {
	return i.id
}

func (i *instance) State() provider.State {
	i.provisioner.mu.Lock()
	defer i.provisioner.mu.Unlock()

	return i.state
}

func (i *instance) Cause() Cause {
	return i.cause
}

func (i *instance) Delete() {
	i.provisioner.mu.Lock()
	defer i.provisioner.mu.Unlock()

	i.removing = true
}

func (i *instance) ReadyWait(ctx context.Context) bool {
	select {
	case <-i.ready:
	case <-i.provisioner.closed:
	case <-ctx.Done():
	}

	return i.State() == provider.StateRunning
}

func (i *instance) ConnectInfo(ctx context.Context) (provider.ConnectInfo, error) {
	return i.provisioner.group.ConnectInfo(ctx, i.id)
}

func (i *instance) RequestedAt() time.Time {
	return i.requestedAt
}

func (i *instance) ProvisionedAt() time.Time {
	return i.provisionedAt
}

func (i *instance) UpdatedAt() time.Time {
	i.provisioner.mu.Lock()
	defer i.provisioner.mu.Unlock()

	return i.updatedAt
}

func (i *instance) DeletedAt() time.Time {
	i.provisioner.mu.Lock()
	defer i.provisioner.mu.Unlock()

	return i.deletedAt
}

func (i *instance) signalReady() {
	i.readyCloseOnce.Do(func() {
		close(i.ready)
	})
}
