package azure

// Copyright 2017 Microsoft Corporation
//
//  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.

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/Azure/go-autorest/autorest"
	"github.com/Azure/go-autorest/tracing"
)

const (
	headerAsyncOperation = "Azure-AsyncOperation"
)

const (
	operationInProgress string = "InProgress"
	operationCanceled   string = "Canceled"
	operationFailed     string = "Failed"
	operationSucceeded  string = "Succeeded"
)

var pollingCodes = [...]int{http.StatusNoContent, http.StatusAccepted, http.StatusCreated, http.StatusOK}

// Future provides a mechanism to access the status and results of an asynchronous request.
// Since futures are stateful they should be passed by value to avoid race conditions.
type Future struct {
	pt pollingTracker
}

// NewFutureFromResponse returns a new Future object initialized
// with the initial response from an asynchronous operation.
func NewFutureFromResponse(resp *http.Response) (Future, error) {
	pt, err := createPollingTracker(resp)
	return Future{pt: pt}, err
}

// Response returns the last HTTP response.
func (f Future) Response() *http.Response {
	if f.pt == nil {
		return nil
	}
	return f.pt.latestResponse()
}

// Status returns the last status message of the operation.
func (f Future) Status() string {
	if f.pt == nil {
		return ""
	}
	return f.pt.pollingStatus()
}

// PollingMethod returns the method used to monitor the status of the asynchronous operation.
func (f Future) PollingMethod() PollingMethodType {
	if f.pt == nil {
		return PollingUnknown
	}
	return f.pt.pollingMethod()
}

// DoneWithContext queries the service to see if the operation has completed.
func (f *Future) DoneWithContext(ctx context.Context, sender autorest.Sender) (done bool, err error) {
	ctx = tracing.StartSpan(ctx, "github.com/Azure/go-autorest/autorest/azure/async.DoneWithContext")
	defer func() {
		sc := -1
		resp := f.Response()
		if resp != nil {
			sc = resp.StatusCode
		}
		tracing.EndSpan(ctx, sc, err)
	}()

	if f.pt == nil {
		return false, autorest.NewError("Future", "Done", "future is not initialized")
	}
	if f.pt.hasTerminated() {
		return true, f.pt.pollingError()
	}
	if err := f.pt.pollForStatus(ctx, sender); err != nil {
		return false, err
	}
	if err := f.pt.checkForErrors(); err != nil {
		return f.pt.hasTerminated(), err
	}
	if err := f.pt.updatePollingState(f.pt.provisioningStateApplicable()); err != nil {
		return false, err
	}
	if err := f.pt.initPollingMethod(); err != nil {
		return false, err
	}
	if err := f.pt.updatePollingMethod(); err != nil {
		return false, err
	}
	return f.pt.hasTerminated(), f.pt.pollingError()
}

// GetPollingDelay returns a duration the application should wait before checking
// the status of the asynchronous request and true; this value is returned from
// the service via the Retry-After response header.  If the header wasn't returned
// then the function returns the zero-value time.Duration and false.
func (f Future) GetPollingDelay() (time.Duration, bool) {
	if f.pt == nil {
		return 0, false
	}
	resp := f.pt.latestResponse()
	if resp == nil {
		return 0, false
	}

	retry := resp.Header.Get(autorest.HeaderRetryAfter)
	if retry == "" {
		return 0, false
	}

	d, err := time.ParseDuration(retry + "s")
	if err != nil {
		panic(err)
	}

	return d, true
}

// WaitForCompletionRef will return when one of the following conditions is met: the long
// running operation has completed, the provided context is cancelled, or the client's
// polling duration has been exceeded.  It will retry failed polling attempts based on
// the retry value defined in the client up to the maximum retry attempts.
// If no deadline is specified in the context then the client.PollingDuration will be
// used to determine if a default deadline should be used.
// If PollingDuration is greater than zero the value will be used as the context's timeout.
// If PollingDuration is zero then no default deadline will be used.
func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Client) (err error) {
	ctx = tracing.StartSpan(ctx, "github.com/Azure/go-autorest/autorest/azure/async.WaitForCompletionRef")
	defer func() {
		sc := -1
		resp := f.Response()
		if resp != nil {
			sc = resp.StatusCode
		}
		tracing.EndSpan(ctx, sc, err)
	}()
	cancelCtx := ctx
	// if the provided context already has a deadline don't override it
	_, hasDeadline := ctx.Deadline()
	if d := client.PollingDuration; !hasDeadline && d != 0 {
		var cancel context.CancelFunc
		cancelCtx, cancel = context.WithTimeout(ctx, d)
		defer cancel()
	}
	// if the initial response has a Retry-After, sleep for the specified amount of time before starting to poll
	if delay, ok := f.GetPollingDelay(); ok {
		if delayElapsed := autorest.DelayForBackoff(delay, 0, cancelCtx.Done()); !delayElapsed {
			err = cancelCtx.Err()
			return
		}
	}
	done, err := f.DoneWithContext(ctx, client)
	for attempts := 0; !done; done, err = f.DoneWithContext(ctx, client) {
		if attempts >= client.RetryAttempts {
			return autorest.NewErrorWithError(err, "Future", "WaitForCompletion", f.pt.latestResponse(), "the number of retries has been exceeded")
		}
		// we want delayAttempt to be zero in the non-error case so
		// that DelayForBackoff doesn't perform exponential back-off
		var delayAttempt int
		var delay time.Duration
		if err == nil {
			// check for Retry-After delay, if not present use the client's polling delay
			var ok bool
			delay, ok = f.GetPollingDelay()
			if !ok {
				delay = client.PollingDelay
			}
		} else {
			// there was an error polling for status so perform exponential
			// back-off based on the number of attempts using the client's retry
			// duration.  update attempts after delayAttempt to avoid off-by-one.
			delayAttempt = attempts
			delay = client.RetryDuration
			attempts++
		}
		// wait until the delay elapses or the context is cancelled
		delayElapsed := autorest.DelayForBackoff(delay, delayAttempt, cancelCtx.Done())
		if !delayElapsed {
			return autorest.NewErrorWithError(cancelCtx.Err(), "Future", "WaitForCompletion", f.pt.latestResponse(), "context has been cancelled")
		}
	}
	return
}

// MarshalJSON implements the json.Marshaler interface.
func (f Future) MarshalJSON() ([]byte, error) {
	return json.Marshal(f.pt)
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (f *Future) UnmarshalJSON(data []byte) error {
	// unmarshal into JSON object to determine the tracker type
	obj := map[string]interface{}{}
	err := json.Unmarshal(data, &obj)
	if err != nil {
		return err
	}
	if obj["method"] == nil {
		return autorest.NewError("Future", "UnmarshalJSON", "missing 'method' property")
	}
	method := obj["method"].(string)
	switch strings.ToUpper(method) {
	case http.MethodDelete:
		f.pt = &pollingTrackerDelete{}
	case http.MethodPatch:
		f.pt = &pollingTrackerPatch{}
	case http.MethodPost:
		f.pt = &pollingTrackerPost{}
	case http.MethodPut:
		f.pt = &pollingTrackerPut{}
	default:
		return autorest.NewError("Future", "UnmarshalJSON", "unsupoorted method '%s'", method)
	}
	// now unmarshal into the tracker
	return json.Unmarshal(data, &f.pt)
}

// PollingURL returns the URL used for retrieving the status of the long-running operation.
func (f Future) PollingURL() string {
	if f.pt == nil {
		return ""
	}
	return f.pt.pollingURL()
}

// GetResult should be called once polling has completed successfully.
// It makes the final GET call to retrieve the resultant payload.
func (f Future) GetResult(sender autorest.Sender) (*http.Response, error) {
	if f.pt.finalGetURL() == "" {
		// we can end up in this situation if the async operation returns a 200
		// with no polling URLs.  in that case return the response which should
		// contain the JSON payload (only do this for successful terminal cases).
		if lr := f.pt.latestResponse(); lr != nil && f.pt.hasSucceeded() {
			return lr, nil
		}
		return nil, autorest.NewError("Future", "GetResult", "missing URL for retrieving result")
	}
	req, err := http.NewRequest(http.MethodGet, f.pt.finalGetURL(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := sender.Do(req)
	if err == nil && resp.Body != nil {
		// copy the body and close it so callers don't have to
		defer resp.Body.Close()
		b, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return resp, err
		}
		resp.Body = ioutil.NopCloser(bytes.NewReader(b))
	}
	return resp, err
}

type pollingTracker interface {
	// these methods can differ per tracker

	// checks the response headers and status code to determine the polling mechanism
	updatePollingMethod() error

	// checks the response for tracker-specific error conditions
	checkForErrors() error

	// returns true if provisioning state should be checked
	provisioningStateApplicable() bool

	// methods common to all trackers

	// initializes a tracker's polling URL and method, called for each iteration.
	// these values can be overridden by each polling tracker as required.
	initPollingMethod() error

	// initializes the tracker's internal state, call this when the tracker is created
	initializeState() error

	// makes an HTTP request to check the status of the LRO
	pollForStatus(ctx context.Context, sender autorest.Sender) error

	// updates internal tracker state, call this after each call to pollForStatus
	updatePollingState(provStateApl bool) error

	// returns the error response from the service, can be nil
	pollingError() error

	// returns the polling method being used
	pollingMethod() PollingMethodType

	// returns the state of the LRO as returned from the service
	pollingStatus() string

	// returns the URL used for polling status
	pollingURL() string

	// returns the URL used for the final GET to retrieve the resource
	finalGetURL() string

	// returns true if the LRO is in a terminal state
	hasTerminated() bool

	// returns true if the LRO is in a failed terminal state
	hasFailed() bool

	// returns true if the LRO is in a successful terminal state
	hasSucceeded() bool

	// returns the cached HTTP response after a call to pollForStatus(), can be nil
	latestResponse() *http.Response
}

type pollingTrackerBase struct {
	// resp is the last response, either from the submission of the LRO or from polling
	resp *http.Response

	// method is the HTTP verb, this is needed for deserialization
	Method string `json:"method"`

	// rawBody is the raw JSON response body
	rawBody map[string]interface{}

	// denotes if polling is using async-operation or location header
	Pm PollingMethodType `json:"pollingMethod"`

	// the URL to poll for status
	URI string `json:"pollingURI"`

	// the state of the LRO as returned from the service
	State string `json:"lroState"`

	// the URL to GET for the final result
	FinalGetURI string `json:"resultURI"`

	// used to hold an error object returned from the service
	Err *ServiceError `json:"error,omitempty"`
}

func (pt *pollingTrackerBase) initializeState() error {
	// determine the initial polling state based on response body and/or HTTP status
	// code.  this is applicable to the initial LRO response, not polling responses!
	pt.Method = pt.resp.Request.Method
	if err := pt.updateRawBody(); err != nil {
		return err
	}
	switch pt.resp.StatusCode {
	case http.StatusOK:
		if ps := pt.getProvisioningState(); ps != nil {
			pt.State = *ps
			if pt.hasFailed() {
				pt.updateErrorFromResponse()
				return pt.pollingError()
			}
		} else {
			pt.State = operationSucceeded
		}
	case http.StatusCreated:
		if ps := pt.getProvisioningState(); ps != nil {
			pt.State = *ps
		} else {
			pt.State = operationInProgress
		}
	case http.StatusAccepted:
		pt.State = operationInProgress
	case http.StatusNoContent:
		pt.State = operationSucceeded
	default:
		pt.State = operationFailed
		pt.updateErrorFromResponse()
		return pt.pollingError()
	}
	return pt.initPollingMethod()
}

func (pt pollingTrackerBase) getProvisioningState() *string {
	if pt.rawBody != nil && pt.rawBody["properties"] != nil {
		p := pt.rawBody["properties"].(map[string]interface{})
		if ps := p["provisioningState"]; ps != nil {
			s := ps.(string)
			return &s
		}
	}
	return nil
}

func (pt *pollingTrackerBase) updateRawBody() error {
	pt.rawBody = map[string]interface{}{}
	if pt.resp.ContentLength != 0 {
		defer pt.resp.Body.Close()
		b, err := ioutil.ReadAll(pt.resp.Body)
		if err != nil {
			return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to read response body")
		}
		// observed in 204 responses over HTTP/2.0; the content length is -1 but body is empty
		if len(b) == 0 {
			return nil
		}
		// put the body back so it's available to other callers
		pt.resp.Body = ioutil.NopCloser(bytes.NewReader(b))
		if err = json.Unmarshal(b, &pt.rawBody); err != nil {
			return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to unmarshal response body")
		}
	}
	return nil
}

func (pt *pollingTrackerBase) pollForStatus(ctx context.Context, sender autorest.Sender) error {
	req, err := http.NewRequest(http.MethodGet, pt.URI, nil)
	if err != nil {
		return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to create HTTP request")
	}

	req = req.WithContext(ctx)
	preparer := autorest.CreatePreparer(autorest.GetPrepareDecorators(ctx)...)
	req, err = preparer.Prepare(req)
	if err != nil {
		return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed preparing HTTP request")
	}
	pt.resp, err = sender.Do(req)
	if err != nil {
		return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to send HTTP request")
	}
	if autorest.ResponseHasStatusCode(pt.resp, pollingCodes[:]...) {
		// reset the service error on success case
		pt.Err = nil
		err = pt.updateRawBody()
	} else {
		// check response body for error content
		pt.updateErrorFromResponse()
		err = pt.pollingError()
	}
	return err
}

// attempts to unmarshal a ServiceError type from the response body.
// if that fails then make a best attempt at creating something meaningful.
// NOTE: this assumes that the async operation has failed.
func (pt *pollingTrackerBase) updateErrorFromResponse() {
	var err error
	if pt.resp.ContentLength != 0 {
		type respErr struct {
			ServiceError *ServiceError `json:"error"`
		}
		re := respErr{}
		defer pt.resp.Body.Close()
		var b []byte
		if b, err = ioutil.ReadAll(pt.resp.Body); err != nil || len(b) == 0 {
			goto Default
		}
		if err = json.Unmarshal(b, &re); err != nil {
			goto Default
		}
		// unmarshalling the error didn't yield anything, try unwrapped error
		if re.ServiceError == nil {
			err = json.Unmarshal(b, &re.ServiceError)
			if err != nil {
				goto Default
			}
		}
		// the unmarshaller will ensure re.ServiceError is non-nil
		// even if there was no content unmarshalled so check the code.
		if re.ServiceError.Code != "" {
			pt.Err = re.ServiceError
			return
		}
	}
Default:
	se := &ServiceError{
		Code:    pt.pollingStatus(),
		Message: "The async operation failed.",
	}
	if err != nil {
		se.InnerError = make(map[string]interface{})
		se.InnerError["unmarshalError"] = err.Error()
	}
	// stick the response body into the error object in hopes
	// it contains something useful to help diagnose the failure.
	if len(pt.rawBody) > 0 {
		se.AdditionalInfo = []map[string]interface{}{
			pt.rawBody,
		}
	}
	pt.Err = se
}

func (pt *pollingTrackerBase) updatePollingState(provStateApl bool) error {
	if pt.Pm == PollingAsyncOperation && pt.rawBody["status"] != nil {
		pt.State = pt.rawBody["status"].(string)
	} else {
		if pt.resp.StatusCode == http.StatusAccepted {
			pt.State = operationInProgress
		} else if provStateApl {
			if ps := pt.getProvisioningState(); ps != nil {
				pt.State = *ps
			} else {
				pt.State = operationSucceeded
			}
		} else {
			return autorest.NewError("pollingTrackerBase", "updatePollingState", "the response from the async operation has an invalid status code")
		}
	}
	// if the operation has failed update the error state
	if pt.hasFailed() {
		pt.updateErrorFromResponse()
	}
	return nil
}

func (pt pollingTrackerBase) pollingError() error {
	if pt.Err == nil {
		return nil
	}
	return pt.Err
}

func (pt pollingTrackerBase) pollingMethod() PollingMethodType {
	return pt.Pm
}

func (pt pollingTrackerBase) pollingStatus() string {
	return pt.State
}

func (pt pollingTrackerBase) pollingURL() string {
	return pt.URI
}

func (pt pollingTrackerBase) finalGetURL() string {
	return pt.FinalGetURI
}

func (pt pollingTrackerBase) hasTerminated() bool {
	return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed) || strings.EqualFold(pt.State, operationSucceeded)
}

func (pt pollingTrackerBase) hasFailed() bool {
	return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed)
}

func (pt pollingTrackerBase) hasSucceeded() bool {
	return strings.EqualFold(pt.State, operationSucceeded)
}

func (pt pollingTrackerBase) latestResponse() *http.Response {
	return pt.resp
}

// error checking common to all trackers
func (pt pollingTrackerBase) baseCheckForErrors() error {
	// for Azure-AsyncOperations the response body cannot be nil or empty
	if pt.Pm == PollingAsyncOperation {
		if pt.resp.Body == nil || pt.resp.ContentLength == 0 {
			return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "for Azure-AsyncOperation response body cannot be nil")
		}
		if pt.rawBody["status"] == nil {
			return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "missing status property in Azure-AsyncOperation response body")
		}
	}
	return nil
}

// default initialization of polling URL/method.  each verb tracker will update this as required.
func (pt *pollingTrackerBase) initPollingMethod() error {
	if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
		return err
	} else if ao != "" {
		pt.URI = ao
		pt.Pm = PollingAsyncOperation
		return nil
	}
	if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
		return err
	} else if lh != "" {
		pt.URI = lh
		pt.Pm = PollingLocation
		return nil
	}
	// it's ok if we didn't find a polling header, this will be handled elsewhere
	return nil
}

// DELETE

type pollingTrackerDelete struct {
	pollingTrackerBase
}

func (pt *pollingTrackerDelete) updatePollingMethod() error {
	// for 201 the Location header is required
	if pt.resp.StatusCode == http.StatusCreated {
		if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
			return err
		} else if lh == "" {
			return autorest.NewError("pollingTrackerDelete", "updateHeaders", "missing Location header in 201 response")
		} else {
			pt.URI = lh
		}
		pt.Pm = PollingLocation
		pt.FinalGetURI = pt.URI
	}
	// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
	if pt.resp.StatusCode == http.StatusAccepted {
		ao, err := getURLFromAsyncOpHeader(pt.resp)
		if err != nil {
			return err
		} else if ao != "" {
			pt.URI = ao
			pt.Pm = PollingAsyncOperation
		}
		// if the Location header is invalid and we already have a polling URL
		// then we don't care if the Location header URL is malformed.
		if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
			return err
		} else if lh != "" {
			if ao == "" {
				pt.URI = lh
				pt.Pm = PollingLocation
			}
			// when both headers are returned we use the value in the Location header for the final GET
			pt.FinalGetURI = lh
		}
		// make sure a polling URL was found
		if pt.URI == "" {
			return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
		}
	}
	return nil
}

func (pt pollingTrackerDelete) checkForErrors() error {
	return pt.baseCheckForErrors()
}

func (pt pollingTrackerDelete) provisioningStateApplicable() bool {
	return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent
}

// PATCH

type pollingTrackerPatch struct {
	pollingTrackerBase
}

func (pt *pollingTrackerPatch) updatePollingMethod() error {
	// by default we can use the original URL for polling and final GET
	if pt.URI == "" {
		pt.URI = pt.resp.Request.URL.String()
	}
	if pt.FinalGetURI == "" {
		pt.FinalGetURI = pt.resp.Request.URL.String()
	}
	if pt.Pm == PollingUnknown {
		pt.Pm = PollingRequestURI
	}
	// for 201 it's permissible for no headers to be returned
	if pt.resp.StatusCode == http.StatusCreated {
		if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
			return err
		} else if ao != "" {
			pt.URI = ao
			pt.Pm = PollingAsyncOperation
		}
	}
	// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
	// note the absence of the "final GET" mechanism for PATCH
	if pt.resp.StatusCode == http.StatusAccepted {
		ao, err := getURLFromAsyncOpHeader(pt.resp)
		if err != nil {
			return err
		} else if ao != "" {
			pt.URI = ao
			pt.Pm = PollingAsyncOperation
		}
		if ao == "" {
			if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
				return err
			} else if lh == "" {
				return autorest.NewError("pollingTrackerPatch", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
			} else {
				pt.URI = lh
				pt.Pm = PollingLocation
			}
		}
	}
	return nil
}

func (pt pollingTrackerPatch) checkForErrors() error {
	return pt.baseCheckForErrors()
}

func (pt pollingTrackerPatch) provisioningStateApplicable() bool {
	return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated
}

// POST

type pollingTrackerPost struct {
	pollingTrackerBase
}

func (pt *pollingTrackerPost) updatePollingMethod() error {
	// 201 requires Location header
	if pt.resp.StatusCode == http.StatusCreated {
		if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
			return err
		} else if lh == "" {
			return autorest.NewError("pollingTrackerPost", "updateHeaders", "missing Location header in 201 response")
		} else {
			pt.URI = lh
			pt.FinalGetURI = lh
			pt.Pm = PollingLocation
		}
	}
	// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
	if pt.resp.StatusCode == http.StatusAccepted {
		ao, err := getURLFromAsyncOpHeader(pt.resp)
		if err != nil {
			return err
		} else if ao != "" {
			pt.URI = ao
			pt.Pm = PollingAsyncOperation
		}
		// if the Location header is invalid and we already have a polling URL
		// then we don't care if the Location header URL is malformed.
		if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
			return err
		} else if lh != "" {
			if ao == "" {
				pt.URI = lh
				pt.Pm = PollingLocation
			}
			// when both headers are returned we use the value in the Location header for the final GET
			pt.FinalGetURI = lh
		}
		// make sure a polling URL was found
		if pt.URI == "" {
			return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
		}
	}
	return nil
}

func (pt pollingTrackerPost) checkForErrors() error {
	return pt.baseCheckForErrors()
}

func (pt pollingTrackerPost) provisioningStateApplicable() bool {
	return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent
}

// PUT

type pollingTrackerPut struct {
	pollingTrackerBase
}

func (pt *pollingTrackerPut) updatePollingMethod() error {
	// by default we can use the original URL for polling and final GET
	if pt.URI == "" {
		pt.URI = pt.resp.Request.URL.String()
	}
	if pt.FinalGetURI == "" {
		pt.FinalGetURI = pt.resp.Request.URL.String()
	}
	if pt.Pm == PollingUnknown {
		pt.Pm = PollingRequestURI
	}
	// for 201 it's permissible for no headers to be returned
	if pt.resp.StatusCode == http.StatusCreated {
		if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
			return err
		} else if ao != "" {
			pt.URI = ao
			pt.Pm = PollingAsyncOperation
		}
	}
	// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
	if pt.resp.StatusCode == http.StatusAccepted {
		ao, err := getURLFromAsyncOpHeader(pt.resp)
		if err != nil {
			return err
		} else if ao != "" {
			pt.URI = ao
			pt.Pm = PollingAsyncOperation
		}
		// if the Location header is invalid and we already have a polling URL
		// then we don't care if the Location header URL is malformed.
		if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
			return err
		} else if lh != "" {
			if ao == "" {
				pt.URI = lh
				pt.Pm = PollingLocation
			}
		}
		// make sure a polling URL was found
		if pt.URI == "" {
			return autorest.NewError("pollingTrackerPut", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
		}
	}
	return nil
}

func (pt pollingTrackerPut) checkForErrors() error {
	err := pt.baseCheckForErrors()
	if err != nil {
		return err
	}
	// if there are no LRO headers then the body cannot be empty
	ao, err := getURLFromAsyncOpHeader(pt.resp)
	if err != nil {
		return err
	}
	lh, err := getURLFromLocationHeader(pt.resp)
	if err != nil {
		return err
	}
	if ao == "" && lh == "" && len(pt.rawBody) == 0 {
		return autorest.NewError("pollingTrackerPut", "checkForErrors", "the response did not contain a body")
	}
	return nil
}

func (pt pollingTrackerPut) provisioningStateApplicable() bool {
	return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated
}

// creates a polling tracker based on the verb of the original request
func createPollingTracker(resp *http.Response) (pollingTracker, error) {
	var pt pollingTracker
	switch strings.ToUpper(resp.Request.Method) {
	case http.MethodDelete:
		pt = &pollingTrackerDelete{pollingTrackerBase: pollingTrackerBase{resp: resp}}
	case http.MethodPatch:
		pt = &pollingTrackerPatch{pollingTrackerBase: pollingTrackerBase{resp: resp}}
	case http.MethodPost:
		pt = &pollingTrackerPost{pollingTrackerBase: pollingTrackerBase{resp: resp}}
	case http.MethodPut:
		pt = &pollingTrackerPut{pollingTrackerBase: pollingTrackerBase{resp: resp}}
	default:
		return nil, autorest.NewError("azure", "createPollingTracker", "unsupported HTTP method %s", resp.Request.Method)
	}
	if err := pt.initializeState(); err != nil {
		return pt, err
	}
	// this initializes the polling header values, we do this during creation in case the
	// initial response send us invalid values; this way the API call will return a non-nil
	// error (not doing this means the error shows up in Future.Done)
	return pt, pt.updatePollingMethod()
}

// gets the polling URL from the Azure-AsyncOperation header.
// ensures the URL is well-formed and absolute.
func getURLFromAsyncOpHeader(resp *http.Response) (string, error) {
	s := resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation))
	if s == "" {
		return "", nil
	}
	if !isValidURL(s) {
		return "", autorest.NewError("azure", "getURLFromAsyncOpHeader", "invalid polling URL '%s'", s)
	}
	return s, nil
}

// gets the polling URL from the Location header.
// ensures the URL is well-formed and absolute.
func getURLFromLocationHeader(resp *http.Response) (string, error) {
	s := resp.Header.Get(http.CanonicalHeaderKey(autorest.HeaderLocation))
	if s == "" {
		return "", nil
	}
	if !isValidURL(s) {
		return "", autorest.NewError("azure", "getURLFromLocationHeader", "invalid polling URL '%s'", s)
	}
	return s, nil
}

// verify that the URL is valid and absolute
func isValidURL(s string) bool {
	u, err := url.Parse(s)
	return err == nil && u.IsAbs()
}

// PollingMethodType defines a type used for enumerating polling mechanisms.
type PollingMethodType string

const (
	// PollingAsyncOperation indicates the polling method uses the Azure-AsyncOperation header.
	PollingAsyncOperation PollingMethodType = "AsyncOperation"

	// PollingLocation indicates the polling method uses the Location header.
	PollingLocation PollingMethodType = "Location"

	// PollingRequestURI indicates the polling method uses the original request URI.
	PollingRequestURI PollingMethodType = "RequestURI"

	// PollingUnknown indicates an unknown polling method and is the default value.
	PollingUnknown PollingMethodType = ""
)

// AsyncOpIncompleteError is the type that's returned from a future that has not completed.
type AsyncOpIncompleteError struct {
	// FutureType is the name of the type composed of a azure.Future.
	FutureType string
}

// Error returns an error message including the originating type name of the error.
func (e AsyncOpIncompleteError) Error() string {
	return fmt.Sprintf("%s: asynchronous operation has not completed", e.FutureType)
}

// NewAsyncOpIncompleteError creates a new AsyncOpIncompleteError with the specified parameters.
func NewAsyncOpIncompleteError(futureType string) AsyncOpIncompleteError {
	return AsyncOpIncompleteError{
		FutureType: futureType,
	}
}
