// Copyright (c) Acrosync LLC. All rights reserved.
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com

package duplicacy

import (
	"strings"
)

type B2Storage struct {
	StorageBase

	client *B2Client
}

// CreateB2Storage creates a B2 storage object.
func CreateB2Storage(accountID string, applicationKey string, downloadURL string, bucket string, storageDir string, threads int) (storage *B2Storage, err error) {

	client := NewB2Client(accountID, applicationKey, downloadURL, storageDir, threads)

	err, _ = client.AuthorizeAccount(0)
	if err != nil {
		return nil, err
	}

	err = client.FindBucket(bucket)
	if err != nil {
		return nil, err
	}

	storage = &B2Storage{
		client: client,
	}

	storage.DerivedStorage = storage
	storage.SetDefaultNestingLevels([]int{0}, 0)
	return storage, nil
}

// ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
func (storage *B2Storage) ListFiles(threadIndex int, dir string) (files []string, sizes []int64, err error) {
	for len(dir) > 0 && dir[len(dir)-1] == '/' {
		dir = dir[:len(dir)-1]
	}
	length := len(dir) + 1

	includeVersions := false
	if dir == "chunks" {
		includeVersions = true
	}

	entries, err := storage.client.ListFileNames(threadIndex, dir, false, includeVersions)
	if err != nil {
		return nil, nil, err
	}

	if dir == "snapshots" {

		subDirs := make(map[string]bool)

		for _, entry := range entries {
			name := entry.FileName[length:]
			subDir := strings.Split(name, "/")[0]
			subDirs[subDir+"/"] = true
		}

		for subDir := range subDirs {
			files = append(files, subDir)
		}
	} else if dir == "chunks" {
		lastFile := ""
		for _, entry := range entries {
			if entry.FileName == lastFile {
				continue
			}
			lastFile = entry.FileName
			if entry.Action == "hide" {
				files = append(files, entry.FileName[length:]+".fsl")
			} else {
				files = append(files, entry.FileName[length:])
			}
			sizes = append(sizes, entry.Size)
		}
	} else {
		for _, entry := range entries {
			files = append(files, entry.FileName[length:])
		}
	}

	return files, sizes, nil
}

// DeleteFile deletes the file or directory at 'filePath'.
func (storage *B2Storage) DeleteFile(threadIndex int, filePath string) (err error) {

	if strings.HasSuffix(filePath, ".fsl") {
		filePath = filePath[:len(filePath)-len(".fsl")]
		entries, err := storage.client.ListFileNames(threadIndex, filePath, true, true)
		if err != nil {
			return err
		}

		toBeDeleted := false

		for _, entry := range entries {
			if entry.FileName != filePath || (!toBeDeleted && entry.Action != "hide") {
				continue
			}

			toBeDeleted = true

			err = storage.client.DeleteFile(threadIndex, filePath, entry.FileID)
			if err != nil {
				return err
			}
		}

		return nil

	} else {
		entries, err := storage.client.ListFileNames(threadIndex, filePath, true, false)
		if err != nil {
			return err
		}

		if len(entries) == 0 {
			return nil
		}
		return storage.client.DeleteFile(threadIndex, filePath, entries[0].FileID)
	}
}

// MoveFile renames the file.
func (storage *B2Storage) MoveFile(threadIndex int, from string, to string) (err error) {

	filePath := ""

	if strings.HasSuffix(from, ".fsl") {
		filePath = to
		if from != to+".fsl" {
			filePath = ""
		}
	} else if strings.HasSuffix(to, ".fsl") {
		filePath = from
		if to != from+".fsl" {
			filePath = ""
		}
	}

	if filePath == "" {
		LOG_FATAL("STORAGE_MOVE", "Moving file '%s' to '%s' is not supported", from, to)
		return nil
	}

	if filePath == from {
		_, err = storage.client.HideFile(threadIndex, from)
		return err
	} else {
		entries, err := storage.client.ListFileNames(threadIndex, filePath, true, true)
		if err != nil {
			return err
		}
		if len(entries) == 0 || entries[0].FileName != filePath || entries[0].Action != "hide" {
			return nil
		}

		return storage.client.DeleteFile(threadIndex, filePath, entries[0].FileID)
	}
}

// CreateDirectory creates a new directory.
func (storage *B2Storage) CreateDirectory(threadIndex int, dir string) (err error) {
	return nil
}

// GetFileInfo returns the information about the file or directory at 'filePath'.
func (storage *B2Storage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) {
	isFossil := false
	if strings.HasSuffix(filePath, ".fsl") {
		isFossil = true
		filePath = filePath[:len(filePath)-len(".fsl")]
	}

	entries, err := storage.client.ListFileNames(threadIndex, filePath, true, isFossil)
	if err != nil {
		return false, false, 0, err
	}

	if len(entries) == 0 || entries[0].FileName != filePath {
		return false, false, 0, nil
	}

	if isFossil {
		if entries[0].Action == "hide" {
			return true, false, entries[0].Size, nil
		} else {
			return false, false, 0, nil
		}
	}
	return true, false, entries[0].Size, nil
}

// DownloadFile reads the file at 'filePath' into the chunk.
func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {

	readCloser, _, err := storage.client.DownloadFile(threadIndex, filePath)
	if err != nil {
		return err
	}

	defer readCloser.Close()

	_, err = RateLimitedCopy(chunk, readCloser, storage.DownloadRateLimit/storage.client.Threads)
	return err
}

// UploadFile writes 'content' to the file at 'filePath'.
func (storage *B2Storage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
	return storage.client.UploadFile(threadIndex, filePath, content, storage.UploadRateLimit/storage.client.Threads)
}

// If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when
// managing snapshots.
func (storage *B2Storage) IsCacheNeeded() bool { return true }

// If the 'MoveFile' method is implemented.
func (storage *B2Storage) IsMoveFileImplemented() bool { return true }

// If the storage can guarantee strong consistency.
func (storage *B2Storage) IsStrongConsistent() bool { return true }

// If the storage supports fast listing of files names.
func (storage *B2Storage) IsFastListing() bool { return true }

// Enable the test mode.
func (storage *B2Storage) EnableTestMode() {
	storage.client.TestMode = true
}
