package azure

import (
	"context"
	"net/http"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
)

// Inspired by/credit goes to https://github.com/Azure/azure-storage-azcopy/blob/97ab7b92e766ad48965ac2933495dff1b04fb2a7/ste/xferRetryNotificationPolicy.go
type contextKey struct {
	name string
}

var (
	timeoutNotifyContextKey = contextKey{"timeoutNotify"}
	retryNotifyContextKey   = contextKey{"retryNotify"}
)

// retryNotificationReceiver should be implemented by code that wishes to be
// notified when a retry happens. Such code must register itself into the
// context, using withRetryNotification, so that the RetryNotificationPolicy
// can invoke the callback when necessary.
type retryNotificationReceiver interface {
	RetryCallback()
}

// withTimeoutNotification returns a context that contains indication of a
// timeout. The retryNotificationPolicy will then set the timeout flag when a
// timeout happens
func withTimeoutNotification(ctx context.Context, timeout *bool) context.Context {
	return context.WithValue(ctx, timeoutNotifyContextKey, timeout)
}

// withRetryNotification returns a context that contains a retry notifier. The
// retryNotificationPolicy will then invoke the callback when a retry happens
func withRetryNotification(ctx context.Context, r retryNotificationReceiver) context.Context { // nolint: unused // may become useful at some point
	return context.WithValue(ctx, retryNotifyContextKey, r)
}

// PolicyFunc is a type that implements the Policy interface.
// Use this type when implementing a stateless policy as a first-class function.
type PolicyFunc func(*policy.Request) (*http.Response, error)

// Do implements the Policy interface on policyFunc.
func (pf PolicyFunc) Do(req *policy.Request) (*http.Response, error) {
	return pf(req)
}

func newRetryNotificationPolicy() policy.Policy {
	getErrorCode := func(resp *http.Response) string {
		// NOTE(prozlach): This is a hacky way to handle all possible cases of
		// emitting error by the Azure backend.
		// In theory we could look just at `x-ms-error-code` HTTP header, but
		// in practice Azure SDK also looks at the body and decodes it as JSON
		// or XML in case when the header is absent.
		// So the idea is to piggy-back on the runtime.NewResponseError that
		// will do the proper decoding for us and just return the ErrorCode
		// field instead.
		return runtime.NewResponseError(resp).(*azcore.ResponseError).ErrorCode
	}

	return PolicyFunc(func(req *policy.Request) (*http.Response, error) {
		response, err := req.Next() // Make the request

		if response == nil {
			return nil, err
		}

		switch response.StatusCode {
		case http.StatusServiceUnavailable:
			// Grab the notification callback out of the context and, if its there, call it
			if notifier, ok := req.Raw().Context().Value(retryNotifyContextKey).(retryNotificationReceiver); ok {
				notifier.RetryCallback()
			}
		case http.StatusInternalServerError:
			errorCodeHeader := getErrorCode(response)
			if bloberror.Code(errorCodeHeader) != bloberror.OperationTimedOut {
				break
			}

			if timeout, ok := req.Raw().Context().Value(timeoutNotifyContextKey).(*bool); ok {
				*timeout = true
			}
		}
		return response, err
	})
}
