package fs

import (
	"context"
	"io"
	"os"
	"sort"
	"time"

	"github.com/pkg/errors"
)

// ErrUnknown is returned by ErrorEntry.ErrorInfo() to indicate that type of an entry is unknown.
var ErrUnknown = errors.Errorf("unknown or unsupported entry type")

// Entry represents a filesystem entry, which can be Directory, File, or Symlink.
type Entry interface {
	os.FileInfo
	Owner() OwnerInfo
	Device() DeviceInfo
	LocalFilesystemPath() string // returns full local filesystem path or "" if not a local filesystem
}

// OwnerInfo describes owner of a filesystem entry.
type OwnerInfo struct {
	UserID  uint32 `json:"uid"`
	GroupID uint32 `json:"gid"`
}

// DeviceInfo describes the device this filesystem entry is on.
type DeviceInfo struct {
	Dev  uint64 `json:"dev"`
	Rdev uint64 `json:"rdev"`
}

// Entries is a list of entries sorted by name.
type Entries []Entry

// Reader allows reading from a file and retrieving its up-to-date file info.
type Reader interface {
	io.ReadCloser
	io.Seeker

	Entry() (Entry, error)
}

// File represents an entry that is a file.
type File interface {
	Entry
	Open(ctx context.Context) (Reader, error)
}

// StreamingFile represents an entry that is a stream.
type StreamingFile interface {
	Entry
	GetReader(ctx context.Context) (io.Reader, error)
}

// Directory represents contents of a directory.
type Directory interface {
	Entry
	Child(ctx context.Context, name string) (Entry, error)
	Readdir(ctx context.Context) (Entries, error)
}

// DirectoryWithSummary is optionally implemented by Directory that provide summary.
type DirectoryWithSummary interface {
	Summary(ctx context.Context) (*DirectorySummary, error)
}

// ErrorEntry represents entry in a Directory that had encountered an error or is unknown/unsupported (ErrUnknown).
type ErrorEntry interface {
	Entry

	ErrorInfo() error
}

// ErrEntryNotFound is returned when an entry is not found.
var ErrEntryNotFound = errors.New("entry not found")

// ReadDirAndFindChild reads all entries from a directory and returns one by name.
// This is a convenience function that may be helpful in implementations of Directory.Child().
func ReadDirAndFindChild(ctx context.Context, d Directory, name string) (Entry, error) {
	children, err := d.Readdir(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "error reading directory")
	}

	e := children.FindByName(name)
	if e == nil {
		return nil, ErrEntryNotFound
	}

	return e, nil
}

// MaxFailedEntriesPerDirectorySummary is the maximum number of failed entries per directory summary.
const MaxFailedEntriesPerDirectorySummary = 10

// EntryWithError describes error encountered when processing an entry.
type EntryWithError struct {
	EntryPath string `json:"path"`
	Error     string `json:"error"`
}

// DirectorySummary represents summary information about a directory.
type DirectorySummary struct {
	TotalFileSize     int64     `json:"size"`
	TotalFileCount    int64     `json:"files"`
	TotalSymlinkCount int64     `json:"symlinks"`
	TotalDirCount     int64     `json:"dirs"`
	MaxModTime        time.Time `json:"maxTime"`
	IncompleteReason  string    `json:"incomplete,omitempty"`

	// number of failed files
	FatalErrorCount   int `json:"numFailed"`
	IgnoredErrorCount int `json:"numIgnoredErrors,omitempty"`

	// first 10 failed entries
	FailedEntries []*EntryWithError `json:"errors,omitempty"`
}

// Clone clones given directory summary.
func (s *DirectorySummary) Clone() DirectorySummary {
	res := *s

	res.FailedEntries = append([]*EntryWithError(nil), s.FailedEntries...)

	return res
}

// Symlink represents a symbolic link entry.
type Symlink interface {
	Entry
	Readlink(ctx context.Context) (string, error)
}

// FindByName returns an entry with a given name, or nil if not found.
func (e Entries) FindByName(n string) Entry {
	i := sort.Search(
		len(e),
		func(i int) bool {
			return e[i].Name() >= n
		},
	)
	if i < len(e) && e[i].Name() == n {
		return e[i]
	}

	return nil
}

// Update returns a copy of Entries with the provided entry included, by either replacing
// existing entry with the same name or inserted in the appropriate place to maintain sorted order.
func (e Entries) Update(newEntry Entry) Entries {
	name := newEntry.Name()
	pos := sort.Search(len(e), func(i int) bool {
		return e[i].Name() >= name
	})

	// append at the end
	if pos >= len(e) {
		return append(append(Entries(nil), e...), newEntry)
	}

	if e[pos].Name() == name {
		if pos > 0 {
			return append(append(append(Entries(nil), e[0:pos]...), newEntry), e[pos+1:]...)
		}

		return append(append(Entries(nil), newEntry), e[pos+1:]...)
	}

	if pos > 0 {
		return append(append(append(Entries(nil), e[0:pos]...), newEntry), e[pos:]...)
	}

	return append(append(Entries(nil), newEntry), e[pos:]...)
}

// Remove returns a copy of Entries with the provided entry removed, while maintaining sorted order.
func (e Entries) Remove(name string) Entries {
	pos := sort.Search(len(e), func(i int) bool {
		return e[i].Name() >= name
	})

	// not found
	if pos >= len(e) {
		return e
	}

	if e[pos].Name() != name {
		return e
	}

	if pos > 0 {
		return append(append(Entries(nil), e[0:pos]...), e[pos+1:]...)
	}

	return append(Entries(nil), e[pos+1:]...)
}

// Sort sorts the entries by name.
func (e Entries) Sort() {
	sort.Slice(e, func(i, j int) bool {
		return e[i].Name() < e[j].Name()
	})
}
