/*
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 secretmanager

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net/url"
	"os"
	"sync"
	"time"

	authV1 "github.com/cloudru-tech/iam-sdk/api/auth/v1"
	smssdk "github.com/cloudru-tech/secret-manager-sdk"
	"github.com/google/uuid"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/keepalive"
	kclient "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
	"github.com/external-secrets/external-secrets/providers/v1/cloudru/secretmanager/adapter"
	"github.com/external-secrets/external-secrets/runtime/esutils"
)

// ProviderSpec returns the provider specification for registration.
func ProviderSpec() *esv1.SecretStoreProvider {
	return &esv1.SecretStoreProvider{CloudruSM: &esv1.CloudruSMProvider{}}
}

// MaintenanceStatus returns the maintenance status of the provider.
func MaintenanceStatus() esv1.MaintenanceStatus {
	return esv1.MaintenanceStatusMaintained
}

var _ esv1.Provider = &Provider{}
var _ esv1.SecretsClient = &Client{}

// Provider is a secrets provider for Cloud.ru Secret Manager.
type Provider struct {
	mu sync.Mutex

	// clients is a map of Cloud.ru Secret Manager clients.
	// Is used to cache the clients to avoid multiple connections,
	// and excess token retrieving with same credentials.
	clients map[string]*adapter.APIClient
}

// NewProvider creates a new Cloud.ru Secret Manager Provider.
func NewProvider() *Provider {
	return &Provider{
		clients: make(map[string]*adapter.APIClient),
	}
}

// NewClient constructs a Cloud.ru Secret Manager Provider.
func (p *Provider) NewClient(
	ctx context.Context,
	store esv1.GenericStore,
	kube kclient.Client,
	namespace string,
) (esv1.SecretsClient, error) {
	if _, err := p.ValidateStore(store); err != nil {
		return nil, fmt.Errorf("invalid store: %w", err)
	}

	csmRef := store.GetSpec().Provider.CloudruSM
	storeKind := store.GetObjectKind().GroupVersionKind().Kind
	cr := NewKubeCredentialsResolver(kube, namespace, storeKind, csmRef.Auth.SecretRef)

	client, err := p.getClient(ctx, cr)
	if err != nil {
		return nil, fmt.Errorf("failed to connect cloud.ru services: %w", err)
	}

	return &Client{
		apiClient: client,
		projectID: csmRef.ProjectID,
	}, nil
}

func (p *Provider) getClient(ctx context.Context, cr adapter.CredentialsResolver) (*adapter.APIClient, error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	discoveryURL, tokenURL, smURL, err := provideEndpoints()
	if err != nil {
		return nil, fmt.Errorf("parse endpoint URLs: %w", err)
	}

	creds, err := cr.Resolve(ctx)
	if err != nil {
		return nil, fmt.Errorf("resolve API credentials: %w", err)
	}

	connStack := fmt.Sprintf("%s,%s+%s", discoveryURL, creds.KeyID, creds.Secret)
	client, ok := p.clients[connStack]
	if ok {
		return client, nil
	}
	iamConn, err := grpc.NewClient(tokenURL,
		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS13})),
		grpc.WithKeepaliveParams(keepalive.ClientParameters{
			Time:                time.Second * 30,
			Timeout:             time.Second * 5,
			PermitWithoutStream: false,
		}),
		grpc.WithUserAgent("external-secrets"),
	)
	if err != nil {
		return nil, fmt.Errorf("initialize cloud.ru IAM gRPC client: initiate connection: %w", err)
	}

	smsClient, err := smssdk.New(&smssdk.Config{Host: smURL},
		grpc.WithKeepaliveParams(keepalive.ClientParameters{
			Time:                time.Second * 30,
			Timeout:             time.Second * 5,
			PermitWithoutStream: false,
		}),
		grpc.WithUserAgent("external-secrets"),
	)
	if err != nil {
		return nil, fmt.Errorf("initialize cloud.ru Secret Manager gRPC client: initiate connection: %w", err)
	}

	iamClient := authV1.NewAuthServiceClient(iamConn)
	client = adapter.NewAPIClient(cr, iamClient, smsClient)

	p.clients[connStack] = client
	return client, nil
}

// ValidateStore validates the store specification.
func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
	if store == nil {
		return nil, errors.New("store is not provided")
	}
	spec := store.GetSpec()
	if spec == nil || spec.Provider == nil || spec.Provider.CloudruSM == nil {
		return nil, errors.New("csm spec is not provided")
	}

	csmProvider := spec.Provider.CloudruSM
	switch {
	case csmProvider.Auth.SecretRef == nil:
		return nil, errors.New("invalid spec: auth.secretRef is required")
	case csmProvider.ProjectID == "":
		return nil, errors.New("invalid spec: projectID is required")
	}
	if _, err := uuid.Parse(csmProvider.ProjectID); err != nil {
		return nil, fmt.Errorf("invalid spec: projectID is invalid UUID: %w", err)
	}

	ref := csmProvider.Auth.SecretRef
	err := esutils.ValidateReferentSecretSelector(store, ref.AccessKeyID)
	if err != nil {
		return nil, fmt.Errorf("invalid spec: auth.secretRef.accessKeyID: %w", err)
	}

	err = esutils.ValidateReferentSecretSelector(store, ref.AccessKeySecret)
	if err != nil {
		return nil, fmt.Errorf("invalid spec: auth.secretRef.accessKeySecret: %w", err)
	}

	return nil, nil
}

// Capabilities returns the provider Capabilities (ReadOnly).
func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
	return esv1.SecretStoreReadOnly
}

func provideEndpoints() (discoveryURL, tokenURL, smURL string, err error) {
	discoveryURL = EndpointsURI
	if du := os.Getenv("CLOUDRU_DISCOVERY_URL"); du != "" {
		var u *url.URL
		u, err = url.Parse(du)
		if err != nil {
			return "", "", "", fmt.Errorf("parse discovery URL: %w", err)
		}

		if u.Scheme != "https" && u.Scheme != "http" {
			return "", "", "", fmt.Errorf("invalid scheme in discovery URL, expected http or https, got %s", u.Scheme)
		}

		discoveryURL = du
	}

	// try to get the endpoints from the environment variables.
	csmAddress := os.Getenv("CLOUDRU_CSM_ADDRESS")
	iamAddress := os.Getenv("CLOUDRU_IAM_ADDRESS")
	if csmAddress != "" && iamAddress != "" {
		return discoveryURL, iamAddress, csmAddress, nil
	}

	// using the discovery URL to get the endpoints.
	var endpoints *EndpointsResponse
	endpoints, err = GetEndpoints(discoveryURL)
	if err != nil {
		return "", "", "", fmt.Errorf("discover cloud.ru API endpoints: %w", err)
	}

	smEndpoint := endpoints.Get("secret-manager")
	if smEndpoint == nil {
		return "", "", "", errors.New("secret-manager API is not available")
	}

	iamEndpoint := endpoints.Get("iam")
	if iamEndpoint == nil {
		return "", "", "", errors.New("iam API is not available")
	}

	return discoveryURL, iamEndpoint.Address, smEndpoint.Address, nil
}
