package main

import (
	"bytes"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"

	"github.com/rs/zerolog/log"

	config "novit.tech/direktil/pkg/bootstrapconfig"

	"novit.tech/direktil/initrd/lvm"
)

func sortedKeys[T any](m map[string]T) (keys []string) {
	keys = make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return
}

func setupLVM(cfg *config.Config) {
	if len(cfg.LVM) == 0 {
		log.Info().Msg("no LVM VG configured.")
		return
	}

	// [dev] = filesystem
	// eg: [/dev/sda1] = ext4
	createdDevs := map[string]string{}

	run("pvscan")
	run("vgscan", "--mknodes")

	for _, vg := range cfg.LVM {
		setupVG(vg)
	}

	for _, vg := range cfg.LVM {
		setupLVs(vg, createdDevs)
	}

	run("vgchange", "--sysinit", "-a", "ly")

	setupCrypt(cfg.Crypt, createdDevs)

	devs := make([]string, 0, len(createdDevs))
	for k := range createdDevs {
		devs = append(devs, k)
	}
	sort.Strings(devs)

	for _, dev := range devs {
		setupFS(dev, createdDevs[dev])
	}
}

func setupVG(vg config.LvmVG) {
	pvs := lvm.PVSReport{}
	err := runJSON(&pvs, "pvs", "--reportformat", "json")
	if err != nil {
		fatalf("failed to list LVM PVs: %v", err)
	}

	vgExists := false
	devNeeded := vg.PVs.N
	for _, pv := range pvs.PVs() {
		if pv.VGName == vg.VG {
			vgExists = true
			devNeeded--
		}
	}

	log := log.With().Str("vg", vg.VG).Logger()

	if devNeeded <= 0 {
		log.Info().Msg("LVM VG has all its devices")
		return
	}

	if vgExists {
		log.Info().Msgf("LVM VG misses %d devices", devNeeded)
	} else {
		log.Info().Msg("LVM VG does not exists, creating")
	}

	devNames := make([]string, 0)
	err = filepath.Walk("/dev", func(n string, fi fs.FileInfo, err error) error {
		if fi.Mode().Type() == os.ModeDevice {
			devNames = append(devNames, n)
		}
		return err
	})
	if err != nil {
		fatalf("failed to walk /dev: %v", err)
	}

	devNames = filter(devNames, func(v string) bool {
		for _, pv := range pvs.PVs() {
			if v == pv.Name {
				return false
			}
		}
		return true
	})

	m := regexpSelectN(vg.PVs.N, vg.PVs.Regexps, devNames)
	if len(m) == 0 {
		log.Error().Strs("regexps", vg.PVs.Regexps).Msg("no device match the regexps")
		fatalf("failed to setup VG %s", vg.VG)
	}

	if vgExists {
		log.Info().Strs("devices", m).Msg("LVM VG: extending")
		run("vgextend", append([]string{vg.VG}, m...)...)
		devNeeded -= len(m)
	} else {
		log.Info().Strs("devices", m).Msg("LVM VG: creating")
		run("vgcreate", append([]string{vg.VG}, m...)...)
		devNeeded -= len(m)
	}

	if devNeeded > 0 {
		fatalf("VG %s does not have enough devices (%d missing)", vg.VG, devNeeded)
	}
}

func setupLVs(vg config.LvmVG, createdDevs map[string]string) {
	lvsRep := lvm.LVSReport{}
	err := runJSON(&lvsRep, "lvs", "--reportformat", "json")
	if err != nil {
		fatalf("lvs failed: %v", err)
	}

	lvs := lvsRep.LVs()

	defaults := vg.Defaults

	for idx, lv := range vg.LVs {
		log := log.With().Str("vg", vg.VG).Str("lv", lv.Name).Logger()

		if contains(lvs, func(v lvm.LV) bool {
			return v.VGName == vg.VG && v.Name == lv.Name
		}) {
			log.Info().Msg("LV exists")
			continue
		}

		log.Info().Msg("LV does not exist")

		if lv.Raid == nil {
			lv.Raid = defaults.Raid
		}

		args := make([]string, 0)

		if lv.Name == "" {
			fatalf("LV[%d] has no name", idx)
		}
		args = append(args, vg.VG, "--name", lv.Name)

		if lv.Size != "" && lv.Extents != "" {
			fatalf("LV has both size and extents defined!")
		} else if lv.Size == "" && lv.Extents == "" {
			fatalf("LV does not have size or extents defined!")
		} else if lv.Size != "" {
			args = append(args, "-L", lv.Size)
		} else /* if lv.Extents != "" */ {
			args = append(args, "-l", lv.Extents)
		}

		if raid := lv.Raid; raid != nil {
			if raid.Mirrors != 0 {
				args = append(args, "--mirrors", strconv.Itoa(raid.Mirrors))
			}
			if raid.Stripes != 0 {
				args = append(args, "--stripes", strconv.Itoa(raid.Stripes))
			}
		}

		log.Info().Strs("args", args).Msg("LV: creating")
		run("lvcreate", args...)

		dev := "/dev/" + vg.VG + "/" + lv.Name
		zeroDevStart(dev)

		fs := lv.FS
		if fs == "" {
			fs = vg.Defaults.FS
		}
		createdDevs[dev] = fs
	}
}

func zeroDevStart(dev string) {
	f, err := os.OpenFile(dev, os.O_WRONLY, 0600)
	if err != nil {
		fatalf("failed to open %s: %v", dev, err)
	}

	defer f.Close()

	_, err = f.Write(make([]byte, 8192))
	if err != nil {
		fatalf("failed to zero the beginning of %s: %v", dev, err)
	}
}

var cryptDevs = map[string]bool{}

func setupCrypt(devSpecs []config.CryptDev, createdDevs map[string]string) {
	var password []byte
	passwordVerified := false

	// flat, expanded devices to open
	devNames := make([]config.CryptDev, 0, len(devSpecs))

	for _, devSpec := range devSpecs {
		if devSpec.Dev == "" && devSpec.Prefix == "" {
			fatalf("crypt: name %q: no dev or match set", devSpec.Name)
		}
		if devSpec.Dev != "" && devSpec.Prefix != "" {
			fatalf("crypt: name %q: both dev (%q) and match (%q) are set", devSpec.Name, devSpec.Dev, devSpec.Prefix)
		}

		if devSpec.Dev != "" {
			// already flat
			devNames = append(devNames, devSpec)
			continue
		}

		matches, err := filepath.Glob(devSpec.Prefix + "*")
		if err != nil {
			fatalf("failed to search for device matches: %v", err)
		}

		for _, m := range matches {
			suffix := m[len(devSpec.Prefix):]

			devNames = append(devNames, config.CryptDev{Dev: m, Name: devSpec.Name + suffix})
		}
	}

	for _, devName := range devNames {
		name, dev := devName.Name, devName.Dev

		if name == "" {
			name = filepath.Base(dev)
		}

		if cryptDevs[name] {
			fatalf("duplicate crypt device name: %s", name)
		}
		cryptDevs[name] = true

	retryOpen:
		if len(password) == 0 {
			password = askSecret("crypt password")

			if len(password) == 0 {
				fatalf("empty password given")
			}
		}

		fs := createdDevs[dev]
		delete(createdDevs, dev)

		tgtDev := "/dev/mapper/" + name

		needFormat := !devInitialized(dev)
		if needFormat {
			if !passwordVerified {
			retry:
				p2 := askSecret("verify crypt password")

				eq := bytes.Equal(password, p2)

				for i := range p2 {
					p2[i] = 0
				}

				if !eq {
					log.Error().Msg("passwords don't match")
					goto retry
				}
			}

			log.Info().Str("dev", dev).Msg("formatting encrypted device")
			cmd := exec.Command("cryptsetup", "luksFormat", dev, "--key-file=-")
			cmd.Stdin = bytes.NewBuffer(password)
			cmd.Stdout = stdout
			cmd.Stderr = stderr
			err := cmd.Run()
			if err != nil {
				fatalf("failed luksFormat: %v", err)
			}

			createdDevs[tgtDev] = fs
		}

		if len(password) == 0 {
			password = askSecret("crypt password")

			if len(password) == 0 {
				fatalf("empty password given")
			}
		}

		log.Info().Str("name", name).Str("dev", dev).Msg("openning encrypted device")
		cmd := exec.Command("cryptsetup", "open", dev, name, "--key-file=-")
		cmd.Stdin = bytes.NewBuffer(password)
		cmd.Stdout = stdout
		cmd.Stderr = stderr
		err := cmd.Run()
		if err != nil {
			// maybe the password is wrong
			for i := range password {
				password[i] = 0
			}
			password = password[:0]
			passwordVerified = false
			goto retryOpen
		}

		if needFormat {
			zeroDevStart(tgtDev)
		}

		passwordVerified = true
	}

	for i := range password {
		password[i] = 0
	}
}

func devInitialized(dev string) bool {
	f, err := os.Open(dev)
	if err != nil {
		fatalf("failed to open %s: %v", dev, err)
	}

	defer f.Close()

	ba := make([]byte, 8192)
	_, err = f.Read(ba)
	if err != nil {
		fatalf("failed to read %s: %v", dev, err)
	}

	for _, b := range ba {
		if b != 0 {
			return true
		}
	}
	return false
}

func setupFS(dev, fs string) {
	if devInitialized(dev) {
		log.Info().Str("dev", dev).Msg("device already formatted")
		return
	}

	if fs == "" {
		fs = "ext4"
	}

	log.Info().Str("dev", dev).Str("fs", fs).Msg("formatting device")
	args := make([]string, 0)

	switch fs {
	case "btrfs":
		args = append(args, "-f")
	case "ext4":
		args = append(args, "-F")
	}

	run("mkfs."+fs, append(args, dev)...)
}
