package ci

import "fmt"

// dagNode holds a job name and its resolved dependencies.
type dagNode struct {
	name  string
	needs []string
}

// TopoSort returns the job names in a valid topological execution order.
// Returns an error if the dependency graph has cycles or references unknown jobs.
func TopoSort(jobs map[string]WorkflowJob) ([]string, error) {
	nodes := make(map[string]*dagNode, len(jobs))
	for name, job := range jobs {
		nodes[name] = &dagNode{name: name, needs: []string(job.Needs)}
	}
	// Validate all dependencies exist.
	for _, node := range nodes {
		for _, dep := range node.needs {
			if _, ok := nodes[dep]; !ok {
				return nil, fmt.Errorf("job %q depends on unknown job %q", node.name, dep)
			}
		}
	}

	var order []string
	visited := make(map[string]bool, len(nodes))
	inStack := make(map[string]bool, len(nodes))

	var visit func(name string) error
	visit = func(name string) error {
		if inStack[name] {
			return fmt.Errorf("cycle detected at job %q", name)
		}
		if visited[name] {
			return nil
		}
		inStack[name] = true
		for _, dep := range nodes[name].needs {
			if err := visit(dep); err != nil {
				return err
			}
		}
		inStack[name] = false
		visited[name] = true
		order = append(order, name)
		return nil
	}

	for name := range nodes {
		if err := visit(name); err != nil {
			return nil, err
		}
	}
	return order, nil
}

// ReadyJobs returns the names of jobs whose dependencies are all in completedJobs.
func ReadyJobs(jobs map[string]WorkflowJob, completedJobs map[string]bool, enqueuedJobs map[string]bool) []string {
	var ready []string
	for name, job := range jobs {
		if enqueuedJobs[name] {
			continue
		}
		allDone := true
		for _, dep := range job.Needs {
			if !completedJobs[dep] {
				allDone = false
				break
			}
		}
		if allDone {
			ready = append(ready, name)
		}
	}
	return ready
}
