package agent

import (
	"bytes"
	"compress/gzip"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/starboard_vulnerability/agent/converter"
	"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/ioz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/security-products/analyzers/trivy-k8s-wrapper/data/analyzers/report"
	"gitlab.com/gitlab-org/security-products/analyzers/trivy-k8s-wrapper/data/errorcodes"
	"gitlab.com/gitlab-org/security-products/analyzers/trivy-k8s-wrapper/prototool"
	"google.golang.org/protobuf/proto"
	corev1 "k8s.io/api/core/v1"
	k8errors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/kubernetes"
)

const (
	labelTrivyVersion = api.AgentKeyPrefix + "/trivy-version"
	labelOcsNext      = api.AgentKeyPrefix + "/ocs-next"
	labelOcsNamespace = api.AgentKeyPrefix + "/ocs-ns"
	// TODO this should be changed to api.AgentIDKey
	labelAgentID = api.AgentKeyPrefix + "/agent-id"
)

type ScanningManager interface {
	deleteChainedConfigmaps(ctx context.Context, targetNamespace string) error
	deployScanningPod(ctx context.Context, podName string, targetNamespace string, serviceAccountName string, image string) error
	deleteScanningPod(podName string) error
	watchScanningPod(ctx context.Context, podName string) (watch.Interface, error)
	extractExitCodeError(code int32, reason string) error
	readChainedConfigmaps(ctx context.Context, targetNamespace string) ([]byte, string, error)
	parseScanningPodPayload(data []byte, trivyVersion string) ([]*Payload, error)
	toVulnerabilityAPIPayload(p *prototool.Payload, trivyVersion string) ([]*Payload, error)
}

type scanningManager struct {
	kubeClientset        kubernetes.Interface
	resourcesManager     resources.Resources
	agentKey             api.AgentKey
	gitlabAgentNamespace string
	scanLogger           *slog.Logger
	scannerTimeout       time.Duration
	reportMaxSizeBytes   uint64
	resourceTypes        []string
}

func (s *scanningManager) deployScanningPod(ctx context.Context, podName string, targetNamespace string, serviceAccountName string, image string) error {
	rq, err := s.resourcesManager.GetResources()
	if err != nil {
		return err
	}

	// User ID 100 is defined in the dockerfile for trivy-k8s-wrapper.
	// See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/merge_requests/1881 for more details.
	ocsPodUserID := int64(100)
	ALLOW := true
	DENY := false

	podSpec := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      podName,
			Namespace: s.gitlabAgentNamespace,
			Labels: map[string]string{
				"app.kubernetes.io/managed-by": "gitlab-agent",
				"app.kubernetes.io/name":       "trivy-scan",
				"app.kubernetes.io/instance":   podName,
			},
		},
		Spec: corev1.PodSpec{
			ServiceAccountName: serviceAccountName,
			Containers: []corev1.Container{
				{
					Name:            "trivy",
					ImagePullPolicy: corev1.PullAlways,
					SecurityContext: &corev1.SecurityContext{
						SeccompProfile: &corev1.SeccompProfile{
							Type: corev1.SeccompProfileTypeRuntimeDefault,
						},
						RunAsUser:                &ocsPodUserID,
						AllowPrivilegeEscalation: &DENY,
						RunAsNonRoot:             &ALLOW,
						Capabilities: &corev1.Capabilities{
							Drop: []corev1.Capability{"ALL"},
						},
					},
					Resources: corev1.ResourceRequirements{
						Limits: corev1.ResourceList{
							corev1.ResourceMemory:           rq.LimitsMemory,
							corev1.ResourceCPU:              rq.LimitsCPU,
							corev1.ResourceEphemeralStorage: rq.LimitsEphemeralStorage,
						},
						Requests: corev1.ResourceList{
							corev1.ResourceMemory:           rq.RequestsMemory,
							corev1.ResourceCPU:              rq.RequestsCPU,
							corev1.ResourceEphemeralStorage: rq.RequestsEphemeralStorage,
						},
					},
					Image: image,
					Env: []corev1.EnvVar{
						{
							Name:  "NAMESPACE",
							Value: targetNamespace,
						},
						{
							Name:  "GITLAB_AGENT_ID",
							Value: strconv.FormatInt(s.agentKey.ID, 10),
						},
						{
							Name:  "GITLAB_AGENT_NS",
							Value: s.gitlabAgentNamespace,
						},
						{
							Name:  "WORKLOADS",
							Value: strings.Join(s.resourceTypes, ","),
						},
						{
							Name:  "TIMEOUT",
							Value: s.scannerTimeout.String(),
						},
						{
							Name:  "REPORT_MAX_SIZE",
							Value: strconv.FormatUint(s.reportMaxSizeBytes, 10),
						},
					},
				},
			},
			RestartPolicy: corev1.RestartPolicyNever,
		},
	}

	if _, errP := s.kubeClientset.CoreV1().Pods(s.gitlabAgentNamespace).Create(ctx, podSpec, metav1.CreateOptions{}); errP != nil {
		// There could be a scenario where The previous OCS Scanning Pod was not deleted.
		// Delete the Pod to ensure that the next scan would be successful.
		if k8errors.IsAlreadyExists(errP) {
			s.scanLogger.Debug("OCS Scanning Pod already exists, deleting")
			if err := s.kubeClientset.CoreV1().Pods(s.gitlabAgentNamespace).Delete(ctx, podName, metav1.DeleteOptions{}); err != nil {
				return fmt.Errorf("could not delete pod: %w", err)
			}
			return errors.New("pod exists. Deleted Pod")
		}
		return errP
	}

	return nil
}

// deleteChainedConfigmaps deletes a chain of configmaps (1 or more configmaps chained by labels) for a certain
// target scan namespace. We delete chained configmaps based on the label agent.gitlab.com/ocs-ns=<TARGET_NAMESPACE>
func (s *scanningManager) deleteChainedConfigmaps(ctx context.Context, targetNamespace string) error {
	labelSelector := fmt.Sprintf("%s=%s, %s=%d", labelOcsNamespace, targetNamespace, labelAgentID, s.agentKey.ID)
	cms, err := s.kubeClientset.CoreV1().ConfigMaps(s.gitlabAgentNamespace).
		List(ctx, metav1.ListOptions{LabelSelector: labelSelector})
	if err != nil {
		return fmt.Errorf("could not list existing configmaps: %w", err)
	}
	for _, cm := range cms.Items {
		if err := s.kubeClientset.CoreV1().ConfigMaps(s.gitlabAgentNamespace).Delete(ctx, cm.GetName(), metav1.DeleteOptions{}); err != nil {
			return fmt.Errorf("could not delete pre-existing ConfigMap: %w", err)
		}
	}
	return nil

}

func (s *scanningManager) deleteScanningPod(podName string) error {
	// Using a separate context in the event that the context was canceled.
	deleteCtx, deleteCtxCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer deleteCtxCancel()
	err := s.kubeClientset.CoreV1().Pods(s.gitlabAgentNamespace).Delete(deleteCtx, podName, metav1.DeleteOptions{})
	return err
}

func (s *scanningManager) watchScanningPod(ctx context.Context, podName string) (watch.Interface, error) {
	return s.kubeClientset.CoreV1().Pods(s.gitlabAgentNamespace).Watch(ctx, metav1.ListOptions{
		FieldSelector: fmt.Sprintf("metadata.name=%s", podName),
	})
}

var exitCodes = map[int32]string{
	errorcodes.KubeClient:        "failed to initialize a kube client",
	errorcodes.FlagsValidation:   "flags validation failed",
	errorcodes.TrivyVersion:      "failed to get Trivy scanner version",
	errorcodes.TrivyScan:         "failed to execute a Trivy scan",
	errorcodes.SizeLimit:         "trivy report size limit",
	errorcodes.DataConvertion:    "could not get a data converter",
	errorcodes.ToReport:          "failed to unmarshal Trivy report",
	errorcodes.PrepareData:       "failed to prepare data",
	errorcodes.ChainedConfigmaps: "failed to create chained ConfigMaps",
}

func (s *scanningManager) extractExitCodeError(code int32, reason string) error {
	if code == 0 {
		return nil
	}
	str, ok := exitCodes[code]
	if !ok {
		return fmt.Errorf("OCS Scanning pod exited with unknown exit code: %v, reason: %s", code, reason)
	}
	return fmt.Errorf("%s | Reason = %s", str, reason)
}

func (s *scanningManager) readChainedConfigmaps(ctx context.Context, targetNamespace string) ([]byte, string, error) {
	// we know the first ConfigMap name
	cmName := fmt.Sprintf("ocs-%s-%d-1", targetNamespace, s.agentKey.ID)
	var allData []byte
	trivyVersion := ""
	for {
		s.scanLogger.Info("Reading ConfigMap", logz.K8sObjectName(cmName))
		// Each namespace scan will return 1 vulnerability based on the response from ParsePodLogsToReport. As there are 2 namespaces this will result in 2 vulnerabilities to be transmitted to Gitlab.
		cm, err := s.kubeClientset.CoreV1().ConfigMaps(s.gitlabAgentNamespace).Get(ctx, cmName, metav1.GetOptions{})
		if err != nil {
			return nil, "", fmt.Errorf("could not read chained ConfigMap %s: %w", cmName, err)
		}
		bytes, ok := cm.BinaryData["data"]
		if !ok {
			return nil, "", errors.New("ConfigMap did not contain data field in binaryData")
		}
		allData = append(allData, bytes...)

		cmName, ok = cm.Labels[labelOcsNext]
		if !ok {
			// we are done
			// before breaking read the trivy version
			v, ok := cm.Labels[labelTrivyVersion]
			if !ok {
				s.scanLogger.Warn(fmt.Sprintf("Missing %s label from ConfigMap", labelTrivyVersion))
			} else {
				trivyVersion = v
			}
			break
		}
	}
	return allData, trivyVersion, nil
}

func (s *scanningManager) parseScanningPodPayload(data []byte, trivyVersion string) ([]*Payload, error) {
	gzDataBytes, err := base64.StdEncoding.DecodeString(string(data))
	if err != nil {
		return nil, fmt.Errorf("could not decode data: %w", err)
	}

	gzipReader, err := gzip.NewReader(bytes.NewReader(gzDataBytes))
	if err != nil {
		return nil, fmt.Errorf("could not initialize a gzip reader: %w", err)
	}

	payload := &prototool.Payload{}
	err = ioz.ReadAllFunc(gzipReader, func(protobufBytes []byte) error {
		return proto.Unmarshal(protobufBytes, payload)
	})
	if err != nil {
		return nil, fmt.Errorf("could not read protobuffer format data: %w", err)
	}

	return s.toVulnerabilityAPIPayload(payload, trivyVersion)

}

// toVulnerabilityAPIPayload gets a prototool version of the Starboard Vulnerability payload and transforms it into
// a Starboard Vulnerability payload. This payload is what we expect at the create starboard vulnerability api
// https://docs.gitlab.com/ee/development/internal_api/#create-starboard-vulnerability
func (s *scanningManager) toVulnerabilityAPIPayload(p *prototool.Payload, trivyVersion string) ([]*Payload, error) {
	payloads := make([]*Payload, len(p.Vulnerabilities))
	protoConverter := converter.Converter{}
	for i, v := range p.Vulnerabilities {
		payloads[i] = &Payload{
			Vulnerability: &report.Vulnerability{
				Name:        v.Name,
				Message:     v.Message,
				Description: v.Description,
				Solution:    v.Solution,
				Severity:    protoConverter.ToSeverity(v.Severity),
				Confidence:  report.ParseConfidenceLevel(v.Confidence),
				Identifiers: protoConverter.ToIdentifiers(v.Identifiers),
				Links:       protoConverter.ToLinks(v.Links),
				Location:    protoConverter.ToLocation(v.Location),
			},
			Scanner: getTrivyScannerPayload(trivyVersion),
		}
	}
	return payloads, nil
}
