package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"strconv"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	corev1 "k8s.io/api/core/v1"
	k8errors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	applyConfigv1 "k8s.io/client-go/applyconfigurations/core/v1"
	coreClientv1 "k8s.io/client-go/kubernetes/typed/core/v1"
)

const (
	scanAnnotationKey         = api.AgentKeyPrefix + "/scan"
	ocsNamespaceAnnotationKey = api.AgentKeyPrefix + "/ocs-ns"
	ocsStatusConfigMapName    = "ocs-status"
	maxRetries                = 5
	retryDelay                = 1 * time.Second
)

type statusRecorder struct {
	configMaps           coreClientv1.ConfigMapInterface
	gitlabAgentNamespace string
	agentKey             api.AgentKey
	persistOcsStatus     bool
}

// initScanStatus checks if persistOcsStatus is enabled
// If it's enabled, it checks if the `ocs-status` config map exists
// If it does not exist, it creates the config map
func (s *statusRecorder) initScanStatus(ctx context.Context) error {
	// Do nothing if persistOcsStatus is disabled
	if !s.persistOcsStatus {
		return nil
	}

	// Check if the ocs-status config map exists
	_, err := s.configMaps.Get(ctx, ocsStatusConfigMapName, metav1.GetOptions{})
	// Config map exists
	if err == nil {
		return nil
	}

	if !k8errors.IsNotFound(err) {
		return fmt.Errorf("error checking for %s config map: %w", ocsStatusConfigMapName, err)
	}

	// Config map doesn't exist, create it
	configMapSpec := s.getConfigMapSpec()

	_, err = s.configMaps.Create(ctx, configMapSpec, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("error creating %s config map: %w", ocsStatusConfigMapName, err)
	}

	return nil
}

func (s *statusRecorder) getConfigMapSpec() *corev1.ConfigMap {
	labels := map[string]string{
		scanAnnotationKey:         "ocs",
		ocsNamespaceAnnotationKey: s.gitlabAgentNamespace,
		api.AgentIDKey:            strconv.FormatInt(s.agentKey.ID, 10),
	}
	return &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      ocsStatusConfigMapName,
			Namespace: s.gitlabAgentNamespace,
			Labels:    labels,
		},
	}
}

// recordScanStart records the start time of the namespace being scanned and sets the status to running in the ocs-status config map
func (s *statusRecorder) recordScanStart(ctx context.Context, scanLogger *slog.Logger, namespace string) {
	// Do nothing if persistOcsStatus is disabled
	if !s.persistOcsStatus {
		return
	}

	// Set namespace data
	namespaceData, err := json.Marshal(map[string]any{
		"status":     scanStatusRunning,
		"start_time": time.Now().Format(time.RFC3339),
	})
	if err != nil {
		scanLogger.Error("Failed to marshal ocs status namespace data", logz.Error(err))
		return
	}

	s.updateMap(ctx, scanLogger, func(ocsStatus *corev1.ConfigMap) error {
		ocsStatus.Data[namespace] = string(namespaceData)
		return nil
	})
}

// recordScanEnd records the end time, vulns found and scan status for the namespace being scanned in the ocs-status config map
func (s *statusRecorder) recordScanEnd(ctx context.Context, scanLogger *slog.Logger, namespace string, vulnerabilitiesFound int, scanStatus string) {
	// Do nothing if persistOcsStatus is disabled
	if !s.persistOcsStatus {
		return
	}

	s.updateMap(ctx, scanLogger, func(ocsStatus *corev1.ConfigMap) error {
		// Get namespace data
		var namespaceData map[string]any
		if data, exists := ocsStatus.Data[namespace]; exists {
			if err := json.Unmarshal([]byte(data), &namespaceData); err != nil {
				return fmt.Errorf("failed to unmarshal ocs-status ConfigMap namespace data: %w", err)
			}
		} else {
			// Set start_time to empty string if namespace data is not present
			namespaceData = make(map[string]any)
			namespaceData["start_time"] = ""
		}

		// Update namespace data
		namespaceData["status"] = scanStatus
		namespaceData["vulnerabilities_found"] = vulnerabilitiesFound
		namespaceData["end_time"] = time.Now().Format(time.RFC3339)
		updatedNamespaceData, err := json.Marshal(namespaceData)
		if err != nil {
			return fmt.Errorf("failed to marshal ocs-status namespace data: %w", err)
		}
		ocsStatus.Data[namespace] = string(updatedNamespaceData)
		return nil
	})
}

func (s *statusRecorder) updateMap(ctx context.Context, scanLogger *slog.Logger, modify func(*corev1.ConfigMap) error) {
	for attempt := range maxRetries {
		// Get ocs status
		ocsStatus, err := s.configMaps.Get(ctx, ocsStatusConfigMapName, metav1.GetOptions{})
		if err != nil {
			scanLogger.Error("Failed to fetch ocs-status ConfigMap", logz.Error(err))
			return
		}

		// Initialize ocs status data if it's empty
		if ocsStatus.Data == nil {
			ocsStatus.Data = make(map[string]string, 1)
		}

		err = modify(ocsStatus)
		if err != nil {
			scanLogger.Error("Failed to modify ocs-status data field", logz.Error(err))
			return
		}

		applyConfig := applyConfigv1.ConfigMap(ocsStatusConfigMapName, s.gitlabAgentNamespace).
			WithData(ocsStatus.Data).
			WithResourceVersion(ocsStatus.ResourceVersion)

		// Apply ocs status
		_, err = s.configMaps.Apply(ctx, applyConfig, metav1.ApplyOptions{FieldManager: api.FieldManager})
		switch {
		case err == nil:
			return
		case k8errors.IsConflict(err):
			scanLogger.Debug("Conflict while applying ocs-status ConfigMap. Retrying", logz.Attempt(attempt), logz.Error(err))
			// Wait before retrying
			select {
			case <-ctx.Done():
				scanLogger.Error("Failed to apply ocs-status ConfigMap due to context error", logz.Error(ctx.Err()))
				return
			case <-time.After(retryDelay):
				// Continue with next attempt
			}

		default:
			scanLogger.Error("Failed to apply ocs-status ConfigMap", logz.Error(err))
			return
		}
	}
}
