// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 oauth2

import (
	"net/http"

	"strings"

	"github.com/apache/pulsar-client-go/oauth2/clock"

	"github.com/pkg/errors"
)

// ClientCredentialsFlow takes care of the mechanics needed for getting an access
// token using the OAuth 2.0 "Client Credentials Flow"
type ClientCredentialsFlow struct {
	options       ClientCredentialsFlowOptions
	exchanger     ClientCredentialsExchanger
	grantProvider GrantProvider
	clock         clock.Clock
}

// ClientCredentialsProvider abstracts getting client credentials
type ClientCredentialsProvider interface {
	GetClientCredentials() (*KeyFile, error)
}

// ClientCredentialsExchanger abstracts exchanging client credentials for tokens
type ClientCredentialsExchanger interface {
	ExchangeClientCredentials(req ClientCredentialsExchangeRequest) (*TokenResult, error)
}

// GrantProvider abstracts the creation of authorization grants from credentials
type GrantProvider interface {
	GetGrant(audience string, options *ClientCredentialsFlowOptions) (*AuthorizationGrant, error)
}

type ClientCredentialsFlowOptions struct {
	KeyFile          string
	IssuerURL        string
	AdditionalScopes []string
}

// DefaultGrantProvider provides authorization grants by loading credentials from a key file
type DefaultGrantProvider struct {
}

// GetGrant creates an authorization grant by loading credentials from the key file and
// merging the scopes from both the options and the key file configuration
func (p *DefaultGrantProvider) GetGrant(audience string, options *ClientCredentialsFlowOptions) (
	*AuthorizationGrant, error) {
	if options == nil {
		return nil, errors.New("client credentials flow options cannot be nil")
	}
	credsProvider := NewClientCredentialsProviderFromKeyFile(options.KeyFile)
	keyFile, err := credsProvider.GetClientCredentials()
	if err != nil {
		return nil, errors.Wrap(err, "could not get client credentials")
	}

	issuerURL := options.IssuerURL
	if issuerURL == "" {
		issuerURL = keyFile.IssuerURL
	}
	if issuerURL == "" {
		return nil, errors.New("issuer url is required for client credentials flow")
	}

	wellKnownEndpoints, err := GetOIDCWellKnownEndpointsFromIssuerURL(issuerURL)
	if err != nil {
		return nil, err
	}
	// Merge the scopes of the options AdditionalScopes with the scopes read from the keyFile config
	var scopesToAdd []string
	if len(options.AdditionalScopes) > 0 {
		scopesToAdd = append(scopesToAdd, options.AdditionalScopes...)
	}

	if keyFile.Scope != "" {
		scopesSplit := strings.Fields(keyFile.Scope)
		scopesToAdd = append(scopesToAdd, scopesSplit...)
	}

	return &AuthorizationGrant{
		Type:              GrantTypeClientCredentials,
		Audience:          audience,
		ClientID:          keyFile.ClientID,
		ClientCredentials: keyFile,
		TokenEndpoint:     wellKnownEndpoints.TokenEndpoint,
		Scopes:            scopesToAdd,
	}, nil
}

func newClientCredentialsFlow(
	options ClientCredentialsFlowOptions,
	exchanger ClientCredentialsExchanger,
	grantProvider GrantProvider,
	clock clock.Clock) *ClientCredentialsFlow {
	return &ClientCredentialsFlow{
		options:       options,
		exchanger:     exchanger,
		grantProvider: grantProvider,
		clock:         clock,
	}
}

// NewDefaultClientCredentialsFlow provides an easy way to build up a default
// client credentials flow with all the correct configuration.
func NewDefaultClientCredentialsFlow(options ClientCredentialsFlowOptions) (*ClientCredentialsFlow, error) {

	tokenRetriever := NewTokenRetriever(&http.Client{})
	return newClientCredentialsFlow(
		options,
		tokenRetriever,
		&DefaultGrantProvider{},
		clock.RealClock{}), nil
}

var _ Flow = &ClientCredentialsFlow{}

func (c *ClientCredentialsFlow) Authorize(audience string) (*AuthorizationGrant, error) {
	grant, err := c.grantProvider.GetGrant(audience, &c.options)
	if err != nil {
		return nil, err
	}

	// test the credentials and obtain an initial access token
	refresher := &ClientCredentialsGrantRefresher{
		exchanger: c.exchanger,
		clock:     c.clock,
	}
	grant, err = refresher.Refresh(grant)
	if err != nil {
		return nil, errors.Wrap(err, "authentication failed using client credentials")
	}
	return grant, nil
}

type ClientCredentialsGrantRefresher struct {
	exchanger ClientCredentialsExchanger
	clock     clock.Clock
}

func NewDefaultClientCredentialsGrantRefresher(clock clock.Clock) (*ClientCredentialsGrantRefresher, error) {
	tokenRetriever := NewTokenRetriever(&http.Client{})
	return &ClientCredentialsGrantRefresher{
		exchanger: tokenRetriever,
		clock:     clock,
	}, nil
}

var _ AuthorizationGrantRefresher = &ClientCredentialsGrantRefresher{}

func (g *ClientCredentialsGrantRefresher) Refresh(grant *AuthorizationGrant) (*AuthorizationGrant, error) {
	if grant.Type != GrantTypeClientCredentials {
		return nil, errors.New("unsupported grant type")
	}

	exchangeRequest := ClientCredentialsExchangeRequest{
		TokenEndpoint: grant.TokenEndpoint,
		Audience:      grant.Audience,
		ClientID:      grant.ClientCredentials.ClientID,
		ClientSecret:  grant.ClientCredentials.ClientSecret,
		Scopes:        grant.Scopes,
	}
	tr, err := g.exchanger.ExchangeClientCredentials(exchangeRequest)
	if err != nil {
		return nil, errors.Wrap(err, "could not exchange client credentials")
	}

	token := convertToOAuth2Token(tr, g.clock)
	grant = &AuthorizationGrant{
		Type:              GrantTypeClientCredentials,
		Audience:          grant.Audience,
		ClientID:          grant.ClientID,
		ClientCredentials: grant.ClientCredentials,
		TokenEndpoint:     grant.TokenEndpoint,
		Token:             &token,
		Scopes:            grant.Scopes,
	}
	return grant, nil
}
