package usecases

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"sync"

	"git.cryptic.systems/volker.raschek/civ/pkg/domain"
	"github.com/Masterminds/semver/v3"
)

// ContainerRuntime is an interface for different container runtimes to return labels
// based on their full qualified container image name. For example:
//
//   imageLabels, err := Load(ctx, "docker.io/library/alpine:latest")
//   imageLabels, err := Load(ctx, "docker.io/library/busybox:latest")
type ContainerRuntime interface {
	GetImageLabels(ctx context.Context, name string) (map[string]string, error)
}

type ConfigLoader interface {
	Load(ctx context.Context) (*domain.Config, error)
}

type LabelVerifier struct {
	config      *domain.Config
	labelLoader ContainerRuntime
	labelStore  *labelStore
}

// Run start the verification process based on the passed config.
func (lv *LabelVerifier) Run(ctx context.Context) error {
	if err := lv.fillLabelStore(ctx); err != nil {
		return err
	}

	wg := new(sync.WaitGroup)
	wg.Add(len(lv.config.Images))

	for image := range lv.config.Images {
		go func(image string) {
			defer func() { wg.Done() }()
			lv.runLabelConstraints(image)
		}(image)
	}

	wg.Wait()

	return nil
}

func (lv *LabelVerifier) runLabelConstraints(image string) {
	for labelKey, labelConstraint := range lv.config.Images[image].LabelConstraints {
		// fetch existing labels from store
		existingLabels := lv.labelStore.GetLabelsForImage(image)

		switch {
		case strings.HasPrefix(labelKey, "%") && strings.HasSuffix(labelKey, "%"):
			m := strings.TrimPrefix(strings.TrimSuffix(labelKey, "%"), "%")

			re, err := regexp.Compile(fmt.Sprintf("^.*%v.*$", m))
			if err != nil {
				labelConstraint.CountResultMessage = err.Error()
			} else {
				state := labelCount(re, labelConstraint.Count, existingLabels)
				labelConstraint.CountResult = &state
			}
		case strings.HasPrefix(labelKey, "%"):
			m := strings.TrimPrefix(labelKey, "%")

			re, err := regexp.Compile(fmt.Sprintf("^.*%v$", m))
			if err != nil {
				labelConstraint.CountResultMessage = err.Error()
			} else {
				state := labelCount(re, labelConstraint.Count, existingLabels)
				labelConstraint.CountResult = &state
			}
		case strings.HasSuffix(labelKey, "%"):
			m := strings.TrimSuffix(labelKey, "%")

			re, err := regexp.Compile(fmt.Sprintf("^%v.*$", m))
			if err != nil {
				labelConstraint.CountResultMessage = err.Error()
			} else {
				state := labelCount(re, labelConstraint.Count, existingLabels)
				labelConstraint.CountResult = &state
			}
		default:
			// labelExists
			if labelConstraint.Exists != nil {
				state := lv.labelExists(labelKey, existingLabels)
				labelConstraint.ExistsResult = state
				if state {
					labelConstraint.ExistsResultMessage = "Label found"
				} else {
					labelConstraint.ExistsResultMessage = "Label not found"
				}
			}

			// labelCompareSemver
			if labelConstraint.CompareSemver != nil {
				labelExistState := lv.labelExists(labelKey, existingLabels)
				if labelExistState {
					parsedSemVer, err := semver.NewVersion(existingLabels[labelKey])
					if err != nil {
						b := false
						labelConstraint.CompareSemverResult = &b
						labelConstraint.CompareSemverResultMessage = err.Error()
					}

					state := labelCompareSemver(labelConstraint.CompareSemver, parsedSemVer)
					labelConstraint.CompareSemverResult = &state
				} else {
					labelConstraint.CompareSemverResult = &labelExistState
					labelConstraint.CompareSemverResultMessage = "Label found"
				}
			}

			// labelCompareString
			if labelConstraint.CompareString != nil {
				state := labelCompareString(labelConstraint.CompareString, existingLabels[labelKey])
				labelConstraint.CompareStringResult = &state
			}
		}
	}
}

func labelCompareSemver(compareSemver *domain.LabelConstraintCompareSemver, parsedSemVer *semver.Version) bool {
	var majorState bool

	// Equal
	if compareSemver.Equal != "" {
		compareSemverEqualVersion, err := semver.NewVersion(compareSemver.Equal)
		if err != nil {
			compareSemver.EqualResultMessage = err.Error()
		} else {
			state := parsedSemVer.Equal(compareSemverEqualVersion)
			compareSemver.EqualResult = &state
			if state {
				compareSemver.EqualResultMessage = fmt.Sprintf("Version %s is equal to %s", parsedSemVer.String(), compareSemverEqualVersion.String())
			} else {
				compareSemver.EqualResultMessage = fmt.Sprintf("Version %s is not equal to %s", parsedSemVer.String(), compareSemverEqualVersion.String())
			}
		}
	}

	// GreaterThan
	if compareSemver.GreaterThan != "" {
		compareSemverGreaterThanVersion, err := semver.NewVersion(compareSemver.GreaterThan)
		if err != nil {
			compareSemver.GreaterThanResultMessage = err.Error()
		} else {
			state := parsedSemVer.GreaterThan(compareSemverGreaterThanVersion)
			compareSemver.GreaterThanResult = &state
			if state {
				compareSemver.GreaterThanResultMessage = fmt.Sprintf("Version %s is greater than %s", parsedSemVer.String(), compareSemverGreaterThanVersion.String())
			} else {
				compareSemver.GreaterThanResultMessage = fmt.Sprintf("Version %s is not greater than %s", parsedSemVer.String(), compareSemverGreaterThanVersion.String())
			}
		}
	}

	// LessThan
	if compareSemver.LessThan != "" {
		compareSemverLessThanVersion, err := semver.NewVersion(compareSemver.LessThan)
		if err != nil {
			compareSemver.LessThanResultMessage = err.Error()
		} else {
			state := parsedSemVer.LessThan(compareSemverLessThanVersion)
			compareSemver.LessThanResult = &state
			if state {
				compareSemver.LessThanResultMessage = fmt.Sprintf("Version %s is lower than %s", parsedSemVer.String(), compareSemverLessThanVersion.String())
			} else {
				compareSemver.LessThanResultMessage = fmt.Sprintf("Version %s is not lower than %s", parsedSemVer.String(), compareSemverLessThanVersion.String())
			}
		}
	}

	return majorState
}

func labelCompareString(compareString *domain.LabelConstraintCompareString, labelValue string) bool {
	var majorState bool = true

	// Equal
	if compareString.Equal != "" {
		state := compareString.Equal == labelValue
		if compareString.Equal == labelValue {
			compareString.EqualResult = &state
			compareString.EqualResultMessage = fmt.Sprintf("%s and %s are equal", labelValue, compareString.Equal)
		} else {
			compareString.EqualResult = &state
			compareString.EqualResultMessage = fmt.Sprintf("%s and %s are not equal", labelValue, compareString.Equal)
		}
	}

	// hasPrefix
	if compareString.HasPrefix != "" {
		state := strings.HasPrefix(labelValue, compareString.HasPrefix)
		if state {
			compareString.HasPrefixResult = &state
			compareString.HasPrefixResultMessage = fmt.Sprintf("%s has prefix %s", labelValue, compareString.HasPrefix)
		} else {
			compareString.HasPrefixResult = &state
			compareString.HasPrefixResultMessage = fmt.Sprintf("%s has not prefix %s", labelValue, compareString.HasPrefix)
		}
	}

	// hasSuffix
	if compareString.HasSuffix != "" {
		state := strings.HasSuffix(labelValue, compareString.HasSuffix)
		if state {
			compareString.HasSuffixResult = &state
			compareString.HasSuffixResultMessage = fmt.Sprintf("%s has suffix %s", labelValue, compareString.HasSuffix)
		} else {
			compareString.HasSuffixResult = &state
			compareString.HasSuffixResultMessage = fmt.Sprintf("%s has not suffix %s", labelValue, compareString.HasSuffix)
		}
	}

	return majorState
}

func labelCount(re *regexp.Regexp, labelConstraintCounter *domain.LabelConstraintCounter, labels map[string]string) bool {
	var majorState bool = true

	var i uint = 0
	for key := range labels {
		if re.MatchString(key) {
			i++
		}
	}

	switch {
	case labelConstraintCounter.Equal != nil:
		switch {
		case i == *labelConstraintCounter.Equal:
			state := true
			labelConstraintCounter.EqualResult = &state
		case i > *labelConstraintCounter.Equal:
			fallthrough
		case i < *labelConstraintCounter.Equal:
			state := false
			labelConstraintCounter.EqualResult = &state
			labelConstraintCounter.EqualResultMessage = fmt.Sprintf("%v is not equal %v", i, *labelConstraintCounter.Equal)
			majorState = false
		}
	case labelConstraintCounter.LessThan != nil:
		switch {
		case i < *labelConstraintCounter.LessThan:
			state := true
			labelConstraintCounter.LessThanResult = &state
		case i >= *labelConstraintCounter.LessThan:
			state := false
			labelConstraintCounter.LessThanResult = &state
			labelConstraintCounter.LessThanResultMessage = fmt.Sprintf("%v is not less than %v", i, *labelConstraintCounter.Equal)
			majorState = false
		}
	case labelConstraintCounter.GreaterThan != nil:
		switch {
		case i < *labelConstraintCounter.GreaterThan:
			state := true
			labelConstraintCounter.GreaterThanResult = &state
		case i >= *labelConstraintCounter.GreaterThan:
			state := false
			labelConstraintCounter.GreaterThanResult = &state
			labelConstraintCounter.GreaterThanResultMessage = fmt.Sprintf("%v is not greater than %v", i, *labelConstraintCounter.Equal)
			majorState = false
		}
	}

	return majorState
}

func (lv *LabelVerifier) labelExists(requiresLabelKey string, existingLabels map[string]string) bool {
	for existingLabelKey := range existingLabels {
		if existingLabelKey == requiresLabelKey {
			return true
		}
	}
	return false
}

// fillLabelStore fills the label store with the labels of the defined images
// from config.
func (lv *LabelVerifier) fillLabelStore(ctx context.Context) error {
	wg := new(sync.WaitGroup)
	wg.Add(len(lv.config.Images))

	errorChannel := make(chan error, len(lv.config.Images))

	for image := range lv.config.Images {
		go func(image string) {
			defer wg.Done()
			labels, err := lv.labelLoader.GetImageLabels(ctx, image)
			if err != nil {
				errorChannel <- err
				return
			}
			lv.labelStore.AddLabelsForImage(image, labels)
		}(image)
	}

	wg.Wait()
	close(errorChannel)

	for {
		err, open := <-errorChannel
		if err != nil {
			return err
		}
		if !open {
			break
		}
	}

	return nil
}

func NewLabelVerifier(config *domain.Config, labelLoader ContainerRuntime) (*LabelVerifier, error) {
	return &LabelVerifier{
		config:      config,
		labelLoader: labelLoader,
		labelStore:  newLabelStore(),
	}, nil
}

type labelStore struct {
	labels map[string]map[string]string
	mutex  *sync.RWMutex
}

func (ls *labelStore) AddLabelsForImage(image string, labels map[string]string) {
	ls.mutex.Lock()
	defer func() { ls.mutex.Unlock() }()
	ls.labels[image] = labels
}

func (ls *labelStore) GetLabelsForImage(image string) map[string]string {
	ls.mutex.RLock()
	defer func() { ls.mutex.RUnlock() }()
	return ls.labels[image]
}

func newLabelStore() *labelStore {
	return &labelStore{
		labels: make(map[string]map[string]string),
		mutex:  new(sync.RWMutex),
	}
}