package container

import (
	"fmt"
	"path/filepath"
	. "syscall"

	"hakurei.app/check"
	"hakurei.app/fhs"
)

// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(target *check.Absolute, mqueue bool) *Ops {
	*f = append(*f, &MountDevOp{target, mqueue, false})
	return f
}

// DevWritable appends an [Op] that mounts a writable subset of host /dev.
//
// There is usually no good reason to write to /dev, so this should always be
// followed by a [RemountOp].
func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
	*f = append(*f, &MountDevOp{target, mqueue, true})
	return f
}

// MountDevOp mounts a subset of host /dev on container path Target.
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
// If Write is true, the resulting mount point is left writable.
type MountDevOp struct {
	Target *check.Absolute
	Mqueue bool
	Write  bool
}

func (d *MountDevOp) Valid() bool                                { return d != nil && d.Target != nil }
func (d *MountDevOp) early(*setupState, syscallDispatcher) error { return nil }
func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
	target := toSysroot(d.Target.String())

	if err := k.mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil {
		return err
	}

	for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
		targetPath := filepath.Join(target, name)
		if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
			return err
		}
		if err := k.bindMount(
			state,
			toHost(fhs.Dev+name),
			targetPath,
			0,
		); err != nil {
			return err
		}
	}
	for i, name := range []string{"stdin", "stdout", "stderr"} {
		if err := k.symlink(
			fhs.Proc+"self/fd/"+string(rune(i+'0')),
			filepath.Join(target, name),
		); err != nil {
			return err
		}
	}
	for _, pair := range [][2]string{
		{fhs.Proc + "self/fd", "fd"},
		{fhs.Proc + "kcore", "core"},
		{"pts/ptmx", "ptmx"},
	} {
		if err := k.symlink(pair[0], filepath.Join(target, pair[1])); err != nil {
			return err
		}
	}

	devShmPath := filepath.Join(target, "shm")
	devPtsPath := filepath.Join(target, "pts")
	for _, name := range []string{devShmPath, devPtsPath} {
		if err := k.mkdir(name, state.ParentPerm); err != nil {
			return err
		}
	}

	if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
		"newinstance,ptmxmode=0666,mode=620"); err != nil {
		return err
	}

	if state.RetainSession {
		if k.isatty(Stdout) {
			consolePath := filepath.Join(target, "console")
			if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
				return err
			}
			if name, err := k.readlink(hostProc.stdout()); err != nil {
				return err
			} else if err = k.bindMount(
				state,
				toHost(name),
				consolePath,
				0,
			); err != nil {
				return err
			}
		}
	}

	if d.Mqueue {
		mqueueTarget := filepath.Join(target, "mqueue")
		if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
			return err
		}
		if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
			return err
		}
	}

	if d.Write {
		return nil
	}

	if err := k.remount(state, target, MS_RDONLY); err != nil {
		return err
	}
	return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
}
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }

func (d *MountDevOp) Is(op Op) bool {
	vd, ok := op.(*MountDevOp)
	return ok && d.Valid() && vd.Valid() &&
		d.Target.Is(vd.Target) &&
		d.Mqueue == vd.Mqueue &&
		d.Write == vd.Write
}
func (*MountDevOp) prefix() (string, bool) { return "mounting", true }
func (d *MountDevOp) String() string {
	if d.Mqueue {
		return fmt.Sprintf("dev on %q with mqueue", d.Target)
	}
	return fmt.Sprintf("dev on %q", d.Target)
}
