package kubelwagen

import (
	"time"

	"github.com/hanwen/go-fuse/fuse"
	"github.com/hanwen/go-fuse/fuse/nodefs"
	"github.com/hanwen/go-fuse/fuse/pathfs"
	"github.com/hanwen/go-fuse/unionfs"
	"github.com/sirupsen/logrus"
)

type WsFs struct {
	pathfs.FileSystem
	req chan RequestCallback
}

type WsFsOpts struct {
	ReadOnly bool
	NonEmpty bool
}

func NewWsFs(opts WsFsOpts, req chan RequestCallback, closer chan bool) *pathfs.PathNodeFs {
	var fs pathfs.FileSystem
	wfs := &WsFs{
		FileSystem: pathfs.NewDefaultFileSystem(),
		req:        req,
	}

	// TODO(barakmich): spin up a goroutine to handle notify requests
	fs = wfs
	if opts.ReadOnly {
		fs = pathfs.NewReadonlyFileSystem(fs)
	}
	fs = unionfs.NewCachingFileSystem(fs, 1*time.Second)
	return pathfs.NewPathNodeFs(fs, nil)
}

func getChannel() chan Response {
	return make(chan Response)
}

func (fs *WsFs) String() string {
	return "kubelwagen"
}

func (fs *WsFs) getResponse(r *Request) (Response, bool) {
	c := getChannel()
	fs.req <- RequestCallback{
		message:  *r,
		response: c,
	}
	resp, ok := <-c
	if !ok {
		logrus.Errorln("Response to request channel closed")
	}
	return resp, ok
}

func (fs *WsFs) OpenDir(name string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
	r := Request{
		Method: MethodOpenDir,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fs.FileSystem.OpenDir(name, context)
	}
	return resp.Dirents, resp.Code
}

func (fs *WsFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
	r := Request{
		Method: MethodGetAttr,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fs.FileSystem.GetAttr(name, context)
	}
	return resp.Stat, resp.Code
}

func (fs *WsFs) StatFs(name string) *fuse.StatfsOut {
	r := Request{
		Method: MethodStatFs,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fs.FileSystem.StatFs(name)
	}
	return resp.Statfs
}

func (fs *WsFs) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
	return nil, fuse.ENOATTR
}

func (fs *WsFs) SetXAttr(name string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status {
	return fuse.ENOSYS
}

func (fs *WsFs) ListXAttr(name string, context *fuse.Context) ([]string, fuse.Status) {
	return nil, fuse.ENOSYS
}

func (fs *WsFs) RemoveXAttr(name string, attr string, context *fuse.Context) fuse.Status {
	return fuse.ENOSYS
}

func (fs *WsFs) Readlink(name string, context *fuse.Context) (string, fuse.Status) {
	r := Request{
		Method: MethodReadLink,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return "", fuse.ENOSYS
	}
	return resp.LinkStr, resp.Code
}

func (fs *WsFs) Mknod(name string, mode uint32, dev uint32, context *fuse.Context) fuse.Status {
	r := Request{
		Method: MethodMknod,
		Path:   name,
		Mode:   mode,
		Dev:    int32(dev),
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Mkdir(name string, mode uint32, context *fuse.Context) fuse.Status {
	r := Request{
		Method: MethodMkdir,
		Path:   name,
		Mode:   mode,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Unlink(name string, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method: MethodUnlink,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Rmdir(name string, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method: MethodRmdir,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Symlink(value string, linkName string, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method:  MethodSymlink,
		Path:    linkName,
		NewPath: value,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Rename(oldName string, newName string, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method:  MethodRename,
		Path:    oldName,
		NewPath: newName,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Link(oldName string, newName string, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method:  MethodLink,
		Path:    oldName,
		NewPath: newName,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Chmod(name string, mode uint32, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method: MethodChmod,
		Path:   name,
		Mode:   mode,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Chown(name string, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method: MethodChown,
		Path:   name,
		UID:    uid,
		GID:    gid,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Truncate(name string, offset uint64, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method: MethodChown,
		Path:   name,
		Offset: int64(offset),
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Access(name string, mode uint32, context *fuse.Context) (code fuse.Status) {
	r := Request{
		Method: MethodAccess,
		Path:   name,
		Mode:   mode,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return fuse.ENOSYS
	}
	return resp.Code
}

func (fs *WsFs) Utimens(name string, Atime *time.Time, Mtime *time.Time, context *fuse.Context) (code fuse.Status) {
	return fuse.ENOSYS
}

func (fs *WsFs) Open(name string, flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) {
	r := Request{
		Method: MethodOpen,
		Flags:  flags,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return nil, fuse.ENOSYS
	}
	if resp.Code != fuse.OK {
		return nil, resp.Code
	}
	return newWsFsFile(resp.FileHandle, fs), resp.Code
}

func (fs *WsFs) Create(name string, flags uint32, mode uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) {
	r := Request{
		Method: MethodCreate,
		Flags:  flags,
		Mode:   mode,
		Path:   name,
	}
	resp, ok := fs.getResponse(&r)
	if !ok {
		return nil, fuse.ENOSYS
	}
	if resp.Code != fuse.OK {
		return nil, resp.Code
	}
	return newWsFsFile(resp.FileHandle, fs), resp.Code
}
