// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package mount

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"syscall"
	"time"

	"github.com/siderolabs/go-retry/retry"
	"golang.org/x/sys/unix"

	"github.com/siderolabs/talos/internal/pkg/selinux"
	"github.com/siderolabs/talos/pkg/xfs"
)

// Point represents a mount point in the filesystem.
type Point struct {
	root         xfs.Root
	keepOpen     bool
	detached     bool
	fstype       string
	source       string
	target       string
	selinuxLabel string
}

// NewPoint creates a new mount point.
func NewPoint(source string, srcfd int, target string, targetfd int, fstype string) *Point {
	return &Point{
		source: source,
		target: target,
		fstype: fstype,
	}
}

// Options represents options for mounting a mount point.
type Options struct {
	SkipIfMounted   bool
	MountAttributes int
	Shared          bool
	Printer         func(string, ...any)
}

// Mount the mount point.
//
//nolint:gocyclo
func (p *Point) Mount(opts Options) error {
	defer p.Release(false) //nolint:errcheck

	if p.detached {
		return nil
	}

	if opts.SkipIfMounted {
		isMounted, err := p.IsMounted()
		if err != nil {
			return err
		}

		if isMounted {
			return nil
		}
	}

	return p.retry(func() error {
		if err := p.moveMount(p.target); err != nil {
			return fmt.Errorf("error mounting %q to %q: %w", p.Source(), p.target, err)
		}

		if err := p.setattr(&unix.MountAttr{
			Attr_set: uint64(opts.MountAttributes),
		}, 0); err != nil {
			return fmt.Errorf("error setting mountattributes on %s: %w", p.target, err)
		}

		if opts.Shared {
			if err := p.Share(); err != nil {
				return fmt.Errorf("error making %q shared: %w", p.target, err)
			}
		}

		return FilterSelinuxLabelErrors(p.Target(), p.FSType(), selinux.SetLabel(p.target, p.selinuxLabel))
	}, false)
}

// FilterSelinuxLabelErrors filters out certain errors when setting the SELinux label on the mount point.
//   - ENOTSUP is ignored for all filesystems, as it indicates that the filesystem does not support extended attributes.
//   - EROFS is ignored for virtiofs, as it indicates that the underlying filesystem is read-only and does not support setting labels.
func FilterSelinuxLabelErrors(target, fstype string, err error) error {
	if err == nil {
		return nil
	}

	if errors.Is(err, unix.ENOTSUP) {
		return nil
	}

	if fstype == "virtiofs" && errors.Is(err, unix.EROFS) {
		return nil
	}

	return fmt.Errorf("error setting selinux label on %q: %w", target, err)
}

// Share makes the mount point shared.
func (p *Point) Share() error {
	if p.detached {
		return syscall.EINVAL
	}

	return p.setattr(&unix.MountAttr{
		Propagation: unix.MS_SHARED,
	}, unix.AT_RECURSIVE)
}

// UnmountOptions represents options for unmounting a mount point.
type UnmountOptions struct {
	Printer   func(string, ...any)
	Recursive bool
}

// Release closes the file descriptor of the underlying mount point.
func (p *Point) Release(force bool) error {
	if p.keepOpen && !force {
		return nil
	}

	return p.root.Close()
}

// Unmount unmounts the mount point, retrying on certain errors.
func (p *Point) Unmount(opts UnmountOptions) error {
	if err := p.Release(true); err != nil {
		return err
	}

	if p.detached {
		return nil
	}

	err := p.retry(func() error {
		return SafeUnmount(context.Background(), opts.Printer, p.target, opts.Recursive)
	}, true)
	if err != nil {
		logSubmounts(opts.Printer, p.target)
		logMountUsers(opts.Printer, p.target)

		return err
	}

	return nil
}

// IsMounted checks if the mount point is mounted by checking the mount on the target.
func (p *Point) IsMounted() (bool, error) {
	f, err := os.Open("/proc/mounts")
	if err != nil {
		return false, err
	}

	defer f.Close() //nolint:errcheck

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())

		if len(fields) < 2 {
			continue
		}

		mountpoint := fields[1]

		if mountpoint == p.target {
			return true, nil
		}
	}

	return false, scanner.Err()
}

// Move the mount point to a new target.
func (p *Point) Move(newTarget string) error {
	return p.moveMount(newTarget)
}

//nolint:gocyclo
func (p *Point) retry(f func() error, isUnmount bool) error {
	return retry.Constant(5*time.Second, retry.WithUnits(50*time.Millisecond)).Retry(func() error {
		if err := f(); err != nil {
			switch err {
			case unix.EBUSY:
				return retry.ExpectedError(err)
			case unix.ENOENT, unix.ENXIO:
				// if udevd triggers BLKRRPART ioctl, partition device entry might disappear temporarily
				return retry.ExpectedError(err)
			case unix.EUCLEAN, unix.EIO:
				if !isUnmount {
					if errRepair := p.root.RepairFS(context.Background()); errRepair != nil {
						return fmt.Errorf("error repairing: %w", errRepair)
					}
				}

				return retry.ExpectedError(err)
			case unix.EINVAL:
				isMounted, checkErr := p.IsMounted()
				if checkErr != nil {
					return retry.ExpectedError(checkErr)
				}

				if !isMounted && !isUnmount {
					if errRepair := p.root.RepairFS(context.Background()); errRepair != nil {
						return fmt.Errorf("error repairing: %w", errRepair)
					}

					return retry.ExpectedError(err)
				}

				if !isMounted && isUnmount { // if partition is already unmounted, ignore EINVAL
					return nil
				}

				return err
			default:
				return err
			}
		}

		return nil
	})
}

func (p *Point) moveMount(target string) error {
	fd, err := p.root.Fd()
	if err != nil {
		if p.Source() != "" {
			if err := unix.MoveMount(unix.AT_FDCWD, p.Source(), unix.AT_FDCWD, target, 0); err != nil {
				return fmt.Errorf("error moving mount from %q to %q: %w", p.Source(), target, err)
			}

			return nil
		}

		return fmt.Errorf("error getting root fd: %w", err)
	}

	if err := unix.MoveMount(fd, "", unix.AT_FDCWD, target, unix.MOVE_MOUNT_F_EMPTY_PATH); err != nil {
		return fmt.Errorf("error moving detached mount fd=%d to %q: %w", fd, target, err)
	}

	return nil
}

// Target returns the target of the mount point.
func (p *Point) Target() string {
	return p.target
}

// Source returns the source of the mount point.
func (p *Point) Source() string {
	if p.source == "" {
		return p.root.Source()
	}

	return p.source
}

// FSType returns the filesystem type of the mount point.
func (p *Point) FSType() string {
	if p.fstype == "" {
		return p.root.FSType()
	}

	return p.fstype
}

// Root returns the underlying xfs.Root of the mount point.
func (p *Point) Root() xfs.Root {
	return p.root
}

// RemountReadOnly remounts the mount point as read-only.
func (p *Point) RemountReadOnly() error {
	if p.detached {
		return nil
	}

	return p.setattr(&unix.MountAttr{
		Attr_set: unix.MOUNT_ATTR_RDONLY,
	}, 0)
}

// RemountReadWrite remounts the mount point as read-write.
func (p *Point) RemountReadWrite() error {
	if p.detached {
		return nil
	}

	return p.setattr(&unix.MountAttr{
		Attr_clr: unix.MOUNT_ATTR_RDONLY,
	}, 0)
}

// SetDisableAccessTime sets or clears the noatime mount attribute.
func (p *Point) SetDisableAccessTime(disable bool) error {
	if p.detached {
		return nil
	}

	if disable {
		return p.setattr(&unix.MountAttr{
			Attr_set: unix.MOUNT_ATTR_NOATIME,
		}, 0)
	}

	return p.setattr(&unix.MountAttr{
		Attr_clr: unix.MOUNT_ATTR_NOATIME,
	}, 0)
}

// SetSecure sets or clears the nosuid and nodev mount attributes.
func (p *Point) SetSecure(secure bool) error {
	if p.detached {
		return nil
	}

	if secure {
		return p.setattr(&unix.MountAttr{
			Attr_set: unix.MOUNT_ATTR_NOSUID | unix.MOUNT_ATTR_NODEV,
		}, 0)
	}

	return p.setattr(&unix.MountAttr{
		Attr_clr: unix.MOUNT_ATTR_NOSUID | unix.MOUNT_ATTR_NODEV,
	}, 0)
}

func (p *Point) setattr(attr *unix.MountAttr, flags int) error {
	if (attr.Attr_set&unix.MOUNT_ATTR_NOATIME) != 0 || (attr.Attr_clr&unix.MOUNT_ATTR_NOATIME) != 0 {
		attr.Attr_clr |= unix.MOUNT_ATTR__ATIME
	}

	fd, err := p.root.Fd()
	if err != nil && !errors.Is(err, os.ErrClosed) {
		return err
	}

	if p.target != "" {
		fd = unix.AT_FDCWD
	} else {
		flags |= unix.AT_EMPTY_PATH
	}

	if err := unix.MountSetattr(fd, p.target, uint(flags), attr); err != nil {
		return fmt.Errorf("setattr failed for fd=%d target=%q flags=%d attr=%#+v: %w", fd, p.target, flags, *attr, err)
	}

	return nil
}
