package upgradeprep

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

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

type State uint32

const (
	StateUnknown State = iota
	StateStopped
	StateStartPending
	StateStopPending
	StateRunning
)

type Service interface {
	Query() (State, error)
	Stop() error
	Close() error
}

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

type Locker interface {
	Terminate(paths []string) error
}

type Config struct {
	ServiceName  string
	ServiceNames []string
	Files        []string
	Attempts     int
	Delay        time.Duration
	Sleep        func(time.Duration)
}

func Prepare(manager Manager, locker Locker, cfg Config) (retErr error) {
	if cfg.Attempts <= 0 {
		cfg.Attempts = 30
	}
	if cfg.Delay <= 0 {
		cfg.Delay = time.Second
	}
	if cfg.Sleep == nil {
		cfg.Sleep = time.Sleep
	}

	for _, name := range configuredServiceNames(cfg) {
		if err := stopServiceForUpgrade(manager, name, cfg); err != nil {
			return err
		}
	}
	return locker.Terminate(cfg.Files)
}

func configuredServiceNames(cfg Config) []string {
	if len(cfg.ServiceNames) == 0 {
		if cfg.ServiceName == "" {
			return nil
		}
		return []string{cfg.ServiceName}
	}
	seen := map[string]bool{}
	out := make([]string, 0, len(cfg.ServiceNames))
	for _, name := range cfg.ServiceNames {
		if name == "" || seen[name] {
			continue
		}
		seen[name] = true
		out = append(out, name)
	}
	return out
}

func stopServiceForUpgrade(manager Manager, name string, cfg Config) (retErr error) {
	service, err := manager.OpenService(name)
	if err != nil {
		if !errors.Is(err, ErrNotInstalled) {
			return err
		}
		return nil
	}
	defer func() {
		closeErr := service.Close()
		if retErr == nil && closeErr != nil {
			retErr = closeErr
		}
	}()

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

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