package agentk

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/remote_development"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/ioz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/prototool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/syncz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/agentcfg"
	"gitlab.com/gitlab-org/cluster-integration/tunnel/tool/retry"
	corev1 "k8s.io/api/core/v1"
)

type remoteDevReconciler interface {
	Run(context.Context) (WorkerSettings, error)
	Stop()
}

type module struct {
	log                  *slog.Logger
	api                  modagent.API
	prerequisitesPollCfg retry.PollConfig
	reconcilerFactory    func(ctx context.Context, namespace string) (remoteDevReconciler, error)
}

func (m *module) Run(ctx context.Context, cfg <-chan *agentcfg.AgentConfiguration) error {
	wh := syncz.NewComparableWorkerHolder(m.prerequisitesPollingWorkerFactory)
	defer wh.StopAndWait()

	// This loop reacts to configuration changes stopping and starting workers.
	for config := range cfg {
		wh.ApplyConfig(ctx, config.RemoteDevelopment.Enabled)
	}
	return nil
}

func (m *module) prerequisitesPollingWorkerFactory(enabled bool) syncz.Worker {
	if !enabled {
		return syncz.WorkerFunc(func(ctx context.Context) {
			// nop worker
		})
	}

	return syncz.WorkerFunc(func(ctx context.Context) {
		m.log.Debug("Remote Development - starting reconciler run")
		defer m.log.Debug("Remote Development - reconciler run ended")

		wh := syncz.NewComparableWorkerHolder(m.workerFactory)
		defer wh.StopAndWait()

		_ = retry.PollWithBackoff(ctx, m.prerequisitesPollCfg, func(ctx context.Context) (error, retry.AttemptResult) { //nolint:staticcheck
			prerequisites, err := m.getWorkspacePrerequisitesFromRailsAPI(ctx)
			if err != nil {
				m.api.HandleProcessingError(ctx, m.log, "Error polling GitLab for prerequisites", err)
				return nil, retry.Backoff
			}
			wh.ApplyConfig(ctx, *prerequisites)
			return nil, retry.Continue
		})
	})
}

func (m *module) workerFactory(prerequisites WorkspacePrerequisites) syncz.Worker {
	return syncz.WorkerFunc(func(ctx context.Context) {
		w := worker{
			log:                                  m.log,
			api:                                  m.api,
			prerequisites:                        prerequisites,
			reconcilerFactory:                    m.reconcilerFactory,
			currentFullReconciliationInterval:    defaultFullReconciliationInterval,
			currentPartialReconciliationInterval: defaultPartialReconciliationInterval,
		}

		err := w.Run(ctx)
		if err != nil && !errz.ContextDone(err) {
			m.log.Error("Error running reconciler", logz.Error(err))
		}
	})
}

func (m *module) getWorkspacePrerequisitesFromRailsAPI(ctx context.Context) (*WorkspacePrerequisites, error) {
	resp, err := m.api.MakeGitLabRequest(ctx, "/prerequisites")
	if err != nil {
		return nil, fmt.Errorf("error making api request: %w", err)
	}
	defer errz.SafeClose(resp.Body, &err)

	m.log.Debug("Made request to the Rails API for prerequisites",
		logz.StatusCode(resp.StatusCode),
		logz.RequestID(resp.Header.Get(httpz.RequestIDHeader)),
	)

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var bodyLen int
	prerequisites := WorkspacePrerequisites{
		SharedNamespace: corev1.NamespaceAll,
	}
	err = ioz.ReadAllFunc(resp.Body, func(body []byte) error {
		bodyLen = len(body)
		return json.Unmarshal(body, &prerequisites)
	})
	if err != nil {
		return nil, fmt.Errorf("response body: %w", err)
	}

	m.log.Debug(
		"Read body from the Rails API",
		logz.PayloadSizeInBytes(bodyLen),
		logz.WorkspaceNamespace(prerequisites.SharedNamespace),
	)
	return &prerequisites, nil
}

func (m *module) DefaultAndValidateConfiguration(config *agentcfg.AgentConfiguration) error {
	prototool.NotNil(&config.RemoteDevelopment)
	// config.RemoteDevelopment.Enabled will default to false if not provided which is expected
	return nil
}

func (m *module) Name() string {
	return remote_development.ModuleName
}
