package servicebootstrap

import (
	"errors"
	"fmt"
	"time"
)

var ErrNotInstalled = errors.New("service not installed")

type State uint32

const (
	StateUnknown State = iota
	StateStopped
	StateStartPending
	StateStopPending
	StateRunning
)

type RecoveryActionType uint8

const (
	RecoveryActionRestart RecoveryActionType = iota + 1
)

type RecoveryAction struct {
	Type  RecoveryActionType
	Delay time.Duration
}

type Spec struct {
	Name                string
	Executable          string
	DisplayName         string
	Description         string
	Dependencies        []string
	RecoveryActions     []RecoveryAction
	RecoveryResetPeriod uint32
}

type WaitConfig struct {
	Attempts int
	Delay    time.Duration
	Sleep    func(time.Duration)
}

type Service interface {
	Query() (State, error)
	Start() error
	SetRecoveryActions([]RecoveryAction, uint32) error
	Close() error
}

type Manager interface {
	OpenService(name string) (Service, error)
	CreateService(spec Spec) (Service, error)
}

func EnsureRunning(manager Manager, spec Spec, wait WaitConfig) (retErr error) {
	if wait.Attempts <= 0 {
		wait.Attempts = 30
	}
	if wait.Delay <= 0 {
		wait.Delay = time.Second
	}
	if wait.Sleep == nil {
		wait.Sleep = time.Sleep
	}

	service, err := manager.OpenService(spec.Name)
	if err != nil {
		if !errors.Is(err, ErrNotInstalled) {
			return err
		}
		service, err = manager.CreateService(spec)
		if err != nil {
			return err
		}
		if err := service.SetRecoveryActions(spec.RecoveryActions, spec.RecoveryResetPeriod); err != nil {
			service.Close()
			return err
		}
	}
	defer func() {
		if service != nil {
			closeErr := service.Close()
			if retErr == nil && closeErr != nil {
				retErr = closeErr
			}
		}
	}()

	started := false
	for attempt := 0; attempt < wait.Attempts; attempt++ {
		state, err := service.Query()
		if err != nil {
			return err
		}
		switch state {
		case StateRunning:
			return nil
		case StateStartPending:
			wait.Sleep(wait.Delay)
			continue
		case StateStopped, StateStopPending, StateUnknown:
			if !started {
				if err := service.Start(); err != nil {
					return err
				}
				started = true
			}
			wait.Sleep(wait.Delay)
		default:
			wait.Sleep(wait.Delay)
		}
	}

	state, err := service.Query()
	if err != nil {
		return err
	}
	return fmt.Errorf("service %q did not reach running state (last state %v)", spec.Name, state)
}
