package operations

import (
	"context"
	"path"
	"time"

	"github.com/pkg/errors"
	"github.com/rclone/rclone/backend/crypt"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/hash"
	"github.com/rclone/rclone/fs/walk"
)

// ListJSONItem in the struct which gets marshalled for each line
type ListJSONItem struct {
	Path          string
	Name          string
	EncryptedPath string `json:",omitempty"`
	Encrypted     string `json:",omitempty"`
	Size          int64
	MimeType      string    `json:",omitempty"`
	ModTime       Timestamp //`json:",omitempty"`
	IsDir         bool
	Hashes        map[string]string `json:",omitempty"`
	ID            string            `json:",omitempty"`
	OrigID        string            `json:",omitempty"`
	Tier          string            `json:",omitempty"`
	IsBucket      bool              `json:",omitempty"`
}

// Timestamp a time in the provided format
type Timestamp struct {
	When   time.Time
	Format string
}

// MarshalJSON turns a Timestamp into JSON
func (t Timestamp) MarshalJSON() (out []byte, err error) {
	if t.When.IsZero() {
		return []byte(`""`), nil
	}
	return []byte(`"` + t.When.Format(t.Format) + `"`), nil
}

// Returns a time format for the given precision
func formatForPrecision(precision time.Duration) string {
	switch {
	case precision <= time.Nanosecond:
		return "2006-01-02T15:04:05.000000000Z07:00"
	case precision <= 10*time.Nanosecond:
		return "2006-01-02T15:04:05.00000000Z07:00"
	case precision <= 100*time.Nanosecond:
		return "2006-01-02T15:04:05.0000000Z07:00"
	case precision <= time.Microsecond:
		return "2006-01-02T15:04:05.000000Z07:00"
	case precision <= 10*time.Microsecond:
		return "2006-01-02T15:04:05.00000Z07:00"
	case precision <= 100*time.Microsecond:
		return "2006-01-02T15:04:05.0000Z07:00"
	case precision <= time.Millisecond:
		return "2006-01-02T15:04:05.000Z07:00"
	case precision <= 10*time.Millisecond:
		return "2006-01-02T15:04:05.00Z07:00"
	case precision <= 100*time.Millisecond:
		return "2006-01-02T15:04:05.0Z07:00"
	}
	return time.RFC3339
}

// ListJSONOpt describes the options for ListJSON
type ListJSONOpt struct {
	Recurse       bool     `json:"recurse"`
	NoModTime     bool     `json:"noModTime"`
	NoMimeType    bool     `json:"noMimeType"`
	ShowEncrypted bool     `json:"showEncrypted"`
	ShowOrigIDs   bool     `json:"showOrigIDs"`
	ShowHash      bool     `json:"showHash"`
	DirsOnly      bool     `json:"dirsOnly"`
	FilesOnly     bool     `json:"filesOnly"`
	HashTypes     []string `json:"hashTypes"` // hash types to show if ShowHash is set, e.g. "MD5", "SHA-1"
}

// ListJSON lists fsrc using the options in opt calling callback for each item
func ListJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt, callback func(*ListJSONItem) error) error {
	var cipher *crypt.Cipher
	if opt.ShowEncrypted {
		fsInfo, _, _, config, err := fs.ConfigFs(fsrc.Name() + ":" + fsrc.Root())
		if err != nil {
			return errors.Wrap(err, "ListJSON failed to load config for crypt remote")
		}
		if fsInfo.Name != "crypt" {
			return errors.New("The remote needs to be of type \"crypt\"")
		}
		cipher, err = crypt.NewCipher(config)
		if err != nil {
			return errors.Wrap(err, "ListJSON failed to make new crypt remote")
		}
	}
	features := fsrc.Features()
	canGetTier := features.GetTier
	format := formatForPrecision(fsrc.Precision())
	isBucket := features.BucketBased && remote == "" && fsrc.Root() == "" // if bucket based remote listing the root mark directories as buckets
	showHash := opt.ShowHash
	hashTypes := fsrc.Hashes().Array()
	if len(opt.HashTypes) != 0 {
		showHash = true
		hashTypes = []hash.Type{}
		for _, hashType := range opt.HashTypes {
			var ht hash.Type
			err := ht.Set(hashType)
			if err != nil {
				return err
			}
			hashTypes = append(hashTypes, ht)
		}
	}
	err := walk.ListR(ctx, fsrc, remote, false, ConfigMaxDepth(ctx, opt.Recurse), walk.ListAll, func(entries fs.DirEntries) (err error) {
		for _, entry := range entries {
			switch entry.(type) {
			case fs.Directory:
				if opt.FilesOnly {
					continue
				}
			case fs.Object:
				if opt.DirsOnly {
					continue
				}
			default:
				fs.Errorf(nil, "Unknown type %T in listing", entry)
			}

			item := ListJSONItem{
				Path: entry.Remote(),
				Name: path.Base(entry.Remote()),
				Size: entry.Size(),
			}
			if !opt.NoModTime {
				item.ModTime = Timestamp{When: entry.ModTime(ctx), Format: format}
			}
			if !opt.NoMimeType {
				item.MimeType = fs.MimeTypeDirEntry(ctx, entry)
			}
			if cipher != nil {
				switch entry.(type) {
				case fs.Directory:
					item.EncryptedPath = cipher.EncryptDirName(entry.Remote())
				case fs.Object:
					item.EncryptedPath = cipher.EncryptFileName(entry.Remote())
				default:
					fs.Errorf(nil, "Unknown type %T in listing", entry)
				}
				item.Encrypted = path.Base(item.EncryptedPath)
			}
			if do, ok := entry.(fs.IDer); ok {
				item.ID = do.ID()
			}
			if o, ok := entry.(fs.Object); opt.ShowOrigIDs && ok {
				if do, ok := fs.UnWrapObject(o).(fs.IDer); ok {
					item.OrigID = do.ID()
				}
			}
			switch x := entry.(type) {
			case fs.Directory:
				item.IsDir = true
				item.IsBucket = isBucket
			case fs.Object:
				item.IsDir = false
				if showHash {
					item.Hashes = make(map[string]string)
					for _, hashType := range hashTypes {
						hash, err := x.Hash(ctx, hashType)
						if err != nil {
							fs.Errorf(x, "Failed to read hash: %v", err)
						} else if hash != "" {
							item.Hashes[hashType.String()] = hash
						}
					}
				}
				if canGetTier {
					if do, ok := x.(fs.GetTierer); ok {
						item.Tier = do.GetTier()
					}
				}
			default:
				fs.Errorf(nil, "Unknown type %T in listing in ListJSON", entry)
			}
			err = callback(&item)
			if err != nil {
				return errors.Wrap(err, "callback failed in ListJSON")
			}

		}
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "error in ListJSON")
	}
	return nil
}
