package xfs

import (
	"bytes"
	"io"
	"io/fs"
	"path"
	"path/filepath"
	"strings"
	"time"

	"golang.org/x/xerrors"

	"github.com/masahiro331/go-xfs-filesystem/xfs/utils"
)

var (
	_ fs.FS        = &FileSystem{}
	_ fs.ReadDirFS = &FileSystem{}
	_ fs.StatFS    = &FileSystem{}

	_ fs.File     = &File{}
	_ fs.FileInfo = &FileInfo{}
	_ fs.DirEntry = dirEntry{}

	ErrOpenSymlink = xerrors.New("symlink open not support")
)

var (
	ErrReadSizeFormat   = "failed to read size error: actual(%d), expected(%d)"
	ErrSeekOffsetFormat = "failed to seek offset error: actual(%d), expected(%d)"
)

// FileSystem is implemented io/fs FS interface
type FileSystem struct {
	r         *io.SectionReader
	PrimaryAG AG
	AGs       []AG

	cache Cache[string, any]
}

func Check(r io.Reader) bool {
	_, err := parseSuperBlock(r)
	if err != nil {
		return false
	}
	return true
}

func NewFS(r io.SectionReader, cache Cache[string, any]) (*FileSystem, error) {
	primaryAG, err := ParseAG(&r)
	if err != nil {
		return nil, xerrors.Errorf("failed to parse primary allocation group: %w", err)
	}

	if cache == nil {
		cache = &mockCache[string, any]{}
	}
	fileSystem := FileSystem{
		r:         &r,
		PrimaryAG: *primaryAG,
		AGs:       []AG{*primaryAG},
		cache:     cache,
	}

	AGSize := int64(primaryAG.SuperBlock.Agblocks) * int64(primaryAG.SuperBlock.BlockSize)
	for i := int64(1); i < int64(primaryAG.SuperBlock.Agcount); i++ {
		n, err := r.Seek(AGSize*i, 0)
		if err != nil {
			return nil, xerrors.Errorf("failed to seek file: %w", err)
		}
		if n != AGSize*i {
			return nil, xerrors.Errorf(ErrSeekOffsetFormat, n, AGSize*i)
		}
		ag, err := ParseAG(&r)
		if err != nil {
			return nil, xerrors.Errorf("failed to parse allocation group %d: %w", i, err)
		}
		fileSystem.AGs = append(fileSystem.AGs, *ag)
	}
	return &fileSystem, nil
}

func (xfs *FileSystem) Close() error {
	return nil
}

// parseTimestamp converts an XFS on-disk timestamp to time.Time.
// XFS has two timestamp formats:
//   - Legacy: upper 32 bits = seconds (signed, since Unix epoch), lower 32 bits = nanoseconds
//     (xfs_timestamp_t is {__be32 t_sec, __be32 t_nsec}, read as big-endian uint64)
//   - Bigtime: 64-bit nanoseconds since Dec 13, 1901 20:45:52 UTC
//
// NOTE: This must be used for all inode timestamp fields (Atime, Mtime, Ctime, Crtime)
// when they are exposed, not just Mtime.
func parseTimestamp(ts uint64, bigtime bool) time.Time {
	if bigtime {
		// Bigtime format: 64-bit nanoseconds since bigtime epoch
		sec := int64(ts/1_000_000_000) - XFS_BIGTIME_EPOCH_OFFSET
		nsec := int64(ts % 1_000_000_000)
		return time.Unix(sec, nsec)
	}

	// Legacy format: upper 32 bits = t_sec (signed), lower 32 bits = t_nsec
	sec := int64(int32(ts >> 32))
	nsec := int64(ts & 0xFFFFFFFF)
	return time.Unix(sec, nsec)
}

func (xfs *FileSystem) Stat(name string) (fs.FileInfo, error) {
	const op = "stat"

	f, err := xfs.Open(name)
	if err != nil {
		info, err := xfs.ReadDirInfo(name)
		if err != nil {
			return nil, xfs.wrapError(op, name, xerrors.Errorf("failed to read dir info: %w", err))
		}
		return info, nil
	}
	return f.Stat()
}

func (xfs *FileSystem) newFile(dirEntry dirEntry) (*File, error) {
	var recs []BmbtRec
	if dirEntry.inode.regularExtent != nil {
		recs = dirEntry.inode.regularExtent.bmbtRecs
	} else if dirEntry.inode.regularBtree != nil {
		recs = dirEntry.inode.regularBtree.bmbtRecs
	} else {
		return nil, xerrors.Errorf("unsupported inode: %+v", dirEntry.inode)
	}

	dt := make(dataTable)
	for _, rec := range recs {
		p := rec.Unpack()
		// Unwritten extents (State == 1) are preallocated but not yet written.
		// They should be read as zero-filled data, so skip adding them to the table.
		if p.State != 0 {
			continue
		}
		physicalBlockOffset := xfs.PrimaryAG.SuperBlock.BlockToPhysicalOffset(p.StartBlock)
		for i := int64(0); i < int64(p.BlockCount); i++ {
			dt[int64(p.StartOff)+i] = physicalBlockOffset + i
		}
	}

	return &File{
		fs:           xfs,
		FileInfo:     dirEntry.FileInfo,
		buffer:       bytes.NewBuffer(nil),
		blockSize:    int64(xfs.PrimaryAG.SuperBlock.BlockSize),
		currentBlock: -1,
		table:        dt,
	}, nil
}

func (xfs *FileSystem) ReadDir(name string) ([]fs.DirEntry, error) {
	const op = "read directory"

	dirEntries, err := xfs.readDirEntry(name)
	if err != nil {
		return nil, xfs.wrapError(op, name, err)
	}
	return dirEntries, nil
}

func (xfs *FileSystem) ReadDirInfo(name string) (fs.FileInfo, error) {
	if name == "/" {
		inode, err := xfs.getRootInode()
		if err != nil {
			return nil, xerrors.Errorf("failed to parse root inode: %w", err)
		}
		return FileInfo{
			name:    "/",
			inode:   inode,
			bigtime: xfs.PrimaryAG.SuperBlock.HasBigtime(),
		}, nil
	}
	name = strings.TrimRight(name, string(filepath.Separator))

	dirs, dir := path.Split(name)
	dirEntries, err := xfs.readDirEntry(dirs)
	if err != nil {
		return nil, xerrors.Errorf("failed to read dir entry: %w", err)
	}
	for _, entry := range dirEntries {
		if entry.Name() == strings.Trim(dir, string(filepath.Separator)) {
			return entry.Info()
		}
	}

	return nil, fs.ErrNotExist
}

func (xfs *FileSystem) getRootInode() (*Inode, error) {
	inode, err := xfs.ParseInode(xfs.PrimaryAG.SuperBlock.Rootino)
	if err != nil {
		return nil, xerrors.Errorf("failed to parse root inode: %w", err)
	}
	return inode, nil
}

// TODO: support ReadFile Interface
func (xfs *FileSystem) ReadFile(name string) ([]byte, error) {
	panic("implement me")
	return []byte{}, nil
}

// TODO: support GlobFS Interface
func (xfs *FileSystem) Glob(pattern string) ([]string, error) {
	panic("implement me")
	return []string{}, nil
}

func (xfs *FileSystem) wrapError(op, path string, err error) error {
	return &fs.PathError{
		Op:   op,
		Path: path,
		Err:  err,
	}
}

func (xfs *FileSystem) Open(name string) (fs.File, error) {
	const op = "open"

	if !fs.ValidPath(name) {
		return nil, xfs.wrapError(op, name, fs.ErrInvalid)
	}

	dirName, fileName := filepath.Split(name)
	dirEntries, err := xfs.ReadDir(dirName)
	if err != nil {
		return nil, xfs.wrapError(op, name, xerrors.Errorf("railed to read directory: %w", err))
	}

	for _, entry := range dirEntries {
		if !entry.IsDir() && entry.Name() == fileName {
			if dir, ok := entry.(dirEntry); ok {
				if dir.Type()&fs.ModeSymlink != 0 {
					return nil, ErrOpenSymlink
				}

				f, err := xfs.newFile(dir)
				if err != nil {
					return nil, xerrors.Errorf("failed to new file: %w", err)
				}

				return f, nil
			}
		}
	}
	return nil, fs.ErrNotExist
}

func (xfs *FileSystem) seekInode(n uint64) (int64, error) {
	offset := int64(xfs.PrimaryAG.SuperBlock.InodeAbsOffset(n))
	off, err := xfs.r.Seek(offset, io.SeekStart)
	if err != nil {
		return 0, err
	}
	if off != offset {
		return 0, xerrors.Errorf(ErrSeekOffsetFormat, off, offset)
	}
	return off, nil
}

func (xfs *FileSystem) seekBlock(n int64) (int64, error) {
	offset := n * int64(xfs.PrimaryAG.SuperBlock.BlockSize)
	off, err := xfs.r.Seek(offset, io.SeekStart)
	if err != nil {
		return 0, err
	}
	if off != offset {
		return 0, xerrors.Errorf(ErrSeekOffsetFormat, off, offset)
	}
	return off, nil
}

func (xfs *FileSystem) readBlock(count uint32) ([]byte, error) {
	blockSize := int(xfs.PrimaryAG.SuperBlock.BlockSize)
	buf := make([]byte, 0, blockSize*int(count))
	for i := uint32(0); i < count; i++ {
		b, err := utils.ReadBlockN(xfs.r, blockSize)
		if err != nil {
			return nil, err
		}
		buf = append(buf, b...)
	}
	return buf, nil
}

func (xfs *FileSystem) readDirEntry(name string) ([]fs.DirEntry, error) {
	inode, err := xfs.getRootInode()
	if err != nil {
		return nil, xerrors.Errorf("failed to get root inode: %w", err)
	}

	fileInfos, err := xfs.listFileInfo(inode.inodeCore.Ino)
	if err != nil {
		return nil, xerrors.Errorf("failed to list root inode directory entries: %w", err)
	}

	currentInode := inode
	dirs := strings.Split(strings.Trim(filepath.Clean(name), string(filepath.Separator)), string(filepath.Separator))
	for i, dir := range dirs {
		found := false
		for _, fileInfo := range fileInfos {
			if fileInfo.Name() == dir {
				if !fileInfo.IsDir() {
					return nil, xerrors.Errorf("%s is file, directory: %w", fileInfo.Name(), fs.ErrNotExist)
				}
				found = true
				currentInode = fileInfo.inode
				break
			}
		}
		// when dir string is empty ("", "."), that is root directory
		if !found && (dir != "" && dir != ".") {
			return nil, fs.ErrNotExist
		}

		fileInfos, err = xfs.listFileInfo(currentInode.inodeCore.Ino)
		if err != nil {
			return nil, xerrors.Errorf("failed to list directory entries inode: %d: %w", currentInode.inodeCore.Ino, err)
		}

		// list last directory
		if i == len(dirs)-1 {
			var dirEntries []fs.DirEntry
			for _, fileInfo := range fileInfos {
				// Skip current directory and parent directory
				// infinit loop in walkDir
				if fileInfo.Name() == "." || fileInfo.Name() == ".." {
					continue
				}

				dirEntries = append(dirEntries, dirEntry{fileInfo})
			}
			return dirEntries, nil
		}
	}
	return nil, fs.ErrNotExist
}

func (xfs *FileSystem) listFileInfo(ino uint64) ([]FileInfo, error) {
	entries, err := xfs.listEntries(ino)
	if err != nil {
		return nil, xerrors.Errorf("failed to list entries: %w", err)
	}

	var fileInfos []FileInfo
	for _, entry := range entries {

		inode, err := xfs.ParseInode(entry.InodeNumber())
		if err != nil {
			return nil, xerrors.Errorf("failed to parse inode %d: %w", entry.InodeNumber(), err)
		}
		// TODO: mode use inode.InodeCore.Mode
		fileInfos = append(fileInfos,
			FileInfo{
				name:    entry.Name(),
				inode:   inode,
				bigtime: xfs.PrimaryAG.SuperBlock.HasBigtime(),
			},
		)
	}
	return fileInfos, nil
}

func (xfs *FileSystem) parseTree(bmbtRecs []BmbtRec) ([]Entry, error) {
	var entries []Entry
	for _, b := range bmbtRecs {
		p := b.Unpack()
		blockEntries, err := xfs.parseDir2Block(p)
		if err != nil {
			return nil, xerrors.Errorf("failed to parse dir2 block: %w", err)
		}
		for _, entry := range blockEntries {
			entries = append(entries, entry)
		}
	}
	return entries, nil
}

func (xfs *FileSystem) listEntries(ino uint64) ([]Entry, error) {
	inode, err := xfs.ParseInode(ino)
	if err != nil {
		return nil, xerrors.Errorf("failed to parse inode: %w", err)
	}

	if !inode.inodeCore.IsDir() {
		return nil, xerrors.New("error inode is not directory")
	}

	var entries []Entry
	if inode.directoryLocal != nil {
		for _, entry := range inode.directoryLocal.entries {
			entries = append(entries, entry)
		}
	} else if inode.directoryExtents != nil {
		if len(inode.directoryExtents.bmbtRecs) == 0 {
			return nil, xerrors.New("directory extents tree bmbtRecs is empty error")
		}
		entries, err = xfs.parseTree(inode.directoryExtents.bmbtRecs)
		if err != nil {
			return nil, xerrors.Errorf("failed to parse extents tree: %w", err)
		}
	} else if inode.directoryBtree != nil {
		if len(inode.directoryBtree.bmbtRecs) == 0 {
			return nil, xerrors.New("directory extents btree bmbtRecs is empty error")
		}
		entries, err = xfs.parseTree(inode.directoryBtree.bmbtRecs)
		if err != nil {
			return nil, xerrors.Errorf("failed to parse btree: %w", err)
		}

	} else {
		return nil, xerrors.New("not found entries")
	}

	return entries, nil
}

// FileInfo is implemented io/fs FileInfo interface
type FileInfo struct {
	name    string
	inode   *Inode
	bigtime bool
}

func (i FileInfo) IsDir() bool {
	return i.inode.inodeCore.IsDir()
}

func (i FileInfo) ModTime() time.Time {
	return parseTimestamp(i.inode.inodeCore.Mtime, i.bigtime)
}

func (i FileInfo) Size() int64 {
	return int64(i.inode.inodeCore.Size)
}

func (i FileInfo) Name() string {
	return i.name
}

func (i FileInfo) Sys() interface{} {
	return nil
}

func (i FileInfo) Mode() fs.FileMode {
	m := i.inode.inodeCore.Mode
	// Bottom 9 bits are same in fs.FileMode and XFS inode (unix permission bits)
	translatedMode := fs.FileMode(m & 0o777)

	// Bits 9-12 in the inode are sticky, setuid, and setgid bits
	if m&0o1000 != 0 {
		translatedMode |= fs.ModeSticky
	}
	if m&0o2000 != 0 {
		translatedMode |= fs.ModeSetgid
	}
	if m&0o4000 != 0 {
		translatedMode |= fs.ModeSetuid
	}

	// bits 13-16 are file type bits, defined in stat.h
	switch m & 0xF000 {
	case 0xC000:
		translatedMode |= fs.ModeSocket
	case 0xA000:
		translatedMode |= fs.ModeSymlink
	case 0x8000:
		// Regular file, no flags to set
	case 0x6000:
		translatedMode |= fs.ModeDevice
	case 0x4000:
		translatedMode |= fs.ModeDir
	case 0x2000:
		translatedMode |= fs.ModeCharDevice | fs.ModeDevice
	case 0x1000:
		translatedMode |= fs.ModeNamedPipe
	default:
		translatedMode |= fs.ModeIrregular
	}

	return translatedMode
}

// dirEntry is implemented io/fs DirEntry interface
type dirEntry struct {
	FileInfo
}

func (d dirEntry) Type() fs.FileMode {
	return d.FileInfo.Mode().Type()
}

func (d dirEntry) Info() (fs.FileInfo, error) { return d.FileInfo, nil }

// File is implemented io/fs File interface
type File struct {
	fs *FileSystem
	FileInfo

	buffer *bytes.Buffer

	blockSize    int64
	currentBlock int64
	table        dataTable
}

// map[offset]
type dataTable map[int64]int64

func (f *File) Stat() (fs.FileInfo, error) {
	return &f.FileInfo, nil
}

func (f *File) Read(buf []byte) (int, error) {
	if f.buffer == nil {
		return 0, io.EOF
	}
	if f.buffer.Len() == 0 {
		f.currentBlock++
		if f.currentBlock*f.blockSize >= f.Size() {
			f.buffer = nil
			return 0, io.EOF
		}
	} else {
		return f.buffer.Read(buf)
	}

	offset, ok := f.table[f.currentBlock]
	if !ok {
		if f.Size()-f.blockSize*f.currentBlock < f.blockSize {
			f.buffer.Write(make([]byte, f.Size()-f.blockSize*f.currentBlock))
		} else {
			f.buffer.Write(make([]byte, f.blockSize))
		}
	} else {
		_, err := f.fs.seekBlock(offset)
		if err != nil {
			return 0, xerrors.Errorf("failed to seek block: %w", err)
		}
		b, err := f.fs.readBlock(1)
		if err != nil {
			return 0, xerrors.Errorf("failed to read block: %w", err)
		}

		if f.Size()-f.blockSize*f.currentBlock < f.blockSize {
			b = b[:f.Size()-f.blockSize*f.currentBlock]
		}
		n, err := f.buffer.Write(b)
		if n != len(b) {
			return 0, xerrors.Errorf("write buffer error: actual(%d), expected(%d)", n, len(b))
		}
	}

	return f.buffer.Read(buf)
}

func (f *File) Close() error {
	return nil
}
