package kas

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/ash2k/stager"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/syncz"
)

var (
	_ syncz.Layer = (*factoriesLayer)(nil)
)

type factoriesLayer struct {
	log       *slog.Logger
	config    func(name string) *modserver.Config
	factories []modserver.Factory
}

func (l *factoriesLayer) ToStageFuncs() ([]stager.StageFunc, error) {
	modules, err := l.constructModules()
	if err != nil {
		return nil, err
	}
	return []stager.StageFunc{
		func(stage stager.Stage) {
			for _, mod := range modules {
				name := mod.Name()
				log := l.log.With(logz.ModuleName(name))
				stage.Go(func(ctx context.Context) error {
					log.Info("Starting")
					err := mod.Run(ctx)
					if err != nil {
						// This is a weird log+return, but we want to log ASAP to aid debugging.
						log.Error("Stopped with error", logz.Error(err))
						return fmt.Errorf("%s: %w", name, err)
					}
					log.Info("Stopped")
					return nil
				})
			}
		},
		func(stage stager.Stage) {
			stage.GoWhenDone(func() error {
				for _, mod := range modules {
					l.log.Info("Stopping", logz.ModuleName(mod.Name()))
				}
				return nil
			})
		},
	}, nil
}

// constructModules constructs modules.
//
// factory.New() must be called from the main goroutine because:
// - it may mutate a gRPC server (register an API) and that can only be done before Serve() is called on the server.
// - we want anything that can fail to fail early, before starting any modules.
// Do not move into stager.StageFunc() below.
func (l *factoriesLayer) constructModules() ([]modserver.Module, error) {
	modules := make([]modserver.Module, 0, len(l.factories))
	for _, factory := range l.factories {
		name := factory.Name()
		module, err := factory.New(l.config(name))
		if err != nil {
			return nil, fmt.Errorf("%s: %w", name, err)
		}
		if module == nil {
			l.log.Debug("Module is not started, because the factory did not create it", logz.ModuleName(name))
			continue
		}
		modules = append(modules, module)
	}
	return modules, nil
}
