//go:build darwin

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"unsafe"

	"github.com/shirou/gopsutil/v4/disk"
	"golang.org/x/sys/unix"

	"github.com/lxc/incus/v6/internal/server/metrics"
	"github.com/lxc/incus/v6/shared/api"
	"github.com/lxc/incus/v6/shared/revert"
	"github.com/lxc/incus/v6/shared/subprocess"
)

func osMountShared(src string, dst string, fstype string, opts []string) error {
	if fstype != "9p" {
		return errors.New("Only 9p shares are supported on Darwin")
	}

	// 9p shares behave strangely on Darwin, as they don't get mounted in the file system, but rather
	// as volumes, and have their own subcommand that doesn't conform to the mount frontend.
	// Bind mounts are not natively supported, so we have to mount the 9p share, then symlink it.

	// Convert relative mounts to absolute from / otherwise dir creation fails or mount fails.
	if !strings.HasPrefix(dst, "/") {
		dst = fmt.Sprintf("/%s", dst)
	}

	// If the path exists and is neither an empty directory nor a broken symbolic link to a volume
	// (indicating with high probability a previous share mount which we didn't clean up properly), we
	// can't safely use it.
	stat, err := os.Lstat(dst)
	if err == nil {
		if stat.IsDir() {
			// Handle directories, failing if not empty.
			entries, err := os.ReadDir(dst)
			if err != nil {
				return fmt.Errorf("Failed to open directory %s: %w", dst, err)
			}

			if len(entries) > 0 {
				return errors.New("Unable to mount shares on non-empty directories")
			}
		} else if stat.Mode()&fs.ModeSymlink != 0 {
			// Handle symbolic links, fail if not broken.
			// Try to follow the link.
			_, err := os.Stat(dst)
			if err == nil {
				return fmt.Errorf("Unable to mount shares on working symbolic link %s", dst)
			}
		} else {
			return fmt.Errorf("Mount destination %s exists and is not empty", dst)
		}

		err = os.Remove(dst)
		if err != nil {
			return fmt.Errorf("Failed to prepare destination %s: %w", dst, err)
		}
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}

	_, err = subprocess.RunCommand("mount_9p", src)
	if err != nil {
		return err
	}

	// Volume naming is very predictable. If a disk has the same name as a 9p share, `mount_9p` fails.
	return os.Symlink("/Volumes/"+src, dst)
}

// osUmount is currently not used, but it is implemented just in case.
func osUmount(src string, dst string, fstype string) error {
	if fstype != "9p" {
		return errors.New("Only 9p shares are supported on Darwin")
	}

	// First, remove the symlink.
	err := os.Remove(dst)
	if err != nil {
		return err
	}

	// Then, unmount the share.
	_, err = subprocess.RunCommand("umount", "/Volumes/"+src)
	return err
}

func osGetFilesystemMetrics(d *Daemon) ([]metrics.FilesystemMetrics, error) {
	partitions, err := disk.Partitions(true)
	if err != nil {
		return nil, err
	}

	sort.Slice(partitions, func(i, j int) bool {
		return partitions[i].Mountpoint < partitions[j].Mountpoint
	})

	fsMetrics := make([]metrics.FilesystemMetrics, 0, len(partitions))
	for _, partition := range partitions {
		var stat syscall.Statfs_t
		err = syscall.Statfs(partition.Mountpoint, &stat)
		if err != nil {
			continue
		}

		bsize := uint64(stat.Bsize)

		fsMetrics = append(fsMetrics, metrics.FilesystemMetrics{
			Device:         partition.Device,
			Mountpoint:     partition.Mountpoint,
			FSType:         partition.Fstype,
			AvailableBytes: stat.Bavail * bsize,
			FreeBytes:      stat.Bfree * bsize,
			SizeBytes:      stat.Blocks * bsize,
		})
	}

	return fsMetrics, nil
}

func macOSVersionName(version string) (string, error) {
	parts := strings.Split(version, ".")
	var major, minor int
	var err error

	if len(parts) > 0 {
		major, err = strconv.Atoi(parts[0])
		if err != nil {
			return "", err
		}
	}

	if len(parts) > 1 {
		minor, err = strconv.Atoi(parts[1])
		if err != nil {
			return "", err
		}
	}

	switch major {
	case 26:
		return "Tahoe", nil
	case 15:
		return "Sequoia", nil
	case 14:
		return "Sonoma", nil
	case 13:
		return "Ventura", nil
	case 12:
		return "Monterey", nil
	case 11:
		return "Big Sur", nil
	case 10:
		switch minor {
		case 16:
			// Apparently, this one can happen.
			return "Big Sur", nil
		case 15:
			return "Catalina", nil
		case 14:
			return "Mojave", nil
		case 13:
			return "High Sierra", nil
		case 12:
			return "Sierra", nil
		case 11:
			return "El Capitan", nil
		case 10:
			return "Yosemite", nil
		case 9:
			return "Mavericks", nil
		case 8:
			return "Mountain Lion", nil
		case 7:
			return "Lion", nil
		case 6:
			return "Snow Leopard", nil
		case 5:
			return "Leopard", nil
		case 4:
			return "Tiger", nil
		case 3:
			return "Panther", nil
		case 2:
			return "Jaguar", nil
		case 1:
			return "Puma", nil
		case 0:
			return "Cheetah", nil
		}
	}

	return "", errors.New("Unknown macOS version")
}

func osGetOSState() *api.InstanceStateOSInfo {
	swVers, err := subprocess.RunCommand("sw_vers")
	if err != nil {
		return nil
	}

	var productName, productVersion string
	for _, line := range strings.Split(strings.TrimSpace(swVers), "\n") {
		key, after, found := strings.Cut(line, ":")
		if !found {
			continue
		}

		value := strings.TrimSpace(after)
		if key == "ProductName" {
			productName = value
		} else if key == "ProductVersion" {
			productVersion = value
		}
	}

	// Add the familiar version name if we are dealing with a known macOS version.
	if productName == "Mac OS X" || productName == "macOS" {
		versionName, err := macOSVersionName(productVersion)
		if err == nil {
			productVersion += " (" + versionName + ")"
		}
	}

	uname := unix.Utsname{}
	err = unix.Uname(&uname)
	if err != nil {
		return nil
	}

	serverName := parseBytes(uname.Nodename[:])

	// Prepare OS struct.
	osInfo := &api.InstanceStateOSInfo{
		OS:            productName,
		OSVersion:     productVersion,
		KernelVersion: parseBytes(uname.Release[:]),
		Hostname:      serverName,
		FQDN:          serverName,
	}

	return osInfo
}

// openPty is is the same as linux.OpenPty for Darwin.
func openPty(uid, gid int64) (*os.File, *os.File, error) {
	reverter := revert.New()
	defer reverter.Fail()

	fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0)
	if err != nil {
		return nil, nil, err
	}

	ptx := os.NewFile(uintptr(fd), "")
	reverter.Add(func() { _ = ptx.Close() })

	// Unlock the ptx and pty.
	_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(ptx.Fd()), unix.TIOCPTYUNLK, 0)
	if errno != 0 {
		return nil, nil, unix.Errno(errno)
	}

	var ptyName [256]byte
	_, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(ptx.Fd()), unix.TIOCPTYGNAME, uintptr(unsafe.Pointer(&ptyName)))
	if errno != 0 {
		return nil, nil, unix.Errno(errno)
	}

	pty, err := os.OpenFile(parseBytes(ptyName[:]), unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0)
	if err != nil {
		return nil, nil, err
	}

	reverter.Add(func() { _ = pty.Close() })

	// Configure both sides
	for _, entry := range []*os.File{ptx, pty} {
		// Get termios.
		t, err := unix.IoctlGetTermios(int(entry.Fd()), unix.TIOCGETA)
		if err != nil {
			return nil, nil, err
		}

		// Set flags.
		t.Cflag |= unix.IMAXBEL
		t.Cflag |= unix.IUTF8
		t.Cflag |= unix.BRKINT
		t.Cflag |= unix.IXANY
		t.Cflag |= unix.HUPCL

		// Set termios.
		err = unix.IoctlSetTermios(int(entry.Fd()), unix.TIOCSETA, t)
		if err != nil {
			return nil, nil, err
		}

		// Set the default window size.
		sz := &unix.Winsize{
			Col: 80,
			Row: 25,
		}

		err = unix.IoctlSetWinsize(int(entry.Fd()), unix.TIOCSWINSZ, sz)
		if err != nil {
			return nil, nil, err
		}

		// Set CLOEXEC.
		_, _, errno = unix.Syscall(unix.SYS_FCNTL, uintptr(entry.Fd()), unix.F_SETFD, unix.FD_CLOEXEC)
		if errno != 0 {
			return nil, nil, unix.Errno(errno)
		}
	}

	// Fix the ownership of the pty side.
	err = unix.Fchown(int(pty.Fd()), int(uid), int(gid))
	if err != nil {
		return nil, nil, err
	}

	reverter.Success()

	return ptx, pty, nil
}

func osSetEnv(post *api.InstanceExecPost, env map[string]string) {
	env["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

	// If running as root, set some env variables.
	if post.User == 0 {
		// Set default value for HOME. Fix /root.
		home, ok := env["HOME"]
		if !ok || home == "/root" {
			env["HOME"] = "/var/root"
		}

		// Set default value for USER.
		_, ok = env["USER"]
		if !ok {
			env["USER"] = "root"
		}
	}

	// Set default value for LANG.
	_, ok := env["LANG"]
	if !ok {
		env["LANG"] = "C.UTF-8"
	}

	// Set the default working directory.
	if post.Cwd == "" {
		post.Cwd = env["HOME"]
		if post.Cwd == "" {
			post.Cwd = osBaseWorkingDirectory
		}
	}
}
