// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package mount

import (
	"errors"
	"io"
	"log"
	"os"
	"runtime/debug"
	"sync"

	"git.arvados.org/arvados.git/sdk/go/arvados"
	"git.arvados.org/arvados.git/sdk/go/keepclient"
	"github.com/arvados/cgofuse/fuse"
	"github.com/sirupsen/logrus"
)

// sharedFile wraps arvados.File with a sync.Mutex, so fuse can safely
// use a single filehandle concurrently on behalf of multiple
// threads/processes.
type sharedFile struct {
	arvados.File
	sync.Mutex
}

// keepFS implements cgofuse's FileSystemInterface.
type keepFS struct {
	fuse.FileSystemBase
	Client     *arvados.Client
	KeepClient *keepclient.KeepClient
	ReadOnly   bool
	Uid        int
	Gid        int
	Logger     logrus.FieldLogger

	root   arvados.CustomFileSystem
	open   map[uint64]*sharedFile
	lastFH uint64
	sync.RWMutex

	// If non-nil, this channel will be closed by Init() to notify
	// other goroutines that the mount is ready.
	ready chan struct{}
}

var (
	invalidFH = ^uint64(0)
)

// newFH wraps f in a sharedFile, adds it to fs's lookup table using a
// new handle number, and returns the handle number.
func (fs *keepFS) newFH(f arvados.File) uint64 {
	fs.Lock()
	defer fs.Unlock()
	if fs.open == nil {
		fs.open = make(map[uint64]*sharedFile)
	}
	fs.lastFH++
	fh := fs.lastFH
	fs.open[fh] = &sharedFile{File: f}
	return fh
}

func (fs *keepFS) lookupFH(fh uint64) *sharedFile {
	fs.RLock()
	defer fs.RUnlock()
	return fs.open[fh]
}

func (fs *keepFS) Init() {
	defer fs.debugPanics()
	fs.root = fs.Client.SiteFileSystem(fs.KeepClient)
	fs.root.MountProject("home", "")
	if fs.ready != nil {
		close(fs.ready)
	}
}

func (fs *keepFS) Create(path string, flags int, mode uint32) (errc int, fh uint64) {
	defer fs.debugPanics()
	fs.debugOp("Create", path)
	if fs.ReadOnly {
		return -fuse.EROFS, invalidFH
	}
	f, err := fs.root.OpenFile(path, flags|os.O_CREATE, os.FileMode(mode))
	if err == os.ErrExist {
		return -fuse.EEXIST, invalidFH
	} else if err != nil {
		return -fuse.EINVAL, invalidFH
	}
	return 0, fs.newFH(f)
}

func (fs *keepFS) Mknod(path string, mode uint32, dev uint64) int {
	defer fs.debugPanics()
	fs.debugOp("Mknod", path)
	if filetype := mode & uint32(^os.ModePerm); filetype != 0 && filetype != uint32(fuse.S_IFREG) {
		return -fuse.ENOSYS
	}
	if fs.ReadOnly {
		_, err := fs.root.Stat(path)
		if err != nil {
			return -fuse.EROFS
		} else {
			return -fuse.EEXIST
		}
	}
	f, err := fs.root.OpenFile(path, os.O_CREATE|os.O_EXCL, os.FileMode(mode)&os.ModePerm)
	if err != nil {
		return fs.errCode("Mknod", path, err)
	}
	f.Close()
	return 0
}

func (fs *keepFS) Open(path string, flags int) (errc int, fh uint64) {
	defer fs.debugPanics()
	fs.debugOp("Open", path)
	if fs.ReadOnly && flags&(os.O_RDWR|os.O_WRONLY|os.O_CREATE) != 0 {
		return -fuse.EROFS, invalidFH
	}
	f, err := fs.root.OpenFile(path, flags, 0)
	if err != nil {
		return -fuse.ENOENT, invalidFH
	} else if fi, err := f.Stat(); err != nil {
		return -fuse.EIO, invalidFH
	} else if fi.IsDir() {
		f.Close()
		return -fuse.EISDIR, invalidFH
	}
	return 0, fs.newFH(f)
}

func (fs *keepFS) Utimens(path string, tmsp []fuse.Timespec) int {
	defer fs.debugPanics()
	fs.debugOp("Utimens", path)
	if fs.ReadOnly {
		return -fuse.EROFS
	}
	f, err := fs.root.OpenFile(path, 0, 0)
	if err != nil {
		return fs.errCode("Utimens", path, err)
	}
	f.Close()
	return 0
}

func (fs *keepFS) errCode(op, path string, err error) (errc int) {
	if err == nil {
		return 0
	}
	defer func() {
		fs.Logger.WithFields(logrus.Fields{
			"op":    op,
			"path":  path,
			"errno": errc,
			"error": err,
		}).Debug("fuse call returned error")
	}()
	if errors.Is(err, os.ErrNotExist) {
		return -fuse.ENOENT
	}
	if errors.Is(err, os.ErrExist) {
		return -fuse.EEXIST
	}
	if errors.Is(err, arvados.ErrInvalidArgument) {
		return -fuse.EINVAL
	}
	if errors.Is(err, arvados.ErrInvalidOperation) {
		return -fuse.ENOSYS
	}
	if errors.Is(err, arvados.ErrDirectoryNotEmpty) {
		return -fuse.ENOTEMPTY
	}
	return -fuse.EIO
}

func (fs *keepFS) Mkdir(path string, mode uint32) int {
	defer fs.debugPanics()
	fs.debugOp("Mkdir", path)
	if fs.ReadOnly {
		return -fuse.EROFS
	}
	f, err := fs.root.OpenFile(path, os.O_CREATE|os.O_EXCL, os.FileMode(mode)|os.ModeDir)
	if err != nil {
		return fs.errCode("Mkdir", path, err)
	}
	f.Close()
	return 0
}

func (fs *keepFS) Opendir(path string) (errc int, fh uint64) {
	defer fs.debugPanics()
	fs.debugOp("Opendir", path)
	f, err := fs.root.OpenFile(path, 0, 0)
	if err != nil {
		return fs.errCode("Opendir", path, err), invalidFH
	} else if fi, err := f.Stat(); err != nil {
		return fs.errCode("Opendir", path, err), invalidFH
	} else if !fi.IsDir() {
		f.Close()
		return -fuse.ENOTDIR, invalidFH
	}
	return 0, fs.newFH(f)
}

func (fs *keepFS) Releasedir(path string, fh uint64) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Releasedir", path)
	return fs.Release(path, fh)
}

func (fs *keepFS) Rmdir(path string) int {
	defer fs.debugPanics()
	fs.debugOp("Rmdir", path)
	return fs.errCode("Rmdir", path, fs.root.Remove(path))
}

func (fs *keepFS) Release(path string, fh uint64) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Release", path)
	fs.Lock()
	defer fs.Unlock()
	defer delete(fs.open, fh)
	if f := fs.open[fh]; f != nil {
		err := f.Close()
		if err != nil {
			return -fuse.EIO
		}
	}
	return 0
}

func (fs *keepFS) Rename(oldname, newname string) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Rename", oldname+" -> "+newname)
	if fs.ReadOnly {
		return -fuse.EROFS
	}
	return fs.errCode("Rename", oldname+" -> "+newname, fs.root.Rename(oldname, newname))
}

func (fs *keepFS) Unlink(path string) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Unlink", path)
	if fs.ReadOnly {
		return -fuse.EROFS
	}
	return fs.errCode("Unlink", path, fs.root.Remove(path))
}

func (fs *keepFS) Truncate(path string, size int64, fh uint64) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Truncate", path)
	if fs.ReadOnly {
		return -fuse.EROFS
	}

	// Sometimes fh is a valid filehandle and we don't need to
	// waste a name lookup.
	if f := fs.lookupFH(fh); f != nil {
		return fs.errCode("Truncate", path, f.Truncate(size))
	}

	// Other times, fh is invalid and we need to lookup path.
	f, err := fs.root.OpenFile(path, os.O_RDWR, 0)
	if err != nil {
		return fs.errCode("Truncate", path, err)
	}
	defer f.Close()
	return fs.errCode("Truncate", path, f.Truncate(size))
}

func (fs *keepFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Getattr", path)
	var fi os.FileInfo
	var err error
	if f := fs.lookupFH(fh); f != nil {
		// Valid filehandle -- ignore path.
		fi, err = f.Stat()
	} else {
		// Invalid filehandle -- lookup path.
		fi, err = fs.root.Stat(path)
	}
	if err != nil {
		return fs.errCode("Getattr", path, err)
	}
	fs.fillStat(stat, fi)
	return 0
}

func (fs *keepFS) Chmod(path string, mode uint32) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Chmod", path)
	if fs.ReadOnly {
		return -fuse.EROFS
	}
	if fi, err := fs.root.Stat(path); err != nil {
		return fs.errCode("Chmod", path, err)
	} else if mode & ^uint32(fuse.S_IFREG|fuse.S_IFDIR|0777) != 0 {
		// Refuse to set mode bits other than
		// regfile/dir/perms
		return -fuse.ENOSYS
	} else if (fi.Mode()&os.ModeDir != 0) != (mode&fuse.S_IFDIR != 0) {
		// Refuse to transform a regular file to a dir, or
		// vice versa
		return -fuse.ENOSYS
	}
	// As long as the change isn't nonsense, chmod is a no-op,
	// because we don't save permission bits.
	return 0
}

func (fs *keepFS) fillStat(stat *fuse.Stat_t, fi os.FileInfo) {
	defer fs.debugPanics()
	var m uint32
	if fi.IsDir() {
		m = m | fuse.S_IFDIR
	} else {
		m = m | fuse.S_IFREG
	}
	m = m | uint32(fi.Mode()&os.ModePerm)
	stat.Mode = m
	stat.Nlink = 1
	stat.Size = fi.Size()
	t := fuse.NewTimespec(fi.ModTime())
	stat.Mtim = t
	stat.Ctim = t
	stat.Atim = t
	stat.Birthtim = t
	stat.Blksize = 1024
	stat.Blocks = (stat.Size + stat.Blksize - 1) / stat.Blksize
	if fs.Uid > 0 && int64(fs.Uid) < 1<<31 {
		stat.Uid = uint32(fs.Uid)
	}
	if fs.Gid > 0 && int64(fs.Gid) < 1<<31 {
		stat.Gid = uint32(fs.Gid)
	}
}

func (fs *keepFS) Write(path string, buf []byte, ofst int64, fh uint64) (n int) {
	defer fs.debugPanics()
	fs.debugOp("Write", path)
	if fs.ReadOnly {
		return -fuse.EROFS
	}
	f := fs.lookupFH(fh)
	if f == nil {
		return -fuse.EBADF
	}
	f.Lock()
	defer f.Unlock()
	if _, err := f.Seek(ofst, io.SeekStart); err != nil {
		return fs.errCode("Write", path, err)
	}
	n, err := f.Write(buf)
	if err != nil {
		return fs.errCode("Write", path, err)
	}
	return n
}

func (fs *keepFS) Read(path string, buf []byte, ofst int64, fh uint64) (n int) {
	defer fs.debugPanics()
	fs.debugOp("Read", path)
	f := fs.lookupFH(fh)
	if f == nil {
		return -fuse.EBADF
	}
	f.Lock()
	defer f.Unlock()
	if _, err := f.Seek(ofst, io.SeekStart); err != nil {
		return fs.errCode("Read", path, err)
	}
	n, err := f.Read(buf)
	for err == nil && n < len(buf) {
		// f is an io.Reader ("If some data is available but
		// not len(p) bytes, Read conventionally returns what
		// is available instead of waiting for more") -- but
		// our caller requires us to either fill buf or reach
		// EOF.
		done := n
		n, err = f.Read(buf[done:])
		n += done
	}
	if err != nil && err != io.EOF {
		return fs.errCode("Read", path, err)
	}
	return n
}

func (fs *keepFS) Readdir(path string,
	fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
	ofst int64,
	fh uint64) (errc int) {
	defer fs.debugPanics()
	fs.debugOp("Readdir", path)
	f := fs.lookupFH(fh)
	if f == nil {
		return -fuse.EBADF
	}
	fill(".", nil, 0)
	fill("..", nil, 0)
	var stat fuse.Stat_t
	fis, err := f.Readdir(-1)
	if err != nil {
		return fs.errCode("Readdir", path, err)
	}
	for _, fi := range fis {
		fs.fillStat(&stat, fi)
		fill(fi.Name(), &stat, 0)
	}
	return 0
}

func (fs *keepFS) Fsync(path string, datasync bool, fh uint64) int {
	defer fs.debugPanics()
	fs.debugOp("Fsync", path)
	f := fs.lookupFH(fh)
	if f == nil {
		return -fuse.EBADF
	}
	return fs.errCode("Fsync", path, f.Sync())
}

func (fs *keepFS) Fsyncdir(path string, datasync bool, fh uint64) int {
	fs.debugOp("Fsyncdir", path)
	return fs.Fsync(path, datasync, fh)
}

// debugPanics (when deferred by keepFS handlers) prints an error and
// stack trace on stderr when a handler crashes. (Without this,
// cgofuse recovers from panics silently and returns EIO.)
func (fs *keepFS) debugPanics() {
	if err := recover(); err != nil {
		log.Printf("(%T) %v", err, err)
		debug.PrintStack()
		panic(err)
	}
}

func (fs *keepFS) debugOp(op, path string) {
	fs.Logger.WithFields(nil).Tracef("fuse call %s %s", op, path)
}
