// Copyright 2016, the Blazer authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package b2 provides a high-level interface to Backblaze's B2 cloud storage
// service.
//
// It is specifically designed to abstract away the Backblaze API details by
// providing familiar Go interfaces, specifically an io.Writer for object
// storage, and an io.Reader for object download.  Handling of transient
// errors, including network and authentication timeouts, is transparent.
//
// Methods that perform network requests accept a context.Context argument.
// Callers should use the context's cancellation abilities to end requests
// early, or to provide timeout or deadline guarantees.
//
// This package is in development and may make API changes.
package b2

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"sync"
	"time"
)

// Client is a Backblaze B2 client.
type Client struct {
	backend beRootInterface

	slock    sync.Mutex
	sWriters map[string]*Writer
	sReaders map[string]*Reader
	sMethods []methodCounter
	opts     clientOptions
}

// NewClient creates and returns a new Client with valid B2 service account
// tokens.
func NewClient(ctx context.Context, account, key string, opts ...ClientOption) (*Client, error) {
	c := &Client{
		backend: &beRoot{
			b2i: &b2Root{},
		},
		sMethods: []methodCounter{
			newMethodCounter(time.Minute, time.Second),
			newMethodCounter(time.Minute*5, time.Second),
			newMethodCounter(time.Hour, time.Minute),
			newMethodCounter(0, 0), // forever
		},
	}
	opts = append(opts, client(c))
	for _, f := range opts {
		f(&c.opts)
	}
	if err := c.backend.authorizeAccount(ctx, account, key, c.opts); err != nil {
		return nil, err
	}
	return c, nil
}

type clientOptions struct {
	client          *Client
	transport       http.RoundTripper
	failSomeUploads bool
	expireTokens    bool
	capExceeded     bool
	apiBase         string
	userAgents      []string
	writerOpts      []WriterOption
}

// A ClientOption allows callers to adjust various per-client settings.
type ClientOption func(*clientOptions)

// UserAgent sets the User-Agent HTTP header.  The default header is
// "blazer/<version>"; the value set here will be prepended to that.  This can
// be set multiple times.
//
// A user agent is generally of the form "<product>/<version> (<comments>)".
func UserAgent(agent string) ClientOption {
	return func(o *clientOptions) {
		o.userAgents = append(o.userAgents, agent)
	}
}

// APIBase returns a ClientOption specifying the URL root of API requests.
func APIBase(url string) ClientOption {
	return func(o *clientOptions) {
		o.apiBase = url
	}
}

// Transport sets the underlying HTTP transport mechanism.  If unset,
// http.DefaultTransport is used.
func Transport(rt http.RoundTripper) ClientOption {
	return func(c *clientOptions) {
		c.transport = rt
	}
}

// FailSomeUploads requests intermittent upload failures from the B2 service.
// This is mostly useful for testing.
func FailSomeUploads() ClientOption {
	return func(c *clientOptions) {
		c.failSomeUploads = true
	}
}

// ExpireSomeAuthTokens requests intermittent authentication failures from the
// B2 service.
func ExpireSomeAuthTokens() ClientOption {
	return func(c *clientOptions) {
		c.expireTokens = true
	}
}

// ForceCapExceeded requests a cap limit from the B2 service.  This causes all
// uploads to be treated as if they would exceed the configure B2 capacity.
func ForceCapExceeded() ClientOption {
	return func(c *clientOptions) {
		c.capExceeded = true
	}
}

func client(cl *Client) ClientOption {
	return func(c *clientOptions) {
		c.client = cl
	}
}

type clientTransport struct {
	client *Client
	rt     http.RoundTripper
}

func (ct *clientTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	m := r.Header.Get("X-Blazer-Method")
	t := ct.rt
	if t == nil {
		t = http.DefaultTransport
	}
	b := time.Now()
	resp, err := t.RoundTrip(r)
	e := time.Now()
	if err != nil {
		return resp, err
	}
	if m != "" && ct.client != nil {
		ct.client.slock.Lock()
		m := method{
			name:     m,
			duration: e.Sub(b),
			status:   resp.StatusCode,
		}
		for _, counter := range ct.client.sMethods {
			counter.record(m)
		}
		ct.client.slock.Unlock()
	}
	return resp, nil
}

// Bucket is a reference to a B2 bucket.
type Bucket struct {
	b beBucketInterface
	r beRootInterface

	c       *Client
	urlPool *urlPool
}

type BucketType string

const (
	UnknownType BucketType = ""
	Private                = "allPrivate"
	Public                 = "allPublic"
	Snapshot               = "snapshot"
)

// BucketAttrs holds a bucket's metadata attributes.
type BucketAttrs struct {
	// Type lists or sets the new bucket type.  If Type is UnknownType during a
	// bucket.Update, the type is not changed.
	Type BucketType

	// Info records user data, limited to ten keys.  If nil during a
	// bucket.Update, the existing bucket info is not modified.  A bucket's
	// metadata can be removed by updating with an empty map.
	Info map[string]string

	// Reports or sets bucket lifecycle rules.  If nil during a bucket.Update,
	// the rules are not modified.  A bucket's rules can be removed by updating
	// with an empty slice.
	LifecycleRules []LifecycleRule

	// The initial list (a JSON array) of CORS rules for this bucket.
	// See CORS Rules (https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules) for an overview and the rule structure.
	CORSRules []CORSRule

	// The default Object Lock retention settings for this bucket. See Object Lock for an overview and the parameter structure.
	// If specified, the existing default bucket retention settings will be replaced with the new settings. If not specified,
	// the setting will remain unchanged. Setting the value requires the writeBucketRetentions capability and that the bucket is Object Lock-enabled.
	// Object Lock: https://www.backblaze.com/docs/cloud-storage-enable-object-lock-with-the-native-api.
	DefaultRetention *Retention
	// The default server-side encryption settings for this bucket. See Server-Side Encryption settings for new files uploaded to this bucket.
	// This field is filtered based on application key capabilities; readBucketEncryption capability is required to access the value.
	// See Server-Side Encryption for an overview and the parameter structure. If specified, the existing default bucket encryption settings will
	// be replaced with the new settings. If not specified, the setting will remain unchanged. Setting the value requires the writeBucketEncryption
	// capability. Server-Side Encryption settings: https://www.backblaze.com/docs/cloud-storage-enable-server-side-encryption-with-the-native-api.
	DefaultServerSideEncryption *ServerSideEncryption

	// If present, the Boolean value specifies whether the bucket has Object Lock enabled. Once Object Lock is enabled on a bucket, it cannot be disabled.
	// A value of true will be accepted if you have writeBucketRetentions capability. But you cannot enable Object Lock on a restricted bucket
	// (e.g. share buckets, snapshot) or on a bucket that contains source replication configuration.
	// A value of false will only be accepted if the bucket does not have Object Lock enabled. After enabling Object Lock on a bucket containing files with
	// metadata over the lower 2,048 byte limit, API requests to b2_update_file_retention and b2_update_file_legal_hold will be rejected. This is because
	// setting file retention or legal hold on a file adds additional headers when the file is downloaded, for example, with b2_download_file_by_name.
	// In such cases, you can use b2_copy_file with a REPLACE metadataDirective to copy the file, give it less metadata, and also specify the fileRetention
	// and legalHold parameters. The original file can then be deleted with b2_delete_file_version.
	// Object Lock: https://www.backblaze.com/docs/cloud-storage-enable-object-lock-with-the-native-api.
	FileLockEnabled bool

	// The configuration to create a Replication Rule. See Cloud Replication Rules. At least one of the asReplicationSource or asReplicationDestination
	// parameters is required, but they can also both be present.
	// NOTE: The first time that you configure Cloud Replication, complete the following tasks to ensure that you have the correct permission:
	// 1. Verify your email address.
	// 2. Have a payment history on file or make a payment.
	ReplicationConfig *ReplicationConfiguration
}

// DefaultServerSideEncryption sets the bucket defaultServerSideEncryption to { "mode": "SSE-B2", "algorithm": "AES256" }
// Must call Bucket.Update() to apply the change.
func DefaultServerSideEncryption() *ServerSideEncryption {
	return &ServerSideEncryption{
		Mode:      "SSE-B2",
		Algorithm: "AES256",
	}
}

type ServerSideEncryption struct {
	Mode      string
	Algorithm string
}

type CORSRule struct {
	Name              string
	AllowedOrigins    []string
	AllowedHeaders    []string
	AllowedOperations []string
	ExposeHeaders     []string
	MaxAgeSeconds     int
}

type Retention struct {
	Mode   string
	Period *RetentionPeriod
}

type RetentionPeriod struct {
	Duration int
	Unit     string
}

type ReplicationConfiguration struct {
	AsReplicationSource AsReplicationSource
}

type AsReplicationSource struct {
	ReplicationRules       []ReplicationRules
	SourceApplicationKeyID string
}

type ReplicationRules struct {
	DestinationBucketID  string
	FileNamePrefix       string
	IncludeExistingFiles bool
	IsEnabled            bool
	Priority             int
	ReplicationRuleName  string
}

// A LifecycleRule describes an object's life cycle, namely how many days after
// uploading an object should be hidden, and after how many days hidden an
// object should be deleted.  Multiple rules may not apply to the same file or
// set of files.  Be careful when using this feature; it can (is designed to)
// delete your data.
type LifecycleRule struct {
	// Prefix specifies all the files in the bucket to which this rule applies.
	Prefix string

	// DaysUploadedUntilHidden specifies the number of days after which a file
	// will automatically be hidden.  0 means "do not automatically hide new
	// files".
	DaysNewUntilHidden int

	// DaysHiddenUntilDeleted specifies the number of days after which a hidden
	// file is deleted.  0 means "do not automatically delete hidden files".
	DaysHiddenUntilDeleted int
}

type b2err struct {
	err              error
	notFoundErr      bool
	isUpdateConflict bool
}

func (e b2err) Error() string {
	return e.err.Error()
}

// IsNotExist reports whether a given error indicates that an object or bucket
// does not exist.
func IsNotExist(err error) bool {
	berr, ok := err.(b2err)
	if !ok {
		return false
	}
	return berr.notFoundErr
}

const uploadURLPoolSize = 100

type urlPool struct {
	ch chan beURLInterface
}

func newURLPool() *urlPool {
	return &urlPool{ch: make(chan beURLInterface, uploadURLPoolSize)}
}

func (p *urlPool) get() beURLInterface {
	select {
	case ue := <-p.ch:
		// if the channel has an upload URL available, use that
		return ue
	default:
		// otherwise return nil, a new upload URL needs to be generated
		return nil
	}
}

func (p *urlPool) put(u beURLInterface) {
	select {
	case p.ch <- u:
		// put the URL back if possible
	default:
		// if the channel is full, throw it away
	}
}

// Bucket returns a bucket if it exists.
func (c *Client) Bucket(ctx context.Context, name string) (*Bucket, error) {
	buckets, err := c.backend.listBuckets(ctx, name)
	if err != nil {
		return nil, err
	}
	for _, bucket := range buckets {
		if bucket.name() == name {
			return &Bucket{
				b:       bucket,
				r:       c.backend,
				c:       c,
				urlPool: newURLPool(),
			}, nil
		}
	}
	return nil, b2err{
		err:         fmt.Errorf("%s: bucket not found", name),
		notFoundErr: true,
	}
}

// NewBucket returns a bucket.  The bucket is created with the given attributes
// if it does not already exist.  If attrs is nil, it is created as a private
// bucket with no info metadata and no lifecycle rules.
func (c *Client) NewBucket(ctx context.Context, name string, attrs *BucketAttrs) (*Bucket, error) {
	buckets, err := c.backend.listBuckets(ctx, name)
	if err != nil {
		return nil, err
	}
	for _, bucket := range buckets {
		if bucket.name() == name {
			return &Bucket{
				b:       bucket,
				r:       c.backend,
				c:       c,
				urlPool: newURLPool(),
			}, nil
		}
	}
	if attrs == nil {
		attrs = &BucketAttrs{Type: Private}
	}
	b, err := c.backend.createBucket(ctx, name, string(attrs.Type), attrs.Info, attrs.LifecycleRules)
	if err != nil {
		return nil, err
	}
	return &Bucket{
		b:       b,
		r:       c.backend,
		c:       c,
		urlPool: newURLPool(),
	}, err
}

// ListBuckets returns all the available buckets.
func (c *Client) ListBuckets(ctx context.Context, bucketTypes ...string) ([]*Bucket, error) {
	bs, err := c.backend.listBuckets(ctx, "", bucketTypes...)
	if err != nil {
		return nil, err
	}
	var buckets []*Bucket
	for _, b := range bs {
		buckets = append(buckets, &Bucket{
			b:       b,
			r:       c.backend,
			c:       c,
			urlPool: newURLPool(),
		})
	}
	return buckets, nil
}

// IsUpdateConflict reports whether a given error is the result of a bucket
// update conflict.
func IsUpdateConflict(err error) bool {
	e, ok := err.(b2err)
	if !ok {
		return false
	}
	return e.isUpdateConflict
}

// Update modifies the given bucket with new attributes.  It is possible that
// this method could fail with an update conflict, in which case you should
// retrieve the latest bucket attributes with Attrs and try again.
func (b *Bucket) Update(ctx context.Context, attrs *BucketAttrs) error {
	return b.b.updateBucket(ctx, attrs)
}

// Attrs retrieves and returns the current bucket's attributes.
func (b *Bucket) Attrs(ctx context.Context) (*BucketAttrs, error) {
	bucket, err := b.c.Bucket(ctx, b.Name())
	if err != nil {
		return nil, err
	}
	b.b = bucket.b
	return b.b.attrs(), nil
}

var bNotExist = regexp.MustCompile("Bucket.*does not exist")

// Delete removes a bucket.  The bucket must be empty.
func (b *Bucket) Delete(ctx context.Context) error {
	err := b.b.deleteBucket(ctx)
	if err == nil {
		return err
	}
	// So, the B2 documentation disagrees with the implementation here, and the
	// error code is not really helpful.  If the bucket doesn't exist, the error is
	// 400, not 404, and the string is "Bucket <name> does not exist".  However, the
	// documentation says it will be "Bucket id <name> does not exist".  In case
	// they update the implementation to match the documentation, we're just going
	// to regexp over the error message and hope it's okay.
	if bNotExist.MatchString(err.Error()) {
		return b2err{
			err:         err,
			notFoundErr: true,
		}
	}
	return err
}

// BaseURL returns the base URL to use for all files uploaded to this bucket.
func (b *Bucket) BaseURL() string {
	return b.b.baseURL()
}

func (b *Bucket) S3URL() string {
	return b.b.s3URL()
}

// Name returns the bucket's name.
func (b *Bucket) Name() string {
	return b.b.name()
}

// Object represents a B2 object.
type Object struct {
	attrs *Attrs
	name  string
	f     beFileInterface
	b     *Bucket
}

// Attrs holds an object's metadata.
type Attrs struct {
	Name            string            // Not used on upload.
	Size            int64             // Not used on upload.
	ContentType     string            // Used on upload, default is "application/octet-stream".
	Status          ObjectState       // Not used on upload.
	UploadTimestamp time.Time         // Not used on upload.
	SHA1            string            // Can be "none" for large files.  If set on upload, will be used for large files.
	LastModified    time.Time         // If present, and there are fewer than 10 keys in the Info field, this is saved on upload.
	Info            map[string]string // Save arbitrary metadata on upload, but limited to 10 keys.
}

// Name returns an object's name
func (o *Object) Name() string {
	return o.name
}

// ID returns an object's id
func (o *Object) ID() string {
	return o.f.id()
}

// Attrs returns an object's attributes.
func (o *Object) Attrs(ctx context.Context) (*Attrs, error) {
	if err := o.ensure(ctx); err != nil {
		return nil, err
	}
	fi, err := o.f.getFileInfo(ctx)
	if err != nil {
		return nil, err
	}
	name, sha, size, ct, info, st, stamp := fi.stats()
	var state ObjectState
	switch st {
	case "upload":
		state = Uploaded
	case "start":
		state = Started
	case "hide":
		state = Hider
	case "folder":
		state = Folder
	}
	var mtime time.Time
	if v, ok := info["src_last_modified_millis"]; ok {
		ms, err := strconv.ParseInt(v, 10, 64)
		if err != nil {
			return nil, err
		}
		mtime = time.Unix(ms/1e3, (ms%1e3)*1e6)
		delete(info, "src_last_modified_millis")
	}
	if v, ok := info["large_file_sha1"]; ok {
		sha = v
	}
	return &Attrs{
		Name:            name,
		Size:            size,
		ContentType:     ct,
		UploadTimestamp: stamp,
		SHA1:            sha,
		Info:            info,
		Status:          state,
		LastModified:    mtime,
	}, nil
}

// ObjectState represents the various states an object can be in.
type ObjectState int

const (
	Unknown ObjectState = iota
	// Started represents a large upload that has been started but not finished
	// or canceled.
	Started
	// Uploaded represents an object that has finished uploading and is complete.
	Uploaded
	// Hider represents an object that exists only to hide another object.  It
	// cannot in itself be downloaded and, in particular, is not a hidden object.
	Hider

	// Folder is a special state given to non-objects that are returned during a
	// List call with a ListDelimiter option.
	Folder
)

// Object returns a reference to the named object in the bucket.  Hidden
// objects cannot be referenced in this manner; they can only be found by
// finding the appropriate reference in ListObjects.
func (b *Bucket) Object(name string) *Object {
	return &Object{
		name: name,
		b:    b,
	}
}

// URL returns the full URL to the given object.
func (o *Object) URL() string {
	return fmt.Sprintf("%s/file/%s/%s", o.b.BaseURL(), o.b.Name(), o.name)
}

// NewWriter returns a new writer for the given object.  Objects that are
// overwritten are not deleted, but are "hidden".
//
// Callers must close the writer when finished and check the error status.
func (o *Object) NewWriter(ctx context.Context, opts ...WriterOption) *Writer {
	ctx, cancel := context.WithCancel(ctx)
	w := &Writer{
		o:      o,
		name:   o.name,
		ctx:    ctx,
		cancel: cancel,
	}
	for _, f := range o.b.c.opts.writerOpts {
		f(w)
	}
	for _, f := range opts {
		f(w)
	}
	return w
}

// NewRangeReader returns a reader for the given object, reading up to length
// bytes.  If length is negative, the rest of the object is read.
func (o *Object) NewRangeReader(ctx context.Context, offset, length int64) *Reader {
	ctx, cancel := context.WithCancel(ctx)
	return &Reader{
		ctx:    ctx,
		cancel: cancel,
		o:      o,
		name:   o.name,
		chunks: make(map[int]*rchunk),
		length: length,
		offset: offset,
	}
}

// NewReader returns a reader for the given object.
func (o *Object) NewReader(ctx context.Context) *Reader {
	return o.NewRangeReader(ctx, 0, -1)
}

func (o *Object) ensure(ctx context.Context) error {
	if o.f == nil {
		f, err := o.b.getObject(ctx, o.name)
		if err != nil {
			return err
		}
		o.f = f.f
	}
	return nil
}

// Delete removes the given object, if it is a regular file or hide marker
func (o *Object) Delete(ctx context.Context) error {
	if err := o.ensure(ctx); err != nil {
		return err
	}
	status := o.f.status()
	if status == "upload" || status == "hide" {
		return o.f.deleteFileVersion(ctx)
	} else {
		return fmt.Errorf("%s is not a regular file or hide marker: %s", o.name, status)
	}
}

// Cancel cancels a large file upload
func (o *Object) Cancel(ctx context.Context) error {
	if err := o.ensure(ctx); err != nil {
		return err
	}
	status := o.f.status()
	if status == "start" {
		// b2.List allows you to iterate through b2.Object structs, which each contain a beFileInterface.
		// However, we need a beLargeFileInterface to call b2.cancel, so just create a new beLargeFileInterface
		// that contains all the same fields as the beFileInterface.
		// This isn't great, but the alternative, making large files a kind of file, or having files and
		// large files have a common parent, seems to introduce unworkable complexity
		lf := o.f.AsLargeFile()
		return lf.cancel(ctx)
	} else {
		return fmt.Errorf("%s is not an unfinished large file: %s", o.name, status)
	}
}

// Hide hides the object from name-based listing.
func (o *Object) Hide(ctx context.Context) error {
	if err := o.ensure(ctx); err != nil {
		return err
	}
	_, err := o.b.b.hideFile(ctx, o.name)
	return err
}

// Reveal unhides (if hidden) the named object.  If there are multiple objects
// of a given name, it will reveal the most recent.
func (b *Bucket) Reveal(ctx context.Context, name string) error {
	iter := b.List(ctx, ListPrefix(name), ListHidden())
	for iter.Next() {
		obj := iter.Object()
		if obj.Name() == name {
			if obj.f.status() == "hide" {
				return obj.Delete(ctx)
			}
			return nil
		}
		if obj.Name() > name {
			break
		}
	}
	return b2err{err: fmt.Errorf("%s: not found", name), notFoundErr: true}
}

// I don't want to import all of ioutil for this.
type discard struct{}

func (discard) Write(p []byte) (int, error) {
	return len(p), nil
}

func (b *Bucket) getObject(ctx context.Context, name string) (*Object, error) {
	fr, err := b.b.downloadFileByName(ctx, name, 0, 0, true)
	if err != nil {
		return nil, err
	}
	io.Copy(discard{}, fr)
	fr.Close()
	return &Object{
		name: name,
		f:    b.b.file(fr.id(), name),
		b:    b,
	}, nil
}

// AuthToken returns an authorization token that can be used to access objects
// in a private bucket.  Only objects that begin with prefix can be accessed.
// The token expires after the given duration.
func (b *Bucket) AuthToken(ctx context.Context, prefix string, valid time.Duration) (string, error) {
	return b.b.getDownloadAuthorization(ctx, prefix, valid, "")
}

// AuthURL returns a URL for the given object with embedded token and,
// possibly, b2ContentDisposition arguments.  Leave b2cd blank for no content
// disposition.
func (o *Object) AuthURL(ctx context.Context, valid time.Duration, b2cd string) (*url.URL, error) {
	token, err := o.b.b.getDownloadAuthorization(ctx, o.name, valid, b2cd)
	if err != nil {
		return nil, err
	}
	urlString := fmt.Sprintf("%s?Authorization=%s", o.URL(), url.QueryEscape(token))
	if b2cd != "" {
		urlString = fmt.Sprintf("%s&b2ContentDisposition=%s", urlString, url.QueryEscape(b2cd))
	}
	u, err := url.Parse(urlString)
	if err != nil {
		return nil, err
	}
	return u, nil
}
