package agent

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

	"github.com/ash2k/stager"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagent"
	"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[int, int])(nil)
)

type LeaderRunnerInterface[CM any] interface {
	WrapModule(modagent.Module[CM]) modagent.Module[CM]
}

type ModuleRunnerInterface[CM any] interface {
	RegisterModules(...modagent.Module[CM]) []stager.StageFunc
}

type FactoriesLayer[CF, CM any] struct {
	Log       *slog.Logger
	Config    func(name string) CF
	LR        LeaderRunnerInterface[CM]
	MR        ModuleRunnerInterface[CM]
	Factories []modagent.Factory[CF, CM]
}

func (l *FactoriesLayer[CF, CM]) ToStageFuncs() ([]stager.StageFunc, error) {
	modules, err := l.constructModules()
	if err != nil {
		return nil, err
	}
	return append(
		l.MR.RegisterModules(modules...),
		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[CF, CM]) constructModules() ([]modagent.Module[CM], error) {
	modules := make([]modagent.Module[CM], 0, len(l.Factories))
	for _, factory := range l.Factories {
		name := factory.Name()
		log := l.Log.With(logz.ModuleName(name))
		module, err := factory.New(l.Config(name))
		if err != nil {
			return nil, fmt.Errorf("%s: %w", name, err)
		}
		if module == nil {
			log.Debug("Module is not started, because the factory did not create it")
			continue
		}
		module = &loggingModuleWrapper[CM]{
			Module: module,
			log:    log,
		}
		if factory.IsProducingLeaderModules() {
			if l.LR == nil {
				return nil, fmt.Errorf("factory producing leader modules when no leader runner is available: %s", factory.Name())
			}
			module = l.LR.WrapModule(module)
		}
		modules = append(modules, module)
	}
	return modules, nil
}

type loggingModuleWrapper[CM any] struct {
	modagent.Module[CM]
	log *slog.Logger
}

func (w *loggingModuleWrapper[CM]) Run(ctx context.Context, cfg <-chan CM) error {
	w.log.Info("Starting")
	err := w.Module.Run(ctx, cfg)
	if err != nil {
		// This is a weird log+return, but we want to log ASAP to aid debugging.
		w.log.Error("Stopped with error", logz.Error(err))
		return fmt.Errorf("%s: %w", w.Name(), err)
	}
	w.log.Info("Stopped")
	return nil
}
