package mount

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/seaweedfs/go-fuse/v2/fuse"
	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
	"github.com/seaweedfs/seaweedfs/weed/pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/util"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const benchDirEntryCount = 200000

// benchSink drains a readdir the way a front end does. sinkLimit is how many
// entries one round accepts, standing in for the kernel reply buffer or the
// WinFsp adapter's batch.
type benchSink struct {
	plus      bool
	takesRef  bool
	sinkLimit int
	count     int
	inodes    []uint64
	lastOff   uint64
	attrs     []fuse.EntryOut
}

func (s *benchSink) reset() {
	s.count = 0
	s.inodes = s.inodes[:0]
	s.attrs = s.attrs[:0]
}

func (s *benchSink) AddEntry(entry fuse.DirEntry) bool {
	if s.count >= s.sinkLimit {
		return false
	}
	s.count++
	s.lastOff = entry.Off
	s.inodes = append(s.inodes, entry.Ino)
	return true
}

func (s *benchSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
	if s.count >= s.sinkLimit {
		return nil
	}
	s.count++
	s.lastOff = entry.Off
	s.inodes = append(s.inodes, entry.Ino)
	s.attrs = append(s.attrs, fuse.EntryOut{})
	return &s.attrs[len(s.attrs)-1]
}

func (s *benchSink) TakesLookupRef() bool { return s.takesRef }

func inodeTableSize(i *InodeToPath) int {
	i.RLock()
	defer i.RUnlock()
	return len(i.inode2path)
}

func newBenchWFS(tb testing.TB, dir util.FullPath, n int) *WFS {
	tb.Helper()

	uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
	if err != nil {
		tb.Fatalf("uid/gid mapper: %v", err)
	}

	root := util.FullPath("/")
	option := &Option{
		ChunkSizeLimit:     1024,
		ConcurrentReaders:  1,
		VolumeServerAccess: "filerProxy",
		FilerAddresses:     []pb.ServerAddress{pb.NewServerAddressWithGrpcPort("127.0.0.1:1", 1)},
		GrpcDialOption:     grpc.WithTransportCredentials(insecure.NewCredentials()),
		FilerMountRootPath: "/",
		MountUid:           99,
		MountGid:           100,
		MountMode:          0o777,
		MountMtime:         time.Now(),
		MountCtime:         time.Now(),
		UidGidMapper:       uidGidMapper,
	}

	wfs := &WFS{
		option:            option,
		signature:         1,
		inodeToPath:       NewInodeToPath(root, 0),
		fhMap:             NewFileHandleToInode(),
		dhMap:             NewDirectoryHandleToInode(),
		fhLockTable:       util.NewLockTable[FileHandleId](),
		hardLinkLockTable: util.NewLockTable[string](),
	}
	wfs.metaCache = meta_cache.NewMetaCache(
		filepath.Join(tb.TempDir(), "meta"),
		uidGidMapper,
		root,
		false,
		func(path util.FullPath) { wfs.inodeToPath.MarkChildrenCached(path) },
		func(path util.FullPath) bool { return wfs.inodeToPath.IsChildrenCached(path) },
		func(meta_cache.EntryInvalidation) {},
		nil,
	)
	tb.Cleanup(wfs.metaCache.Shutdown)

	now := time.Now()
	ctx := context.Background()
	if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{
		FullPath: dir,
		Attr:     filer.Attr{Mode: os.ModeDir | 0o755, Mtime: now, Crtime: now, Uid: 99, Gid: 100},
	}, 0); err != nil {
		tb.Fatalf("insert dir: %v", err)
	}
	for i := 0; i < n; i++ {
		child := dir.Child(fmt.Sprintf("image-%08d.jpg", i))
		if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{
			FullPath: child,
			// The filer stamps an inode on every entry it stores, so a listing
			// arrives with one and never has to derive its own.
			Attr: filer.Attr{Mode: 0o644, Mtime: now, Crtime: now, Uid: 99, Gid: 100, FileSize: 4 << 20, Inode: child.AsInode(now.Unix())},
			// A real file has chunks, and building them is most of what decoding
			// an entry costs.
			// No FileId: BeforeEntrySerialization reparses that legacy string
			// over Fid on the way in, which would make every entry's chunk
			// byte-identical instead of varying per file.
			Chunks: []*filer_pb.FileChunk{{
				Size: 4 << 20, ModifiedTsNs: now.UnixNano(),
				ETag: "1a2b3c4d5e6f7890",
				Fid:  &filer_pb.FileId{VolumeId: 3, FileKey: uint64(i), Cookie: 0x1637037d},
			}},
		}, 0); err != nil {
			tb.Fatalf("insert entry %d: %v", i, err)
		}
	}

	// Mark the tree cached so the listing is served from the meta cache and no
	// filer client is dialled.
	wfs.inodeToPath.MarkChildrenCached(root)
	wfs.inodeToPath.Lookup(dir, now.Unix(), true, false, 0, true)
	wfs.inodeToPath.MarkChildrenCached(dir)

	return wfs
}

// walkOnce enumerates the whole directory the way a front end does: repeated
// rounds against one handle until the listing runs dry, returning whatever
// references the round took.
func walkOnce(tb testing.TB, wfs *WFS, dirInode uint64, sink *benchSink, forgets bool) int {
	dhid, _ := wfs.AcquireDirectoryHandle()
	defer wfs.ReleaseDirectoryHandle(dhid)

	total := 0
	offset := uint64(0)
	for {
		sink.reset()
		status := wfs.doReadDirectory(&fuse.ReadIn{
			InHeader: fuse.InHeader{NodeId: dirInode},
			Fh:       uint64(dhid),
			Offset:   offset,
			Size:     1 << 20,
		}, sink, sink.plus)
		if status != fuse.OK {
			tb.Fatalf("readdir: %v", status)
		}
		if sink.count == 0 {
			return total
		}
		total += sink.count
		if forgets {
			// HasInode keeps this to the entries that really hold a reference,
			// so a listing that took none is not charged for a bogus Forget.
			for _, ino := range sink.inodes {
				if wfs.inodeToPath.HasInode(ino) {
					wfs.Forget(ino, 1)
				}
			}
		}
		if sink.lastOff <= offset {
			return total
		}
		offset = sink.lastOff
	}
}

var benchCases = []struct {
	name string
	plus bool
	// ref mirrors the sink's TakesLookupRef. The kernel's reports true whatever
	// the mode, exactly as fuseDirEntryList does; it is the mode that decides
	// whether a reference is actually granted.
	ref bool
	// forgets is what the front end does after a round: the kernel returns one
	// FORGET per readdirplus entry and none for a plain readdir, and the WinFsp
	// adapter hands back everything a round gave it.
	forgets bool
}{
	{"kernel_readdir", false, true, false},
	{"kernel_readdirplus", true, true, true},
	{"winfsp_readdirplus", true, false, true},
}

func BenchmarkReadDirectory(b *testing.B) {
	dir := util.FullPath("/images")
	for _, tc := range benchCases {
		b.Run(tc.name, func(b *testing.B) {
			wfs := newBenchWFS(b, dir, benchDirEntryCount)
			dirInode, _ := wfs.inodeToPath.GetInode(dir)
			sink := &benchSink{plus: tc.plus, takesRef: tc.ref, sinkLimit: 4096}

			b.ResetTimer()
			b.ReportAllocs()
			for i := 0; i < b.N; i++ {
				if got := walkOnce(b, wfs, dirInode, sink, tc.forgets); got != benchDirEntryCount+2 {
					b.Fatalf("listed %d entries, want %d", got, benchDirEntryCount+2)
				}
			}
			b.StopTimer()
			// What the listing left in the inode table, over the root and the
			// directory itself.
			b.ReportMetric(float64(inodeTableSize(wfs.inodeToPath)), "inodes_left")
		})
	}
}
