package agentk

import (
	"context"
	"log/slog"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
)

// worker is responsible for coordinating and executing full and
// partial reconciliations by managing the lifecycle of a reconciler
type worker struct {
	log                                  *slog.Logger
	api                                  modagent.API
	prerequisites                        WorkspacePrerequisites
	partialReconciliationTimer           *time.Timer
	fullReconciliationTimer              *time.Timer
	currentFullReconciliationInterval    time.Duration
	currentPartialReconciliationInterval time.Duration
	reconcilerFactory                    func(ctx context.Context, namespace string) (remoteDevReconciler, error)
}

func (w *worker) Run(ctx context.Context) error {
	agentKey, err := w.api.GetAgentKey(ctx)
	if err != nil {
		return err
	}

	// full reconciliation should be started immediately, as we set currentFullReconciliationInterval to 0
	// upon module start/restart
	w.fullReconciliationTimer = time.NewTimer(0)
	defer w.fullReconciliationTimer.Stop()

	// provide a default value for the partial reconciliation timer
	// however this value will be overridden by the first full reconciliation
	w.partialReconciliationTimer = time.NewTimer(w.currentPartialReconciliationInterval)
	defer w.partialReconciliationTimer.Stop()

	var (
		activeReconciler remoteDevReconciler
	)
	defer func() {
		// this nil check is needed in case the context is canceled before the first
		// full reconciliation has even been scheduled to execute. In such a case, the
		// active reconciler will still be nil and the call to Stop may be skipped
		if activeReconciler != nil {
			activeReconciler.Stop()
		}
	}()

	done := ctx.Done()
	for {
		select {
		// this check allows the goroutine to immediately exit
		// if the context cancellation is invoked while waiting on
		// either of the timers
		case <-done:
			return nil

		case <-w.fullReconciliationTimer.C:
			// full reconciliation is implemented by creating a new reconciler
			// while discarding the state accrued in the previous reconciler
			w.log.Info("starting full reconciliation")
			if activeReconciler != nil {
				activeReconciler.Stop()
			}

			// Full reconciliation could have been alternatively implemented by re-using a reconciler vs
			// the current approach of destroying the existing reconciler and creating a new one
			// This has been done for the following reasons
			//  1. The current approach of stopping/starting a reconciler is conceptually
			//		equivalent to a module restart with minimal changes to the reconciliation logic/code.
			//		Full reconciliation implemented with reconciler re-use would've required introducing special handling in the
			// 		reconciliation code. This means that any future updates to the reconciliation logic would've
			//		led to increased complexity due to possible impact on full & partial reconciliation logic
			//		leading to increased maintenance cost.
			//  2. Reconciler re-use is inadequate when dealing with issues that occur due to corruption/
			//		mishandling of internal state, for example memory leaks that may occur due to bugs in
			//		reconciliation logic. The current approach will be able to deal with these as the core logic
			// 		requires teardown of the active reconciler and creation of a new one
			activeReconciler, err = w.reconcilerFactory(ctx, w.prerequisites.SharedNamespace)
			if err != nil {
				return err
			}
			workerSettings, execError := activeReconciler.Run(ctx)
			if execError != nil {
				w.api.HandleProcessingError(ctx, w.log, "Remote Development - full reconciliation cycle ended with error", execError, logz.AgentKey(agentKey))
			} else {
				w.setFullReconciliationInterval(workerSettings.FullReconciliationInterval)

				if w.setPartialReconciliationInterval(workerSettings.PartialReconciliationInterval) {
					w.resetTimer(w.partialReconciliationTimer, w.currentPartialReconciliationInterval)
				}
			}

			// Timer is reset after the work has been completed
			// If the timer were reset before reconciliation is executed, there may be a scenario
			// where the next timer tick occurs immediately after the reconciler finishes its
			// execution (because Run() takes too long for some reason)
			w.fullReconciliationTimer.Reset(w.currentFullReconciliationInterval)

			w.log.Debug("finishing full reconciliation", logz.FullReconciliationInterval(w.currentFullReconciliationInterval), logz.PartialReconciliationInterval(w.currentPartialReconciliationInterval))
		case <-w.partialReconciliationTimer.C:
			w.log.Info("starting partial reconciliation")
			workerSettings, execError := activeReconciler.Run(ctx)
			if execError != nil {
				w.api.HandleProcessingError(ctx, w.log, "Remote Development - partial reconciliation cycle ended with error", execError, logz.AgentKey(agentKey))
			} else {
				if w.setFullReconciliationInterval(workerSettings.FullReconciliationInterval) {
					w.resetTimer(w.fullReconciliationTimer, w.currentFullReconciliationInterval)
				}

				w.setPartialReconciliationInterval(workerSettings.PartialReconciliationInterval)
			}

			// Timer is reset after the work has been completed
			// If the timer were reset before reconciler is executed, there may be a scenario
			// where the next timer tick occurs immediately after the reconciler finishes its
			// execution (because Run() takes too long for some reason)
			w.partialReconciliationTimer.Reset(w.currentPartialReconciliationInterval)

			w.log.Debug("finishing partial reconciliation", logz.FullReconciliationInterval(w.currentFullReconciliationInterval), logz.PartialReconciliationInterval(w.currentPartialReconciliationInterval))
		}
	}
}

func (w *worker) setFullReconciliationInterval(newFullReconciliationInterval time.Duration) bool {
	if w.currentFullReconciliationInterval != newFullReconciliationInterval {
		w.currentFullReconciliationInterval = newFullReconciliationInterval
		return true
	}
	return false
}

func (w *worker) setPartialReconciliationInterval(newPartialReconciliationInterval time.Duration) bool {
	if w.currentPartialReconciliationInterval != newPartialReconciliationInterval {
		w.currentPartialReconciliationInterval = newPartialReconciliationInterval
		return true
	}
	return false
}

func (w *worker) resetTimer(timer *time.Timer, interval time.Duration) {
	if !timer.Stop() {
		<-timer.C
	}
	timer.Reset(interval)
}
