// Command bootbuilder writes a FAT32 filesystem image file from a directory
// tree, byte by byte, with an MBR or GPT partition table around it.
//
// It produces an image FILE. It never opens a raw disk or a physical device.
package main

import (
	"errors"
	"flag"
	"fmt"
	"math"
	"os"
	"strconv"
	"strings"
)

const appName = "bootbuilder"

// ---------------------------------------------------------------------------
// Shared Techlosoft UX helpers (verbatim across the tool line)
// ---------------------------------------------------------------------------

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])
}

// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------

// usage prints the usage block on stdout. Explicit help exits 0 from main.
func usage() { fmt.Fprint(os.Stdout, usageText()) }

// usageTo prints the same block to an arbitrary stream; a bad invocation sends
// it to stderr and exits 1.
func usageTo(w *os.File) { fmt.Fprint(w, usageText()) }

func fail(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "%s: %s\n", appName, fmt.Sprintf(format, args...))
	os.Exit(1)
}

func usageErr(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "%s: %s\n\n", appName, fmt.Sprintf(format, args...))
	usageTo(os.Stderr)
	os.Exit(1)
}

func usageText() string {
	var sb strings.Builder
	n := appName
	fmt.Fprintf(&sb, `%s - writes a FAT32 image file from a directory tree (Techlosoft Deployment Media Center)

USAGE
  %s plan    --src <dir> --size 8GB [--scheme mbr|gpt] [--cluster-size 4096] [--json]
  %s build   --src <dir> --out usb.img --size 8GB [--scheme mbr|gpt] [--label NAME]
                 [--cluster-size 4096] [--force] [--json]
  %s inspect <image> [--json]
  %s verify  <image> --against <dir> [--json]
  %s help | -h | --help

COMMANDS
  plan      Report the exact layout that build would produce - cluster size,
            cluster count, FAT sectors, data start, slack and the capacity
            check - WITHOUT writing anything.
  build     Write the image FILE named by --out. A FAT32 filesystem is
            constructed from scratch and the contents of --src are placed in
            it, long names intact. Nothing outside --out is written.
  inspect   Parse an existing image back from bytes with the independent
            reader and dump geometry, partition table and directory listing.
  verify    Re-read an image and compare every file against a source tree,
            byte for byte, by SHA-256.

FLAGS
  --src <dir>          Source tree whose contents become the volume root.
  --out <file>         Image file to write. Refused if it exists without --force.
  --size <size>        Total image size: 8GB, 512MiB, 2TiB or raw bytes.
                       Suffixes are binary (1KB = 1024 B).
  --scheme mbr|gpt     Partition table. mbr = one 0x0C (FAT32 LBA) entry.
                       gpt = protective MBR + GPT with an EFI System Partition
                       entry, primary and backup headers. Default mbr.
  --label <name>       FAT volume label, up to 11 characters. Default BOOTBUILDER.
  --cluster-size <n>   Override the cluster size in bytes (512..65536, power of
                       two). By default it is chosen from the volume size using
                       Microsoft's own FAT32 table.
  --against <dir>      Source tree to verify an image against.
  --force              Allow overwriting an existing --out.
  --json               Machine-readable JSON output (plan, build, inspect, verify).

EXAMPLES
  %s plan  --src ./winpe --size 8GB
  %s build --src ./winpe --out usb.img --size 8GB --scheme gpt --label WINPE
  %s inspect usb.img
  %s verify usb.img --against ./winpe --json

This tool writes IMAGE FILES ONLY. It contains no code path that opens a raw
disk, a block device or a physical drive. Flags may appear before or after
positional arguments.
`, n, n, n, n, n, n, n, n, n, n)
	return sb.String()
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

func main() {
	args := os.Args[1:]
	if len(args) == 0 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// questions the program needs and stay on screen. Printing usage and
		// exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usageTo(os.Stderr)
		os.Exit(1)
	}
	switch args[0] {
	case "help", "-h", "--help":
		usage()
		os.Exit(0)
	}
	cmd := args[0]
	rest := args[1:]
	for _, a := range rest {
		if a == "-h" || a == "--help" || a == "help" {
			usage()
			os.Exit(0)
		}
	}
	switch cmd {
	case "plan":
		cmdPlan(rest)
	case "build":
		cmdBuild(rest)
	case "inspect":
		cmdInspect(rest)
	case "verify":
		cmdVerify(rest)
	default:
		usageErr("unknown command %q", cmd)
	}
}

// ---------------------------------------------------------------------------
// Flag plumbing
// ---------------------------------------------------------------------------

var valueFlags = map[string]bool{
	"src": true, "s": true,
	"out": true, "o": true,
	"size":         true,
	"scheme":       true,
	"label":        true,
	"cluster-size": true,
	"against":      true, "a": true,
}

func newFlagSet(name string) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.SetOutput(os.Stderr)
	fs.Usage = func() { usageTo(os.Stderr) }
	return fs
}

// parseSize accepts "8GB", "512MiB", "2TiB", "1.5g" or a raw byte count.
// Suffixes are binary: 1KB == 1KiB == 1024 bytes.
func parseSize(s string) (int64, error) {
	t := strings.TrimSpace(s)
	if t == "" {
		return 0, errors.New("empty size")
	}
	t = strings.ReplaceAll(t, "_", "")
	t = strings.ReplaceAll(t, ",", "")
	up := strings.ToUpper(t)
	up = strings.TrimSuffix(up, "B") // 8GB -> 8G, 8GIB -> 8GI, 8B -> 8
	up = strings.TrimSuffix(up, "I") // 8GI -> 8G
	up = strings.TrimSpace(up)       // tolerate "8 G"
	mult := int64(1)
	if up != "" {
		switch up[len(up)-1] {
		case 'K':
			mult = 1 << 10
		case 'M':
			mult = 1 << 20
		case 'G':
			mult = 1 << 30
		case 'T':
			mult = 1 << 40
		case 'P':
			mult = 1 << 50
		}
	}
	if mult != 1 {
		up = up[:len(up)-1]
	}
	up = strings.TrimSpace(up)
	if up == "" {
		return 0, fmt.Errorf("invalid size %q", s)
	}
	if n, err := strconv.ParseInt(up, 10, 64); err == nil {
		if n < 0 {
			return 0, fmt.Errorf("size %q must not be negative", s)
		}
		if mult > 1 && n > math.MaxInt64/mult {
			return 0, fmt.Errorf("size %q overflows int64", s)
		}
		return n * mult, nil
	}
	f, err := strconv.ParseFloat(up, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid size %q", s)
	}
	if f < 0 {
		return 0, fmt.Errorf("size %q must not be negative", s)
	}
	v := f * float64(mult)
	if v > math.MaxInt64 {
		return 0, fmt.Errorf("size %q overflows int64", s)
	}
	return int64(v), nil
}
