// Package fitness implements "fitness functions" — automated architecture
// tests an architect puts in CI so the design's key properties (no
// dependency cycles, layering rules, coupling limits) are enforced
// mechanically instead of policed by hand in reviews. This is
// "evolutionary architecture" (Ford et al.) made concrete: architecture
// as a continuously-tested property. Stdlib-only.
package fitness

import "sort"

// Graph is a module/service dependency graph (A->B = "A depends on B"),
// each node tagged with a layer.
type Graph struct {
	deps  map[string][]string
	layer map[string]string
	nodes map[string]bool
}

func NewGraph() *Graph {
	return &Graph{deps: map[string][]string{}, layer: map[string]string{}, nodes: map[string]bool{}}
}

func (g *Graph) Add(from, to string) {
	g.nodes[from], g.nodes[to] = true, true
	g.deps[from] = append(g.deps[from], to)
}
func (g *Graph) SetLayer(node, layer string) { g.nodes[node] = true; g.layer[node] = layer }
func (g *Graph) FanOut(node string) int      { return len(g.deps[node]) }

func (g *Graph) sortedNodes() []string {
	out := make([]string, 0, len(g.nodes))
	for n := range g.nodes {
		out = append(out, n)
	}
	sort.Strings(out)
	return out
}

// hasCycle returns whether a dependency cycle exists, via DFS coloring.
func (g *Graph) hasCycle() bool {
	const (
		white = 0
		gray  = 1
		black = 2
	)
	color := map[string]int{}
	var visit func(string) bool
	visit = func(n string) bool {
		color[n] = gray
		deps := append([]string(nil), g.deps[n]...)
		sort.Strings(deps)
		for _, m := range deps {
			if color[m] == gray {
				return true // back-edge -> cycle
			}
			if color[m] == white && visit(m) {
				return true
			}
		}
		color[n] = black
		return false
	}
	for _, n := range g.sortedNodes() {
		if color[n] == white && visit(n) {
			return true
		}
	}
	return false
}

// Rule is one architecture constraint. Check returns violation messages
// (empty == the rule holds).
type Rule interface {
	Name() string
	Check(*Graph) []string
}

// NoCycles forbids any dependency cycle (the distributed-monolith smell).
type NoCycles struct{}

func (NoCycles) Name() string { return "no-cycles" }
func (NoCycles) Check(g *Graph) []string {
	if g.hasCycle() {
		return []string{"dependency cycle detected (services cannot deploy independently)"}
	}
	return nil
}

// LayerRule forbids a layer from depending on another layer.
type LayerRule struct{ From, To string }

// Layering enforces a set of layer rules.
type Layering struct{ Rules []LayerRule }

func (Layering) Name() string { return "layering" }
func (l Layering) Check(g *Graph) []string {
	var out []string
	for _, from := range g.sortedNodes() {
		deps := append([]string(nil), g.deps[from]...)
		sort.Strings(deps)
		for _, to := range deps {
			for _, r := range l.Rules {
				if g.layer[from] == r.From && g.layer[to] == r.To {
					out = append(out, from+" ("+r.From+") -> "+to+" ("+r.To+") violates layering")
				}
			}
		}
	}
	return out
}

// MaxFanOut caps how many dependencies a single node may have (a coupling
// budget).
type MaxFanOut struct{ Limit int }

func (MaxFanOut) Name() string { return "max-fan-out" }
func (m MaxFanOut) Check(g *Graph) []string {
	var out []string
	for _, n := range g.sortedNodes() {
		if g.FanOut(n) > m.Limit {
			out = append(out, n+" has fan-out "+itoa(g.FanOut(n))+" > "+itoa(m.Limit))
		}
	}
	return out
}

// Report is the aggregate result of running fitness functions.
type Report struct {
	Passed     bool
	Violations map[string][]string // rule name -> violations
}

// Evaluate runs all rules and returns a report; Passed is false if ANY
// rule is violated. Wire this into a CI test (TestArchitecture) so the
// build fails when the architecture regresses.
func Evaluate(g *Graph, rules []Rule) Report {
	rep := Report{Passed: true, Violations: map[string][]string{}}
	for _, r := range rules {
		if v := r.Check(g); len(v) > 0 {
			rep.Violations[r.Name()] = v
			rep.Passed = false
		}
	}
	return rep
}

func itoa(i int) string {
	if i == 0 {
		return "0"
	}
	neg := i < 0
	if neg {
		i = -i
	}
	var b [12]byte
	p := len(b)
	for i > 0 {
		p--
		b[p] = byte('0' + i%10)
		i /= 10
	}
	if neg {
		p--
		b[p] = '-'
	}
	return string(b[p:])
}
