package chroot

import (
	"errors"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/helper/polyfill"
)

// ChrootHelper is a helper to implement billy.Chroot.
type ChrootHelper struct {
	underlying billy.Filesystem
	base       string
}

// New creates a new filesystem wrapping up the given 'fs'.
// The created filesystem has its base in the given ChrootHelperectory of the
// underlying filesystem.
func New(fs billy.Basic, base string) billy.Filesystem {
	return &ChrootHelper{
		underlying: polyfill.New(fs),
		base:       base,
	}
}

func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
	if isCrossBoundaries(filename) {
		return "", billy.ErrCrossedBoundary
	}

	return fs.Join(fs.Root(), filename), nil
}

func isCrossBoundaries(path string) bool {
	path = filepath.ToSlash(path)
	path = filepath.Clean(path)

	return strings.HasPrefix(path, ".."+string(filepath.Separator))
}

func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
	fullpath, err := fs.underlyingPath(filename)
	if err != nil {
		return nil, err
	}

	f, err := fs.underlying.Create(fullpath)
	if err != nil {
		return nil, err
	}

	return newFile(fs, f, filename), nil
}

func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
	fullpath, err := fs.underlyingPath(filename)
	if err != nil {
		return nil, err
	}

	f, err := fs.underlying.Open(fullpath)
	if err != nil {
		return nil, err
	}

	return newFile(fs, f, filename), nil
}

func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
	fullpath, err := fs.underlyingPath(filename)
	if err != nil {
		return nil, err
	}

	f, err := fs.underlying.OpenFile(fullpath, flag, mode)
	if err != nil {
		return nil, err
	}

	return newFile(fs, f, filename), nil
}

func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
	fullpath, err := fs.underlyingPath(filename)
	if err != nil {
		return nil, err
	}

	return fs.underlying.Stat(fullpath)
}

func (fs *ChrootHelper) Rename(from, to string) error {
	var err error
	from, err = fs.underlyingPath(from)
	if err != nil {
		return err
	}

	to, err = fs.underlyingPath(to)
	if err != nil {
		return err
	}

	return fs.underlying.Rename(from, to)
}

func (fs *ChrootHelper) Remove(path string) error {
	fullpath, err := fs.underlyingPath(path)
	if err != nil {
		return err
	}

	return fs.underlying.Remove(fullpath)
}

func (fs *ChrootHelper) Join(elem ...string) string {
	return fs.underlying.Join(elem...)
}

func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
	fullpath, err := fs.underlyingPath(dir)
	if err != nil {
		return nil, err
	}

	f, err := fs.underlying.(billy.TempFile).TempFile(fullpath, prefix)
	if err != nil {
		return nil, err
	}

	return newFile(fs, f, fs.Join(dir, filepath.Base(f.Name()))), nil
}

func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
	fullpath, err := fs.underlyingPath(path)
	if err != nil {
		return nil, err
	}

	return fs.underlying.(billy.Dir).ReadDir(fullpath)
}

func (fs *ChrootHelper) MkdirAll(filename string, perm os.FileMode) error {
	fullpath, err := fs.underlyingPath(filename)
	if err != nil {
		return err
	}

	return fs.underlying.(billy.Dir).MkdirAll(fullpath, perm)
}

func (fs *ChrootHelper) Lstat(filename string) (os.FileInfo, error) {
	fullpath, err := fs.underlyingPath(filename)
	if err != nil {
		return nil, err
	}

	return fs.underlying.(billy.Symlink).Lstat(fullpath)
}

func (fs *ChrootHelper) Symlink(target, link string) error {
	target = filepath.FromSlash(target)

	// only rewrite target if it's already absolute
	if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
		target = fs.Join(fs.Root(), target)
		target = filepath.Clean(filepath.FromSlash(target))
	}

	link, err := fs.underlyingPath(link)
	if err != nil {
		return err
	}

	return fs.underlying.(billy.Symlink).Symlink(target, link)
}

func (fs *ChrootHelper) Readlink(link string) (string, error) {
	fullpath, err := fs.underlyingPath(link)
	if err != nil {
		return "", err
	}

	target, err := fs.underlying.(billy.Symlink).Readlink(fullpath)
	if err != nil {
		return "", err
	}

	if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
		return target, nil
	}

	target, err = filepath.Rel(fs.base, target)
	if err != nil {
		return "", err
	}

	return string(os.PathSeparator) + target, nil
}

func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error {
	fullpath, err := fs.underlyingPath(path)
	if err != nil {
		return err
	}

	c, ok := fs.underlying.(billy.Chmod)
	if !ok {
		return errors.New("underlying fs does not implement billy.Chmod")
	}
	return c.Chmod(fullpath, mode)
}

func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) {
	fullpath, err := fs.underlyingPath(path)
	if err != nil {
		return nil, err
	}

	return New(fs.underlying, fullpath), nil
}

func (fs *ChrootHelper) Root() string {
	return fs.base
}

func (fs *ChrootHelper) Underlying() billy.Basic {
	return fs.underlying
}

// Capabilities implements the Capable interface.
func (fs *ChrootHelper) Capabilities() billy.Capability {
	return billy.Capabilities(fs.underlying)
}

type file struct {
	billy.File
	name string
}

func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
	filename = fs.Join(fs.Root(), filename)
	filename, _ = filepath.Rel(fs.Root(), filename)

	return &file{
		File: f,
		name: filename,
	}
}

func (f *file) Name() string {
	return f.name
}
