package s2stoken

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

	"code.justin.tv/amzn/TwitchS2S2/internal/s2s2err"

	"code.justin.tv/amzn/TwitchS2S2/c7s"
	"code.justin.tv/amzn/TwitchS2S2/internal/interfaces"
	"code.justin.tv/amzn/TwitchS2S2/internal/oidc/oidciface"
	"code.justin.tv/amzn/TwitchS2S2/internal/opwrap"
	"code.justin.tv/amzn/TwitchS2S2/internal/token"
	"code.justin.tv/video/metrics-middleware/v2/operation"
)

// ClientCredentialsError is returned on server errors when creating client
// credentials
type ClientCredentialsError struct {
	Message string
}

func (err ClientCredentialsError) Error() string {
	return s2s2err.S2S2ErrorString("error requesting client credentials: "+err.Message, "ClientCredentials")
}

// ClientCredentials are returns as part of the client credentials flow of
// OAuth2. This will require an assertion from the Assertions client.
//
// See https://tools.ietf.org/html/rfc6749#section-4.4
type ClientCredentials struct {
	Assertions       token.Tokens
	Client           interfaces.HTTPClient
	Config           *c7s.Config
	OIDC             oidciface.OIDCAPI
	OperationStarter *operation.Starter
}

// Token implements token.Tokens
func (cc *ClientCredentials) Token(ctx context.Context, options *token.Options) (_ *token.Token, err error) {
	ctx, op := cc.OperationStarter.StartOp(ctx, opwrap.GetClientCredentials)
	defer opwrap.EndWithError(op, err)

	authorizationGrant, err := cc.Assertions.Token(ctx, &token.Options{})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", cc.OIDC.Configuration().TokenEndpoint, nil)
	if err != nil {
		return nil, err
	}

	values := make(url.Values)
	values.Set("grant_type", "twitch_s2s_service_credentials")
	values.Set("assertion", authorizationGrant.AccessToken)
	values.Set("scope", cc.Config.TokenScope)

	req.Body = ioutil.NopCloser(bytes.NewReader([]byte(values.Encode())))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("x-host", cc.Config.Issuer)

	res, err := cc.Client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return nil, &ClientCredentialsError{Message: parseAuthServerError(res.Body)}
	}

	var clientCredentials token.Token
	if err := json.NewDecoder(res.Body).Decode(&clientCredentials); err != nil {
		return nil, err
	}
	clientCredentials.Issued = time.Now().UTC()

	return &clientCredentials, nil
}
