package agent

import (
	"context"
	"errors"
	"fmt"

	"github.com/robfig/cron/v3"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v16/internal/module/modagent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v16/internal/module/starboard_vulnerability"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v16/internal/tool/syncz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v16/pkg/agentcfg"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
	"k8s.io/apimachinery/pkg/util/wait"
)

var ErrMissingCadence = errors.New("cadence is a required field")

const (
	defaultTrivyResourceLimitsCpu      = "500m"
	defaultTrivyResourceLimitsMemory   = "500Mi"
	defaultTrivyResourceRequestsCpu    = "100m"
	defaultTrivyResourceRequestsMemory = "100Mi"
)

type module struct {
	log                       *zap.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{
					agentId:                 config.AgentId,
					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) IsRunnableConfiguration(cfg *agentcfg.AgentConfiguration) bool {
	// starboard_vulnerability module is always enabled as it vulnerability scan could be enforced by security policies
	return true
}

func (m *module) DefaultAndValidateConfiguration(cfg *agentcfg.AgentConfiguration) error {
	// If cfg.ContainerScanning is nil, this function will always default ResourceRequirement
	err := m.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.ContainerScanning != nil && cfg.ContainerScanning.Cadence != "" {
		if _, err := cron.ParseStandard(cfg.ContainerScanning.Cadence); err != nil {
			return fmt.Errorf("cadence is invalid: %w", err)
		}
	}

	return nil
}

// Default resourceRequirements even if containerScanning is nil since vulnerability scan could be enforced by security policies
func (m *module) defaultAndValidateResourceRequirements(cfg *agentcfg.AgentConfiguration) error {
	newResourceRequirements := &agentcfg.ResourceRequirements{
		Limits: &agentcfg.Resource{
			Cpu:    defaultTrivyResourceLimitsCpu,
			Memory: defaultTrivyResourceLimitsMemory,
		},
		Requests: &agentcfg.Resource{
			Cpu:    defaultTrivyResourceRequestsCpu,
			Memory: defaultTrivyResourceRequestsMemory,
		},
	}

	if cfg.ContainerScanning == nil {
		cfg.ContainerScanning = &agentcfg.ContainerScanningCF{
			ResourceRequirements: newResourceRequirements,
		}
		return nil
	}

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

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

	_, err := parseAndValidateResource(newResourceRequirements.Limits)
	if err != nil {
		return err
	}
	_, err = parseAndValidateResource(newResourceRequirements.Requests)
	if err != nil {
		return err
	}

	cfg.ContainerScanning.ResourceRequirements = newResourceRequirements
	return nil
}

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

func isConfigEqual(c1, c2 configurationToUpdateData) bool {
	return c1.agentId == c2.agentId && 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 *agentcfg.Resource, parsedResource *agentcfg.Resource) {
	if resources.Cpu != "" {
		parsedResource.Cpu = resources.Cpu
	}
	if resources.Memory != "" {
		parsedResource.Memory = resources.Memory
	}
}
