// Copyright 2022 Google LLC
//
// 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 storage

import (
	"context"
	"io"
	"time"

	"cloud.google.com/go/iam/apiv1/iampb"
	gax "github.com/googleapis/gax-go/v2"
	"google.golang.org/api/option"
)

// TODO(noahdietz): Move existing factory methods to this file.

// storageClient is an internal-only interface designed to separate the
// transport-specific logic of making Storage API calls from the logic of the
// client library.
//
// Implementation requirements beyond implementing the interface include:
// * factory method(s) must accept a `userProject string` param
// * `settings` must be retained per instance
// * `storageOption`s must be resolved in the order they are received
// * all API errors must be wrapped in the gax-go APIError type
// * any unimplemented interface methods must return a StorageUnimplementedErr
//
// TODO(noahdietz): This interface is currently not used in the production code
// paths
type storageClient interface {

	// Top-level methods.

	GetServiceAccount(ctx context.Context, project string, opts ...storageOption) (string, error)
	CreateBucket(ctx context.Context, project, bucket string, attrs *BucketAttrs, enableObjectRetention *bool, opts ...storageOption) (*BucketAttrs, error)
	ListBuckets(ctx context.Context, project string, opts ...storageOption) *BucketIterator
	Close() error

	// Bucket methods.

	DeleteBucket(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) error
	GetBucket(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error)
	UpdateBucket(ctx context.Context, bucket string, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error)
	LockBucketRetentionPolicy(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) error
	ListObjects(ctx context.Context, bucket string, q *Query, opts ...storageOption) *ObjectIterator

	// Object metadata methods.

	DeleteObject(ctx context.Context, bucket, object string, gen int64, conds *Conditions, opts ...storageOption) error
	GetObject(ctx context.Context, params *getObjectParams, opts ...storageOption) (*ObjectAttrs, error)
	UpdateObject(ctx context.Context, params *updateObjectParams, opts ...storageOption) (*ObjectAttrs, error)
	RestoreObject(ctx context.Context, params *restoreObjectParams, opts ...storageOption) (*ObjectAttrs, error)

	// Default Object ACL methods.

	DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error
	ListDefaultObjectACLs(ctx context.Context, bucket string, opts ...storageOption) ([]ACLRule, error)
	UpdateDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, role ACLRole, opts ...storageOption) error

	// Bucket ACL methods.

	DeleteBucketACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error
	ListBucketACLs(ctx context.Context, bucket string, opts ...storageOption) ([]ACLRule, error)
	UpdateBucketACL(ctx context.Context, bucket string, entity ACLEntity, role ACLRole, opts ...storageOption) error

	// Object ACL methods.

	DeleteObjectACL(ctx context.Context, bucket, object string, entity ACLEntity, opts ...storageOption) error
	ListObjectACLs(ctx context.Context, bucket, object string, opts ...storageOption) ([]ACLRule, error)
	UpdateObjectACL(ctx context.Context, bucket, object string, entity ACLEntity, role ACLRole, opts ...storageOption) error

	// Media operations.

	ComposeObject(ctx context.Context, req *composeObjectRequest, opts ...storageOption) (*ObjectAttrs, error)
	RewriteObject(ctx context.Context, req *rewriteObjectRequest, opts ...storageOption) (*rewriteObjectResponse, error)

	NewRangeReader(ctx context.Context, params *newRangeReaderParams, opts ...storageOption) (*Reader, error)
	OpenWriter(params *openWriterParams, opts ...storageOption) (*io.PipeWriter, error)

	// IAM methods.

	GetIamPolicy(ctx context.Context, resource string, version int32, opts ...storageOption) (*iampb.Policy, error)
	SetIamPolicy(ctx context.Context, resource string, policy *iampb.Policy, opts ...storageOption) error
	TestIamPermissions(ctx context.Context, resource string, permissions []string, opts ...storageOption) ([]string, error)

	// HMAC Key methods.

	GetHMACKey(ctx context.Context, project, accessID string, opts ...storageOption) (*HMACKey, error)
	ListHMACKeys(ctx context.Context, project, serviceAccountEmail string, showDeletedKeys bool, opts ...storageOption) *HMACKeysIterator
	UpdateHMACKey(ctx context.Context, project, serviceAccountEmail, accessID string, attrs *HMACKeyAttrsToUpdate, opts ...storageOption) (*HMACKey, error)
	CreateHMACKey(ctx context.Context, project, serviceAccountEmail string, opts ...storageOption) (*HMACKey, error)
	DeleteHMACKey(ctx context.Context, project, accessID string, opts ...storageOption) error

	// Notification methods.
	ListNotifications(ctx context.Context, bucket string, opts ...storageOption) (map[string]*Notification, error)
	CreateNotification(ctx context.Context, bucket string, n *Notification, opts ...storageOption) (*Notification, error)
	DeleteNotification(ctx context.Context, bucket string, id string, opts ...storageOption) error
}

// settings contains transport-agnostic configuration for API calls made via
// the storageClient inteface. All implementations must utilize settings
// and respect those that are applicable.
type settings struct {
	// retry is the complete retry configuration to use when evaluating if an
	// API call should be retried.
	retry *retryConfig

	// gax is a set of gax.CallOption to be conveyed to gax.Invoke.
	// Note: Not all storageClient interfaces will must use gax.Invoke.
	gax []gax.CallOption

	// idempotent indicates if the call is idempotent or not when considering
	// if the call should be retried or not.
	idempotent bool

	// clientOption is a set of option.ClientOption to be used during client
	// transport initialization. See https://pkg.go.dev/google.golang.org/api/option
	// for a list of supported options.
	clientOption []option.ClientOption

	// userProject is the user project that should be billed for the request.
	userProject string

	metricsContext *metricsContext
}

func initSettings(opts ...storageOption) *settings {
	s := &settings{}
	resolveOptions(s, opts...)
	return s
}

func resolveOptions(s *settings, opts ...storageOption) {
	for _, o := range opts {
		o.Apply(s)
	}
}

// callSettings is a helper for resolving storage options against the settings
// in the context of an individual call. This is to ensure that client-level
// default settings are not mutated by two different calls getting options.
//
// Example: s := callSettings(c.settings, opts...)
func callSettings(defaults *settings, opts ...storageOption) *settings {
	if defaults == nil {
		return nil
	}
	// This does not make a deep copy of the pointer/slice fields, but all
	// options replace the settings fields rather than modify their values in
	// place.
	cs := *defaults
	resolveOptions(&cs, opts...)
	return &cs
}

// makeStorageOpts is a helper for generating a set of storageOption based on
// idempotency, retryConfig, and userProject. All top-level client operations
// will generally have to pass these options through the interface.
func makeStorageOpts(isIdempotent bool, retry *retryConfig, userProject string) []storageOption {
	opts := []storageOption{idempotent(isIdempotent)}
	if retry != nil {
		opts = append(opts, withRetryConfig(retry))
	}
	if userProject != "" {
		opts = append(opts, withUserProject(userProject))
	}
	return opts
}

// storageOption is the transport-agnostic call option for the storageClient
// interface.
type storageOption interface {
	Apply(s *settings)
}

func withRetryConfig(rc *retryConfig) storageOption {
	return &retryOption{rc}
}

type retryOption struct {
	rc *retryConfig
}

func (o *retryOption) Apply(s *settings) { s.retry = o.rc }

func idempotent(i bool) storageOption {
	return &idempotentOption{i}
}

type idempotentOption struct {
	idempotency bool
}

func (o *idempotentOption) Apply(s *settings) { s.idempotent = o.idempotency }

func withClientOptions(opts ...option.ClientOption) storageOption {
	return &clientOption{opts: opts}
}

type clientOption struct {
	opts []option.ClientOption
}

func (o *clientOption) Apply(s *settings) { s.clientOption = o.opts }

func withUserProject(project string) storageOption {
	return &userProjectOption{project}
}

type userProjectOption struct {
	project string
}

func (o *userProjectOption) Apply(s *settings) { s.userProject = o.project }

type openWriterParams struct {
	// Writer configuration

	// ctx is the context used by the writer routine to make all network calls
	// and to manage the writer routine - see `Writer.ctx`.
	// Required.
	ctx context.Context
	// chunkSize - see `Writer.ChunkSize`.
	// Optional.
	chunkSize int
	// chunkRetryDeadline - see `Writer.ChunkRetryDeadline`.
	// Optional.
	chunkRetryDeadline time.Duration

	// Object/request properties

	// bucket - see `Writer.o.bucket`.
	// Required.
	bucket string
	// attrs - see `Writer.ObjectAttrs`.
	// Required.
	attrs *ObjectAttrs
	// forceEmptyContentType - Disables auto-detect of Content-Type
	// Optional.
	forceEmptyContentType bool
	// conds - see `Writer.o.conds`.
	// Optional.
	conds *Conditions
	// encryptionKey - see `Writer.o.encryptionKey`
	// Optional.
	encryptionKey []byte
	// sendCRC32C - see `Writer.SendCRC32C`.
	// Optional.
	sendCRC32C bool

	// Writer callbacks

	// donec - see `Writer.donec`.
	// Required.
	donec chan struct{}
	// setError callback for reporting errors - see `Writer.error`.
	// Required.
	setError func(error)
	// progress callback for reporting upload progress - see `Writer.progress`.
	// Required.
	progress func(int64)
	// setObj callback for reporting the resulting object - see `Writer.obj`.
	// Required.
	setObj func(*ObjectAttrs)
}

type newRangeReaderParams struct {
	bucket         string
	conds          *Conditions
	encryptionKey  []byte
	gen            int64
	length         int64
	object         string
	offset         int64
	readCompressed bool // Use accept-encoding: gzip. Only works for HTTP currently.
}

type getObjectParams struct {
	bucket, object string
	gen            int64
	encryptionKey  []byte
	conds          *Conditions
	softDeleted    bool
}

type updateObjectParams struct {
	bucket, object    string
	uattrs            *ObjectAttrsToUpdate
	gen               int64
	encryptionKey     []byte
	conds             *Conditions
	overrideRetention *bool
}

type restoreObjectParams struct {
	bucket, object string
	gen            int64
	encryptionKey  []byte
	conds          *Conditions
	copySourceACL  bool
}

type composeObjectRequest struct {
	dstBucket     string
	dstObject     destinationObject
	srcs          []sourceObject
	predefinedACL string
	sendCRC32C    bool
}

type sourceObject struct {
	name          string
	bucket        string
	gen           int64
	conds         *Conditions
	encryptionKey []byte
}

type destinationObject struct {
	name          string
	bucket        string
	conds         *Conditions
	attrs         *ObjectAttrs // attrs to set on the destination object.
	encryptionKey []byte
	keyName       string
}

type rewriteObjectRequest struct {
	srcObject                sourceObject
	dstObject                destinationObject
	predefinedACL            string
	token                    string
	maxBytesRewrittenPerCall int64
}

type rewriteObjectResponse struct {
	resource *ObjectAttrs
	done     bool
	written  int64
	size     int64
	token    string
}
