package dsl

import (
	"strings"
)

// PushEvent carries the normalized subset of a repository branch-update event
// used by MeshX trigger matching.
type PushEvent struct {
	Ref               string
	ChangedPaths      []string
	ChangedPathsKnown bool
}

// HasTrigger reports whether a AgentTeam Automation `on` value includes name in the
// accepted string, list, or map forms.
func HasTrigger(on any, name string) bool {
	_, ok := triggerPayload(on, name)
	return ok
}

// HasManualTrigger reports whether a AgentTeam Automation `on` value
// includes manual in the accepted string, list, or map forms.
func HasManualTrigger(on any) bool {
	return HasTrigger(on, "manual")
}

// HasScheduleTrigger reports whether a AgentTeam Automation `on` value includes
// schedule in the accepted string, list, or map forms.
func HasScheduleTrigger(on any) bool {
	return HasTrigger(on, "schedule")
}

// HasPushTrigger reports whether a AgentTeam Automation `on` value includes push in
// the accepted string, list, or map forms.
func HasPushTrigger(on any) bool {
	return HasTrigger(on, "push")
}

// PushPathFiltersPresent reports whether the push trigger uses path filters.
func PushPathFiltersPresent(on any) bool {
	payload, ok := triggerPayload(on, "push")
	if !ok {
		return false
	}
	options := triggerOptions(payload)
	return len(triggerStringList(options["paths"])) > 0 || len(triggerStringList(options["paths-ignore"])) > 0
}

// PushBranchMatches reports whether a branch push matches exact push branch
// filters.
func PushBranchMatches(on any, branch string) bool {
	payload, ok := triggerPayload(on, "push")
	if !ok {
		return false
	}
	branch = pushBranchName(branch)
	if branch == "" {
		return false
	}
	options := triggerOptions(payload)
	branches := triggerStringList(options["branches"])
	if len(branches) > 0 && !containsExactBranch(branches, branch) {
		return false
	}
	return !containsExactBranch(triggerStringList(options["branches-ignore"]), branch)
}

// MatchesPushTrigger reports whether a normalized push event matches the MVP
// AgentTeam Automation push trigger subset. It supports string/list/map trigger
// forms plus branches, branches-ignore, paths, and paths-ignore.
func MatchesPushTrigger(on any, event PushEvent) bool {
	payload, ok := triggerPayload(on, "push")
	if !ok {
		return false
	}
	options := triggerOptions(payload)
	if !matchesPushBranchFilters(pushBranchName(event.Ref), options) {
		return false
	}
	return matchesPushPathFilters(event, options)
}

func triggerPayload(on any, name string) (any, bool) {
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, false
	}
	switch typed := on.(type) {
	case string:
		return nil, strings.TrimSpace(typed) == name
	case []any:
		for _, item := range typed {
			if _, ok := triggerPayload(item, name); ok {
				return nil, true
			}
		}
	case []string:
		for _, item := range typed {
			if strings.TrimSpace(item) == name {
				return nil, true
			}
		}
	case map[string]any:
		for event, payload := range typed {
			if strings.TrimSpace(event) == name {
				return payload, true
			}
		}
	case map[string]string:
		for event, payload := range typed {
			if strings.TrimSpace(event) == name {
				return payload, true
			}
		}
	case map[any]any:
		for event, payload := range typed {
			eventName, ok := event.(string)
			if ok && strings.TrimSpace(eventName) == name {
				return payload, true
			}
		}
	}
	return nil, false
}

func triggerOptions(payload any) map[string]any {
	switch typed := payload.(type) {
	case map[string]any:
		return typed
	case map[string]string:
		out := make(map[string]any, len(typed))
		for key, value := range typed {
			out[key] = value
		}
		return out
	case map[any]any:
		out := make(map[string]any, len(typed))
		for key, value := range typed {
			text, ok := key.(string)
			if ok {
				out[text] = value
			}
		}
		return out
	default:
		return nil
	}
}

func matchesPushBranchFilters(branch string, options map[string]any) bool {
	branches := triggerStringList(options["branches"])
	branchesIgnore := triggerStringList(options["branches-ignore"])
	if len(branches) == 0 && len(branchesIgnore) == 0 {
		return true
	}
	if branch == "" {
		return false
	}
	if len(branches) > 0 && !anyGlobMatch(branches, branch) {
		return false
	}
	if len(branchesIgnore) > 0 && anyGlobMatch(branchesIgnore, branch) {
		return false
	}
	return true
}

func matchesPushPathFilters(event PushEvent, options map[string]any) bool {
	paths := triggerStringList(options["paths"])
	pathsIgnore := triggerStringList(options["paths-ignore"])
	if len(paths) == 0 && len(pathsIgnore) == 0 {
		return true
	}
	if !event.ChangedPathsKnown {
		return false
	}
	if len(event.ChangedPaths) == 0 {
		return false
	}
	if len(paths) > 0 && !anyPathMatches(paths, event.ChangedPaths) {
		return false
	}
	if len(pathsIgnore) > 0 && allPathsMatch(pathsIgnore, event.ChangedPaths) {
		return false
	}
	return true
}

func triggerStringList(value any) []string {
	switch typed := value.(type) {
	case string:
		if trimmed := strings.TrimSpace(typed); trimmed != "" {
			return []string{trimmed}
		}
	case []string:
		return compactStrings(typed)
	case []any:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			text, ok := item.(string)
			if ok {
				out = append(out, text)
			}
		}
		return compactStrings(out)
	}
	return nil
}

func compactStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			out = append(out, trimmed)
		}
	}
	return out
}

func containsExactBranch(values []string, branch string) bool {
	branch = strings.TrimSpace(branch)
	for _, value := range values {
		if strings.TrimSpace(value) == branch {
			return true
		}
	}
	return false
}

func pushBranchName(ref string) string {
	ref = strings.TrimSpace(ref)
	if strings.HasPrefix(ref, "refs/heads/") {
		return strings.TrimPrefix(ref, "refs/heads/")
	}
	if strings.HasPrefix(ref, "refs/") {
		return ""
	}
	return ref
}

func anyPathMatches(patterns []string, paths []string) bool {
	for _, path := range paths {
		if anyGlobMatch(patterns, path) {
			return true
		}
	}
	return false
}

func allPathsMatch(patterns []string, paths []string) bool {
	for _, path := range paths {
		if !anyGlobMatch(patterns, path) {
			return false
		}
	}
	return true
}

func anyGlobMatch(patterns []string, value string) bool {
	for _, pattern := range patterns {
		if globMatch(pattern, value) {
			return true
		}
	}
	return false
}

func globMatch(pattern string, value string) bool {
	pattern = strings.TrimSpace(pattern)
	value = strings.TrimSpace(value)
	if pattern == "" || value == "" {
		return false
	}
	if pattern == value {
		return true
	}
	return globMatchAt(pattern, value, 0, 0)
}

func globMatchAt(pattern string, value string, patternIndex int, valueIndex int) bool {
	for patternIndex < len(pattern) {
		switch pattern[patternIndex] {
		case '*':
			if patternIndex+1 < len(pattern) && pattern[patternIndex+1] == '*' {
				patternIndex += 2
				if patternIndex == len(pattern) {
					return true
				}
				for valueIndex <= len(value) {
					if globMatchAt(pattern, value, patternIndex, valueIndex) {
						return true
					}
					if valueIndex == len(value) {
						break
					}
					valueIndex++
				}
				return false
			}
			patternIndex++
			if patternIndex == len(pattern) {
				return !strings.Contains(value[valueIndex:], "/")
			}
			for {
				if globMatchAt(pattern, value, patternIndex, valueIndex) {
					return true
				}
				if valueIndex == len(value) || value[valueIndex] == '/' {
					return false
				}
				valueIndex++
			}
		case '?':
			if valueIndex == len(value) || value[valueIndex] == '/' {
				return false
			}
			patternIndex++
			valueIndex++
		default:
			if valueIndex == len(value) || pattern[patternIndex] != value[valueIndex] {
				return false
			}
			patternIndex++
			valueIndex++
		}
	}
	return valueIndex == len(value)
}
