// Copyright 2023 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package main

import (
	"fmt"
	"sort"
	"strings"

	"golang.org/x/net/context"
)

const (
	EmptyLabel           BugLabelType = ""
	SubsystemLabel       BugLabelType = "subsystems"
	PriorityLabel        BugLabelType = "prio"
	NoRemindersLabel     BugLabelType = "no-reminders"
	OriginLabel          BugLabelType = "origin"
	MissingBackportLabel BugLabelType = "missing-backport"
)

type BugPrio string

const (
	LowPrioBug    BugPrio = "low"
	NormalPrioBug BugPrio = "normal"
	HighPrioBug   BugPrio = "high"
)

type oneOf []string
type subsetOf []string
type trueFalse struct{}

func makeLabelSet(c context.Context, ns string) *labelSet {
	ret := map[BugLabelType]interface{}{
		PriorityLabel: oneOf([]string{
			string(LowPrioBug),
			string(NormalPrioBug),
			string(HighPrioBug),
		}),
		NoRemindersLabel:     trueFalse{},
		MissingBackportLabel: trueFalse{},
	}
	service := getSubsystemService(c, ns)
	if service != nil {
		names := []string{}
		for _, item := range service.List() {
			names = append(names, item.Name)
		}
		ret[SubsystemLabel] = subsetOf(names)
	}

	originLabels := []string{}
	for _, repo := range getKernelRepos(c, ns) {
		if repo.LabelIntroduced != "" {
			originLabels = append(originLabels, repo.LabelIntroduced)
		}
		if repo.LabelReached != "" {
			originLabels = append(originLabels, repo.LabelReached)
		}
	}

	if len(originLabels) > 0 {
		ret[OriginLabel] = subsetOf(originLabels)
	}

	return &labelSet{
		c:      c,
		ns:     ns,
		labels: ret,
	}
}

type labelSet struct {
	c      context.Context
	ns     string
	labels map[BugLabelType]interface{}
}

func (s *labelSet) FindLabel(label BugLabelType) bool {
	_, ok := s.labels[label]
	return ok
}

func (s *labelSet) ValidateValues(label BugLabelType, values []BugLabel) string {
	rules := s.labels[label]
	if rules == nil {
		return ""
	}
	switch v := rules.(type) {
	case oneOf:
		if len(values) != 1 {
			return "You must specify only one of the allowed values"
		}
		if !stringInList([]string(v), values[0].Value) {
			return fmt.Sprintf("%q is not among the allowed values", values[0].Value)
		}
	case subsetOf:
		for _, item := range values {
			if !stringInList([]string(v), item.Value) {
				return fmt.Sprintf("%q is not among the allowed values", item.Value)
			}
		}
	case trueFalse:
		if len(values) != 1 || values[0].Value != "" {
			return "This label does not support any values"
		}
	}
	return ""
}

func (s *labelSet) Help() string {
	var sortedKeys []BugLabelType
	for key := range s.labels {
		sortedKeys = append(sortedKeys, key)
	}
	sort.Slice(sortedKeys, func(i, j int) bool {
		return string(sortedKeys[i]) < string(sortedKeys[j])
	})

	var help strings.Builder
	for _, key := range sortedKeys {
		if help.Len() > 0 {
			help.WriteString(", ")
		}
		if key == SubsystemLabel {
			help.WriteString(fmt.Sprintf("%s: {.. see below ..}", key))
			continue
		}
		switch v := s.labels[key].(type) {
		case oneOf:
			help.WriteString(string(key))
			help.WriteString(": {")
			list := []string(v)
			for i := range list {
				if i > 0 {
					help.WriteString(", ")
				}
				help.WriteString(list[i])
			}
			help.WriteByte('}')
		case trueFalse:
			help.WriteString(string(key))
		}
	}

	var sb strings.Builder
	writeWrapped(&sb, help.String())
	if _, ok := s.labels[SubsystemLabel]; ok {
		url := subsystemListURL(s.c, s.ns)
		if url != "" {
			sb.WriteString(fmt.Sprintf("\nThe list of subsystems: %s", url))
		}
	}
	return sb.String()
}

func writeWrapped(sb *strings.Builder, str string) {
	const limit = 80
	lineLen := 0
	for _, token := range strings.Fields(str) {
		if lineLen >= limit ||
			lineLen > 0 && lineLen+len(token) >= limit {
			sb.WriteByte('\n')
			lineLen = 0
		}
		if lineLen > 0 {
			sb.WriteString(" ")
		}
		sb.WriteString(token)
		lineLen += len(token)
	}
}

func (bug *Bug) HasLabel(label BugLabelType, value string) bool {
	for _, item := range bug.Labels {
		if item.Label == label && item.Value == value {
			return true
		}
	}
	return false
}

func (bug *Bug) LabelValues(label BugLabelType) []BugLabel {
	var ret []BugLabel
	for _, item := range bug.Labels {
		if item.Label == label {
			ret = append(ret, item)
		}
	}
	return ret
}

func (bug *Bug) SetLabels(set *labelSet, values []BugLabel) error {
	var label BugLabelType
	for _, v := range values {
		if label != EmptyLabel && label != v.Label {
			return fmt.Errorf("values of the same label type are expected")
		}
		label = v.Label
	}
	if errStr := set.ValidateValues(label, values); errStr != "" {
		return fmt.Errorf("%s", errStr)
	}
	bug.UnsetLabels(label)
	bug.Labels = append(bug.Labels, values...)
	return nil
}

func (bug *Bug) UnsetLabels(labels ...BugLabelType) map[BugLabelType]struct{} {
	toDelete := map[BugLabelType]struct{}{}
	notFound := map[BugLabelType]struct{}{}
	for _, name := range labels {
		toDelete[name] = struct{}{}
		notFound[name] = struct{}{}
	}
	var newList []BugLabel
	for _, item := range bug.Labels {
		if _, ok := toDelete[item.Label]; ok {
			delete(notFound, item.Label)
			continue
		}
		newList = append(newList, item)
	}
	bug.Labels = newList
	return notFound
}

func (bug *Bug) HasUserLabel(label BugLabelType) bool {
	for _, item := range bug.Labels {
		if item.Label == label && item.SetBy != "" {
			return true
		}
	}
	return false
}

func (bug *Bug) prio() BugPrio {
	for _, label := range bug.LabelValues(PriorityLabel) {
		return BugPrio(label.Value)
	}
	return NormalPrioBug
}

var bugPrioOrder = map[BugPrio]int{
	LowPrioBug:    1,
	NormalPrioBug: 2,
	HighPrioBug:   3,
}

func (bp BugPrio) LessThan(other BugPrio) bool {
	return bugPrioOrder[bp] < bugPrioOrder[other]
}
