package plg_backend_nfs

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"strings"

	. "github.com/mickael-kerjean/filestash/server/common"

	"github.com/vmware/go-nfs-client/nfs"
	"github.com/vmware/go-nfs-client/nfs/rpc"
	"github.com/vmware/go-nfs-client/nfs/util"
	"github.com/vmware/go-nfs-client/nfs/xdr"
)

type NfsShare struct {
	mount *nfs.Mount
	v     *nfs.Target
	auth  rpc.Auth
	ctx   context.Context
	uid   uint32
	gid   uint32
	gids  []uint32
}

func init() {
	Backend.Register("nfs", NfsShare{})
	util.DefaultLogger.SetDebug(false)
}

func (this NfsShare) Init(params map[string]string, app *App) (IBackend, error) {
	if params["hostname"] == "" {
		return nil, ErrNotFound
	}
	if params["machine_name"] == "" {
		params["machine_name"] = "Filestash"
	}
	params["username"] = params["uid"]
	uid, gid, gids := ExtractUserInfo(params["uid"], params["gid"], params["gids"])
	Log.Debug("plg_backend_nfs::userInfo user=%s uid=%d gid=%d gids=%+v", params["uid"], uid, gid, gids)

	mount, err := nfs.DialMount(params["hostname"])
	if err != nil {
		return nil, err
	}
	auth := NewAuthUnix(params["machine_name"], uid, gid, gids, params["gids"])
	v, err := mount.Mount(
		params["target"],
		auth,
	)
	if err != nil {
		return nil, err
	}
	return NfsShare{mount, v, auth, app.Context, uid, gid, toGids(gids)}, nil
}

func toGids(gids []GroupLabel) []uint32 {
	g := make([]uint32, len(gids))
	for i, _ := range gids {
		g[i] = gids[i].Id
	}
	return g
}

func (this NfsShare) LoginForm() Form {
	return Form{
		Elmnts: []FormElement{
			FormElement{
				Name:  "type",
				Type:  "hidden",
				Value: "nfs",
			},
			FormElement{
				Name:        "hostname",
				Type:        "text",
				Placeholder: "Hostname",
			},
			FormElement{
				Name:        "target",
				Type:        "text",
				Placeholder: "Mount Path",
			},
			FormElement{
				Name:        "advanced",
				Type:        "enable",
				Placeholder: "Advanced",
				Target:      []string{"nfs_uid", "nfs_gid", "nfs_gids", "nfs_machinename", "nfs_chroot"},
			},
			FormElement{
				Id:          "nfs_uid",
				Name:        "uid",
				Type:        "text",
				Placeholder: "UID",
			},
			FormElement{
				Id:          "nfs_gid",
				Name:        "gid",
				Type:        "text",
				Placeholder: "GID",
			},
			FormElement{
				Id:          "nfs_gids",
				Name:        "gids",
				Type:        "text",
				Placeholder: "Auxiliary GIDs",
			},
			FormElement{
				Id:          "nfs_machinename",
				Name:        "machine_name",
				Type:        "text",
				Placeholder: "Machine Name",
			},
			FormElement{
				Id:          "nfs_chroot",
				Name:        "path",
				Type:        "text",
				Placeholder: "Chroot",
			},
		},
	}
}

func (this NfsShare) Meta(path string) Metadata {
	f, _, err := this.v.Lookup(strings.TrimSuffix(path, "/"))
	if err != nil {
		return Metadata{}
	} else if f == nil {
		return Metadata{}
	}
	fattr, ok := f.(*nfs.Fattr)
	if ok == false {
		return Metadata{}
	}

	if fattr == nil { // happen at the root
		return Metadata{}
	}
	var (
		r, w  bool
		perms = fattr.Mode().Perm()
	)
	if perms&0002 != 0 {
		w = true
	}
	if perms&0004 != 0 {
		r = true
	}
	if fattr.UID == this.uid {
		if perms&0400 != 0 {
			r = true
		}
		if perms&0200 != 0 {
			w = true
		}
	}
	if (fattr.GID == this.gid) || isIn(fattr.GID, this.gids) {
		if perms&0040 != 0 {
			r = true
		}
		if perms&0020 != 0 {
			w = true
		}
	}
	return Metadata{
		CanSee:             NewBool(r),
		CanCreateFile:      NewBool(w),
		CanCreateDirectory: NewBool(w),
		CanRename:          NewBool(w),
		CanMove:            NewBool(w),
		CanUpload:          NewBool(w),
		CanDelete:          NewBool(w),
	}
}

func isIn(id uint32, list []uint32) bool {
	for i, _ := range list {
		if list[i] == id {
			return true
		}
	}
	return false
}

func (this NfsShare) Ls(path string) ([]os.FileInfo, error) {
	defer this.Close()
	files := make([]os.FileInfo, 0)

	dirs, err := this.v.ReadDirPlus(path)
	if err != nil {
		return files, err
	}
	for _, dir := range dirs {
		if dir.FileName == "." || dir.FileName == ".." {
			continue
		} else if dir.Attr.Attr.Type != 1 && dir.Attr.Attr.Type != 2 {
			// don't show anything else than file and folder
			continue
		}
		if len(this.gids) > 0 { // filter out what users don't have access
			hasAccess := false
			for _, gid := range this.gids {
				if gid == dir.Attr.Attr.GID {
					hasAccess = true
				}
			}
			if this.gid == dir.Attr.Attr.GID {
				hasAccess = true
			}
			if hasAccess == false {
				continue
			}
		}
		files = append(files, File{
			FName: dir.FileName,
			FType: func() string {
				if dir.Attr.Attr.Type == 1 {
					return "file"
				}
				return "directory"
			}(),
			FSize: int64(dir.Attr.Attr.Filesize),
			FTime: int64(dir.Attr.Attr.Ctime.Seconds),
		})
	}
	return files, nil
}

func (this NfsShare) Cat(path string) (io.ReadCloser, error) {
	go func() {
		<-this.ctx.Done()
		this.Close()
	}()
	rc, err := this.v.OpenFile(path, 0777)
	if err != nil {
		return nil, err
	}
	return rc, nil
}

func (this NfsShare) Mkdir(path string) error {
	defer this.Close()
	_, err := this.v.Mkdir(this.nfsPath(path), 0775)
	return err
}

func (this NfsShare) Rm(path string) error {
	defer this.Close()
	if strings.HasSuffix(path, "/") {
		return this.v.RemoveAll(this.nfsPath(path))
	}
	return this.v.Remove(path)
}

// this wasn't implemented in the original lib and considering
// PR aren't handled by vmware, we did come with the implementation as
// of RFC1813 in: https://www.rfc-editor.org/rfc/rfc1813#section-3.3.14
func (this NfsShare) Mv(from string, to string) error {
	defer this.Close()

	f, fName := filepath.Split(this.nfsPath(from))
	_, fh, err := this.v.Lookup(f)
	if err != nil {
		return err
	}
	t, tName := filepath.Split(this.nfsPath(to))
	_, th, err := this.v.Lookup(t)
	if err != nil {
		return err
	}

	type RenameArgs struct {
		rpc.Header
		From nfs.Diropargs3
		To   nfs.Diropargs3
	}
	const RENAME3res = 14
	res, err := this.v.Call(&RenameArgs{
		Header: rpc.Header{
			Rpcvers: 2,
			Prog:    nfs.Nfs3Prog,
			Vers:    nfs.Nfs3Vers,
			Proc:    RENAME3res,
			Cred:    this.auth,
			Verf:    rpc.AuthNull,
		},
		From: nfs.Diropargs3{
			FH:       fh,
			Filename: fName,
		},
		To: nfs.Diropargs3{
			FH:       th,
			Filename: tName,
		},
	})
	if err != nil {
		return err
	}
	status, err := xdr.ReadUint32(res)
	if err != nil {
		return err
	}
	return nfs.NFS3Error(status)
}

func (this NfsShare) Touch(path string) error {
	return this.Save(path, strings.NewReader(""))
}

func (this NfsShare) Save(path string, file io.Reader) error {
	defer this.Close()
	w, err := this.v.OpenFile(path, 0644)
	if err != nil {
		return err
	}
	_, err = io.Copy(w, file)
	w.Close()
	return err
}

func (this NfsShare) Close() {
	this.v.Close()
	this.mount.Close()
}

func (this NfsShare) nfsPath(path string) string {
	return strings.TrimSuffix(path, "/")
}
