package service

import (
	"fmt"
	"regexp"
	"strings"
	"unicode/utf8"

	"gopkg.in/yaml.v3"
)

// workflowYAMLDeclaresEvent reports whether a workflow has a structurally valid
// declaration for event. Event declarations written as a top-level scalar or
// sequence have no configuration node; mapping declarations must use either a
// null value or a mapping value. Invalid shapes fail closed.
func workflowYAMLDeclaresEvent(content []byte, event string) bool {
	node, found, valid := workflowYAMLEventConfiguration(content, event)
	if !valid || !found || node == nil {
		return valid && found
	}
	return workflowEventConfigurationIsNull(node) || node.Kind == yaml.MappingNode
}

// workflowYAMLMatchesMergeGroup evaluates the GitHub Actions merge_group
// trigger against both its activity and target branch. baseRef may be either a
// branch name or a refs/heads/* ref.
func workflowYAMLMatchesMergeGroup(content []byte, activity, baseRef string) bool {
	node, found, valid := workflowYAMLEventConfiguration(content, "merge_group")
	if !valid || !found {
		return false
	}
	if node == nil || workflowEventConfigurationIsNull(node) {
		return true
	}
	if node.Kind != yaml.MappingNode {
		return false
	}

	var types, branches, branchesIgnore []string
	seen := make(map[string]struct{}, len(node.Content)/2)
	for i := 0; i+1 < len(node.Content); i += 2 {
		key, ok := workflowYAMLResolveAlias(node.Content[i])
		if !ok {
			return false
		}
		if key.Kind != yaml.ScalarNode || key.Tag != "!!str" {
			return false
		}
		if _, duplicate := seen[key.Value]; duplicate {
			return false
		}
		seen[key.Value] = struct{}{}

		values, ok := workflowStringList(node.Content[i+1])
		if !ok || len(values) == 0 {
			return false
		}
		switch key.Value {
		case "types":
			types = values
		case "branches":
			branches = values
		case "branches-ignore":
			branchesIgnore = values
		default:
			return false
		}
	}

	if len(types) > 0 && !stringListContains(types, activity) {
		return false
	}
	if len(branches) > 0 && len(branchesIgnore) > 0 {
		return false
	}

	branch := strings.TrimPrefix(baseRef, "refs/heads/")
	if branch == "" && (len(branches) > 0 || len(branchesIgnore) > 0) {
		return false
	}
	if len(branches) > 0 {
		return workflowBranchPatternsMatch(branches, branch)
	}
	if len(branchesIgnore) > 0 {
		return workflowBranchIgnorePatternsMatch(branchesIgnore, branch)
	}
	return true
}

// workflowYAMLEventConfiguration returns the event's mapping value. A nil node
// with found=true represents the valid scalar/sequence shorthand. valid=false
// means the workflow trigger structure is malformed or ambiguous.
func workflowYAMLEventConfiguration(content []byte, event string) (node *yaml.Node, found, valid bool) {
	var root yaml.Node
	if yaml.Unmarshal(content, &root) != nil || len(root.Content) != 1 {
		return nil, false, false
	}
	doc, ok := workflowYAMLResolveAlias(root.Content[0])
	if !ok {
		return nil, false, false
	}
	if doc.Kind != yaml.MappingNode {
		return nil, false, false
	}

	var on *yaml.Node
	for i := 0; i+1 < len(doc.Content); i += 2 {
		key, ok := workflowYAMLResolveAlias(doc.Content[i])
		if !ok {
			return nil, false, false
		}
		if key.Kind != yaml.ScalarNode || key.Tag != "!!str" || key.Value != "on" {
			continue
		}
		if on != nil {
			return nil, false, false
		}
		on = doc.Content[i+1]
	}
	if on == nil {
		return nil, false, true
	}
	on, ok = workflowYAMLResolveAlias(on)
	if !ok {
		return nil, false, false
	}

	switch on.Kind {
	case yaml.ScalarNode:
		if on.Tag != "!!str" {
			return nil, false, false
		}
		return nil, on.Value == event, true
	case yaml.SequenceNode:
		for _, child := range on.Content {
			child, ok = workflowYAMLResolveAlias(child)
			if !ok {
				return nil, false, false
			}
			if child.Kind != yaml.ScalarNode || child.Tag != "!!str" {
				return nil, false, false
			}
			found = found || child.Value == event
		}
		return nil, found, true
	case yaml.MappingNode:
		for i := 0; i+1 < len(on.Content); i += 2 {
			key, ok := workflowYAMLResolveAlias(on.Content[i])
			if !ok {
				return nil, false, false
			}
			if key.Kind != yaml.ScalarNode || key.Tag != "!!str" {
				return nil, false, false
			}
			if key.Value != event {
				continue
			}
			if found {
				return nil, false, false
			}
			found = true
			node, ok = workflowYAMLResolveAlias(on.Content[i+1])
			if !ok {
				return nil, false, false
			}
		}
		return node, found, true
	default:
		return nil, false, false
	}
}

// workflowYAMLResolveAlias returns the concrete node represented by node.
// Alias chains are followed defensively so malformed cyclic aliases fail
// closed instead of hanging trigger evaluation.
func workflowYAMLResolveAlias(node *yaml.Node) (*yaml.Node, bool) {
	seen := make(map[*yaml.Node]struct{})
	for node != nil && node.Kind == yaml.AliasNode {
		if _, duplicate := seen[node]; duplicate || node.Alias == nil {
			return nil, false
		}
		seen[node] = struct{}{}
		node = node.Alias
	}
	return node, node != nil
}

func workflowEventConfigurationIsNull(node *yaml.Node) bool {
	return node != nil && node.Kind == yaml.ScalarNode && node.Tag == "!!null"
}

func workflowStringList(node *yaml.Node) ([]string, bool) {
	var ok bool
	node, ok = workflowYAMLResolveAlias(node)
	if !ok {
		return nil, false
	}
	switch node.Kind {
	case yaml.ScalarNode:
		if node.Tag != "!!str" || node.Value == "" {
			return nil, false
		}
		return []string{node.Value}, true
	case yaml.SequenceNode:
		values := make([]string, 0, len(node.Content))
		for _, child := range node.Content {
			child, ok = workflowYAMLResolveAlias(child)
			if !ok {
				return nil, false
			}
			if child.Kind != yaml.ScalarNode || child.Tag != "!!str" || child.Value == "" {
				return nil, false
			}
			values = append(values, child.Value)
		}
		return values, true
	default:
		return nil, false
	}
}

func stringListContains(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func workflowBranchPatternsMatch(patterns []string, branch string) bool {
	matched := false
	hasPositive := false
	for _, pattern := range patterns {
		negative := strings.HasPrefix(pattern, "!")
		if negative {
			pattern = strings.TrimPrefix(pattern, "!")
		} else {
			hasPositive = true
		}
		patternMatched, err := workflowBranchPatternMatch(pattern, branch)
		if err != nil {
			return false
		}
		if patternMatched {
			matched = !negative
		}
	}
	return hasPositive && matched
}

func workflowBranchIgnorePatternsMatch(patterns []string, branch string) bool {
	for _, pattern := range patterns {
		if strings.HasPrefix(pattern, "!") {
			return false
		}
		matched, err := workflowBranchPatternMatch(pattern, branch)
		if err != nil {
			return false
		}
		if matched {
			return false
		}
	}
	return true
}

// workflowBranchPatternMatch implements the filter-pattern syntax documented
// by GitHub Actions. '*' does not cross '/', '**' does, '?' and '+' quantify
// the preceding atom, and bracket classes are limited to ASCII alphanumerics
// and their ranges.
func workflowBranchPatternMatch(pattern, branch string) (bool, error) {
	if !utf8.ValidString(pattern) {
		return false, fmt.Errorf("pattern is not valid UTF-8")
	}
	type atom struct {
		expression   string
		quantifiable bool
	}
	atoms := make([]atom, 0, len(pattern))
	for i := 0; i < len(pattern); {
		switch pattern[i] {
		case '\\':
			if i+1 >= len(pattern) {
				return false, fmt.Errorf("trailing escape")
			}
			literal, size := utf8.DecodeRuneInString(pattern[i+1:])
			atoms = append(atoms, atom{expression: regexp.QuoteMeta(string(literal)), quantifiable: true})
			i += 1 + size
		case '*':
			if i+1 < len(pattern) && pattern[i+1] == '*' {
				i += 2
				for i < len(pattern) && pattern[i] == '*' {
					i++
				}
				if i < len(pattern) && pattern[i] == '/' {
					atoms = append(atoms, atom{expression: "(?:.*/)?"})
					i++
				} else {
					atoms = append(atoms, atom{expression: ".*"})
				}
			} else {
				atoms = append(atoms, atom{expression: "[^/]*"})
				i++
			}
		case '?', '+':
			if len(atoms) == 0 || !atoms[len(atoms)-1].quantifiable {
				return false, fmt.Errorf("%c has no preceding atom", pattern[i])
			}
			quantifier := string(pattern[i])
			atoms[len(atoms)-1] = atom{expression: "(?:" + atoms[len(atoms)-1].expression + ")" + quantifier}
			i++
		case '[':
			end := strings.IndexByte(pattern[i+1:], ']')
			if end < 0 {
				return false, fmt.Errorf("unclosed character class")
			}
			end += i + 1
			class := pattern[i+1 : end]
			if !validWorkflowCharacterClass(class) {
				return false, fmt.Errorf("invalid character class")
			}
			atoms = append(atoms, atom{expression: "[" + class + "]", quantifiable: true})
			i = end + 1
		default:
			literal, size := utf8.DecodeRuneInString(pattern[i:])
			atoms = append(atoms, atom{expression: regexp.QuoteMeta(string(literal)), quantifiable: true})
			i += size
		}
	}

	var expression strings.Builder
	expression.WriteByte('^')
	for _, token := range atoms {
		expression.WriteString(token.expression)
	}
	expression.WriteByte('$')
	compiled, err := regexp.Compile(expression.String())
	if err != nil {
		return false, err
	}
	return compiled.MatchString(branch), nil
}

func validWorkflowCharacterClass(class string) bool {
	if class == "" {
		return false
	}
	for i := 0; i < len(class); i++ {
		if isASCIIAlphanumeric(class[i]) {
			continue
		}
		if class[i] != '-' || i == 0 || i+1 >= len(class) ||
			!sameASCIIAlphanumericRange(class[i-1], class[i+1]) || class[i-1] > class[i+1] {
			return false
		}
	}
	return true
}

func isASCIIAlphanumeric(value byte) bool {
	return value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || value >= '0' && value <= '9'
}

func sameASCIIAlphanumericRange(left, right byte) bool {
	return left >= 'a' && left <= 'z' && right >= 'a' && right <= 'z' ||
		left >= 'A' && left <= 'Z' && right >= 'A' && right <= 'Z' ||
		left >= '0' && left <= '9' && right >= '0' && right <= '9'
}
