package service

import (
	"context"
	"fmt"
	"sync"

	"github.com/go-logr/logr"
)

var (
	errStopByCaller = fmt.Errorf("stop by caller")
)

type Service interface {
	Name() string
	Dummy() bool
	DisableRun() bool
}

type RunnableService interface {
	Service

	// Run the service and *MUST* block until ctx is done.
	Run(ctx context.Context) error
}

type ManagerParams struct {
	Logger   logr.Logger
	Services []Service
}

type Manager struct {
	logger   logr.Logger
	cancel   context.CancelCauseFunc
	wg       *sync.WaitGroup
	services map[string]Service
	runnable map[string]RunnableService
}

func NewManager(p ManagerParams) *Manager {
	m := &Manager{
		logger:   p.Logger.WithName("ServiceManager"),
		wg:       &sync.WaitGroup{},
		services: make(map[string]Service),
		runnable: make(map[string]RunnableService),
	}

	for _, svc := range p.Services {
		m.services[svc.Name()] = svc

		switch svc := svc.(type) {
		case RunnableService:
			m.runnable[svc.Name()] = svc
		}

		if svc.Dummy() {
			m.logger.Info("service is dummy", "service", svc.Name())
		}
	}

	return m
}

// RunServices runs all services in background and wait for them to exit.
func (m *Manager) RunServices() error {
	ctx, cancel := context.WithCancelCause(context.Background())
	m.cancel = cancel

	for _, svc := range m.runnable {
		if svc.Dummy() || svc.DisableRun() {
			m.logger.Info("skip running service cause it is dummy or disable", "service", svc.Name())
			continue
		}

		m.wg.Add(1)
		go func(s RunnableService) {
			defer m.wg.Done()

			m.logger.Info("start to run a service", "service", s.Name())
			if err := s.Run(ctx); err != nil {
				m.stop(fmt.Errorf("failed to run the service %s: %s", s.Name(), err))
			}
			m.logger.Info("a service is stopped", "service", s.Name())
		}(svc)
	}

	m.wg.Wait()

	err := context.Cause(ctx)
	if err == nil {
		return nil
	}
	if err.Error() == errStopByCaller.Error() {
		return nil
	}
	return err
}

// StopServices stops all services and wait for them to exit.
func (m *Manager) StopServices() {
	m.stop(errStopByCaller)
	m.wg.Wait()
}

func (m *Manager) stop(cause error) {
	m.cancel(cause)
}
