package mount

import (
	"context"
	"errors"
	"os"
	"syscall"
	"time"

	"github.com/seaweedfs/go-fuse/v2/fuse"
	"google.golang.org/protobuf/proto"

	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)

/** Create a directory
 *
 * Note that the mode argument may not have the type specification
 * bits set, i.e. S_ISDIR(mode) can be false.  To obtain the
 * correct directory type bits use  mode|S_IFDIR
 * */
func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out *fuse.EntryOut) (code fuse.Status) {

	if wfs.IsOverQuotaWithUncommitted() {
		return fuse.Status(syscall.ENOSPC)
	}

	var s fuse.Status
	if name, s = checkName(name); s != fuse.OK {
		return s
	}

	now := time.Now().Unix()

	dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId)
	if code != fuse.OK {
		return
	}

	entryFullPath := dirFullPath.Child(name)

	// Pre-allocate the mount's local inode and stamp it into the create
	// request so both the mount and the filer agree on object identity from
	// the start. Without this, the filer assigns its own inode in CreateEntry
	// and the cached entry then reports a different value than the one we
	// return to the kernel here.
	inode := wfs.inodeToPath.AllocateInode(entryFullPath, now)
	newEntry := &filer_pb.Entry{
		Name:        name,
		IsDirectory: true,
		Attributes: &filer_pb.FuseAttributes{
			Mtime:    now,
			Crtime:   now,
			Ctime:    now,
			FileMode: uint32(os.ModeDir) | in.Mode,
			Uid:      in.Uid,
			Gid:      in.Gid,
			Inode:    inode,
		},
	}

	wfs.mapPbIdFromLocalToFiler(newEntry)
	// Defer restoring to local uid/gid AFTER the entry is sent to the filer
	// but BEFORE outputPbEntry writes attributes to the kernel.  We restore
	// explicitly below instead of using defer so the kernel gets local values.

	request := &filer_pb.CreateEntryRequest{
		// Defensive: dirFullPath is clean by construction for mount-originated
		// mutations, but could carry invalid-UTF-8 bytes if metaCache was
		// populated from a non-gRPC source (direct store write, legacy import).
		// Sanitizing here keeps the marshal strictly per-request on the off
		// chance invalid bytes do reach us.
		Directory:                dirFullPath.Sanitized(),
		Entry:                    newEntry,
		Signatures:               []int32{wfs.signature},
		SkipCheckParentDirectory: true,
		// mkdir(2) is exclusive by contract: creating an existing path must
		// fail with EEXIST. Without OExcl the filer treats a concurrent
		// duplicate as an update and reports success to both callers; the
		// kernel's pre-mkdir lookup only catches the duplicate when the
		// winner's create is already visible, which a cross-node race defeats.
		// The filer routes an OExcl create to the entry's ring owner, so its
		// per-path lock arbitrates every creator in the cluster.
		OExcl: true,
	}

	glog.V(1).Infof("mkdir: %v", request)
	resp, err := wfs.streamCreateEntry(context.Background(), request)
	if err != nil {
		glog.V(0).Infof("mkdir %s: %v", entryFullPath, err)
	} else {
		event := resp.GetMetadataEvent()
		if event == nil {
			event = metadataCreateEvent(string(dirFullPath), newEntry)
		}
		if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
			glog.Warningf("mkdir %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
			wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
		}
		wfs.inodeToPath.TouchDirectory(dirFullPath)
		wfs.touchDirMtimeCtimeBest(dirFullPath)
		wfs.inodeToPath.AdjustSubdirCount(dirFullPath, 1)
	}

	glog.V(3).Infof("mkdir %s: %v", entryFullPath, err)

	if err != nil {
		wfs.mapPbIdFromFilerToLocal(newEntry)
		if errors.Is(err, filer_pb.ErrEntryAlreadyExists) {
			// Lost a create race: the path exists on the filer but not yet in
			// this mount's cache. Drop the parent's children cache so the next
			// lookup fetches the winner's entry instead of waiting for the
			// metadata subscription to deliver it.
			wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
			return fuse.Status(syscall.EEXIST)
		}
		return fuse.EIO
	}

	// Map uid/gid back to local-space before writing attributes to the
	// kernel.  The kernel (especially macFUSE) caches these and uses them
	// for subsequent permission checks on children.
	wfs.mapPbIdFromFilerToLocal(newEntry)

	inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, true, false, inode, true)

	// The newly created directory is guaranteed to be empty, so mark it as
	// cached immediately to avoid a needless filer round-trip on the first
	// Lookup or ReadDir inside this directory.
	wfs.inodeToPath.MarkChildrenCached(entryFullPath)

	wfs.outputPbEntry(out, inode, newEntry)

	return fuse.OK

}

/** Remove a directory */
func (wfs *WFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) {

	if name == "." {
		return fuse.Status(syscall.EINVAL)
	}
	if name == ".." {
		return fuse.Status(syscall.ENOTEMPTY)
	}

	// Sanitize before it reaches DeleteEntryRequest.Name; see sanitizeFuseName.
	name = sanitizeFuseName(name)

	dirFullPath, code := wfs.inodeToPath.GetPath(header.NodeId)
	if code != fuse.OK {
		return
	}
	entryFullPath := dirFullPath.Child(name)

	targetEntry, _, targetCode := wfs.maybeLoadEntry(entryFullPath)
	if targetCode != fuse.OK {
		targetEntry = nil
	}

	// POSIX: enforce sticky bit on the parent directory.
	if dirEntry, _, dirCode := wfs.maybeLoadEntry(dirFullPath); dirCode == fuse.OK && dirEntry != nil && dirEntry.Attributes != nil {
		targetUid := uint32(0)
		if targetEntry != nil && targetEntry.Attributes != nil {
			targetUid = targetEntry.Attributes.Uid
		}
		if code := checkStickyBit(dirEntry.Attributes.FileMode, dirEntry.Attributes.Uid, targetUid, header.Uid); code != fuse.OK {
			return code
		}
	}

	glog.V(3).Infof("remove directory: %v", entryFullPath)
	deleteReq := &filer_pb.DeleteEntryRequest{
		Directory:            string(dirFullPath),
		Name:                 name,
		IsDeleteData:         true,
		IgnoreRecursiveError: true, // ignore recursion error since the OS should manage it
		Signatures:           []int32{wfs.signature},
	}
	resp, err := wfs.streamDeleteEntry(context.Background(), deleteReq)
	if err != nil {
		glog.V(1).Infof("remove %s: %v", entryFullPath, err)
		if filer.IsNonEmptyFolderError(err) {
			return fuse.Status(syscall.ENOTEMPTY)
		}
		return fuse.ENOENT
	}

	event := metadataDeleteEvent(string(dirFullPath), name, true)
	if resp != nil && resp.MetadataEvent != nil {
		event = resp.MetadataEvent
	}
	if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
		glog.Warningf("rmdir %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
		wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
	}
	// The filer serialized the delete against concurrent updates and returned
	// the entry as it stood; the snapshot loaded above may predate one.
	if oldEntry := resp.GetMetadataEvent().GetEventNotification().GetOldEntry(); oldEntry.GetAttributes() != nil {
		targetEntry = proto.Clone(oldEntry).(*filer_pb.Entry)
		wfs.mapPbIdFromFilerToLocal(targetEntry)
	}
	wfs.inodeToPath.RemovePath(entryFullPath, func(inode uint64) {
		if targetEntry != nil {
			wfs.rememberRemovedDir(inode, targetEntry)
		}
	})
	wfs.inodeToPath.TouchDirectory(dirFullPath)
	wfs.touchDirMtimeCtimeBest(dirFullPath)
	wfs.inodeToPath.AdjustSubdirCount(dirFullPath, -1)

	return fuse.OK

}
