// Package requirements provides aggregation and derivation of skill requirements.
package requirements

import (
	"sort"

	"github.com/initializ/forge/forge-skills/contract"
	"gopkg.in/yaml.v3"
)

// AggregateRequirements merges requirements from all entries that have ForgeReqs set.
//
// Promotion rules:
//   - var in both required (skill A) and optional (skill B) → required
//   - var in one_of (skill A) and required (skill B) → stays in required (group still exists)
//   - one_of groups kept separate per skill
func AggregateRequirements(entries []contract.SkillEntry) *contract.AggregatedRequirements {
	binSet := make(map[string]bool)
	reqSet := make(map[string]bool)
	optSet := make(map[string]bool)
	deniedSet := make(map[string]bool)
	egressSet := make(map[string]bool)
	phaseSet := make(map[string]bool)
	var oneOfGroups [][]string

	var denyCommands []contract.SkillCommandFilter
	var denyOutput []contract.SkillOutputFilter
	var denyPrompts []contract.SkillCommandFilter
	var denyResponses []contract.SkillCommandFilter
	cmdPatternSeen := make(map[string]bool)
	outPatternSeen := make(map[string]bool)
	promptPatternSeen := make(map[string]bool)
	responsePatternSeen := make(map[string]bool)

	binReqMap := make(map[string]contract.BinRequirement)

	for _, e := range entries {
		// Collect forge-level metadata (denied_tools, egress_domains, guardrails)
		if e.Metadata != nil && e.Metadata.Metadata != nil {
			if forgeMap, ok := e.Metadata.Metadata["forge"]; ok {
				if raw, ok := forgeMap["denied_tools"]; ok {
					if arr, ok := raw.([]any); ok {
						for _, v := range arr {
							if s, ok := v.(string); ok {
								deniedSet[s] = true
							}
						}
					}
				}
				if raw, ok := forgeMap["egress_domains"]; ok {
					if arr, ok := raw.([]any); ok {
						for _, v := range arr {
							if s, ok := v.(string); ok {
								egressSet[s] = true
							}
						}
					}
				}
				if raw, ok := forgeMap["workflow_phase"]; ok {
					if s, ok := raw.(string); ok && s != "" {
						phaseSet[s] = true
					}
				}
				if raw, ok := forgeMap["guardrails"]; ok {
					// Re-marshal to yaml, unmarshal into SkillGuardrailConfig
					data, err := yaml.Marshal(raw)
					if err == nil {
						var gc contract.SkillGuardrailConfig
						if err := yaml.Unmarshal(data, &gc); err == nil {
							for _, c := range gc.DenyCommands {
								if !cmdPatternSeen[c.Pattern] {
									cmdPatternSeen[c.Pattern] = true
									denyCommands = append(denyCommands, c)
								}
							}
							for _, o := range gc.DenyOutput {
								if !outPatternSeen[o.Pattern] {
									outPatternSeen[o.Pattern] = true
									denyOutput = append(denyOutput, o)
								}
							}
							for _, p := range gc.DenyPrompts {
								if !promptPatternSeen[p.Pattern] {
									promptPatternSeen[p.Pattern] = true
									denyPrompts = append(denyPrompts, p)
								}
							}
							for _, r := range gc.DenyResponses {
								if !responsePatternSeen[r.Pattern] {
									responsePatternSeen[r.Pattern] = true
									denyResponses = append(denyResponses, r)
								}
							}
						}
					}
				}
			}
		}

		if e.ForgeReqs == nil {
			continue
		}
		for _, b := range e.ForgeReqs.Bins {
			binSet[b.Name] = true
			// Keep richer entry: one with more fields set wins
			if existing, ok := binReqMap[b.Name]; !ok || isRicher(b, existing) {
				binReqMap[b.Name] = b
			}
		}
		if e.ForgeReqs.Env != nil {
			for _, v := range e.ForgeReqs.Env.Required {
				reqSet[v] = true
			}
			if len(e.ForgeReqs.Env.OneOf) > 0 {
				oneOfGroups = append(oneOfGroups, e.ForgeReqs.Env.OneOf)
			}
			for _, v := range e.ForgeReqs.Env.Optional {
				optSet[v] = true
			}
		}
	}

	// Promotion: optional vars that appear in required get promoted
	for v := range optSet {
		if reqSet[v] {
			delete(optSet, v)
		}
	}

	// Build sorted BinRequirements from the map
	sortedBins := sortedKeys(binSet)
	binReqs := make([]contract.BinRequirement, 0, len(sortedBins))
	for _, name := range sortedBins {
		if br, ok := binReqMap[name]; ok {
			binReqs = append(binReqs, br)
		} else {
			binReqs = append(binReqs, contract.BinRequirement{Name: name})
		}
	}

	agg := &contract.AggregatedRequirements{
		Bins:            sortedBins,
		BinRequirements: binReqs,
		EnvOneOf:        oneOfGroups,
		DeniedTools:     sortedKeys(deniedSet),
		EgressDomains:   sortedKeys(egressSet),
		WorkflowPhases:  sortedKeys(phaseSet),
	}
	agg.EnvRequired = sortedKeys(reqSet)
	agg.EnvOptional = sortedKeys(optSet)

	if len(denyCommands) > 0 || len(denyOutput) > 0 || len(denyPrompts) > 0 || len(denyResponses) > 0 {
		agg.SkillGuardrails = &contract.SkillGuardrailConfig{
			DenyCommands:  denyCommands,
			DenyOutput:    denyOutput,
			DenyPrompts:   denyPrompts,
			DenyResponses: denyResponses,
		}
	}

	return agg
}

// AggregateDescriptorRequirements computes the maximum TimeoutHint across descriptors.
func AggregateDescriptorRequirements(descs []contract.SkillDescriptor) int {
	maxTimeout := 0
	for _, d := range descs {
		if d.TimeoutHint > maxTimeout {
			maxTimeout = d.TimeoutHint
		}
	}
	return maxTimeout
}

// isRicher returns true if a has more install metadata than b.
func isRicher(a, b contract.BinRequirement) bool {
	countFields := func(br contract.BinRequirement) int {
		n := 0
		if br.Version != "" {
			n++
		}
		if br.AptPackage != "" {
			n++
		}
		if br.ApkPackage != "" {
			n++
		}
		if br.DirectURL != "" {
			n++
		}
		if len(br.CustomLines) > 0 {
			n++
		}
		return n
	}
	return countFields(a) > countFields(b)
}

func sortedKeys(m map[string]bool) []string {
	if len(m) == 0 {
		return nil
	}
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}
