// 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 (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net"
	"net/url"
	"time"

	gcs "cloud.google.com/go/storage"
	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/googleapi"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

type GCSStorage struct {
	StorageBase

	bucket     *gcs.BucketHandle
	storageDir string

	numberOfThreads int
	TestMode        bool
}

type GCSConfig struct {
	ClientID     string          `json:"client_id"`
	ClientSecret string          `json:"client_secret"`
	Endpoint     oauth2.Endpoint `json:"end_point"`
	Token        oauth2.Token    `json:"token"`
}

// CreateGCSStorage creates a GCD storage object.
func CreateGCSStorage(tokenFile string, bucketName string, storageDir string, threads int) (storage *GCSStorage, err error) {

	ctx := context.Background()

	description, err := ioutil.ReadFile(tokenFile)
	if err != nil {
		return nil, err
	}

	var object map[string]interface{}

	err = json.Unmarshal(description, &object)
	if err != nil {
		return nil, err
	}

	isServiceAccount := false
	if value, ok := object["type"]; ok {
		if authType, ok := value.(string); ok && authType == "service_account" {
			isServiceAccount = true
		}
	}

	var tokenSource oauth2.TokenSource

	if isServiceAccount {
		config, err := google.JWTConfigFromJSON(description, gcs.ScopeReadWrite)
		if err != nil {
			return nil, err
		}
		tokenSource = config.TokenSource(ctx)
	} else {
		gcsConfig := &GCSConfig{}
		if err := json.Unmarshal(description, gcsConfig); err != nil {
			return nil, err
		}

		config := oauth2.Config{
			ClientID:     gcsConfig.ClientID,
			ClientSecret: gcsConfig.ClientSecret,
			Endpoint:     gcsConfig.Endpoint,
		}
		tokenSource = config.TokenSource(ctx, &gcsConfig.Token)
	}

	options := option.WithTokenSource(tokenSource)
	client, err := gcs.NewClient(ctx, options)

	bucket := client.Bucket(bucketName)

	if len(storageDir) > 0 && storageDir[len(storageDir)-1] != '/' {
		storageDir += "/"
	}

	storage = &GCSStorage{
		bucket:          bucket,
		storageDir:      storageDir,
		numberOfThreads: threads,
	}

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

func (storage *GCSStorage) shouldRetry(backoff *int, err error) (bool, error) {

	retry := false
	message := ""
	if err == nil {
		return false, nil
	} else if e, ok := err.(*googleapi.Error); ok {
		if 500 <= e.Code && e.Code < 600 {
			// Retry for 5xx response codes.
			message = fmt.Sprintf("HTTP status code %d", e.Code)
			retry = true
		} else if e.Code == 429 {
			// Too many requests{
			message = "HTTP status code 429"
			retry = true
		} else if e.Code == 403 {
			// User Rate Limit Exceeded
			message = "User Rate Limit Exceeded"
			retry = true
		}
	} else if e, ok := err.(*url.Error); ok {
		message = e.Error()
		retry = true
	} else if err == io.ErrUnexpectedEOF {
		// Retry on unexpected EOFs and temporary network errors.
		message = "Unexpected EOF"
		retry = true
	} else if err, ok := err.(net.Error); ok {
		message = "Temporary network error"
		retry = err.Temporary()
	}

	if !retry || *backoff >= 256 {
		return false, err
	}

	delay := float32(*backoff) * rand.Float32()
	LOG_INFO("GCS_RETRY", "%s; retrying after %.2f seconds", message, delay)
	time.Sleep(time.Duration(float32(*backoff) * float32(time.Second)))
	*backoff *= 2
	return true, nil
}

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

	query := gcs.Query{
		Prefix: storage.storageDir + dir + "/",
	}
	dirOnly := false
	prefixLength := len(query.Prefix)

	if dir == "snapshots" {
		query.Delimiter = "/"
		dirOnly = true
	}

	files := []string{}
	sizes := []int64{}
	iter := storage.bucket.Objects(context.Background(), &query)
	for {
		attributes, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return nil, nil, err
		}

		if dirOnly {
			if len(attributes.Prefix) != 0 {
				prefix := attributes.Prefix
				files = append(files, prefix[prefixLength:])
			}
		} else {
			if len(attributes.Prefix) == 0 {
				files = append(files, attributes.Name[prefixLength:])
				sizes = append(sizes, attributes.Size)
			}
		}
	}

	return files, sizes, nil
}

// DeleteFile deletes the file or directory at 'filePath'.
func (storage *GCSStorage) DeleteFile(threadIndex int, filePath string) (err error) {
	err = storage.bucket.Object(storage.storageDir + filePath).Delete(context.Background())
	if err == gcs.ErrObjectNotExist {
		return nil
	}
	return err
}

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

	source := storage.bucket.Object(storage.storageDir + from)
	destination := storage.bucket.Object(storage.storageDir + to)

	_, err = destination.CopierFrom(source).Run(context.Background())
	if err != nil {
		return err
	}

	return storage.DeleteFile(threadIndex, from)
}

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

// GetFileInfo returns the information about the file or directory at 'filePath'.
func (storage *GCSStorage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) {
	object := storage.bucket.Object(storage.storageDir + filePath)

	attributes, err := object.Attrs(context.Background())

	if err != nil {
		if err == gcs.ErrObjectNotExist {
			return false, false, 0, nil
		} else {
			return false, false, 0, err
		}
	}

	return true, false, attributes.Size, nil
}

// DownloadFile reads the file at 'filePath' into the chunk.
func (storage *GCSStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
	readCloser, err := storage.bucket.Object(storage.storageDir + filePath).NewReader(context.Background())
	if err != nil {
		return err
	}
	defer readCloser.Close()
	_, err = RateLimitedCopy(chunk, readCloser, storage.DownloadRateLimit/storage.numberOfThreads)
	return err
}

// UploadFile writes 'content' to the file at 'filePath'.
func (storage *GCSStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {

	backoff := 1
	for {
		writeCloser := storage.bucket.Object(storage.storageDir + filePath).NewWriter(context.Background())
		defer writeCloser.Close()
		reader := CreateRateLimitedReader(content, storage.UploadRateLimit/storage.numberOfThreads)
		_, err = io.Copy(writeCloser, reader)

		if retry, e := storage.shouldRetry(&backoff, err); e == nil && !retry {
			break
		} else if retry {
			continue
		} else {
			return err
		}
	}

	return err
}

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

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

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

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

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