//go:build unix

package nfs

import (
	"context"
	"fmt"
	"net"
	"strings"

	"github.com/go-git/go-billy/v5"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/log"
	"github.com/rclone/rclone/vfs"
	"github.com/willscott/go-nfs"
)

// Handler returns a NFS backing that exposes a given file system in response to all mount requests.
type Handler struct {
	vfs     *vfs.VFS
	opt     Options
	billyFS *FS
	Cache
}

// NewHandler creates a handler for the provided filesystem
func NewHandler(vfs *vfs.VFS, opt *Options) (handler nfs.Handler, err error) {
	h := &Handler{
		vfs:     vfs,
		opt:     *opt,
		billyFS: &FS{vfs: vfs},
	}
	h.opt.HandleLimit = h.opt.Limit()
	h.Cache, err = h.getCache()
	if err != nil {
		return nil, fmt.Errorf("failed to make cache: %w", err)
	}
	nfs.SetLogger(&logIntercepter{Level: nfs.DebugLevel})
	return h, nil
}

// Mount backs Mount RPC Requests, allowing for access control policies.
func (h *Handler) Mount(ctx context.Context, conn net.Conn, req nfs.MountRequest) (status nfs.MountStatus, hndl billy.Filesystem, auths []nfs.AuthFlavor) {
	auths = []nfs.AuthFlavor{nfs.AuthFlavorNull}
	return nfs.MountStatusOk, h.billyFS, auths
}

// Change provides an interface for updating file attributes.
func (h *Handler) Change(fs billy.Filesystem) billy.Change {
	if c, ok := fs.(billy.Change); ok {
		return c
	}
	return nil
}

// FSStat provides information about a filesystem.
func (h *Handler) FSStat(ctx context.Context, f billy.Filesystem, s *nfs.FSStat) error {
	total, _, free := h.vfs.Statfs()
	s.TotalSize = uint64(total)
	s.FreeSize = uint64(free)
	s.AvailableSize = uint64(free)
	return nil
}

// ToHandle takes a file and represents it with an opaque handle to reference it.
// In stateless nfs (when it's serving a unix fs) this can be the device + inode
// but we can generalize with a stateful local cache of handed out IDs.
func (h *Handler) ToHandle(f billy.Filesystem, s []string) (b []byte) {
	defer log.Trace("nfs", "path=%q", s)("handle=%X", &b)
	return h.Cache.ToHandle(f, s)
}

// FromHandle converts from an opaque handle to the file it represents
func (h *Handler) FromHandle(b []byte) (f billy.Filesystem, s []string, err error) {
	defer log.Trace("nfs", "handle=%X", b)("path=%q, err=%v", &s, &err)
	return h.Cache.FromHandle(b)
}

// HandleLimit exports how many file handles can be safely stored by this cache.
func (h *Handler) HandleLimit() int {
	return h.Cache.HandleLimit()
}

// InvalidateHandle invalidates the handle passed - used on rename and delete
func (h *Handler) InvalidateHandle(f billy.Filesystem, b []byte) (err error) {
	defer log.Trace("nfs", "handle=%X", b)("err=%v", &err)
	return h.Cache.InvalidateHandle(f, b)
}

// Limit overrides the --nfs-cache-handle-limit value if out-of-range
func (o *Options) Limit() int {
	if o.HandleLimit < 0 {
		return 1000000
	}
	if o.HandleLimit <= 5 {
		return 5
	}
	return o.HandleLimit
}

// OnUnmountFunc registers a function to call when externally unmounted
var OnUnmountFunc func()

func onUnmount() {
	fs.Infof(nil, "unmount detected")
	if OnUnmountFunc != nil {
		OnUnmountFunc()
	}
}

// logIntercepter intercepts noisy go-nfs logs and reroutes them to DEBUG
type logIntercepter struct {
	Level nfs.LogLevel
}

// Intercept intercepts go-nfs logs and calls fs.Debugf instead
func (l *logIntercepter) Intercept(args ...interface{}) {
	args = append([]interface{}{"[NFS DEBUG] "}, args...)
	argsS := fmt.Sprint(args...)
	fs.Debugf(nil, "%v", argsS)
}

// Interceptf intercepts go-nfs logs and calls fs.Debugf instead
func (l *logIntercepter) Interceptf(format string, args ...interface{}) {
	argsS := fmt.Sprint(args...)
	// bit of a workaround... the real fix is probably https://github.com/willscott/go-nfs/pull/28
	if strings.Contains(argsS, "mount.Umnt") {
		onUnmount()
	}

	fs.Debugf(nil, "[NFS DEBUG] "+format, args...)
}

// Debug reroutes go-nfs Debug messages to Intercept
func (l *logIntercepter) Debug(args ...interface{}) {
	l.Intercept(args...)
}

// Debugf reroutes go-nfs Debugf messages to Interceptf
func (l *logIntercepter) Debugf(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// Error reroutes go-nfs Error messages to Intercept
func (l *logIntercepter) Error(args ...interface{}) {
	l.Intercept(args...)
}

// Errorf reroutes go-nfs Errorf messages to Interceptf
func (l *logIntercepter) Errorf(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// Fatal reroutes go-nfs Fatal messages to Intercept
func (l *logIntercepter) Fatal(args ...interface{}) {
	l.Intercept(args...)
}

// Fatalf reroutes go-nfs Fatalf messages to Interceptf
func (l *logIntercepter) Fatalf(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// GetLevel returns the nfs.LogLevel
func (l *logIntercepter) GetLevel() nfs.LogLevel {
	return l.Level
}

// Info reroutes go-nfs Info messages to Intercept
func (l *logIntercepter) Info(args ...interface{}) {
	l.Intercept(args...)
}

// Infof reroutes go-nfs Infof messages to Interceptf
func (l *logIntercepter) Infof(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// Panic reroutes go-nfs Panic messages to Intercept
func (l *logIntercepter) Panic(args ...interface{}) {
	l.Intercept(args...)
}

// Panicf reroutes go-nfs Panicf messages to Interceptf
func (l *logIntercepter) Panicf(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// ParseLevel parses the nfs.LogLevel
func (l *logIntercepter) ParseLevel(level string) (nfs.LogLevel, error) {
	return nfs.Log.ParseLevel(level)
}

// Print reroutes go-nfs Print messages to Intercept
func (l *logIntercepter) Print(args ...interface{}) {
	l.Intercept(args...)
}

// Printf reroutes go-nfs Printf messages to Intercept
func (l *logIntercepter) Printf(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// SetLevel sets the nfs.LogLevel
func (l *logIntercepter) SetLevel(level nfs.LogLevel) {
	l.Level = level
}

// Trace reroutes go-nfs Trace messages to Intercept
func (l *logIntercepter) Trace(args ...interface{}) {
	l.Intercept(args...)
}

// Tracef reroutes go-nfs Tracef messages to Interceptf
func (l *logIntercepter) Tracef(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}

// Warn reroutes go-nfs Warn messages to Intercept
func (l *logIntercepter) Warn(args ...interface{}) {
	l.Intercept(args...)
}

// Warnf reroutes go-nfs Warnf messages to Interceptf
func (l *logIntercepter) Warnf(format string, args ...interface{}) {
	l.Interceptf(format, args...)
}
