package agent

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

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/agentcfg"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes"
)

const (
	// The Trivy Scan timeout is configured by the user, or set to a default timeout of 15 minutes. We want to give an extra 5 minutes on top of
	// this timeout, to allow the scan to complete, to read the chained configmaps and transmitting the vulnerability report.
	additionalNamespaceScannerTimeout = 5 * time.Minute

	podEvictedStatus = "Evicted"
)

type NamespaceScanner interface {
	scan(ctx context.Context, scanLogger *slog.Logger, targetNamespace string, reporter Reporter, scanningManager ScanningManager) ([]string, error)
}

type namespaceScanner struct {
	kubeClientset             kubernetes.Interface
	gitlabAgentNamespace      string
	gitlabAgentServiceAccount string
	agentKey                  api.AgentKey
	ocsServiceAccountName     string
	trivyK8sWrapperImage      *agentcfg.TrivyK8SWrapperImage
	scannerTimeout            time.Duration
	deleteReportArtifact      bool
	severityThreshold         string
}

func newNamespaceScanner(
	kubeClientset kubernetes.Interface,
	gitlabAgentNamespace string,
	gitlabAgentServiceAccount string,
	agentKey api.AgentKey,
	ocsServiceAccountName string,
	trivyK8sWrapperImage *agentcfg.TrivyK8SWrapperImage,
	scannerTimeout time.Duration,
	deleteReportArtifact bool,
	severityThreshold string,
) *namespaceScanner {
	return &namespaceScanner{
		kubeClientset:             kubeClientset,
		gitlabAgentNamespace:      gitlabAgentNamespace,
		gitlabAgentServiceAccount: gitlabAgentServiceAccount,
		agentKey:                  agentKey,
		ocsServiceAccountName:     ocsServiceAccountName,
		trivyK8sWrapperImage:      trivyK8sWrapperImage,
		scannerTimeout:            scannerTimeout,
		deleteReportArtifact:      deleteReportArtifact,
		severityThreshold:         severityThreshold,
	}
}

func (ns *namespaceScanner) scan(ctx context.Context, scanLogger *slog.Logger, targetNamespace string, reporter Reporter, manager ScanningManager) ([]string, error) {
	// build the timeout
	scannerTimeout := ns.scannerTimeout + additionalNamespaceScannerTimeout

	ctx, cancel := context.WithTimeout(ctx, scannerTimeout)
	defer cancel()

	var uuids []string
	podName := fmt.Sprintf("trivy-scan-%s", targetNamespace)
	scanLogger = scanLogger.With(logz.PodName(podName))

	if err := manager.deleteChainedConfigmaps(ctx, targetNamespace); err != nil {
		return nil, fmt.Errorf("could not delete configmaps, before deploying scanning pod: %w", err)
	}

	image := fmt.Sprintf("%s:%s", ns.trivyK8sWrapperImage.Repository, ns.trivyK8sWrapperImage.Tag)

	scanLogger.Debug("Deploying OCS Scanning Pod")
	if err := manager.deployScanningPod(ctx, podName, targetNamespace, ns.ocsServiceAccountName, image, ns.severityThreshold); err != nil {
		return nil, fmt.Errorf("could not deploy scanning pod: %w", err)
	}

	defer func() {
		scanLogger.Debug("Deleting OCS Scanning Pod")
		if err := manager.deleteScanningPod(podName); err != nil {
			scanLogger.Error("Error deleting Pod", logz.Error(err))
		}
	}()

	scanLogger.Debug("Start watcher for OCS Scanning Pod")
	watcher, err := manager.watchScanningPod(ctx, podName)
	if err != nil {
		return nil, fmt.Errorf("could not start watcher for OCS Scanning Pod: %w", err)
	}
	defer watcher.Stop()

	done := ctx.Done()
	for {
		select {
		case <-done:
			scanLogger.Debug("Stopping watcher as context canceled")
			return nil, ctx.Err()
		case event, ok := <-watcher.ResultChan():
			if !ok {
				return nil, errors.New("channel closed unexpectedly")
			}

			pod, ok := event.Object.(*corev1.Pod)
			if !ok {
				return nil, errors.New("watcher received unexpected object that is not a Pod")
			}

			scanLogger.Debug("pod status", logz.PodStatus(string(pod.Status.Phase)))

			switch pod.Status.Phase {
			case corev1.PodPending:
				for i := range pod.Status.ContainerStatuses {
					waitState := pod.Status.ContainerStatuses[i].State.Waiting
					if waitState != nil {
						msg, reason := waitState.Message, waitState.Reason
						switch reason {
						case ErrImagePullBackOff, ErrImageInspect, ErrImagePull, ErrImageNeverPull, ErrInvalidImageName:
							return nil, fmt.Errorf("waiting on OCS Scanning Pod error: %s reason: %s", msg, reason)
						default:
							scanLogger.Info("Waiting on OCS Scanning Pod", logz.Msg(msg), logz.Reason(reason))
						}
					}
				}
			case corev1.PodFailed:
				scanLogger.Error("OCS Scanning Pod failed")

				if ns.deleteReportArtifact {
					scanLogger.Info("Deleting remaining chained configmaps")
					if err := manager.deleteChainedConfigmaps(ctx, targetNamespace); err != nil {
						return nil, fmt.Errorf("could not delete configmaps: %w", err)
					}
				}

				if pod.Status.Reason == podEvictedStatus {
					return nil, fmt.Errorf(
						"OCS Scanning pod evicted due to low resources. Please configure higher resource limits. "+
							"See: https://docs.gitlab.com/ee/user/clusters/agent/vulnerabilities.html#configure-scanner-resource-requirements. "+
							"Error: %s", pod.Status.Message)
				}

				if len(pod.Status.ContainerStatuses) != 1 {
					return nil, errors.New("OCS Scanning pod should have only one container")
				}
				if pod.Status.ContainerStatuses[0].State.Terminated != nil {
					return nil, manager.extractExitCodeError(pod.Status.ContainerStatuses[0].State.Terminated.ExitCode,
						pod.Status.ContainerStatuses[0].State.Terminated.Reason)
				}
				return nil, errors.New("OCS Scanning pod exited with an error. Could not retrieve an exit code")
			case corev1.PodSucceeded:
				scanLogger.Info("OCS Scanning Pod Succeeded")
				scanLogger.Info("Reading chained configmaps")
				// this defer is intentional
				defer func() { //nolint:gocritic
					if ns.deleteReportArtifact {
						scanLogger.Info("Deleting chained configmaps")
						if err := manager.deleteChainedConfigmaps(ctx, targetNamespace); err != nil {
							scanLogger.Error("Could not delete ConfigMaps", logz.Error(err))
						}
					}
				}()
				payload, trivyVersion, err := manager.readChainedConfigmaps(ctx, targetNamespace)
				if err != nil {
					return nil, fmt.Errorf("could not read chained ConfigMaps: %w", err)
				}

				payloads, err := manager.parseScanningPodPayload(payload, trivyVersion)
				if err != nil {
					return nil, fmt.Errorf("could not parse OCS scanning pod report: %w", err)
				}

				uuids, err = reporter.Transmit(ctx, payloads)
				if err != nil {
					return nil, fmt.Errorf("error transmitting vulnerability reports: %w", err)
				}
				scanLogger.Info("Transmitted vulnerabilities to Gitlab")
				return uuids, nil
			case corev1.PodRunning:
				// Pod is running, continue watching
				continue
			case corev1.PodUnknown:
				// Pod might still be running but unreachable, continue watching
				continue
			default:
				// Unhandled pod status, continue watching
				continue
			}
		}
	}
}
