// Package iac is a miniature Infrastructure-as-Code engine — the model
// behind Terraform/Pulumi: declarative resources with dependencies (a
// DAG), a PLAN that diffs desired vs current state, an APPLY that
// converges in dependency order, persistent STATE, idempotency (re-apply
// is a no-op), and DRIFT detection (the world changed out of band).
// Stdlib-only and deterministic.
package iac

import (
	"fmt"
	"sort"
)

// Resource is a declared piece of infrastructure. DependsOn names other
// resources that must exist first (the edges of the dependency DAG).
type Resource struct {
	Name      string
	Type      string
	Attrs     map[string]string
	DependsOn []string
}

// State is the last-applied set of resources, keyed by name (Terraform's
// state file).
type State map[string]Resource

// Action is what Apply will do to a resource.
type Action int

const (
	NoOp Action = iota
	Create
	Update
	Delete
)

func (a Action) String() string { return [...]string{"no-op", "create", "update", "delete"}[a] }

// Change is one planned/applied action.
type Change struct {
	Action Action
	Name   string
	Diff   string // human-readable diff for Update
}

// Plan diffs desired config against current state, returning the changes
// needed (create/update/delete/no-op) in dependency order. It errors on a
// missing dependency or a dependency cycle.
func Plan(desired []Resource, state State) ([]Change, error) {
	byName := map[string]Resource{}
	for _, r := range desired {
		byName[r.Name] = r
	}
	// Validate dependencies exist.
	for _, r := range desired {
		for _, d := range r.DependsOn {
			if _, ok := byName[d]; !ok {
				return nil, fmt.Errorf("iac: resource %q depends on undefined %q", r.Name, d)
			}
		}
	}
	order, err := topoSort(desired)
	if err != nil {
		return nil, err
	}

	var changes []Change
	// creates/updates in dependency order (deps first)
	for _, name := range order {
		want := byName[name]
		cur, exists := state[name]
		switch {
		case !exists:
			changes = append(changes, Change{Action: Create, Name: name})
		case !equal(cur, want):
			changes = append(changes, Change{Action: Update, Name: name, Diff: diff(cur, want)})
		default:
			changes = append(changes, Change{Action: NoOp, Name: name})
		}
	}
	// deletes (in state, not desired) in REVERSE dependency order
	var dels []string
	for name := range state {
		if _, ok := byName[name]; !ok {
			dels = append(dels, name)
		}
	}
	sort.Sort(sort.Reverse(sort.StringSlice(dels)))
	for _, name := range dels {
		changes = append(changes, Change{Action: Delete, Name: name})
	}
	return changes, nil
}

// Apply runs the plan and returns the changes applied plus the new state.
// Applying the same desired config twice is a no-op (idempotent).
func Apply(desired []Resource, state State) ([]Change, State, error) {
	changes, err := Plan(desired, state)
	if err != nil {
		return nil, state, err
	}
	byName := map[string]Resource{}
	for _, r := range desired {
		byName[r.Name] = r
	}
	next := State{}
	for k, v := range state {
		next[k] = v
	}
	for _, c := range changes {
		switch c.Action {
		case Create, Update:
			next[c.Name] = byName[c.Name]
		case Delete:
			delete(next, c.Name)
		}
	}
	return changes, next, nil
}

// Drift reports resources where the actual world (live) differs from the
// last-applied state — someone changed infra out of band. These are what
// the next apply would "fix" (and what GitOps self-heal, pa-08, reverts).
func Drift(state, live State) []string {
	var out []string
	for name, want := range state {
		got, ok := live[name]
		if !ok || !equal(got, want) {
			out = append(out, name)
		}
	}
	for name := range live {
		if _, ok := state[name]; !ok {
			out = append(out, name) // an unmanaged resource appeared
		}
	}
	sort.Strings(out)
	return out
}

// topoSort orders resources so dependencies come first (Kahn's
// algorithm). Errors on a cycle.
func topoSort(resources []Resource) ([]string, error) {
	deps := map[string][]string{}
	indeg := map[string]int{}
	for _, r := range resources {
		if _, ok := indeg[r.Name]; !ok {
			indeg[r.Name] = 0
		}
		for _, d := range r.DependsOn {
			deps[d] = append(deps[d], r.Name) // edge d -> r (d before r)
			indeg[r.Name]++
		}
	}
	// deterministic: process ready nodes in sorted order
	var ready []string
	for n, d := range indeg {
		if d == 0 {
			ready = append(ready, n)
		}
	}
	sort.Strings(ready)
	var order []string
	for len(ready) > 0 {
		n := ready[0]
		ready = ready[1:]
		order = append(order, n)
		var newly []string
		for _, m := range deps[n] {
			indeg[m]--
			if indeg[m] == 0 {
				newly = append(newly, m)
			}
		}
		sort.Strings(newly)
		ready = append(ready, newly...)
		sort.Strings(ready)
	}
	if len(order) != len(indeg) {
		return nil, fmt.Errorf("iac: dependency cycle detected")
	}
	return order, nil
}

func equal(a, b Resource) bool {
	if a.Type != b.Type || len(a.Attrs) != len(b.Attrs) {
		return false
	}
	for k, v := range a.Attrs {
		if b.Attrs[k] != v {
			return false
		}
	}
	return true
}

func diff(before, after Resource) string {
	var b []string
	keys := map[string]bool{}
	for k := range before.Attrs {
		keys[k] = true
	}
	for k := range after.Attrs {
		keys[k] = true
	}
	var ks []string
	for k := range keys {
		ks = append(ks, k)
	}
	sort.Strings(ks)
	for _, k := range ks {
		if before.Attrs[k] != after.Attrs[k] {
			b = append(b, fmt.Sprintf("%s: %q -> %q", k, before.Attrs[k], after.Attrs[k]))
		}
	}
	return fmt.Sprintf("%v", b)
}
