package agent

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

	"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/tool/errz"
	"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/retry"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/agentcfg"
)

const (
	policiesPollInterval      = 30 * time.Second
	policiesPollInitBackoff   = 10 * time.Second
	policiesPollMaxBackoff    = 5 * time.Minute
	policiesPollResetDuration = 10 * time.Minute
	policiesPollBackoffFactor = 2.0
	policiesPollJitter        = 5.0
)

type configurationToUpdateData struct {
	agentKey                api.AgentKey
	containerScanningConfig *agentcfg.ContainerScanningCF
}

type securityPoliciesWorker struct {
	log     *slog.Logger
	api     modagent.API
	updater chan<- configurationToUpdateData
}

type SecurityPolicyConfiguration struct {
	Cadence    string    `json:"cadence"`
	Namespaces []string  `json:"namespaces"`
	UpdatedAt  time.Time `json:"updated_at"`
}

type getSecurityPoliciesResponse struct {
	Policies []*SecurityPolicyConfiguration `json:"configurations"`
}

func (w *securityPoliciesWorker) Run(ctx context.Context) {
	_ = retry.PollWithBackoff(ctx, securityPoliciesPollConfig(), func(ctx context.Context) (error, retry.AttemptResult) { //nolint:staticcheck
		response, err := w.requestPolicy(ctx)
		if err != nil {
			w.log.Error("Error checking security policies", logz.Error(err))
			return nil, retry.Backoff
		}

		agentKey, err := w.api.GetAgentKey(ctx)
		if err != nil {
			w.log.Error("Could not retrieve agent information", logz.Error(err))
			return nil, retry.Backoff
		}

		updateData := configurationToUpdateData{
			agentKey: agentKey,
		}

		if len(response.Policies) > 0 {
			updateData.containerScanningConfig, err = convertPolicy(response.Policies[0])
			if err != nil {
				w.log.Error("Could not convert OCS policy", logz.Error(err))
				return nil, retry.Backoff
			}
		}

		select {
		case w.updater <- updateData:
			return nil, retry.Continue
		case <-ctx.Done():
			return nil, retry.Done
		}
	})
}

func (w *securityPoliciesWorker) requestPolicy(ctx context.Context) (policiesResponse *getSecurityPoliciesResponse, retError error) {
	resp, err := w.api.MakeGitLabRequest(
		ctx,
		"/policies_configuration",
		modagent.WithRequestMethod(http.MethodGet),
	)
	if err != nil {
		return nil, fmt.Errorf("could not retrieve security policies: %w", err)
	}

	defer errz.SafeClose(resp.Body, &retError)
	switch resp.StatusCode {
	case http.StatusNotFound, http.StatusPaymentRequired:
		// The project does not have an ultimate license.
		// Treat this the same as an empty policy response.
		return new(getSecurityPoliciesResponse), nil
	case http.StatusOK:
		securityPoliciesResponse := new(getSecurityPoliciesResponse)
		err = ioz.ReadAllFunc(resp.Body, func(body []byte) error {
			return json.Unmarshal(body, securityPoliciesResponse)
		})
		if err != nil {
			return nil, fmt.Errorf("response body: %w", err)
		}

		return securityPoliciesResponse, nil
	default:
		return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
	}
}

func convertPolicy(policy *SecurityPolicyConfiguration) (*agentcfg.ContainerScanningCF, error) {
	cfg := &agentcfg.ContainerScanningCF{
		Cadence:             policy.Cadence,
		VulnerabilityReport: &agentcfg.VulnerabilityReport{Namespaces: policy.Namespaces},
	}
	err := DefaultAndValidateCsConfiguration(cfg)
	return cfg, err
}

func securityPoliciesPollConfig() retry.PollConfig {
	return retry.NewPollConfigFactory(policiesPollInterval, retry.NewExponentialBackoffFactory(
		policiesPollInitBackoff,
		policiesPollMaxBackoff,
		policiesPollResetDuration,
		policiesPollBackoffFactor,
		policiesPollJitter,
	))()
}
