package filesystem

import (
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"time"

	copyDir "github.com/otiai10/copy"
)

type fileStat struct {
	name    string
	size    int64
	mode    fs.FileMode
	modTime time.Time
}

func (fs fileStat) Name() string       { return fs.name }
func (fs fileStat) Size() int64        { return fs.size }
func (fs fileStat) Mode() fs.FileMode  { return fs.mode }
func (fs fileStat) ModTime() time.Time { return fs.modTime }
func (fs fileStat) IsDir() bool        { return fs.mode.IsDir() }
func (fs fileStat) Sys() any           { return nil }

type FileSystem struct {
	ReadFile          func(string) ([]byte, error)
	ReadDir           func(string) ([]fs.DirEntry, error)
	WriteFile         func(string, []byte, fs.FileMode) error
	DeleteFile        func(string) error
	MkdirTemp         func(string, string) (string, error)
	RemoveAll         func(string) error
	FileExists        func(string) (bool, error)
	Glob              func(string) ([]string, error)
	FileExistsAt      func(string) bool
	DirectoryExistsAt func(string) bool
	Dir               func(string) string
	Stat              func(string) (os.FileInfo, error)
	Getwd             func() (string, error)
	Chdir             func(string) error
	Abs               func(string) (string, error)
	EvalSymlinks      func(string) (string, error)
	CopyDir           func(src, dst string) error
}

func DefaultFileSystem() *FileSystem {
	dfs := FileSystem{
		ReadDir:      os.ReadDir,
		WriteFile:    os.WriteFile,
		DeleteFile:   os.Remove,
		MkdirTemp:    os.MkdirTemp,
		RemoveAll:    os.RemoveAll,
		Glob:         filepath.Glob,
		Getwd:        os.Getwd,
		Chdir:        os.Chdir,
		EvalSymlinks: filepath.EvalSymlinks,
		Dir:          filepath.Dir,
	}

	dfs.Stat = dfs.stat
	dfs.ReadFile = dfs.readFile
	dfs.FileExistsAt = dfs.fileExistsAtDefault
	dfs.DirectoryExistsAt = dfs.directoryExistsDefault
	dfs.FileExists = dfs.fileExistsDefault
	dfs.Abs = dfs.absDefault
	dfs.CopyDir = dfs.copyDirDefault
	return &dfs
}

func FromFileSystem(params FileSystem) *FileSystem {
	dfs := DefaultFileSystem()

	if params.ReadFile != nil {
		dfs.ReadFile = params.ReadFile
	}
	if params.ReadDir != nil {
		dfs.ReadDir = params.ReadDir
	}
	if params.DeleteFile != nil {
		dfs.DeleteFile = params.DeleteFile
	}
	if params.FileExists != nil {
		dfs.FileExists = params.FileExists
	}
	if params.Glob != nil {
		dfs.Glob = params.Glob
	}
	if params.FileExistsAt != nil {
		dfs.FileExistsAt = params.FileExistsAt
	}
	if params.DirectoryExistsAt != nil {
		dfs.DirectoryExistsAt = params.DirectoryExistsAt
	}
	if params.Stat != nil {
		dfs.Stat = params.Stat
	}
	if params.Getwd != nil {
		dfs.Getwd = params.Getwd
	}
	if params.Chdir != nil {
		dfs.Chdir = params.Chdir
	}
	if params.Abs != nil {
		dfs.Abs = params.Abs
	}
	if params.EvalSymlinks != nil {
		dfs.EvalSymlinks = params.EvalSymlinks
	}
	if params.Dir != nil {
		dfs.Dir = params.Dir
	}
	if params.CopyDir != nil {
		dfs.CopyDir = params.CopyDir
	}
	if params.WriteFile != nil {
		dfs.WriteFile = params.WriteFile
	}
	if params.MkdirTemp != nil {
		dfs.MkdirTemp = params.MkdirTemp
	}
	if params.RemoveAll != nil {
		dfs.RemoveAll = params.RemoveAll
	}
	return dfs
}

func (filesystem *FileSystem) stat(name string) (os.FileInfo, error) {
	if name == "-" {
		return fileStat{mode: 0}, nil
	}
	return os.Stat(name)
}

func (filesystem *FileSystem) readFile(name string) ([]byte, error) {
	if name == "-" {
		return io.ReadAll(os.Stdin)
	}
	return os.ReadFile(name)
}

func (filesystem *FileSystem) fileExistsAtDefault(path string) bool {
	path, err := filesystem.resolveSymlinks(path)
	if err != nil {
		return false
	}
	fileInfo, err := filesystem.Stat(path)
	return err == nil && fileInfo.Mode().IsRegular()
}

func (filesystem *FileSystem) fileExistsDefault(path string) (bool, error) {
	path, err := filesystem.resolveSymlinks(path)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, err
	}
	_, err = filesystem.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (filesystem *FileSystem) directoryExistsDefault(path string) bool {
	path, err := filesystem.resolveSymlinks(path)
	if err != nil {
		return false
	}
	fileInfo, err := filesystem.Stat(path)
	return err == nil && fileInfo.Mode().IsDir()
}

func (filesystem *FileSystem) resolveSymlinks(path string) (string, error) {
	if !filepath.IsAbs(path) && !filepath.IsLocal(path) {
		basePath, err := filesystem.Getwd()
		if err != nil {
			return "", err
		}

		basePath, err = filesystem.EvalSymlinks(basePath)
		if err != nil {
			return "", err
		}
		path, err := filesystem.EvalSymlinks(filepath.Join(basePath, path))
		if err != nil {
			return "", err
		}
		return path, nil
	}
	return path, nil
}

func (filesystem *FileSystem) absDefault(path string) (string, error) {
	path, err := filesystem.resolveSymlinks(path)
	if err != nil {
		return "", err
	}
	return filepath.Abs(path)
}

// copyDirDefault recursively copies a directory tree, preserving permissions.
func (filesystem *FileSystem) copyDirDefault(src string, dst string) error {
	return copyDir.Copy(src, dst, copyDir.Options{Sync: true})
}
