// WinImageKit is a dependency-graph-ordered deployment recipe runner.
//
// Unlike a flat sequential runner, WinImageKit's manifest steps declare
// depends_on relationships. WinImageKit validates the resulting graph
// (rejecting unknown dependency references and dependency cycles),
// computes a topological execution order, and runs each step's
// check/install pair honoring that order. A failed step blocks only its
// transitive dependents -- unrelated branches of the graph keep running.
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"sort"
	"strings"
	"time"
)

const version = "0.1.0"

// ---------------------------------------------------------------------
// flag reordering (shared convention across the tool suite)
// ---------------------------------------------------------------------

func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

// ---------------------------------------------------------------------
// manifest types
// ---------------------------------------------------------------------

type Step struct {
	Name      string   `json:"name"`
	Check     string   `json:"check"`
	Install   string   `json:"install"`
	DependsOn []string `json:"depends_on"`
}

type Manifest struct {
	Name  string `json:"name"`
	Steps []Step `json:"steps"`
}

func loadManifest(path string) (*Manifest, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("reading manifest: %w", err)
	}
	var m Manifest
	if err := json.Unmarshal(data, &m); err != nil {
		return nil, fmt.Errorf("parsing manifest JSON: %w", err)
	}
	if len(m.Steps) == 0 {
		return nil, fmt.Errorf("manifest has no steps")
	}
	seen := map[string]bool{}
	for _, s := range m.Steps {
		if s.Name == "" {
			return nil, fmt.Errorf("manifest contains a step with no name")
		}
		if seen[s.Name] {
			return nil, fmt.Errorf("duplicate step name: %q", s.Name)
		}
		seen[s.Name] = true
	}
	return &m, nil
}

// ---------------------------------------------------------------------
// graph validation + topological sort
// ---------------------------------------------------------------------

// validateGraph checks that every depends_on reference resolves to a real
// step and that the dependency graph is acyclic. It returns a slice of
// human-readable problem descriptions; an empty slice means the graph is
// valid.
func validateGraph(m *Manifest) []string {
	var problems []string
	byName := map[string]Step{}
	for _, s := range m.Steps {
		byName[s.Name] = s
	}

	// 1. unknown dependency references
	for _, s := range m.Steps {
		for _, dep := range s.DependsOn {
			if _, ok := byName[dep]; !ok {
				problems = append(problems, fmt.Sprintf(
					"step %q depends on %q, which does not exist in this manifest", s.Name, dep))
			}
		}
	}
	if len(problems) > 0 {
		// Cycle detection over an incomplete/invalid graph is not
		// meaningful (and could reference missing nodes); report the
		// reference problems first, on their own.
		return problems
	}

	// 2. cycle detection via DFS with a 3-color scheme, reporting the
	//    actual cycle path when one is found.
	const (
		white = 0 // unvisited
		gray  = 1 // in progress (on current DFS stack)
		black = 2 // fully explored
	)
	color := map[string]int{}
	var stack []string
	var cyclePath []string

	var visit func(name string) bool
	visit = func(name string) bool {
		color[name] = gray
		stack = append(stack, name)
		for _, dep := range byName[name].DependsOn {
			switch color[dep] {
			case white:
				if visit(dep) {
					return true
				}
			case gray:
				// found a cycle: extract the loop portion of the stack
				start := 0
				for i, n := range stack {
					if n == dep {
						start = i
						break
					}
				}
				cyclePath = append([]string{}, stack[start:]...)
				cyclePath = append(cyclePath, dep)
				return true
			case black:
				// already fully explored, no cycle through here
			}
		}
		stack = stack[:len(stack)-1]
		color[name] = black
		return false
	}

	// visit in deterministic (declared) order so repeated runs report the
	// same cycle consistently
	for _, s := range m.Steps {
		if color[s.Name] == white {
			if visit(s.Name) {
				problems = append(problems, fmt.Sprintf(
					"dependency cycle detected: %s", strings.Join(cyclePath, " -> ")))
				return problems
			}
		}
	}

	return problems
}

// topoOrder computes a valid topological execution order via Kahn's
// algorithm. The graph must already be validated (no missing refs, no
// cycles) -- callers must call validateGraph first. Ties are broken by
// step name for deterministic, reproducible ordering.
func topoOrder(m *Manifest) []string {
	byName := map[string]Step{}
	indegree := map[string]int{}
	dependents := map[string][]string{} // dep -> steps that depend on it

	for _, s := range m.Steps {
		byName[s.Name] = s
		if _, ok := indegree[s.Name]; !ok {
			indegree[s.Name] = 0
		}
	}
	for _, s := range m.Steps {
		indegree[s.Name] += len(s.DependsOn)
		for _, dep := range s.DependsOn {
			dependents[dep] = append(dependents[dep], s.Name)
		}
	}

	var ready []string
	for name, deg := range indegree {
		if deg == 0 {
			ready = append(ready, name)
		}
	}
	sort.Strings(ready)

	var order []string
	for len(ready) > 0 {
		// pop smallest (deterministic)
		next := ready[0]
		ready = ready[1:]
		order = append(order, next)

		var newlyReady []string
		for _, dependent := range dependents[next] {
			indegree[dependent]--
			if indegree[dependent] == 0 {
				newlyReady = append(newlyReady, dependent)
			}
		}
		sort.Strings(newlyReady)
		ready = append(ready, newlyReady...)
		sort.Strings(ready)
	}

	return order
}

// ---------------------------------------------------------------------
// execution
// ---------------------------------------------------------------------

func runShell(cmd string) error {
	var c *exec.Cmd
	if runtime.GOOS == "windows" {
		c = exec.Command("cmd", "/C", cmd)
	} else {
		c = exec.Command("sh", "-c", cmd)
	}
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	return c.Run()
}

type logEntry struct {
	Name      string `json:"name"`
	Verdict   string `json:"verdict"`
	Timestamp string `json:"timestamp"`
	DryRun    bool   `json:"dry_run"`
}

func writeLog(w *bufio.Writer, name, verdict string, dryRun bool) {
	if w == nil {
		return
	}
	e := logEntry{Name: name, Verdict: verdict, Timestamp: time.Now().UTC().Format(time.RFC3339), DryRun: dryRun}
	b, err := json.Marshal(e)
	if err != nil {
		return
	}
	w.Write(b)
	w.WriteByte('\n')
}

// apply executes the manifest's steps in dependency order. Returns true if
// every step that was attempted succeeded (SKIP or INSTALLED), false if any
// step FAILED or was BLOCKED.
func apply(m *Manifest, order []string, dryRun bool, logPath string) bool {
	byName := map[string]Step{}
	for _, s := range m.Steps {
		byName[s.Name] = s
	}

	var logWriter *bufio.Writer
	var logFile *os.File
	if logPath != "" {
		f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
		if err != nil {
			fmt.Fprintf(os.Stderr, "warning: could not open log file %s: %v\n", logPath, err)
		} else {
			logFile = f
			logWriter = bufio.NewWriter(f)
			defer func() {
				logWriter.Flush()
				logFile.Close()
			}()
		}
	}

	fmt.Printf("Execution order: %s\n", strings.Join(order, " -> "))
	if dryRun {
		fmt.Println("Mode: DRY RUN (no install commands will be executed; partial-failure blocking is not applied)")
	} else {
		fmt.Println("Mode: APPLY (failure blocks only dependents, not unrelated branches)")
	}
	fmt.Println()

	// status per step: "" (not yet processed), "skip", "installed",
	// "failed", "blocked"
	status := map[string]string{}
	overallOK := true

	for _, name := range order {
		s := byName[name]

		if !dryRun {
			// check whether any direct dependency is failed/blocked --
			// because we process in topo order, this correctly propagates
			// transitively (a blocked step's own dependents will see it
			// as blocked when they check their direct deps).
			blockedBy := ""
			for _, dep := range s.DependsOn {
				if status[dep] == "failed" || status[dep] == "blocked" {
					blockedBy = dep
					break
				}
			}
			if blockedBy != "" {
				status[name] = "blocked"
				overallOK = false
				fmt.Printf("[%s] BLOCKED (dependency %q did not succeed)\n", name, blockedBy)
				writeLog(logWriter, name, "BLOCKED", dryRun)
				continue
			}
		}

		checkErr := runShellQuiet(s.Check)
		if checkErr == nil {
			status[name] = "skip"
			fmt.Printf("[%s] SKIP (already satisfied)\n", name)
			writeLog(logWriter, name, "SKIP", dryRun)
			continue
		}

		if dryRun {
			fmt.Printf("[%s] WOULD INSTALL (check failed)\n", name)
			writeLog(logWriter, name, "WOULD_INSTALL", dryRun)
			continue
		}

		if err := runShell(s.Install); err != nil {
			status[name] = "failed"
			overallOK = false
			fmt.Printf("[%s] FAILED (%v)\n", name, err)
			writeLog(logWriter, name, "FAILED", dryRun)
			continue
		}

		status[name] = "installed"
		fmt.Printf("[%s] INSTALLED\n", name)
		writeLog(logWriter, name, "INSTALLED", dryRun)
	}

	return overallOK
}

func runShellQuiet(cmd string) error {
	var c *exec.Cmd
	if runtime.GOOS == "windows" {
		c = exec.Command("cmd", "/C", cmd)
	} else {
		c = exec.Command("sh", "-c", cmd)
	}
	return c.Run()
}

// ---------------------------------------------------------------------
// commands
// ---------------------------------------------------------------------

func usage() {
	fmt.Fprintf(os.Stderr, `WinImageKit %s -- dependency-graph-ordered deployment recipe runner

Usage:
  winimagekit apply <manifest.json> [--dry-run] [--log run.log]
  winimagekit validate <manifest.json>
  winimagekit -h | --help | help

Commands:
  apply       Validate the dependency graph, compute a topological
              execution order, then run each step's check/install pair.
              A failed step blocks only its (transitive) dependents;
              unrelated branches of the graph keep running.
  validate    Validate the dependency graph only (unknown dependency
              references, cycles) without executing anything.

Flags for apply:
  --dry-run       Report what would run, in dependency order, without
                  executing any install command and without applying
                  partial-failure blocking (nothing is really failing).
  --log <path>    Append a JSON-lines record per step outcome to <path>.
`, version)
}

func main() {
	if len(os.Args) < 2 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// one question the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}

	switch os.Args[1] {
	case "-h", "--help", "help":
		usage()
		os.Exit(0)
	case "apply":
		cmdApply(os.Args[2:])
	case "validate":
		cmdValidate(os.Args[2:])
	default:
		usage()
		os.Exit(1)
	}
}

func cmdValidate(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" {
			fmt.Println("Usage: winimagekit validate <manifest.json>")
			os.Exit(0)
		}
	}
	if len(args) < 1 {
		fmt.Fprintln(os.Stderr, "Usage: winimagekit validate <manifest.json>")
		os.Exit(1)
	}
	path := args[0]

	m, err := loadManifest(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	if !reportValidation(m) {
		os.Exit(1)
	}
}

// reportValidation prints the validation result for an already-loaded manifest
// and reports whether the graph is usable, leaving the caller to decide what
// that means. cmdValidate turns a false into exit status 1 for scripts; the
// guided session, which is the double-clicked-in-Explorer path, prints the same
// verdict and carries on to its "press Enter to close" prompt — exiting there
// would take the console window down with it, which is the very bug the guided
// session exists to fix.
func reportValidation(m *Manifest) bool {
	problems := validateGraph(m)
	if len(problems) > 0 {
		fmt.Printf("INVALID: manifest %q has %d problem(s):\n", m.Name, len(problems))
		for _, p := range problems {
			fmt.Printf("  - %s\n", p)
		}
		return false
	}

	order := topoOrder(m)
	fmt.Printf("OK: manifest %q (%d steps) has a valid dependency graph\n", m.Name, len(m.Steps))
	fmt.Printf("Execution order: %s\n", strings.Join(order, " -> "))
	return true
}

func cmdApply(args []string) {
	valueFlags := map[string]bool{"log": true}
	args = reorderFlags(args, valueFlags)

	var dryRun bool
	var logPath string
	var positional []string

	for i := 0; i < len(args); i++ {
		a := args[i]
		switch {
		case a == "-h" || a == "--help":
			fmt.Println("Usage: winimagekit apply <manifest.json> [--dry-run] [--log run.log]")
			os.Exit(0)
		case a == "--dry-run" || a == "-dry-run":
			dryRun = true
		case a == "--log" || a == "-log":
			if i+1 >= len(args) {
				fmt.Fprintln(os.Stderr, "error: --log requires a path argument")
				os.Exit(1)
			}
			i++
			logPath = args[i]
		case strings.HasPrefix(a, "-"):
			fmt.Fprintf(os.Stderr, "error: unknown flag %q\n", a)
			os.Exit(1)
		default:
			positional = append(positional, a)
		}
	}

	if len(positional) < 1 {
		fmt.Fprintln(os.Stderr, "Usage: winimagekit apply <manifest.json> [--dry-run] [--log run.log]")
		os.Exit(1)
	}
	path := positional[0]

	m, err := loadManifest(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	problems := validateGraph(m)
	if len(problems) > 0 {
		fmt.Fprintf(os.Stderr, "REFUSING TO RUN: manifest %q has %d problem(s):\n", m.Name, len(problems))
		for _, p := range problems {
			fmt.Fprintf(os.Stderr, "  - %s\n", p)
		}
		os.Exit(1)
	}

	order := topoOrder(m)
	fmt.Printf("Manifest %q: %d steps, dependency graph valid\n", m.Name, len(m.Steps))

	ok := apply(m, order, dryRun, logPath)
	if !ok {
		fmt.Fprintln(os.Stderr, "\nOne or more steps FAILED or were BLOCKED; see output above.")
		os.Exit(1)
	}
}
