package main

import (
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"

	yaml "gopkg.in/yaml.v2"
	"novit.tech/direktil/pkg/sysfs"
)

var loopOffset = 0

func bootV1() {
	log.Print("-- boot v1 --")

	// find and mount /boot
	bootMatch := param("boot", "")
	bootMounted := false
	if bootMatch != "" {
		bootFS := param("boot.fs", "vfat")
		for i := 0; ; i++ {
			devNames := sysfs.DeviceByProperty("block", bootMatch)

			if len(devNames) == 0 {
				if i > 30 {
					fatal("boot partition not found after 30s")
				}
				log.Print("boot partition not found, retrying")
				time.Sleep(1 * time.Second)
				continue
			}

			devFile := filepath.Join("/dev", devNames[0])

			log.Print("boot partition found: ", devFile)

			mount(devFile, "/boot", bootFS, bootMountFlags, "")
			bootMounted = true
			break
		}
	} else {
		log.Print("Assuming /boot is already populated.")
	}

	// load config
	cfgPath := param("config", "/boot/config.yaml")

	layersDir = filepath.Join("/boot", bootVersion, "layers")
	applyConfig(cfgPath, bootMounted)

	finalizeBoot()
}

func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) {
	cfgBytes, err := os.ReadFile(cfgPath)
	if err != nil {
		fatalf("failed to read %s: %v", cfgPath, err)
	}

	cfg = &configV1{}
	if err := yaml.Unmarshal(cfgBytes, cfg); err != nil {
		fatal("failed to load config: ", err)
	}

	// mount layers
	if len(cfg.Layers) == 0 {
		fatal("no layers configured!")
	}

	log.Printf("wanted layers: %q", cfg.Layers)

	layersInMemory := paramBool("layers-in-mem", false)

	const layersInMemDir = "/layers-in-mem"
	if layersInMemory {
		mkdir(layersInMemDir, 0700)
		mount("layers-mem", layersInMemDir, "tmpfs", 0, "")
	}

	lowers := make([]string, len(cfg.Layers))
	for i, layer := range cfg.Layers {
		path := layerPath(layer)

		info, err := os.Stat(path)
		if err != nil {
			fatal(err)
		}

		log.Printf("layer %s found (%d bytes)", layer, info.Size())

		if layersInMemory {
			log.Print("  copying to memory...")
			targetPath := filepath.Join(layersInMemDir, layer)
			cp(path, targetPath)
			path = targetPath
		}

		dir := "/layers/" + layer

		lowers[i] = dir

		mountSquahfs(path, dir)
	}

	// prepare system root
	mount("mem", "/changes", "tmpfs", 0, "")

	mkdir("/changes/workdir", 0755)
	mkdir("/changes/upperdir", 0755)

	mount("overlay", "/system", "overlay", rootMountFlags,
		"lowerdir="+strings.Join(lowers, ":")+",upperdir=/changes/upperdir,workdir=/changes/workdir")

	// make root rshared (default in systemd, required by Kubernetes 1.10+)
	// equivalent to "mount --make-rshared /"
	// see kernel's Documentation/sharedsubtree.txt (search rshared)
	if err := syscall.Mount("", "/system", "", syscall.MS_SHARED|syscall.MS_REC, ""); err != nil {
		fatalf("FATAL: mount --make-rshared / failed: %v", err)
	}

	if bootMounted {
		if layersInMemory {
			if err := syscall.Unmount("/boot", 0); err != nil {
				log.Print("WARNING: failed to unmount /boot: ", err)
				time.Sleep(2 * time.Second)
			}

		} else {
			mount("/boot", "/system/boot", "", syscall.MS_BIND, "")
		}
	}

	// - write configuration
	log.Print("writing /config.yaml")
	if err := os.WriteFile("/system/config.yaml", cfgBytes, 0600); err != nil {
		fatal("failed: ", err)
	}

	// - write files
	for _, fileDef := range cfg.Files {
		log.Print("writing ", fileDef.Path)

		filePath := filepath.Join("/system", fileDef.Path)

		if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
			log.Printf("failed to create dir %s: %v", filepath.Dir(fileDef.Path), err)
		}

		mode := fileDef.Mode
		if mode == 0 {
			mode = 0644
		}

		err = os.WriteFile(filePath, []byte(fileDef.Content), mode)
		if err != nil {
			fatalf("failed to write %s: %v", fileDef.Path, err)
		}
	}

	// - groups
	for _, group := range cfg.Groups {
		log.Print("creating group ", group.Name)

		opts := make([]string, 0)
		opts = append(opts /* chroot */, "/system", "groupadd", "-r")
		if group.Gid != 0 {
			opts = append(opts, "-g", strconv.Itoa(group.Gid))
		}
		opts = append(opts, group.Name)

		run("chroot", opts...)
	}

	// - user
	for _, user := range cfg.Users {
		log.Print("creating user ", user.Name)

		opts := make([]string, 0)
		opts = append(opts /* chroot */, "/system", "useradd", "-r")
		if user.Gid != 0 {
			opts = append(opts, "-g", strconv.Itoa(user.Gid))
		}
		if user.Uid != 0 {
			opts = append(opts, "-u", strconv.Itoa(user.Uid))
		}
		opts = append(opts, user.Name)

		run("chroot", opts...)
	}

	return
}

func finalizeBoot() {
	// clean zombies
	cleanZombies()

	// switch root
	log.Print("switching root")
	err := syscall.Exec("/sbin/switch_root", []string{"switch_root",
		"-c", "/dev/console", "/system", "/sbin/init"}, os.Environ())
	fatal("switch_root failed: ", err)
}
