// 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/.

//go:build linux

package xfs

import (
	"context"
	"fmt"
	"io/fs"
	"os"
	"strings"

	"golang.org/x/sys/unix"
)

// UnixRoot represents a filesystem wrapper for Unix-like systems.
type UnixRoot struct {
	Shadow string
	FS     FS

	mntfd int
}

// Interface guard.
var _ interface {
	Root
} = (*UnixRoot)(nil)

// OpenFS opens the underlying filesystem.
func (root *UnixRoot) OpenFS() error {
	var err error

	if root.mntfd != 0 {
		return nil
	}

	root.mntfd, err = root.FS.Open()
	if err != nil {
		return fmt.Errorf("failed to create root filesystem: %w", err)
	}

	return nil
}

// Close closes the underlying filesystem.
func (root *UnixRoot) Close() error {
	if root.mntfd == 0 {
		return nil
	}

	root.mntfd = 0

	return root.FS.Close()
}

// RepairFS repairs the underlying filesystem if necessary.
func (root *UnixRoot) RepairFS(ctx context.Context) error {
	return root.FS.Repair(ctx)
}

// Fd returns the file descriptor of the mounted root filesystem.
// It returns an error if the filesystem is not open or has been closed.
// Usage of the returned file descriptior is no longer thread safe.
func (root *UnixRoot) Fd() (int, error) {
	if root.mntfd == 0 {
		return 0, os.ErrClosed
	}

	return root.mntfd, nil
}

// Mkdir creates a new directory in the root filesystem with the specified name and permissions.
func (root *UnixRoot) Mkdir(name string, perm os.FileMode) error {
	return unix.Mkdirat(root.mntfd, strings.TrimLeft(name, "/"), uint32(perm))
}

// Open opens a file in the root filesystem with the specified name in read-only mode.
func (root *UnixRoot) Open(name string) (fs.File, error) {
	return root.OpenFile(strings.TrimLeft(name, "/"), unix.O_RDONLY, 0)
}

// OpenFile opens a file in the root filesystem with the specified name, flags, and permissions.
func (root *UnixRoot) OpenFile(name string, flags int, perm os.FileMode) (File, error) {
	fd, err := unix.Openat(root.mntfd, strings.TrimLeft(name, "/"), flags, uint32(perm))
	if err != nil {
		return nil, err
	}

	return os.NewFile(uintptr(fd), strings.TrimLeft(name, "/")), nil
}

// Remove removes a file or directory from the root filesystem.
func (root *UnixRoot) Remove(name string) error {
	flags := 0

	info, err := root.stat(strings.TrimLeft(name, "/"))
	if err != nil {
		return err
	}

	if info.IsDir() {
		flags = unix.AT_REMOVEDIR
	}

	return unix.Unlinkat(root.mntfd, strings.TrimLeft(name, "/"), flags)
}

// Rename renames a file or directory in the root filesystem from old to new.
func (root *UnixRoot) Rename(oldname, newname string) error {
	return unix.Renameat(root.mntfd, strings.TrimLeft(oldname, "/"), root.mntfd, strings.TrimLeft(newname, "/"))
}

func (root *UnixRoot) stat(name string) (os.FileInfo, error) {
	f, err := root.Open(strings.TrimLeft(name, "/"))
	if err != nil {
		return nil, err
	}
	defer f.Close() //nolint:errcheck

	return f.Stat()
}

// Source returns the source of the underlying filesystem.
func (root *UnixRoot) Source() string {
	return root.FS.Source()
}

// FSType returns the type of the underlying filesystem.
func (root *UnixRoot) FSType() string {
	return root.FS.FSType()
}
