package container

import (
	"context"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"runtime"
	"syscall"

	"hakurei.app/container/seccomp"
	"hakurei.app/container/std"
	"hakurei.app/ext"
	"hakurei.app/internal/netlink"
	"hakurei.app/internal/params"
	"hakurei.app/message"
)

type osFile interface {
	Name() string
	io.Writer
	fs.File
}

// syscallDispatcher provides methods that make state-dependent system calls as
// part of their behaviour.
type syscallDispatcher interface {
	// new starts a goroutine with a new instance of syscallDispatcher.
	// A syscallDispatcher must never be used in any goroutine other than the one owning it,
	// just synchronising access is not enough, as this is for test instrumentation.
	new(f func(k syscallDispatcher))

	// lockOSThread provides [runtime.LockOSThread].
	lockOSThread()

	// setPtracer provides [SetPtracer].
	setPtracer(pid uintptr) error
	// setDumpable provides [SetDumpable].
	setDumpable(dumpable uintptr) error
	// setNoNewPrivs provides [SetNoNewPrivs].
	setNoNewPrivs() error

	// lastcap provides [LastCap].
	lastcap(msg message.Msg) uintptr
	// capset provides capset.
	capset(hdrp *capHeader, datap *[2]capData) error
	// capBoundingSetDrop provides capBoundingSetDrop.
	capBoundingSetDrop(cap uintptr) error
	// capAmbientClearAll provides capAmbientClearAll.
	capAmbientClearAll() error
	// capAmbientRaise provides capAmbientRaise.
	capAmbientRaise(cap uintptr) error
	// isatty provides [Isatty].
	isatty(fd int) bool
	// receive provides [Receive].
	receive(key string, e any, fdp *int) (closeFunc func() error, err error)

	// bindMount provides procPaths.bindMount.
	bindMount(msg message.Msg, source, target string, flags uintptr) error
	// remount provides procPaths.remount.
	remount(msg message.Msg, target string, flags uintptr) error
	// mountTmpfs provides mountTmpfs.
	mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
	// mountOverlay provides mountOverlay.
	mountOverlay(target string, options [][2]string) error
	// ensureFile provides ensureFile.
	ensureFile(name string, perm, pperm os.FileMode) error
	// mustLoopback provides mustLoopback.
	mustLoopback(ctx context.Context, msg message.Msg)

	// seccompLoad provides [seccomp.Load].
	seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
	// notify provides [signal.Notify].
	notify(c chan<- os.Signal, sig ...os.Signal)
	// start starts [os/exec.Cmd].
	start(c *exec.Cmd) error
	// signal signals the underlying process of [os/exec.Cmd].
	signal(c *exec.Cmd, sig os.Signal) error
	// evalSymlinks provides [filepath.EvalSymlinks].
	evalSymlinks(path string) (string, error)

	// exit provides [os.Exit].
	exit(code int)
	// getpid provides [os.Getpid].
	getpid() int
	// stat provides [os.Stat].
	stat(name string) (os.FileInfo, error)
	// mkdir provides [os.Mkdir].
	mkdir(name string, perm os.FileMode) error
	// mkdirTemp provides [os.MkdirTemp].
	mkdirTemp(dir, pattern string) (string, error)
	// mkdirAll provides [os.MkdirAll].
	mkdirAll(path string, perm os.FileMode) error
	// readdir provides [os.ReadDir].
	readdir(name string) ([]os.DirEntry, error)
	// openNew provides [os.Open].
	openNew(name string) (osFile, error)
	// writeFile provides [os.WriteFile].
	writeFile(name string, data []byte, perm os.FileMode) error
	// createTemp provides [os.CreateTemp].
	createTemp(dir, pattern string) (osFile, error)
	// remove provides os.Remove.
	remove(name string) error
	// newFile provides os.NewFile.
	newFile(fd uintptr, name string) *os.File
	// symlink provides os.Symlink.
	symlink(oldname, newname string) error
	// readlink provides [os.Readlink].
	readlink(name string) (string, error)

	// umask provides syscall.Umask.
	umask(mask int) (oldmask int)
	// sethostname provides syscall.Sethostname
	sethostname(p []byte) (err error)
	// chdir provides syscall.Chdir
	chdir(path string) (err error)
	// fchdir provides syscall.Fchdir
	fchdir(fd int) (err error)
	// open provides syscall.Open
	open(path string, mode int, perm uint32) (fd int, err error)
	// close provides syscall.Close
	close(fd int) (err error)
	// pivotRoot provides syscall.PivotRoot
	pivotRoot(newroot, putold string) (err error)
	// mount provides syscall.Mount
	mount(source, target, fstype string, flags uintptr, data string) (err error)
	// unmount provides syscall.Unmount
	unmount(target string, flags int) (err error)
	// wait4 provides syscall.Wait4
	wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)

	// printf provides the Printf method of [log.Logger].
	printf(msg message.Msg, format string, v ...any)
	// fatal provides the Fatal method of [log.Logger]
	fatal(msg message.Msg, v ...any)
	// fatalf provides the Fatalf method of [log.Logger]
	fatalf(msg message.Msg, format string, v ...any)
}

// direct implements syscallDispatcher on the current kernel.
type direct struct{}

func (k direct) new(f func(k syscallDispatcher)) { go f(k) }

func (direct) lockOSThread() { runtime.LockOSThread() }

func (direct) setPtracer(pid uintptr) error       { return ext.SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error               { return setNoNewPrivs() }

func (direct) lastcap(msg message.Msg) uintptr                 { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error            { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error                       { return capAmbientClearAll() }
func (direct) capAmbientRaise(cap uintptr) error               { return capAmbientRaise(cap) }
func (direct) isatty(fd int) bool                              { return ext.Isatty(fd) }
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
	return params.Receive(key, e, fdp)
}

func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
	return hostProc.bindMount(msg, source, target, flags)
}
func (direct) remount(msg message.Msg, target string, flags uintptr) error {
	return hostProc.remount(msg, target, flags)
}
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
	return mountTmpfs(k, fsname, target, flags, size, perm)
}
func (k direct) mountOverlay(target string, options [][2]string) error {
	return mountOverlay(target, options)
}
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
	return ensureFile(name, perm, pperm)
}
func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
	var lo int
	if ifi, err := net.InterfaceByName("lo"); err != nil {
		msg.GetLogger().Fatalln(err)
	} else {
		lo = ifi.Index
	}

	c, err := netlink.DialRoute(0)
	if err != nil {
		msg.GetLogger().Fatalln(err)
	}

	must := func(err error) {
		if err == nil {
			return
		}
		if closeErr := c.Close(); closeErr != nil {
			msg.Verbosef("cannot close RTNETLINK: %v", closeErr)
		}

		switch err.(type) {
		case *os.SyscallError:
			msg.GetLogger().Fatalf("cannot %v", err)

		case syscall.Errno:
			msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)

		default:
			if err == context.DeadlineExceeded || err == context.Canceled {
				msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
			}
			msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
		}
	}
	must(c.SendNewaddrLo(ctx, uint32(lo)))
	must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
		Family: syscall.AF_UNSPEC,
		Index:  int32(lo),
		Flags:  syscall.IFF_UP,
		Change: syscall.IFF_UP,
	}))
	must(c.Close())
}

func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
	return seccomp.Load(rules, flags)
}
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
func (direct) start(c *exec.Cmd) error                     { return c.Start() }
func (direct) signal(c *exec.Cmd, sig os.Signal) error     { return c.Process.Signal(sig) }
func (direct) evalSymlinks(path string) (string, error)    { return filepath.EvalSymlinks(path) }

func (direct) exit(code int)                                 { os.Exit(code) }
func (direct) getpid() int                                   { return os.Getpid() }
func (direct) stat(name string) (os.FileInfo, error)         { return os.Stat(name) }
func (direct) mkdir(name string, perm os.FileMode) error     { return os.Mkdir(name, perm) }
func (direct) mkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) }
func (direct) mkdirAll(path string, perm os.FileMode) error  { return os.MkdirAll(path, perm) }
func (direct) readdir(name string) ([]os.DirEntry, error)    { return os.ReadDir(name) }
func (direct) openNew(name string) (osFile, error)           { return os.Open(name) }
func (direct) writeFile(name string, data []byte, perm os.FileMode) error {
	return os.WriteFile(name, data, perm)
}
func (direct) createTemp(dir, pattern string) (osFile, error) {
	return os.CreateTemp(dir, pattern)
}
func (direct) remove(name string) error {
	return os.Remove(name)
}
func (direct) newFile(fd uintptr, name string) *os.File {
	return os.NewFile(fd, name)
}
func (direct) symlink(oldname, newname string) error {
	return os.Symlink(oldname, newname)
}
func (direct) readlink(name string) (string, error) {
	return os.Readlink(name)
}

func (direct) umask(mask int) (oldmask int)     { return syscall.Umask(mask) }
func (direct) sethostname(p []byte) (err error) { return syscall.Sethostname(p) }
func (direct) chdir(path string) (err error)    { return syscall.Chdir(path) }
func (direct) fchdir(fd int) (err error)        { return syscall.Fchdir(fd) }
func (direct) open(path string, mode int, perm uint32) (fd int, err error) {
	return syscall.Open(path, mode, perm)
}
func (direct) close(fd int) (err error) {
	return syscall.Close(fd)
}
func (direct) pivotRoot(newroot, putold string) (err error) {
	return syscall.PivotRoot(newroot, putold)
}
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
	return mount(source, target, fstype, flags, data)
}
func (direct) unmount(target string, flags int) (err error) {
	return syscall.Unmount(target, flags)
}
func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
	return syscall.Wait4(pid, wstatus, options, rusage)
}

func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(msg message.Msg, v ...any)                 { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
