package cephfs

import (
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"strings"
	"time"

	"github.com/ceph/go-ceph/internal/log"
)

var (
	errIsDir = errors.New("is a directory")
)

// MountWrapper provides a wrapper type that adapts a CephFS Mount into a
// io.FS compatible type.
type MountWrapper struct {
	mount       *MountInfo
	enableTrace bool
}

type fileWrapper struct {
	parent *MountWrapper
	file   *File
	name   string
}

type dirWrapper struct {
	parent    *MountWrapper
	directory *Directory
	name      string
}

type dentryWrapper struct {
	parent *MountWrapper
	de     *DirEntryPlus
}

type infoWrapper struct {
	parent *MountWrapper
	sx     *CephStatx
	name   string
}

// Wrap a CephFS Mount object into a new type that is compatible with Go's io.FS
// interface. CephFS Mounts are not compatible with io.FS directly because the
// go-ceph library predates the addition of io.FS to Go as well as the fact that
// go-ceph attempts to provide APIs that match the cephfs libraries first and
// foremost.
func Wrap(mount *MountInfo) *MountWrapper {
	wm := &MountWrapper{mount: mount}
	debugf(wm, "Wrap", "created")
	return wm
}

/* MountWrapper:
** Implements https://pkg.go.dev/io/fs#FS
** Wraps cephfs.MountInfo
 */

// SetTracing configures the MountWrapper and objects connected to it for debug
// tracing. True enables tracing and false disables it. A debug logging
// function must also be set using go-ceph's common.log.SetDebugf function.
func (mw *MountWrapper) SetTracing(enable bool) {
	mw.enableTrace = enable
}

// identify the MountWrapper object for logging purposes.
func (mw *MountWrapper) identify() string {
	return fmt.Sprintf("MountWrapper<%p>", mw)
}

// trace returns true if debug tracing is enabled.
func (mw *MountWrapper) trace() bool {
	return mw.enableTrace
}

// Open opens the named file. This may be either a regular file or a directory.
// Directories opened with this function will return object compatible with the
// io.ReadDirFile interface.
func (mw *MountWrapper) Open(name string) (fs.File, error) {
	debugf(mw, "Open", "(%v)", name)
	// there are a bunch of patterns that fsTetster/testfs looks for that seems
	// under-documented. They mainly seem to try and enforce "clean" paths.
	// look for them and reject them here because ceph libs won't reject on
	// its own
	if strings.HasPrefix(name, "/") ||
		strings.HasSuffix(name, "/.") ||
		strings.Contains(name, "//") ||
		strings.Contains(name, "/./") ||
		strings.Contains(name, "/../") {
		return nil, &fs.PathError{Op: "open", Path: name, Err: errInvalid}
	}

	d, err := mw.mount.OpenDir(name)
	if err == nil {
		debugf(mw, "Open", "(%v): dir ok", name)
		dw := &dirWrapper{parent: mw, directory: d, name: name}
		return dw, nil
	}
	if !errors.Is(err, errNotDir) {
		debugf(mw, "Open", "(%v): dir error: %v", name, err)
		return nil, &fs.PathError{Op: "open", Path: name, Err: err}
	}

	f, err := mw.mount.Open(name, os.O_RDONLY, 0)
	if err == nil {
		debugf(mw, "Open", "(%v): file ok", name)
		fw := &fileWrapper{parent: mw, file: f, name: name}
		return fw, nil
	}
	debugf(mw, "Open", "(%v): file error: %v", name, err)
	return nil, &fs.PathError{Op: "open", Path: name, Err: err}
}

/* fileWrapper:
** Implements https://pkg.go.dev/io/fs#FS
** Wraps cephfs.File
 */

func (fw *fileWrapper) Stat() (fs.FileInfo, error) {
	debugf(fw, "Stat", "()")
	sx, err := fw.file.Fstatx(StatxBasicStats, AtSymlinkNofollow)
	if err != nil {
		debugf(fw, "Stat", "() -> err:%v", err)
		return nil, &fs.PathError{Op: "stat", Path: fw.name, Err: err}
	}
	debugf(fw, "Stat", "() ok")
	return &infoWrapper{fw.parent, sx, path.Base(fw.name)}, nil
}

func (fw *fileWrapper) Read(b []byte) (int, error) {
	debugf(fw, "Read", "(...)")
	return fw.file.Read(b)
}

func (fw *fileWrapper) Close() error {
	debugf(fw, "Close", "()")
	return fw.file.Close()
}

func (fw *fileWrapper) identify() string {
	return fmt.Sprintf("fileWrapper<%p>[%v]", fw, fw.name)
}

func (fw *fileWrapper) trace() bool {
	return fw.parent.trace()
}

/* dirWrapper:
** Implements https://pkg.go.dev/io/fs#ReadDirFile
** Wraps cephfs.Directory
 */

func (dw *dirWrapper) Stat() (fs.FileInfo, error) {
	debugf(dw, "Stat", "()")
	sx, err := dw.parent.mount.Statx(dw.name, StatxBasicStats, AtSymlinkNofollow)
	if err != nil {
		debugf(dw, "Stat", "() -> err:%v", err)
		return nil, &fs.PathError{Op: "stat", Path: dw.name, Err: err}
	}
	debugf(dw, "Stat", "() ok")
	return &infoWrapper{dw.parent, sx, path.Base(dw.name)}, nil
}

func (dw *dirWrapper) Read(_ []byte) (int, error) {
	debugf(dw, "Read", "(...)")
	return 0, &fs.PathError{Op: "read", Path: dw.name, Err: errIsDir}
}

func (dw *dirWrapper) ReadDir(n int) ([]fs.DirEntry, error) {
	debugf(dw, "ReadDir", "(%v)", n)
	if n > 0 {
		return dw.readDirSome(n)
	}
	return dw.readDirAll()
}

const defaultDirReadCount = 256 // how many entries to read per loop

func (dw *dirWrapper) readDirAll() ([]fs.DirEntry, error) {
	debugf(dw, "readDirAll", "()")
	var (
		err     error
		egroup  []fs.DirEntry
		entries = make([]fs.DirEntry, 0)
		size    = defaultDirReadCount
	)
	for {
		egroup, err = dw.readDirSome(size)
		entries = append(entries, egroup...)
		if err == io.EOF {
			err = nil
			break
		}
		if err != nil {
			break
		}
	}
	debugf(dw, "readDirAll", "() -> len:%v, err:%v", len(entries), err)
	return entries, err
}

func (dw *dirWrapper) readDirSome(n int) ([]fs.DirEntry, error) {
	debugf(dw, "readDirSome", "(%v)", n)
	var (
		idx     int
		err     error
		entry   *DirEntryPlus
		entries = make([]fs.DirEntry, n)
	)
	for {
		entry, err = dw.directory.ReadDirPlus(StatxBasicStats, AtSymlinkNofollow)
		debugf(dw, "readDirSome", "(%v): got entry:%v, err:%v", n, entry, err)
		if err != nil || entry == nil {
			break
		}
		switch entry.Name() {
		case ".", "..":
			continue
		}
		entries[idx] = &dentryWrapper{dw.parent, entry}
		idx++
		if idx >= n {
			break
		}
	}
	if idx == 0 {
		debugf(dw, "readDirSome", "(%v): EOF", n)
		return nil, io.EOF
	}
	debugf(dw, "readDirSome", "(%v): got entry:%v, err:%v", n, entries[:idx], err)
	return entries[:idx], err
}

func (dw *dirWrapper) Close() error {
	debugf(dw, "Close", "()")
	return dw.directory.Close()
}

func (dw *dirWrapper) identify() string {
	return fmt.Sprintf("dirWrapper<%p>[%v]", dw, dw.name)
}

func (dw *dirWrapper) trace() bool {
	return dw.parent.trace()
}

/* dentryWrapper:
** Implements https://pkg.go.dev/io/fs#DirEntry
** Wraps cephfs.DirEntryPlus
 */

func (dew *dentryWrapper) Name() string {
	debugf(dew, "Name", "()")
	return dew.de.Name()
}

func (dew *dentryWrapper) IsDir() bool {
	v := dew.de.DType() == DTypeDir
	debugf(dew, "IsDir", "() -> %v", v)
	return v
}

func (dew *dentryWrapper) Type() fs.FileMode {
	m := dew.de.Statx().Mode
	v := cephModeToFileMode(m).Type()
	debugf(dew, "Type", "() -> %v", v)
	return v
}

func (dew *dentryWrapper) Info() (fs.FileInfo, error) {
	debugf(dew, "Info", "()")
	sx := dew.de.Statx()
	name := dew.de.Name()
	return &infoWrapper{dew.parent, sx, name}, nil
}

func (dew *dentryWrapper) identify() string {
	return fmt.Sprintf("dentryWrapper<%p>[%v]", dew, dew.de.Name())
}

func (dew *dentryWrapper) trace() bool {
	return dew.parent.trace()
}

/* infoWrapper:
** Implements https://pkg.go.dev/io/fs#FileInfo
** Wraps cephfs.CephStatx
 */

func (iw *infoWrapper) Name() string {
	debugf(iw, "Name", "()")
	return iw.name
}

func (iw *infoWrapper) Size() int64 {
	debugf(iw, "Size", "() -> %v", iw.sx.Size)
	return int64(iw.sx.Size)
}

func (iw *infoWrapper) Sys() any {
	debugf(iw, "Sys", "()")
	return iw.sx
}

func (iw *infoWrapper) Mode() fs.FileMode {
	v := cephModeToFileMode(iw.sx.Mode)
	debugf(iw, "Mode", "() -> %#o -> %#o/%v", iw.sx.Mode, uint32(v), v.Type())
	return v
}

func (iw *infoWrapper) IsDir() bool {
	v := iw.sx.Mode&modeIFMT == modeIFDIR
	debugf(iw, "IsDir", "() -> %v", v)
	return v
}

func (iw *infoWrapper) ModTime() time.Time {
	v := time.Unix(iw.sx.Mtime.Sec, iw.sx.Mtime.Nsec)
	debugf(iw, "ModTime", "() -> %v", v)
	return v
}

func (iw *infoWrapper) identify() string {
	return fmt.Sprintf("infoWrapper<%p>[%v]", iw, iw.name)
}

func (iw *infoWrapper) trace() bool {
	return iw.parent.trace()
}

/* copy and paste values from the linux headers. We always need to use
** the linux header values, regardless of the platform go-ceph is built
** for. Rather than jumping through header hoops, copy and paste is
** more consistent and reliable.
 */
const (
	/* file type mask */
	modeIFMT = uint16(0170000)
	/* file types */
	modeIFDIR  = uint16(0040000)
	modeIFCHR  = uint16(0020000)
	modeIFBLK  = uint16(0060000)
	modeIFREG  = uint16(0100000)
	modeIFIFO  = uint16(0010000)
	modeIFLNK  = uint16(0120000)
	modeIFSOCK = uint16(0140000)
	/* protection bits */
	modeISUID = uint16(0004000)
	modeISGID = uint16(0002000)
	modeISVTX = uint16(0001000)
)

// cephModeToFileMode takes a linux compatible cephfs mode value
// and returns a Go-compatiable os-agnostic FileMode value.
func cephModeToFileMode(m uint16) fs.FileMode {
	// start with permission bits
	mode := fs.FileMode(m & 0777)
	// file type - inspired by go's src/os/stat_linux.go
	switch m & modeIFMT {
	case modeIFBLK:
		mode |= fs.ModeDevice
	case modeIFCHR:
		mode |= fs.ModeDevice | fs.ModeCharDevice
	case modeIFDIR:
		mode |= fs.ModeDir
	case modeIFIFO:
		mode |= fs.ModeNamedPipe
	case modeIFLNK:
		mode |= fs.ModeSymlink
	case modeIFREG:
		// nothing to do
	case modeIFSOCK:
		mode |= fs.ModeSocket
	}
	// protection bits
	if m&modeISUID != 0 {
		mode |= fs.ModeSetuid
	}
	if m&modeISGID != 0 {
		mode |= fs.ModeSetgid
	}
	if m&modeISVTX != 0 {
		mode |= fs.ModeSticky
	}
	return mode
}

// wrapperObject helps identify an object to be logged.
type wrapperObject interface {
	identify() string
	trace() bool
}

// debugf formats info about a function and logs it.
func debugf(o wrapperObject, fname, format string, args ...any) {
	if o.trace() {
		log.Debugf(fmt.Sprintf("%v.%v: %s", o.identify(), fname, format), args...)
	}
}
