package agent

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

	"github.com/robfig/cron/v3"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/starboard_vulnerability"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/starboard_vulnerability/agent/resources"
	"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"
	"google.golang.org/protobuf/proto"
	"k8s.io/apimachinery/pkg/util/wait"
)

const (
	kindPod                   = "Pod"
	kindReplicaSet            = "ReplicaSet"
	kindReplicationController = "ReplicationController"
	kindStatefulSet           = "StatefulSet"
	kindDaemonSet             = "DaemonSet"
	kindCronJob               = "CronJob"
	kindJob                   = "Job"
	kindDeployment            = "Deployment"

	defaultTrivyK8sWrapperImageRepository        = "registry.gitlab.com/security-products/trivy-k8s-wrapper"
	defaultTrivyK8sWrapperImageTag               = "0.10.5"
	defaultTrivyResourceLimitsCPU                = "500m"
	defaultTrivyResourceLimitsMemory             = "500Mi"
	defaultTrivyResourceLimitsEphemeralStorage   = "3Gi"
	defaultTrivyResourceRequestsCPU              = "100m"
	defaultTrivyResourceRequestsMemory           = "100Mi"
	defaultTrivyResourceRequestsEphemeralStorage = "1Gi"
	defaultGitlabAgentNamespace                  = "gitlab-agent"
	defaultGitlabAgentServiceAccount             = "gitlab-agent"
	defaultScannerTimeout                        = 5 * time.Minute
	defaultMaxReportSizeBytes                    = 100000000 // 100MB
	defaultDeleteReportArtifact                  = true
	defaultSeverityThreshold                     = agentcfg.SeverityLevelEnum_UNKNOWN
)

type module struct {
	log                       *slog.Logger
	api                       modagent.API
	policiesUpdateDataChannel chan configurationToUpdateData
	workerFactory             func(configurationToUpdateData) syncz.Worker
}

func (m *module) Run(ctx context.Context, repositoryConfig <-chan *agentcfg.AgentConfiguration) error {
	policiesHolder := m.newSecurityPoliciesWorkerHolder(ctx, m.policiesUpdateDataChannel)
	defer policiesHolder.stop()

	wh := syncz.NewWorkerHolder[configurationToUpdateData](m.workerFactory, isConfigEqual)
	defer wh.StopAndWait()

	var securityPoliciesEnabled bool
	var resourceRequirements *agentcfg.ResourceRequirements

	for {
		select {
		case config, ok := <-repositoryConfig:
			if !ok {
				return nil
			}

			// Set local var resourceRequirements. This is used if securityPolicies is enabled as resourceRequirements can only be defined from the agent config
			resourceRequirements = config.ContainerScanning.ResourceRequirements

			if !securityPoliciesEnabled {
				// Do not update if config does not contain ContainerScanning configuration
				if config.ContainerScanning.Cadence == "" {
					continue
				}
				data := configurationToUpdateData{
					agentKey:                api.AgentKey{ID: config.AgentId, Type: api.AgentTypeKubernetes},
					containerScanningConfig: config.ContainerScanning,
				}

				wh.ApplyConfig(ctx, data)
			}
		case data := <-m.policiesUpdateDataChannel:
			if data.containerScanningConfig == nil {
				m.log.Debug("ContainerScanning config is empty, security policies are disabled")
				securityPoliciesEnabled = false
			} else {
				m.log.Debug("ContainerScanning config is present, security policies are enabled")
				securityPoliciesEnabled = true
				if resourceRequirements != nil {
					data.containerScanningConfig.ResourceRequirements = resourceRequirements
				}
				wh.ApplyConfig(ctx, data)
			}
		}
	}
}

func (m *module) newSecurityPoliciesWorkerHolder(ctx context.Context, updater chan<- configurationToUpdateData) *securityPoliciesWorkerHolder {
	workerHolder := &securityPoliciesWorkerHolder{
		worker: securityPoliciesWorker{
			api:     m.api,
			log:     m.log,
			updater: updater,
		},
	}
	ctx, workerHolder.cancel = context.WithCancel(ctx)
	workerHolder.wg.StartWithContext(ctx, workerHolder.worker.Run)

	return workerHolder
}

func (m *module) DefaultAndValidateConfiguration(cfg *agentcfg.AgentConfiguration) error {
	prototool.NotNil(&cfg.ContainerScanning)
	return DefaultAndValidateCsConfiguration(cfg.ContainerScanning)
}

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

func isConfigEqual(c1, c2 configurationToUpdateData) bool {
	return c1.agentKey == c2.agentKey && proto.Equal(c1.containerScanningConfig, c2.containerScanningConfig)
}

type securityPoliciesWorkerHolder struct {
	worker securityPoliciesWorker
	wg     wait.Group
	cancel context.CancelFunc
}

func (h *securityPoliciesWorkerHolder) stop() {
	h.cancel()
	h.wg.Wait()
}

func defaultResources(resources, parsedResource *agentcfg.Resource) {
	if resources.Cpu != "" {
		parsedResource.Cpu = resources.Cpu
	}
	if resources.Memory != "" {
		parsedResource.Memory = resources.Memory
	}
	if resources.EphemeralStorage != "" {
		parsedResource.EphemeralStorage = resources.EphemeralStorage
	}
}

// Default resourceRequirements even if containerScanning is nil since vulnerability scan could be enforced by security policies
func defaultAndValidateResourceRequirements(cfg *agentcfg.ContainerScanningCF) error {
	newResourceRequirements := &agentcfg.ResourceRequirements{
		Limits: &agentcfg.Resource{
			Cpu:              defaultTrivyResourceLimitsCPU,
			Memory:           defaultTrivyResourceLimitsMemory,
			EphemeralStorage: defaultTrivyResourceLimitsEphemeralStorage,
		},
		Requests: &agentcfg.Resource{
			Cpu:              defaultTrivyResourceRequestsCPU,
			Memory:           defaultTrivyResourceRequestsMemory,
			EphemeralStorage: defaultTrivyResourceRequestsEphemeralStorage,
		},
	}

	resourceRequirements := cfg.ResourceRequirements
	if resourceRequirements == nil {
		cfg.ResourceRequirements = newResourceRequirements
		return nil
	}

	if resourceRequirements.Limits != nil {
		defaultResources(resourceRequirements.Limits, newResourceRequirements.Limits)
	}
	if resourceRequirements.Requests != nil {
		defaultResources(resourceRequirements.Requests, newResourceRequirements.Requests)
	}

	// we do this just for validation purposes
	rm := resources.Manager{
		Requirements: newResourceRequirements,
	}
	_, err := rm.GetResources()
	if err != nil {
		return err
	}

	cfg.ResourceRequirements = newResourceRequirements
	return nil
}

func DefaultAndValidateCsConfiguration(cfg *agentcfg.ContainerScanningCF) error {
	// If cfg.ContainerScanning is nil, this function will always default ResourceRequirement
	err := defaultAndValidateResourceRequirements(cfg)
	if err != nil {
		return fmt.Errorf("resource requirements is invalid: %w", err)
	}

	// Only parse cadence if it's present since agentConfig can contain only resourceRequirements
	if cfg.Cadence != "" {
		if _, err := cron.ParseStandard(cfg.Cadence); err != nil {
			return fmt.Errorf("cadence is invalid: %w", err)
		}
	}

	prototool.Duration(&cfg.ScannerTimeout, defaultScannerTimeout)
	prototool.DefaultVal(&cfg.ReportMaxSize, defaultMaxReportSizeBytes)
	prototool.DefaultValPtr(&cfg.DeleteReportArtifact, defaultDeleteReportArtifact)
	prototool.DefaultValPtr(&cfg.SeverityThreshold, defaultSeverityThreshold)

	prototool.NotNil(&cfg.VulnerabilityReport)
	prototool.DefaultSlice(&cfg.VulnerabilityReport.ResourceTypes, []string{
		kindPod,
		kindReplicaSet,
		kindReplicationController,
		kindStatefulSet,
		kindDaemonSet,
		kindCronJob,
		kindJob,
		kindDeployment,
	})

	prototool.NotNil(&cfg.TrivyK8SWrapperImage)
	prototool.DefaultVal(&cfg.TrivyK8SWrapperImage.Repository, defaultTrivyK8sWrapperImageRepository)
	prototool.DefaultVal(&cfg.TrivyK8SWrapperImage.Tag, defaultTrivyK8sWrapperImageTag)

	return nil
}
