// Copyright 2024 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 gensupport

import (
	"errors"
	"io"
	"net"
	"strings"
	"time"

	"github.com/googleapis/gax-go/v2"
	"google.golang.org/api/googleapi"
)

// Backoff is an interface around gax.Backoff's Pause method, allowing tests to provide their
// own implementation.
type Backoff interface {
	Pause() time.Duration
}

// These are declared as global variables so that tests can overwrite them.
var (
	// Default per-chunk deadline for resumable uploads.
	defaultRetryDeadline = 32 * time.Second
	// Default backoff timer.
	backoff = func() Backoff {
		return &gax.Backoff{Initial: 100 * time.Millisecond}
	}
	// syscallRetryable is a platform-specific hook, specified in retryable_linux.go
	syscallRetryable func(error) bool = func(err error) bool { return false }
)

const (
	// statusTooManyRequests is returned by the storage API if the
	// per-project limits have been temporarily exceeded. The request
	// should be retried.
	// https://cloud.google.com/storage/docs/json_api/v1/status-codes#standardcodes
	statusTooManyRequests = 429

	// statusRequestTimeout is returned by the storage API if the
	// upload connection was broken. The request should be retried.
	statusRequestTimeout = 408
)

// shouldRetry indicates whether an error is retryable for the purposes of this
// package, unless a ShouldRetry func is specified by the RetryConfig instead.
// It follows guidance from
// https://cloud.google.com/storage/docs/exponential-backoff .
func shouldRetry(status int, err error) bool {
	if 500 <= status && status <= 599 {
		return true
	}
	if status == statusTooManyRequests || status == statusRequestTimeout {
		return true
	}
	if err == io.ErrUnexpectedEOF {
		return true
	}
	// Transient network errors should be retried.
	if syscallRetryable(err) {
		return true
	}
	if err, ok := err.(interface{ Temporary() bool }); ok {
		if err.Temporary() {
			return true
		}
	}
	var opErr *net.OpError
	if errors.As(err, &opErr) {
		if strings.Contains(opErr.Error(), "use of closed network connection") {
			// TODO: check against net.ErrClosed (go 1.16+) instead of string
			return true
		}
	}

	// If Go 1.13 error unwrapping is available, use this to examine wrapped
	// errors.
	if err, ok := err.(interface{ Unwrap() error }); ok {
		return shouldRetry(status, err.Unwrap())
	}
	return false
}

// RetryConfig allows configuration of backoff timing and retryable errors.
type RetryConfig struct {
	Backoff     *gax.Backoff
	ShouldRetry func(err error) bool
}

// Get a new backoff object based on the configured values.
func (r *RetryConfig) backoff() Backoff {
	if r == nil || r.Backoff == nil {
		return backoff()
	}
	return &gax.Backoff{
		Initial:    r.Backoff.Initial,
		Max:        r.Backoff.Max,
		Multiplier: r.Backoff.Multiplier,
	}
}

// This is kind of hacky; it is necessary because ShouldRetry expects to
// handle HTTP errors via googleapi.Error, but the error has not yet been
// wrapped with a googleapi.Error at this layer, and the ErrorFunc type
// in the manual layer does not pass in a status explicitly as it does
// here. So, we must wrap error status codes in a googleapi.Error so that
// ShouldRetry can parse this correctly.
func (r *RetryConfig) errorFunc() func(status int, err error) bool {
	if r == nil || r.ShouldRetry == nil {
		return shouldRetry
	}
	return func(status int, err error) bool {
		if status >= 400 {
			return r.ShouldRetry(&googleapi.Error{Code: status})
		}
		return r.ShouldRetry(err)
	}
}
