// Copyright 2023 Google LLC
//
// 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.

// Package oauth2adapt helps converts types used in [cloud.google.com/go/auth]
// and [golang.org/x/oauth2].
package oauth2adapt

import (
	"context"
	"encoding/json"
	"errors"

	"cloud.google.com/go/auth"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
)

// TokenProviderFromTokenSource converts any [golang.org/x/oauth2.TokenSource]
// into a [cloud.google.com/go/auth.TokenProvider].
func TokenProviderFromTokenSource(ts oauth2.TokenSource) auth.TokenProvider {
	return &tokenProviderAdapter{ts: ts}
}

type tokenProviderAdapter struct {
	ts oauth2.TokenSource
}

// Token fulfills the [cloud.google.com/go/auth.TokenProvider] interface. It
// is a light wrapper around the underlying TokenSource.
func (tp *tokenProviderAdapter) Token(context.Context) (*auth.Token, error) {
	tok, err := tp.ts.Token()
	if err != nil {
		var err2 *oauth2.RetrieveError
		if ok := errors.As(err, &err2); ok {
			return nil, AuthErrorFromRetrieveError(err2)
		}
		return nil, err
	}
	return &auth.Token{
		Value:  tok.AccessToken,
		Expiry: tok.Expiry,
	}, nil
}

// TokenSourceFromTokenProvider converts any
// [cloud.google.com/go/auth.TokenProvider] into a
// [golang.org/x/oauth2.TokenSource].
func TokenSourceFromTokenProvider(tp auth.TokenProvider) oauth2.TokenSource {
	return &tokenSourceAdapter{tp: tp}
}

type tokenSourceAdapter struct {
	tp auth.TokenProvider
}

// Token fulfills the [golang.org/x/oauth2.TokenSource] interface. It
// is a light wrapper around the underlying TokenProvider.
func (ts *tokenSourceAdapter) Token() (*oauth2.Token, error) {
	tok, err := ts.tp.Token(context.Background())
	if err != nil {
		var err2 *auth.Error
		if ok := errors.As(err, &err2); ok {
			return nil, AddRetrieveErrorToAuthError(err2)
		}
		return nil, err
	}
	return &oauth2.Token{
		AccessToken: tok.Value,
		Expiry:      tok.Expiry,
	}, nil
}

// AuthCredentialsFromOauth2Credentials converts a [golang.org/x/oauth2/google.Credentials]
// to a [cloud.google.com/go/auth.Credentials].
func AuthCredentialsFromOauth2Credentials(creds *google.Credentials) *auth.Credentials {
	if creds == nil {
		return nil
	}
	return auth.NewCredentials(&auth.CredentialsOptions{
		TokenProvider: TokenProviderFromTokenSource(creds.TokenSource),
		JSON:          creds.JSON,
		ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
			return creds.ProjectID, nil
		}),
		UniverseDomainProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
			return creds.GetUniverseDomain()
		}),
	})
}

// Oauth2CredentialsFromAuthCredentials converts a [cloud.google.com/go/auth.Credentials]
// to a [golang.org/x/oauth2/google.Credentials].
func Oauth2CredentialsFromAuthCredentials(creds *auth.Credentials) *google.Credentials {
	if creds == nil {
		return nil
	}
	// Throw away errors as old credentials are not request aware. Also, no
	// network requests are currently happening for this use case.
	projectID, _ := creds.ProjectID(context.Background())

	return &google.Credentials{
		TokenSource: TokenSourceFromTokenProvider(creds.TokenProvider),
		ProjectID:   projectID,
		JSON:        creds.JSON(),
		UniverseDomainProvider: func() (string, error) {
			return creds.UniverseDomain(context.Background())
		},
	}
}

type oauth2Error struct {
	ErrorCode        string `json:"error"`
	ErrorDescription string `json:"error_description"`
	ErrorURI         string `json:"error_uri"`
}

// AddRetrieveErrorToAuthError returns the same error provided and adds a
// [golang.org/x/oauth2.RetrieveError] to the error chain by setting the `Err` field on the
// [cloud.google.com/go/auth.Error].
func AddRetrieveErrorToAuthError(err *auth.Error) *auth.Error {
	if err == nil {
		return nil
	}
	e := &oauth2.RetrieveError{
		Response: err.Response,
		Body:     err.Body,
	}
	err.Err = e
	if len(err.Body) > 0 {
		var oErr oauth2Error
		// ignore the error as it only fills in extra details
		json.Unmarshal(err.Body, &oErr)
		e.ErrorCode = oErr.ErrorCode
		e.ErrorDescription = oErr.ErrorDescription
		e.ErrorURI = oErr.ErrorURI
	}
	return err
}

// AuthErrorFromRetrieveError returns an [cloud.google.com/go/auth.Error] that
// wraps the provided [golang.org/x/oauth2.RetrieveError].
func AuthErrorFromRetrieveError(err *oauth2.RetrieveError) *auth.Error {
	if err == nil {
		return nil
	}
	return &auth.Error{
		Response: err.Response,
		Body:     err.Body,
		Err:      err,
	}
}