package vfs

import (
	"archive/zip"
	"context"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"strings"

	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
	"git.kmsign.ru/royalcat/tstor/src/iio"
	"github.com/bodgit/sevenzip"
	"github.com/nwaples/rardecode/v2"
)

var ArchiveFactories = map[string]FsFactory{
	".zip": func(ctx context.Context, f File) (Filesystem, error) {
		stat, err := f.Stat()
		if err != nil {
			return nil, err
		}
		return NewArchive(ctx, stat.Name(), f, stat.Size(), ZipLoader), nil
	},
	".rar": func(ctx context.Context, f File) (Filesystem, error) {
		stat, err := f.Stat()
		if err != nil {
			return nil, err
		}
		return NewArchive(ctx, stat.Name(), f, stat.Size(), RarLoader), nil
	},
	".7z": func(ctx context.Context, f File) (Filesystem, error) {
		stat, err := f.Stat()
		if err != nil {
			return nil, err
		}
		return NewArchive(ctx, stat.Name(), f, stat.Size(), SevenZipLoader), nil
	},
}

type archiveLoader func(ctx context.Context, r ctxio.ReaderAt, size int64) (map[string]*archiveFile, error)

var _ Filesystem = &ArchiveFS{}

type ArchiveFS struct {
	name string

	r ctxio.ReaderAt

	Size int64

	files func() (map[string]File, error)
}

func NewArchive(ctx context.Context, name string, r ctxio.ReaderAt, size int64, loader archiveLoader) *ArchiveFS {
	return &ArchiveFS{
		name: name,
		r:    r,
		Size: size,
		files: OnceValueWOErr(func() (map[string]File, error) {
			zipFiles, err := loader(ctx, r, size)
			if err != nil {
				return nil, err
			}
			// TODO make optional
			singleDir := true
			for k := range zipFiles {
				if !strings.HasPrefix(k, "/"+name+"/") {
					singleDir = false
					break
				}
			}

			files := make(map[string]File, len(zipFiles))
			for k, v := range zipFiles {
				// TODO make optional
				if strings.Contains(k, "/__MACOSX/") {
					continue
				}

				if singleDir {
					k, _ = strings.CutPrefix(k, "/"+name)
				}

				files[k] = v
			}

			// FIXME
			files["/.forcegallery"] = NewMemoryFile(".forcegallery", []byte{})

			return files, nil
		}),
	}
}

// Unlink implements Filesystem.
func (a *ArchiveFS) Unlink(ctx context.Context, filename string) error {
	return ErrNotImplemented
}

func (a *ArchiveFS) Open(ctx context.Context, filename string) (File, error) {
	files, err := a.files()
	if err != nil {
		return nil, err
	}

	return getFile(files, filename)
}

func (fs *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
	files, err := fs.files()
	if err != nil {
		return nil, err
	}

	return listDirFromFiles(files, path)
}

// Stat implements Filesystem.
func (afs *ArchiveFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
	files, err := afs.files()
	if err != nil {
		return nil, err
	}

	if file, ok := files[filename]; ok {
		return file.Stat()
	}

	for p, _ := range files {
		if strings.HasPrefix(p, filename) {
			return newDirInfo(path.Base(filename)), nil
		}
	}

	return nil, ErrNotExist
}

// Info implements Filesystem.
func (a *ArchiveFS) Info() (fs.FileInfo, error) {
	return &fileInfo{
		name:  a.name,
		size:  a.Size,
		isDir: true,
	}, nil
}

// IsDir implements Filesystem.
func (a *ArchiveFS) IsDir() bool {
	return true
}

// Name implements Filesystem.
func (a *ArchiveFS) Name() string {
	return a.name
}

// Type implements Filesystem.
func (a *ArchiveFS) Type() fs.FileMode {
	return fs.ModeDir
}

var _ File = &archiveFile{}

func NewArchiveFile(name string, readerFunc func() (iio.Reader, error), size int64) *archiveFile {
	return &archiveFile{
		name:       name,
		readerFunc: readerFunc,
		size:       size,
	}
}

type archiveFile struct {
	name string

	readerFunc func() (iio.Reader, error)
	reader     iio.Reader
	size       int64
}

func (d *archiveFile) Stat() (fs.FileInfo, error) {
	return newFileInfo(d.name, d.size), nil
}

func (d *archiveFile) load() error {
	if d.reader != nil {
		return nil
	}
	r, err := d.readerFunc()
	if err != nil {
		return err
	}

	d.reader = r

	return nil
}

func (d *archiveFile) Size() int64 {
	return d.size
}

func (d *archiveFile) IsDir() bool {
	return false
}

func (d *archiveFile) Close(ctx context.Context) (err error) {
	if d.reader != nil {
		err = d.reader.Close()
		d.reader = nil
	}

	return
}

func (d *archiveFile) Read(ctx context.Context, p []byte) (n int, err error) {
	if err := d.load(); err != nil {
		return 0, err
	}

	return d.reader.Read(p)
}

func (d *archiveFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
	if err := d.load(); err != nil {
		return 0, err
	}

	return d.reader.ReadAt(p, off)
}

var _ archiveLoader = ZipLoader

func ZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
	reader := ctxio.IoReaderAt(ctx, ctxreader)

	zr, err := zip.NewReader(reader, size)
	if err != nil {
		return nil, err
	}

	out := make(map[string]*archiveFile)
	for i := range zr.File {
		zipFile := zr.File[i]
		if zipFile.FileInfo().IsDir() {
			continue
		}

		rf := func() (iio.Reader, error) {
			zr, err := zipFile.Open()
			if err != nil {
				return nil, err
			}

			return iio.NewDiskTeeReader(zr)
		}

		out[AbsPath(zipFile.Name)] = NewArchiveFile(zipFile.Name, rf, zipFile.FileInfo().Size())
	}

	return out, nil
}

var _ archiveLoader = SevenZipLoader

func SevenZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
	reader := ctxio.IoReaderAt(ctx, ctxreader)

	r, err := sevenzip.NewReader(reader, size)
	if err != nil {
		return nil, err
	}

	out := make(map[string]*archiveFile)
	for _, f := range r.File {
		f := f
		if f.FileInfo().IsDir() {
			continue
		}

		rf := func() (iio.Reader, error) {
			zr, err := f.Open()
			if err != nil {
				return nil, err
			}

			return iio.NewDiskTeeReader(zr)
		}

		af := NewArchiveFile(f.Name, rf, f.FileInfo().Size())
		n := filepath.Join(string(os.PathSeparator), f.Name)

		out[n] = af
	}

	return out, nil
}

var _ archiveLoader = RarLoader

func RarLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
	reader := ctxio.IoReadSeekerWrapper(ctx, ctxreader, size)

	r, err := rardecode.NewReader(reader)
	if err != nil {
		return nil, err
	}

	out := make(map[string]*archiveFile)
	for {
		header, err := r.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}

		rf := func() (iio.Reader, error) {
			return iio.NewDiskTeeReader(r)
		}

		n := filepath.Join(string(os.PathSeparator), header.Name)

		af := NewArchiveFile(header.Name, rf, header.UnPackedSize)

		out[n] = af
	}

	return out, nil
}