package driver

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strconv"
	"strings"
)

// Version is a string representing the storage driver version, of the form
// Major.Minor.
// The registry must accept storage drivers with equal major version and greater
// minor version, but may not be compatible with older storage driver versions.
type Version string

// Major returns the major (primary) component of a version.
func (version Version) Major() uint {
	majorPart, _, _ := strings.Cut(string(version), ".")
	major, _ := strconv.ParseUint(majorPart, 10, 0)
	return uint(major)
}

// Minor returns the minor (secondary) component of a version.
func (version Version) Minor() uint {
	_, minorPart, _ := strings.Cut(string(version), ".")
	minor, _ := strconv.ParseUint(minorPart, 10, 0)
	return uint(minor)
}

// CurrentVersion is the current storage driver Version.
const CurrentVersion Version = "0.1"

// WalkOptions provides options to the walk function that may adjust its behaviour
type WalkOptions struct {
	// If StartAfterHint is set, the walk may start with the first item lexographically
	// after the hint, but it is not guaranteed and drivers may start the walk from the path.
	StartAfterHint string
}

func WithStartAfterHint(startAfterHint string) func(*WalkOptions) {
	return func(s *WalkOptions) {
		s.StartAfterHint = startAfterHint
	}
}

// StorageDriver defines methods that a Storage Driver must implement for a
// filesystem-like key/value object storage. Storage Drivers are automatically
// registered via an internal registration mechanism, and generally created
// via the StorageDriverFactory interface (https://godoc.org/github.com/distribution/distribution/registry/storage/driver/factory).
// Please see the aforementioned factory package for example code showing how to get an instance
// of a StorageDriver
type StorageDriver interface {
	// Name returns the human-readable "name" of the driver, useful in error
	// messages and logging. By convention, this will just be the registration
	// name, but drivers may provide other information here.
	Name() string

	// GetContent retrieves the content stored at "path" as a []byte.
	// This should primarily be used for small objects.
	GetContent(ctx context.Context, path string) ([]byte, error)

	// PutContent stores the []byte content at a location designated by "path".
	// This should primarily be used for small objects.
	PutContent(ctx context.Context, path string, content []byte) error

	// Reader retrieves an io.ReadCloser for the content stored at "path"
	// with a given byte offset.
	// May be used to resume reading a stream by providing a nonzero offset.
	Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error)

	// Writer returns a FileWriter which will store the content written to it
	// at the location designated by "path" after the call to Commit.
	// A path may be appended to if it has not been committed, or if the
	// existing committed content is zero length.
	//
	// The behaviour of appending to paths with non-empty committed content is
	// undefined. Specific implementations may document their own behavior.
	Writer(ctx context.Context, path string, append bool) (FileWriter, error)

	// Stat retrieves the FileInfo for the given path, including the current
	// size in bytes and the creation time.
	Stat(ctx context.Context, path string) (FileInfo, error)

	// List returns a list of the objects that are direct descendants of the
	// given path.
	List(ctx context.Context, path string) ([]string, error)

	// Move moves an object stored at sourcePath to destPath, removing the
	// original object.
	// Note: This may be no more efficient than a copy followed by a delete for
	// many implementations.
	Move(ctx context.Context, sourcePath string, destPath string) error

	// Delete recursively deletes all objects stored at "path" and its subpaths.
	Delete(ctx context.Context, path string) error

	// RedirectURL returns a URL which the client of the request r may use
	// to retrieve the content stored at path. Returning the empty string
	// signals that the request may not be redirected.
	RedirectURL(r *http.Request, path string) (string, error)

	// Walk traverses a filesystem defined within driver, starting
	// from the given path, calling f on each file.
	// If the returned error from the WalkFn is ErrSkipDir and fileInfo refers
	// to a directory, the directory will not be entered and Walk
	// will continue the traversal.
	// If the returned error from the WalkFn is ErrFilledBuffer, processing stops.
	Walk(ctx context.Context, path string, f WalkFn, options ...func(*WalkOptions)) error
}

// FileWriter provides an abstraction for an opened writable file-like object in
// the storage backend. The FileWriter must flush all content written to it on
// the call to Close, but is only required to make its content readable on a
// call to Commit.
type FileWriter interface {
	io.WriteCloser

	// Size returns the number of bytes written to this FileWriter.
	Size() int64

	// Cancel removes any written content from this FileWriter.
	Cancel(context.Context) error

	// Commit flushes all content written to this FileWriter and makes it
	// available for future calls to StorageDriver.GetContent and
	// StorageDriver.Reader.
	Commit(context.Context) error
}

// PathRegexp is the regular expression which each file path must match. A
// file path is absolute, beginning with a slash and containing a positive
// number of path components separated by slashes, where each component is
// restricted to alphanumeric characters or a period, underscore, or
// hyphen.
var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._-]+)+$`)

// ErrUnsupportedMethod may be returned in the case where a StorageDriver implementation does not support an optional method.
type ErrUnsupportedMethod struct {
	DriverName string
}

func (err ErrUnsupportedMethod) Error() string {
	return fmt.Sprintf("%s: unsupported method", err.DriverName)
}

// PathNotFoundError is returned when operating on a nonexistent path.
type PathNotFoundError struct {
	Path       string
	DriverName string
}

func (err PathNotFoundError) Error() string {
	return fmt.Sprintf("%s: Path not found: %s", err.DriverName, err.Path)
}

// InvalidPathError is returned when the provided path is malformed.
type InvalidPathError struct {
	Path       string
	DriverName string
}

func (err InvalidPathError) Error() string {
	return fmt.Sprintf("%s: invalid path: %s", err.DriverName, err.Path)
}

// InvalidOffsetError is returned when attempting to read or write from an
// invalid offset.
type InvalidOffsetError struct {
	Path       string
	Offset     int64
	DriverName string
}

func (err InvalidOffsetError) Error() string {
	return fmt.Sprintf("%s: invalid offset: %d for path: %s", err.DriverName, err.Offset, err.Path)
}

// Error is a catch-all error type which captures an error string and
// the driver type on which it occurred.
type Error struct {
	DriverName string
	Detail     error
}

func (err Error) Error() string {
	return fmt.Sprintf("%s: %s", err.DriverName, err.Detail)
}

func (err Error) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		DriverName string `json:"driver"`
		Detail     string `json:"detail"`
	}{
		DriverName: err.DriverName,
		Detail:     err.Detail.Error(),
	})
}

// Errors provides the envelope for multiple errors
// for use within the storagedriver implementations.
type Errors struct {
	DriverName string
	Errs       []error
}

var _ error = Errors{}

func (e Errors) Error() string {
	switch len(e.Errs) {
	case 0:
		return fmt.Sprintf("%s: <nil>", e.DriverName)
	case 1:
		return fmt.Sprintf("%s: %s", e.DriverName, e.Errs[0].Error())
	default:
		msg := "errors:\n"
		for _, err := range e.Errs {
			msg += err.Error() + "\n"
		}
		return fmt.Sprintf("%s: %s", e.DriverName, msg)
	}
}

// MarshalJSON converts slice of errors into the format
// that is serializable by JSON.
func (e Errors) MarshalJSON() ([]byte, error) {
	tmpErrs := struct {
		DriverName string   `json:"driver"`
		Details    []string `json:"details"`
	}{
		DriverName: e.DriverName,
	}

	if len(e.Errs) == 0 {
		tmpErrs.Details = make([]string, 0)
		return json.Marshal(tmpErrs)
	}

	for _, err := range e.Errs {
		tmpErrs.Details = append(tmpErrs.Details, err.Error())
	}

	return json.Marshal(tmpErrs)
}
