package s2s2

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"

	"code.justin.tv/amzn/TwitchS2S2/internal/httpwrap"
	"code.justin.tv/amzn/TwitchS2S2/internal/origin"
	"code.justin.tv/amzn/TwitchS2S2/internal/token"
)

// authenticatingHTTPClient provides an implementation of HTTPClient that
// authenticates all requests to a target
//
// if it receives a parsable www authenticate header, it will re-authenticate
// with the target expected scopes.
type authenticatingHTTPClient struct {
	Inner        httpwrap.HTTPClient
	AccessTokens token.Tokens
	// LambdaName is the target lambda for this HTTP client.
	// if empty string, this will default to using the req.URL to determine the
	// target host.
	LambdaName string
}

func (h *authenticatingHTTPClient) Do(req *http.Request) (*http.Response, error) {
	targetHost, err := h.targetHost(req)
	if err != nil {
		return nil, err
	}

	accessToken, err := h.AccessTokens.Token(req.Context(), token.NewOptions().
		WithHost(targetHost))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+accessToken.AccessToken)

	var bodyCopy io.ReadCloser
	if req.Body != nil && req.Body != http.NoBody {
		// TODO: make this configurable
		// copy the body in case we need to send it again on an authenticate challenge
		var buf bytes.Buffer
		req.Body = ioutil.NopCloser(io.TeeReader(req.Body, &buf))
		bodyCopy = ioutil.NopCloser(&buf)
	}

	res, err := h.Inner.Do(req)
	if err != nil {
		return nil, err
	}

	switch authErr := parseAuthorizationError(res).(type) {
	case *invalidTokenError:
		return h.reauthorizeAndRoundTrip(req, accessToken, bodyCopy, authErr)
	case *insufficientScopeError:
		return h.reauthorizeAndRoundTrip(req, accessToken, bodyCopy, authErr)
	}

	return res, err
}

func (h *authenticatingHTTPClient) reauthorizeAndRoundTrip(
	req *http.Request,
	accessToken *token.Token,
	bodyCopy io.ReadCloser,
	authErr authorizationError,
) (*http.Response, error) {
	targetHost, err := h.targetHost(req)
	if err != nil {
		return nil, err
	}

	targetScope := authErr.AuthenticateChallenge().Scope().Extend(accessToken.Scope)
	accessToken, err = h.AccessTokens.Token(req.Context(), token.NewOptions().
		WithHost(targetHost).
		WithScope(targetScope),
	)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+accessToken.AccessToken)
	if bodyCopy != nil {
		req.Body = bodyCopy
	}

	return h.Inner.Do(req)
}

func (h *authenticatingHTTPClient) targetHost(req *http.Request) (string, error) {
	if h.LambdaName != "" {
		// case when our transport is a lambda transport
		return h.LambdaName, nil
	}
	// generic HTTP transport
	targetHost := origin.CanonicalWebOriginFromURL(req.URL)
	if targetHost == origin.UndefinedWebOrigin {
		// do not expect to get here
		return "", fmt.Errorf("could not determine web origin from url<%s>", req.URL.String())
	}
	return targetHost, nil
}
