// Package storage implements operations for s3 and fs.
package storage

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/peak/s5cmd/storage/url"
	"github.com/peak/s5cmd/strutil"
)

var (
	// ErrGivenObjectNotFound indicates a specified object is not found.
	ErrGivenObjectNotFound = fmt.Errorf("given object not found")

	// ErrNoObjectFound indicates there are no objects found from a given directory.
	ErrNoObjectFound = fmt.Errorf("no object found")
)

// Storage is an interface for storage operations that is common
// to local filesystem and remote object storage.
type Storage interface {
	// Stat returns the Object structure describing object. If src is not
	// found, ErrGivenObjectNotFound is returned.
	Stat(ctx context.Context, src *url.URL) (*Object, error)

	// List the objects and directories/prefixes in the src.
	List(ctx context.Context, src *url.URL, followSymlinks bool) <-chan *Object

	// Delete deletes the given src.
	Delete(ctx context.Context, src *url.URL) error

	// MultiDelete deletes all items returned from given urls in batches.
	MultiDelete(ctx context.Context, urls <-chan *url.URL) <-chan *Object

	// Copy src to dst, optionally setting the given metadata. Src and dst
	// arguments are of the same type. If src is a remote type, server side
	// copying will be used.
	Copy(ctx context.Context, src, dst *url.URL, metadata Metadata) error
}

func NewLocalClient(opts Options) *Filesystem {
	return &Filesystem{dryRun: opts.DryRun}
}

func NewRemoteClient(_ *url.URL, opts Options) (*S3, error) {
	return newS3Storage(opts, cachedSession)
}

func NewClient(url *url.URL, opts Options) (Storage, error) {
	if url.IsRemote() {
		return NewRemoteClient(url, opts)
	}
	return NewLocalClient(opts), nil
}

// Options stores configuration for storage.
type Options struct {
	MaxRetries  int
	Endpoint    string
	Region      string
	NoVerifySSL bool
	DryRun      bool
}

// Object is a generic type which contains metadata for storage items.
type Object struct {
	URL          *url.URL     `json:"key,omitempty"`
	Etag         string       `json:"etag,omitempty"`
	ModTime      *time.Time   `json:"last_modified,omitempty"`
	Type         ObjectType   `json:"type,omitempty"`
	Size         int64        `json:"size,omitempty"`
	StorageClass StorageClass `json:"storage_class,omitempty"`
	Err          error        `json:"error,omitempty"`
}

// String returns the string representation of Object.
func (o *Object) String() string {
	return o.URL.String()
}

// JSON returns the JSON representation of Object.
func (o *Object) JSON() string {
	return strutil.JSON(o)
}

// ObjectType is the type of Object.
type ObjectType struct {
	mode os.FileMode
}

// String returns the string representation of ObjectType.
func (o ObjectType) String() string {
	switch mode := o.mode; {
	case mode.IsRegular():
		return "file"
	case mode.IsDir():
		return "directory"
	case mode&os.ModeSymlink != 0:
		return "symlink"
	}
	return ""
}

// MarshalJSON returns the stringer of ObjectType as a marshalled json.
func (o ObjectType) MarshalJSON() ([]byte, error) {
	return json.Marshal(o.String())
}

// IsDir checks if the object is a directory.
func (o ObjectType) IsDir() bool {
	return o.mode.IsDir()
}

// IsSymlink checks if the object is a symbolic link.
func (o ObjectType) IsSymlink() bool {
	return o.mode&os.ModeSymlink != 0
}

// ShouldProcessUrl returns true if follow symlinks is enabled.
// If follow symlinks is disabled we should not process the url.
// (this check is needed only for local files)
func ShouldProcessUrl(url *url.URL, followSymlinks bool) bool {
	if followSymlinks {
		return true
	}

	if url.IsRemote() {
		return true
	}
	fi, err := os.Lstat(url.Absolute())
	if err != nil {
		return false
	}

	// do not process symlinks
	return fi.Mode()&os.ModeSymlink == 0
}

// dateFormat is a constant time template for the bucket.
const dateFormat = "2006/01/02 15:04:05"

// Bucket is a container for storage objects.
type Bucket struct {
	CreationDate time.Time `json:"created_at"`
	Name         string    `json:"name"`
}

// String returns the string representation of Bucket.
func (b Bucket) String() string {
	return fmt.Sprintf("%s  s3://%s", b.CreationDate.Format(dateFormat), b.Name)
}

// JSON returns the JSON representation of Bucket.
func (b Bucket) JSON() string {
	return strutil.JSON(b)
}

// StorageClass represents the storage used to store an object.
type StorageClass string

func (s StorageClass) IsGlacier() bool {
	return s == "GLACIER"
}

// notImplemented is a structure which is used on the unsupported operations.
type notImplemented struct {
	apiType string
	method  string
}

// Error returns the string representation of Error for notImplemented.
func (e notImplemented) Error() string {
	return fmt.Sprintf("%q is not supported on %q storage", e.method, e.apiType)
}

type Metadata map[string]string

// NewMetadata will return an empty metadata object.
func NewMetadata() Metadata {
	return Metadata{}
}

func (m Metadata) ACL() string {
	return m["ACL"]
}

func (m Metadata) SetACL(acl string) Metadata {
	m["ACL"] = acl
	return m
}

func (m Metadata) StorageClass() string {
	return m["StorageClass"]
}

func (m Metadata) SetStorageClass(class string) Metadata {
	m["StorageClass"] = class
	return m
}

func (m Metadata) ContentType() string {
	return m["ContentType"]
}

func (m Metadata) SetContentType(contentType string) Metadata {
	m["ContentType"] = contentType
	return m
}

func (m Metadata) SSE() string {
	return m["EncryptionMethod"]
}

func (m Metadata) SetSSE(sse string) Metadata {
	m["EncryptionMethod"] = sse
	return m
}

func (m Metadata) SSEKeyID() string {
	return m["EncryptionKeyID"]
}

func (m Metadata) SetSSEKeyID(kid string) Metadata {
	m["EncryptionKeyID"] = kid
	return m
}
