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/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
)

type moduleHolder[CM any] struct {
	module      modagent.Module[CM]
	cfg2pipe    chan CM
	pipe2module chan CM
}

func (h moduleHolder[CM]) runModule(ctx context.Context) error {
	context.AfterFunc(ctx, func() {
		// This doesn't race with writing in runPipe() because stager orders context cancellation:
		// runPipe's context is canceled and method exits before the context in this method is canceled.
		close(h.pipe2module)
	})
	err := h.module.Run(ctx, h.pipe2module)
	if err != nil {
		return err
	}

	if ctx.Err() == nil {
		// The module terminated prematurely and there wasn't an error.
		// Thus, it violated the `Module.Run` contract by terminating without being asked to do so.
		// This is a programming error in the module itself, thus we panic.
		panic(fmt.Errorf("the module '%s' terminated from its Run method without a stop. This is a programming error in that module. Please open an issue in https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/issues", h.module.Name()))
	}

	return nil
}

func (h moduleHolder[CM]) runPipe(ctx context.Context) error {
	var (
		nilablePipe2module chan<- CM
		cfgToSend          CM
	)
	// The loop consumes the incoming items from the configuration channel (cfg2pipe) and only sends the last
	// received item to the module (pipe2module). This allows to skip configuration changes that happened while the module was handling the
	// previous configuration change.
	done := ctx.Done()
	for {
		select {
		case <-done: // case #1
			return nil
		case cfgToSend = <-h.cfg2pipe: // case #2
			nilablePipe2module = h.pipe2module // enable case #3
		case nilablePipe2module <- cfgToSend: // case #3, disabled when nilablePipe2module == nil i.e. when there is nothing to send
			// config sent
			var zero CM
			cfgToSend = zero         // help GC
			nilablePipe2module = nil // disable case #3
		}
	}
}

// ConfigurationWatcher provides a mechanism to receive new configuration objects.
type ConfigurationWatcher[CD any] interface {
	Watch(context.Context, func(context.Context, CD))
}

type ModuleRunner[CD, CM any] struct {
	Log         *slog.Logger
	Watcher     ConfigurationWatcher[CD]
	Data2Config func(CD) (CM, []any) // second return value is extra attributes for the logger
	holders     []moduleHolder[CM]
}

// RegisterModules registers modules with the runner. It returns staged functions to run modules.
func (r *ModuleRunner[CD, CM]) RegisterModules(modules ...modagent.Module[CM]) []stager.StageFunc {
	holders := make([]moduleHolder[CM], 0, len(modules))
	for _, module := range modules {
		holder := moduleHolder[CM]{
			module:      module,
			cfg2pipe:    make(chan CM),
			pipe2module: make(chan CM),
		}
		holders = append(holders, holder)
		r.holders = append(r.holders, holder)
	}
	return []stager.StageFunc{
		func(stage stager.Stage) {
			for _, holder := range holders {
				stage.Go(holder.runModule)
			}
		},
		func(stage stager.Stage) {
			for _, holder := range holders {
				stage.Go(holder.runPipe)
			}
		},
	}
}

func (r *ModuleRunner[CD, CM]) RunConfigurationRefresh(ctx context.Context) error {
	r.Watcher.Watch(ctx, func(ctx context.Context, data CD) {
		config, attrs := r.Data2Config(data)
		err := r.applyConfiguration(config, attrs)
		if err != nil {
			if !errz.ContextDone(err) {
				r.Log.With(attrs...).Error("Failed to apply configuration", logz.AnyJSONValue(logz.AgentConfig, config), logz.Error(err))
			}
			return
		}
	})
	return nil
}

func (r *ModuleRunner[CD, CM]) applyConfiguration(config CM, attrs []any) error {
	r.Log.With(attrs...).Debug("Applying configuration", logz.AnyJSONValue(logz.AgentConfig, config))
	// Default and validate before setting for use.
	for _, holder := range r.holders {
		err := holder.module.DefaultAndValidateConfiguration(config)
		if err != nil {
			return fmt.Errorf("%s: %w", holder.module.Name(), err)
		}
	}
	// Set for use.
	for _, holder := range r.holders {
		holder.cfg2pipe <- config
	}
	return nil
}
