// Command bootforge is a checksum manifest generator and verifier for
// files and directory trees.
//
// PRODUCT SCOPE NOTE: BootForge's full product concept (see ../plan.md)
// is an ISO / boot-media workshop — mounting ISO images, writing bootable
// USB drives, and managing virtual drives. Those features all require
// privileged, OS-specific APIs (loopback/virtual-disk mounting, raw disk
// writes) that cannot be implemented portably in a dependency-free Go CLI
// that only uses the standard library. This prototype instead implements
// the part of "verify a boot image" that IS honestly portable and useful
// today: a SHA-256 checksum manifest generator and verifier for a single
// large file (such as an .iso) or a directory tree. This is the exact
// mechanism a real "verify this boot image wasn't corrupted in transit"
// feature would rely on under the hood.
//
// ISO mounting, USB writing, and virtual drives remain on the roadmap.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

const version = "0.1.0"

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()
		return
	case "manifest":
		cmdManifest(os.Args[2:])
	case "verify":
		cmdVerify(os.Args[2:])
	default:
		fmt.Fprintf(os.Stderr, "bootforge: unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprintf(os.Stderr, `BootForge %s — checksum manifest generator and verifier

Roadmap note: ISO mounting, bootable USB writing, and virtual drives need
privileged OS-specific access and are NOT implemented here. This build is
the manifest/verify engine that a real "verify my boot image" feature
would rely on. See README.txt for details.

Usage:
  bootforge manifest <path> -o manifest.bfm [--json]
  bootforge verify   <path> --manifest manifest.bfm [--json]
  bootforge help

Commands:
  manifest   Walk <path> (a file or directory) and write a SHA-256
             checksum manifest to the file given by -o.
  verify     Re-walk <path> and compare its current SHA-256 checksums
             against a previously generated manifest.

Run 'bootforge <command> -h' for command-specific flags.
`, version)
}

// reorderFlags moves all flag tokens (and their values, for flags listed in
// valueFlags) to the front of args and all positional arguments to the end.
// This works around Go's flag package stopping at the first positional
// argument, which matters here because path arguments legitimately appear
// before flags on the command line (e.g. "bootforge manifest ./iso -o m.bfm").
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...)
}

func humanBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

// FileEntry is one file's record in a manifest.
type FileEntry struct {
	Path   string `json:"path"`
	Size   int64  `json:"size"`
	SHA256 string `json:"sha256"`
}

// Manifest is the on-disk JSON structure written by "bootforge manifest"
// and consumed by "bootforge verify".
type Manifest struct {
	GeneratedAtUTC string      `json:"generated_at_utc"`
	Root           string      `json:"root"`
	FileCount      int         `json:"file_count"`
	TotalBytes     int64       `json:"total_bytes"`
	Files          []FileEntry `json:"files"`
}

// hashFile computes the SHA-256 checksum and size of a single file.
func hashFile(path string) (sum string, size int64, err error) {
	f, err := os.Open(path)
	if err != nil {
		return "", 0, err
	}
	defer f.Close()

	h := sha256.New()
	n, err := io.Copy(h, f)
	if err != nil {
		return "", 0, err
	}
	return hex.EncodeToString(h.Sum(nil)), n, nil
}

// scanPath walks root (a single file or a directory tree) and returns a
// checksum entry for every regular file found. For a single file, the
// entry's Path is just the file's base name. For a directory, Path is the
// file's path relative to root, forward-slash normalized. Entries are
// returned sorted by path for deterministic manifest output.
func scanPath(root string) ([]FileEntry, error) {
	info, err := os.Stat(root)
	if err != nil {
		return nil, err
	}

	if info.Mode().IsRegular() {
		sum, size, err := hashFile(root)
		if err != nil {
			return nil, err
		}
		return []FileEntry{{Path: filepath.Base(root), Size: size, SHA256: sum}}, nil
	}

	if !info.IsDir() {
		return nil, fmt.Errorf("%s is neither a regular file nor a directory", root)
	}

	var entries []FileEntry
	err = filepath.Walk(root, func(p string, fi os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if fi.IsDir() {
			return nil
		}
		if !fi.Mode().IsRegular() {
			return nil
		}
		rel, err := filepath.Rel(root, p)
		if err != nil {
			return err
		}
		rel = filepath.ToSlash(rel)
		sum, size, err := hashFile(p)
		if err != nil {
			return err
		}
		entries = append(entries, FileEntry{Path: rel, Size: size, SHA256: sum})
		return nil
	})
	if err != nil {
		return nil, err
	}

	sort.Slice(entries, func(i, j int) bool { return entries[i].Path < entries[j].Path })
	return entries, nil
}

func manifestUsage() {
	fmt.Fprintf(os.Stderr, `Usage: bootforge manifest <path> -o manifest.bfm [--json]

Walk <path> (a single file or a directory) and write a SHA-256 checksum
manifest in JSON to the file given by -o.

Flags:
  -o        Output manifest file path (required)
  --json    Print the summary to stdout as JSON instead of plain text
`)
}

func cmdManifest(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" {
			manifestUsage()
			return
		}
	}

	reordered := reorderFlags(args, map[string]bool{"o": true})

	fs := flag.NewFlagSet("manifest", flag.ExitOnError)
	output := fs.String("o", "", "output manifest file path")
	jsonOut := fs.Bool("json", false, "print summary as JSON")
	fs.Usage = manifestUsage
	fs.Parse(reordered)

	positional := fs.Args()
	if len(positional) != 1 {
		fmt.Fprintln(os.Stderr, "bootforge manifest: expected exactly one <path> argument")
		manifestUsage()
		os.Exit(1)
	}
	if *output == "" {
		fmt.Fprintln(os.Stderr, "bootforge manifest: -o <output file> is required")
		manifestUsage()
		os.Exit(1)
	}

	root := positional[0]
	entries, err := scanPath(root)
	if err != nil {
		fmt.Fprintf(os.Stderr, "bootforge manifest: %v\n", err)
		os.Exit(1)
	}

	var total int64
	for _, e := range entries {
		total += e.Size
	}

	m := Manifest{
		GeneratedAtUTC: time.Now().UTC().Format(time.RFC3339),
		Root:           root,
		FileCount:      len(entries),
		TotalBytes:     total,
		Files:          entries,
	}

	data, err := json.MarshalIndent(m, "", "  ")
	if err != nil {
		fmt.Fprintf(os.Stderr, "bootforge manifest: %v\n", err)
		os.Exit(1)
	}
	data = append(data, '\n')

	if err := os.WriteFile(*output, data, 0o644); err != nil {
		fmt.Fprintf(os.Stderr, "bootforge manifest: writing %s: %v\n", *output, err)
		os.Exit(1)
	}

	if *jsonOut {
		summary := struct {
			Output     string `json:"output"`
			FileCount  int    `json:"file_count"`
			TotalBytes int64  `json:"total_bytes"`
		}{*output, len(entries), total}
		out, _ := json.MarshalIndent(summary, "", "  ")
		fmt.Println(string(out))
		return
	}

	fmt.Printf("Manifest written to %s\n", *output)
	fmt.Printf("  Files:      %d\n", len(entries))
	fmt.Printf("  Total size: %s (%d bytes)\n", humanBytes(total), total)
}

func verifyUsage() {
	fmt.Fprintf(os.Stderr, `Usage: bootforge verify <path> --manifest manifest.bfm [--json]

Re-walk <path> (a single file or a directory), recompute SHA-256 for
whatever is found, and compare it against a manifest previously written
by "bootforge manifest". Reports OK / MISMATCH / MISSING / EXTRA for
each path and exits non-zero if any MISMATCH or MISSING entries exist.

Flags:
  --manifest    Manifest file to verify against (required)
  --json        Print the result as JSON instead of plain text
`)
}

func cmdVerify(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" {
			verifyUsage()
			return
		}
	}

	reordered := reorderFlags(args, map[string]bool{"manifest": true})

	fs := flag.NewFlagSet("verify", flag.ExitOnError)
	manifestPath := fs.String("manifest", "", "manifest file to verify against")
	jsonOut := fs.Bool("json", false, "print result as JSON")
	fs.Usage = verifyUsage
	fs.Parse(reordered)

	positional := fs.Args()
	if len(positional) != 1 {
		fmt.Fprintln(os.Stderr, "bootforge verify: expected exactly one <path> argument")
		verifyUsage()
		os.Exit(1)
	}
	if *manifestPath == "" {
		fmt.Fprintln(os.Stderr, "bootforge verify: --manifest <file> is required")
		verifyUsage()
		os.Exit(1)
	}

	root := positional[0]

	data, err := os.ReadFile(*manifestPath)
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Fprintf(os.Stderr, "bootforge verify: manifest file not found: %s\n", *manifestPath)
		} else {
			fmt.Fprintf(os.Stderr, "bootforge verify: reading manifest %s: %v\n", *manifestPath, err)
		}
		os.Exit(1)
	}

	var m Manifest
	if err := json.Unmarshal(data, &m); err != nil {
		fmt.Fprintf(os.Stderr, "bootforge verify: manifest %s is not valid JSON: %v\n", *manifestPath, err)
		os.Exit(1)
	}

	current, err := scanPath(root)
	if err != nil {
		fmt.Fprintf(os.Stderr, "bootforge verify: %v\n", err)
		os.Exit(1)
	}

	expected := make(map[string]FileEntry, len(m.Files))
	for _, e := range m.Files {
		expected[e.Path] = e
	}
	found := make(map[string]FileEntry, len(current))
	for _, e := range current {
		found[e.Path] = e
	}

	var okCount int
	var mismatched, missing, extra []string

	// Walk expected paths in sorted order for deterministic output.
	expPaths := make([]string, 0, len(expected))
	for p := range expected {
		expPaths = append(expPaths, p)
	}
	sort.Strings(expPaths)

	for _, p := range expPaths {
		exp := expected[p]
		got, ok := found[p]
		switch {
		case !ok:
			missing = append(missing, p)
			if !*jsonOut {
				fmt.Printf("MISSING   %s\n", p)
			}
		case got.SHA256 != exp.SHA256:
			mismatched = append(mismatched, p)
			if !*jsonOut {
				fmt.Printf("MISMATCH  %s\n", p)
			}
		default:
			okCount++
		}
	}

	extraPaths := make([]string, 0)
	for p := range found {
		if _, ok := expected[p]; !ok {
			extraPaths = append(extraPaths, p)
		}
	}
	sort.Strings(extraPaths)
	for _, p := range extraPaths {
		extra = append(extra, p)
		if !*jsonOut {
			fmt.Printf("EXTRA     %s\n", p)
		}
	}

	if *jsonOut {
		result := struct {
			OK              int      `json:"ok"`
			Mismatched      int      `json:"mismatched"`
			Missing         int      `json:"missing"`
			Extra           int      `json:"extra"`
			MismatchedPaths []string `json:"mismatched_paths"`
			MissingPaths    []string `json:"missing_paths"`
			ExtraPaths      []string `json:"extra_paths"`
		}{okCount, len(mismatched), len(missing), len(extra), mismatched, missing, extra}
		out, _ := json.MarshalIndent(result, "", "  ")
		fmt.Println(string(out))
	} else {
		fmt.Printf("%d OK, %d mismatched, %d missing, %d extra\n", okCount, len(mismatched), len(missing), len(extra))
	}

	if len(mismatched) > 0 || len(missing) > 0 {
		os.Exit(1)
	}
}
