// The earlyinit is part of the Rosa OS initramfs and serves as the system init.
//
// This program is an internal detail of Rosa OS and is not usable on its own.
// It is not covered by the compatibility promise.
package main

import (
	"context"
	"crypto/rand"
	"io"
	"log"
	"os"
	"os/signal"
	"runtime"
	"runtime/pprof"
	"slices"
	"strings"
	. "syscall"

	"hakurei.app/internal/kobject"
	"hakurei.app/internal/report"
	"hakurei.app/internal/uevent"
	"hakurei.app/message"
)

var r report.Reporter

func init() {
	log.SetFlags(0)
	log.SetPrefix("earlyinit: ")
	r.SetOutput(log.Default())

	// this handles SIGQUIT to provide useful debugging information without
	// terminating, and prevents the runtime from throwing on the must family
	// of early error reporting functions, DO NOT REMOVE
	c := make(chan os.Signal, 1)
	signal.Notify(c, SIGQUIT)
	go func() {
		for {
			<-c
			if p := pprof.Lookup("goroutine"); p == nil {
				log.Println("initial built-in goroutine profile does not exist")
			} else if err := p.WriteTo(os.Stderr, 2); err != nil {
				log.Println(err)
			}
		}
	}()
}

// fatal calls [log.Println] with v and blocks forever. Must be called from
// main. Must not be used after error reporting is set up.
func fatal(v ...any) {
	log.Println(v...)
	log.Println("unable to continue, please reboot and resolve the problem manually")
	log.SetOutput(io.Discard)
	select {}
}

// must calls fatal with err if it is non-nil.
func must(err error) {
	if err != nil {
		fatal(err)
		select {}
	}
}

// mustSyscall is like must, but with an additional action name.
func mustSyscall(action string, err error) {
	if err != nil {
		fatal("cannot "+action+":", err)
		select {}
	}
}

// must1 is like must, but with an additional passed through value.
func must1[T any](v T, err error) T {
	must(err)
	return v
}

const (
	// optionSystem specifies devpath of the system device.
	optionSystem = "system"

	// flagVerbose increases output verbosity.
	flagVerbose = "verbose"
	// flagStrict sets [report.DStrict] on r.
	flagStrict = "strict"
	// flagNoRecover sets [report.DNoRecover] on r.
	flagNoRecover = "no_recover"
)

func main() {
	runtime.LockOSThread()

	var (
		option map[string]string
		flags  []string
	)
	if len(os.Args) > 1 {
		option = make(map[string]string)
		for _, s := range os.Args[1:] {
			key, value, ok := strings.Cut(s, "=")
			if !ok {
				flags = append(flags, s)
				continue
			}
			option[key] = value
		}
	}

	{
		var flag uint64
		if slices.Contains(flags, flagStrict) {
			flag |= report.DStrict
		}
		if slices.Contains(flags, flagNoRecover) {
			flag |= report.DNoRecover
		}
		log.Printf("reporting flags %x", flag)
		r.SetFlags(flag)
	}

	msg := message.New(log.Default())
	msg.SwapVerbose(slices.Contains(flags, flagVerbose))

	mustSyscall("mount devtmpfs", Mount(
		"devtmpfs",
		"/dev/",
		"devtmpfs",
		MS_NOSUID|MS_NOEXEC,
		"",
	))
	must(os.Mkdir("/dev/pts/", 0))
	mustSyscall("mount devpts", Mount(
		"devpts",
		"/dev/pts/",
		"devpts",
		MS_NOSUID|MS_NOEXEC,
		"mode=620,ptmxmode=666",
	))
	must(os.Mkdir("/dev/shm/", 0))
	mustSyscall("mount shm", Mount(
		"shm",
		"/dev/shm/",
		"tmpfs",
		MS_NOSUID|MS_NODEV,
		"",
	))

	// The kernel might be unable to set up the console. When that happens,
	// printk is called with "Warning: unable to open an initial console."
	// and the init runs with no files. The checkfds runtime function
	// populates 0-2 by opening /dev/null for them.
	//
	// This check replaces 1 and 2 with /dev/kmsg to improve the chance
	// of output being visible to the user.
	if fi, err := os.Stdout.Stat(); err == nil {
		if stat, ok := fi.Sys().(*Stat_t); ok {
			if stat.Rdev == 0x103 {
				var fd int
				if fd, err = Open(
					"/dev/kmsg",
					O_WRONLY|O_CLOEXEC,
					0,
				); err != nil {
					log.Fatalf("cannot open kmsg: %v", err)
				}

				if err = Dup3(fd, Stdout, 0); err != nil {
					log.Fatalf("cannot open stdout: %v", err)
				}
				if err = Dup3(fd, Stderr, 0); err != nil {
					log.Fatalf("cannot open stderr: %v", err)
				}

				if err = Close(fd); err != nil {
					log.Printf("cannot close kmsg: %v", err)
				}
			}
		}
	}

	// staying in rootfs, these are no longer used
	must(os.Remove("/root"))
	must(os.Remove("/init"))

	must(os.Mkdir("/proc", 0))
	mustSyscall("mount proc", Mount(
		"proc",
		"/proc",
		"proc",
		MS_NOSUID|MS_NOEXEC|MS_NODEV,
		"hidepid=1",
	))

	must(os.Mkdir("/sys", 0))
	mustSyscall("mount sysfs", Mount(
		"sysfs",
		"/sys",
		"sysfs",
		0,
		"",
	))

	conn := must1(uevent.Dial(-128 * 1024 * 1024))
	events := make(chan *uevent.Message, 1<<10)
	var uuid uevent.UUID
	must1(rand.Read(uuid[:]))
	ctx, cancel := context.WithCancel(context.Background())

	go consume(ctx, msg, &r, conn, uuid, events)
	s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) {
		p := make([]string, 0, len(env))
		for k, v := range env {
			p = append(p, k+"="+v)
		}
		slices.Sort(p)
		log.Printf("change %s: %s", o.DevPath, strings.Join(p, ", "))
	}, func(err error) {
		severity := report.Inconsistent
		if e, ok := err.(kobject.EventError); ok && e.Kind == kobject.EBadTarget {
			severity = report.Trivial
		}
		r.Dispatch(
			severity,
			"processed inconsistent uevent",
			err,
		)
	})
	go func() {
		s.Consume(ctx, events)

		log.Println("closing NETLINK_KOBJECT_UEVENT socket")
		cancel()
		if err := conn.Close(); err != nil {
			log.Fatal(err) // not reached
		}
	}()

	must(os.Mkdir("/system", 0))
	if devpath := option[optionSystem]; devpath == "" {
		fatal("system must be nonempty")
	} else {
		log.Printf("waiting for devpath pattern %q", devpath)
		mustMountSystem(ctx, s, devpath)
	}

	// after top level has been set up
	mustSyscall("remount root", Mount(
		"",
		"/",
		"",
		MS_REMOUNT|MS_BIND|
			MS_RDONLY|MS_NODEV|MS_NOSUID|MS_NOEXEC,
		"",
	))

	must(os.WriteFile(
		"/sys/module/firmware_class/parameters/path",
		[]byte("/system/lib/firmware"),
		0,
	))
	go dispatchModprobe(ctx, s)

}
