/*
Copyright © The ESO Authors

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

    https://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 mysterybox

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

	lru "github.com/hashicorp/golang-lru"
	"github.com/nebius/gosdk/auth"
	"golang.org/x/sync/singleflight"
	"k8s.io/utils/clock"

	"github.com/external-secrets/external-secrets/providers/v1/nebius/common/sdk/iam"
)

const (
	errInvalidSubjectCreds = "invalid subject credentials: malformed JSON"
)

// TokenGetter is an interface for generating and retrieving authentication tokens.
type TokenGetter interface {
	GetToken(ctx context.Context, apiDomain, subjectCreds string, caCert []byte) (string, error)
}

type tokenCacheKey struct {
	APIDomain        string
	PublicKeyID      string
	ServiceAccountID string
	PrivateKeyHash   string
}

func (k *tokenCacheKey) String() string {
	return k.APIDomain + "|" + k.PublicKeyID + "|" + k.ServiceAccountID + "|" + k.PrivateKeyHash
}

// CachedTokenGetter is responsible for managing Nebius IAM token caching and token exchange processes.
type CachedTokenGetter struct {
	TokenExchanger iam.TokenExchanger
	Clock          clock.Clock
	tokenCache     *lru.Cache
	sf             singleflight.Group
}

// NewCachedTokenGetter initializes a CachedTokenGetter with the specified cache size, token exchanger, and clock.
// Returns a CachedTokenGetter instance and an error if LRU cache creation fails.
func NewCachedTokenGetter(cacheSize int, tokenExchanger iam.TokenExchanger, clock clock.Clock) (*CachedTokenGetter, error) {
	cache, err := lru.New(cacheSize)
	if err != nil {
		return nil, err
	}
	return &CachedTokenGetter{
		tokenCache:     cache,
		TokenExchanger: tokenExchanger,
		Clock:          clock,
	}, nil
}

func isTokenExpired(token *iam.Token, clk clock.Clock) bool {
	now := clk.Now()
	if token.ExpiresAt.After(now) {
		total := token.ExpiresAt.Sub(token.IssuedAt)
		remaining := token.ExpiresAt.Sub(now)
		if remaining > total/10 {
			return false
		}
	}
	return true
}

// GetToken retrieves an IAM token for the given API domain and subject credentials, using a cache to optimize requests.
// It exchanges credentials for a new token if no valid cached token exists or the cached token is nearing expiration.
func (c *CachedTokenGetter) GetToken(ctx context.Context, apiDomain, subjectCreds string, caCert []byte) (string, error) {
	byteCreds := []byte(subjectCreds)
	cacheKey, err := buildTokenCacheKey(byteCreds, apiDomain)

	if err != nil {
		return "", err
	}

	value, ok := c.tokenCache.Get(*cacheKey)
	if ok {
		token := value.(*iam.Token)
		tokenExpired := isTokenExpired(token, c.Clock)
		if !tokenExpired {
			return token.Token, nil
		}
	}

	tokenCacheKeyString := cacheKey.String()

	token, err, _ := c.sf.Do(tokenCacheKeyString, func() (any, error) {
		if v, ok := c.tokenCache.Get(*cacheKey); ok {
			tok := v.(*iam.Token)
			if !isTokenExpired(tok, c.Clock) {
				return tok.Token, nil
			}
		}

		newToken, err := c.TokenExchanger.ExchangeIamToken(ctx, apiDomain, subjectCreds, c.Clock.Now(), caCert)
		if err != nil {
			return "", fmt.Errorf("could not exchange creds to iam token: %w", MapGrpcErrors("create token", err))
		}

		c.tokenCache.Add(*cacheKey, newToken)
		return newToken.Token, nil
	})

	if err != nil {
		return "", err
	}
	return token.(string), nil
}

func buildTokenCacheKey(subjectCreds []byte, apiDomain string) (*tokenCacheKey, error) {
	parsedSubjectCreds := &auth.ServiceAccountCredentials{}
	err := json.Unmarshal(subjectCreds, parsedSubjectCreds)
	if err != nil {
		return nil, errors.New(errInvalidSubjectCreds)
	}
	return &tokenCacheKey{
		APIDomain:        apiDomain,
		PublicKeyID:      parsedSubjectCreds.SubjectCredentials.KeyID,
		ServiceAccountID: parsedSubjectCreds.SubjectCredentials.Subject,
		PrivateKeyHash:   HashBytes([]byte(parsedSubjectCreds.SubjectCredentials.PrivateKey)),
	}, nil
}

var _ TokenGetter = &CachedTokenGetter{}
