/*
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 beyondtrust provides a Password Safe secrets provider for External Secrets Operator.
package beyondtrust

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
	"github.com/BeyondTrust/go-client-library-passwordsafe/api/entities"
	"github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
	managedaccount "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
	"github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
	"github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
	"github.com/cenkalti/backoff/v4"
	v1 "k8s.io/api/core/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	"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"
	esutils "github.com/external-secrets/external-secrets/runtime/esutils"
	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
)

const (
	errNilStore             = "nil store found"
	errMissingStoreSpec     = "store is missing spec"
	errMissingProvider      = "storeSpec is missing provider"
	errInvalidProvider      = "invalid provider spec. Missing field in store %s"
	errInvalidHostURL       = "invalid host URL"
	errMissingAuthMethod    = "no authentication method configured: set auth.apiKey, or auth.clientId and auth.clientSecret"
	errIncompleteOAuth      = "both clientId and clientSecret must be set for OAuth authentication"
	errNoSuchKeyFmt         = "no such key in secret: %q"
	errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
	errNotImplemented       = "not implemented"

	usernameFieldName    = "username"
	folderNameFieldName  = "folder_name"
	fileNameFieldName    = "file_name"
	titleFieldName       = "title"
	descriptionFieldName = "description"
	ownerIDFieldName     = "owner_id"
	groupIDFieldName     = "group_id"
	ownerTypeFieldName   = "owner_type"
	secretTypeFieldName  = "secret_type"
	secretTypeCredential = "CREDENTIAL"
	secretTypeSecret     = "SECRET"
)

var (
	errSecretRefAndValueConflict = errors.New("cannot specify both secret reference and value")
	errMissingSecretName         = errors.New("must specify a secret name")
	errMissingSecretKey          = errors.New("must specify a secret key")
	// ESOLogger is the logger instance for the Beyondtrust provider.
	ESOLogger              = ctrl.Log.WithName("provider").WithName("beyondtrust")
	maxFileSecretSizeBytes = 5000000
)

// Provider is a Password Safe secrets provider implementing NewClient and ValidateStore for the esv1.Provider interface.
type Provider struct {
	apiURL        string
	retrievaltype string
	decrypt       bool
	authenticate  auth.AuthenticationObj
	log           logging.LogrLogger
	separator     string
}

// AuthenticatorInput is used to pass parameters to the getAuthenticator function.
type AuthenticatorInput struct {
	Config                     *esv1.BeyondtrustProvider
	HTTPClientObj              utils.HttpClientObj
	BackoffDefinition          *backoff.ExponentialBackOff
	APIURL                     string
	APIVersion                 string
	ClientID                   string
	ClientSecret               string
	APIKey                     string
	Logger                     *logging.LogrLogger
	RetryMaxElapsedTimeMinutes int
}

// Capabilities implements v1beta1.Provider.
func (*Provider) Capabilities() esv1.SecretStoreCapabilities {
	return esv1.SecretStoreReadWrite
}

// Close implements v1beta1.SecretsClient.
func (*Provider) Close(_ context.Context) error {
	return nil
}

// DeleteSecret implements v1beta1.SecretsClient.
func (*Provider) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
	return errors.New(errNotImplemented)
}

// GetSecretMap implements v1beta1.SecretsClient.
func (*Provider) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
	return make(map[string][]byte), errors.New(errNotImplemented)
}

// Validate implements v1beta1.SecretsClient.
func (p *Provider) Validate() (esv1.ValidationResult, error) {
	timeout := 15 * time.Second
	clientURL := p.apiURL

	if err := esutils.NetworkValidate(clientURL, timeout); err != nil {
		ESOLogger.Error(err, "Network Validate", "clientURL:", clientURL)
		return esv1.ValidationResultError, err
	}

	return esv1.ValidationResultReady, nil
}

// SecretExists checks if a secret exists in the provider.
func (p *Provider) SecretExists(_ context.Context, pushSecretRef esv1.PushSecretRemoteRef) (bool, error) {
	logger := logging.NewLogrLogger(&ESOLogger)
	secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes, false)

	if err != nil {
		return false, err
	}

	_, err = secretObj.SearchSecretByTitleFlow(pushSecretRef.GetRemoteKey())

	if err == nil {
		return true, nil
	}

	return false, nil
}

// NewClient this is where we initialize the SecretClient and return it for the controller to use.
func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
	config := store.GetSpec().Provider.Beyondtrust
	logger := logging.NewLogrLogger(&ESOLogger)

	storeKind := store.GetKind()
	clientID, clientSecret, apiKey, err := loadCredentialsFromConfig(ctx, config, kube, namespace, storeKind)
	if err != nil {
		return nil, fmt.Errorf("error loading credentials: %w", err)
	}

	certificate, certificateKey, err := loadCertificateFromConfig(ctx, config, kube, namespace, storeKind)
	if err != nil {
		return nil, fmt.Errorf("error loading certificate: %w", err)
	}

	clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes := getConfigValues(config)

	backoffDefinition := getBackoffDefinition(retryMaxElapsedTimeMinutes)

	params := utils.ValidationParams{
		ApiKey:                     apiKey,
		ClientID:                   clientID,
		ClientSecret:               clientSecret,
		ApiUrl:                     &config.Server.APIURL,
		ApiVersion:                 config.Server.APIVersion,
		ClientTimeOutInSeconds:     clientTimeOutInSeconds,
		Separator:                  &separator,
		VerifyCa:                   config.Server.VerifyCA,
		Logger:                     logger,
		Certificate:                certificate,
		CertificateKey:             certificateKey,
		RetryMaxElapsedTimeMinutes: &retryMaxElapsedTimeMinutes,
		MaxFileSecretSizeBytes:     &maxFileSecretSizeBytes,
	}

	if err := validateInputs(params); err != nil {
		return nil, fmt.Errorf("error in Inputs: %w", err)
	}

	httpClient, err := utils.GetHttpClient(clientTimeOutInSeconds, config.Server.VerifyCA, certificate, certificateKey, logger)
	if err != nil {
		return nil, fmt.Errorf("error creating HTTP client: %w", err)
	}

	authenticatorInput := AuthenticatorInput{
		Config:                     config,
		HTTPClientObj:              *httpClient,
		BackoffDefinition:          backoffDefinition,
		APIURL:                     config.Server.APIURL,
		APIVersion:                 config.Server.APIVersion,
		ClientID:                   clientID,
		ClientSecret:               clientSecret,
		APIKey:                     apiKey,
		Logger:                     logger,
		RetryMaxElapsedTimeMinutes: retryMaxElapsedTimeMinutes,
	}

	authenticate, err := getAuthenticator(authenticatorInput)

	if err != nil {
		return nil, fmt.Errorf("error authenticating: %w", err)
	}

	return &Provider{
		apiURL:        config.Server.APIURL,
		retrievaltype: config.Server.RetrievalType,
		authenticate:  *authenticate,
		log:           *logger,
		separator:     separator,
		decrypt:       config.Server.Decrypt,
	}, nil
}

func loadCredentialsFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, string, error) {
	if config.Auth.APIKey != nil {
		apiKey, err := loadConfigSecret(ctx, config.Auth.APIKey, kube, namespace, storeKind)
		return "", "", apiKey, err
	}
	clientID, err := loadConfigSecret(ctx, config.Auth.ClientID, kube, namespace, storeKind)
	if err != nil {
		return "", "", "", fmt.Errorf("error loading clientID: %w", err)
	}
	clientSecret, err := loadConfigSecret(ctx, config.Auth.ClientSecret, kube, namespace, storeKind)
	if err != nil {
		return "", "", "", fmt.Errorf("error loading clientSecret: %w", err)
	}
	return clientID, clientSecret, "", nil
}

func loadCertificateFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, error) {
	if config.Auth.Certificate == nil || config.Auth.CertificateKey == nil {
		return "", "", nil
	}
	certificate, err := loadConfigSecret(ctx, config.Auth.Certificate, kube, namespace, storeKind)
	if err != nil {
		return "", "", fmt.Errorf("error loading Certificate: %w", err)
	}
	certificateKey, err := loadConfigSecret(ctx, config.Auth.CertificateKey, kube, namespace, storeKind)
	if err != nil {
		return "", "", fmt.Errorf("error loading Certificate Key: %w", err)
	}
	return certificate, certificateKey, nil
}

func getConfigValues(config *esv1.BeyondtrustProvider) (int, string, int) {
	clientTimeOutInSeconds := 45
	separator := "/"
	retryMaxElapsedTimeMinutes := 15

	if config.Server.ClientTimeOutSeconds != 0 {
		clientTimeOutInSeconds = config.Server.ClientTimeOutSeconds
	}

	if config.Server.Separator != "" {
		separator = config.Server.Separator
	}

	return clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes
}

func getBackoffDefinition(retryMaxElapsedTimeMinutes int) *backoff.ExponentialBackOff {
	backoffDefinition := backoff.NewExponentialBackOff()
	backoffDefinition.InitialInterval = 1 * time.Second
	backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Minute
	backoffDefinition.RandomizationFactor = 0.5

	return backoffDefinition
}

func validateInputs(params utils.ValidationParams) error {
	return utils.ValidateInputs(params)
}

func getAuthenticator(input AuthenticatorInput) (*auth.AuthenticationObj, error) {
	parametersObj := auth.AuthenticationParametersObj{
		HTTPClient:                 input.HTTPClientObj,
		BackoffDefinition:          input.BackoffDefinition,
		EndpointURL:                input.APIURL,
		APIVersion:                 input.APIVersion,
		ApiKey:                     input.APIKey,
		Logger:                     input.Logger,
		RetryMaxElapsedTimeSeconds: input.RetryMaxElapsedTimeMinutes,
	}

	if input.Config.Auth.APIKey != nil {
		parametersObj.ApiKey = input.APIKey

		return auth.AuthenticateUsingApiKey(parametersObj)
	}

	parametersObj.ClientID = input.ClientID
	parametersObj.ClientSecret = input.ClientSecret

	return auth.Authenticate(parametersObj)
}

func loadConfigSecret(ctx context.Context, ref *esv1.BeyondTrustProviderSecretRef, kube client.Client, defaultNamespace, storeKind string) (string, error) {
	if ref.SecretRef == nil {
		return ref.Value, nil
	}
	if err := validateSecretRef(ref); err != nil {
		return "", err
	}
	return resolvers.SecretKeyRef(ctx, kube, storeKind, defaultNamespace, ref.SecretRef)
}

func validateSecretRef(ref *esv1.BeyondTrustProviderSecretRef) error {
	if ref.SecretRef != nil {
		if ref.Value != "" {
			return errSecretRefAndValueConflict
		}
		if ref.SecretRef.Name == "" {
			return errMissingSecretName
		}
		if ref.SecretRef.Key == "" {
			return errMissingSecretKey
		}
	}

	return nil
}

// GetAllSecrets retrieves all secrets from Beyondtrust.
func (p *Provider) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
	return nil, errors.New("GetAllSecrets not implemented")
}

// GetSecret reads the secret from the Password Safe server and returns it. The controller uses the value here to
// create the Kubernetes secret.
func (p *Provider) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
	managedAccountType := !strings.EqualFold(p.retrievaltype, secretTypeSecret)

	retrievalPaths := utils.ValidatePaths([]string{ref.Key}, managedAccountType, p.separator, &p.log)

	if len(retrievalPaths) != 1 {
		return nil, errors.New(errInvalidRetrievalPath)
	}

	retrievalPath := retrievalPaths[0]

	_, err := p.authenticate.GetPasswordSafeAuthentication()
	if err != nil {
		return nil, fmt.Errorf("error getting authentication: %w", err)
	}

	managedFetch := func() (string, error) {
		ESOLogger.Info("retrieve managed account value", "retrievalPath:", retrievalPath)
		manageAccountObj, _ := managedaccount.NewManagedAccountObj(p.authenticate, &p.log)
		return manageAccountObj.GetSecret(retrievalPath, p.separator)
	}
	unmanagedFetch := func() (string, error) {
		ESOLogger.Info("retrieve secrets safe value", "retrievalPath:", retrievalPath)
		secretObj, _ := secrets.NewSecretObj(p.authenticate, &p.log, maxFileSecretSizeBytes, p.decrypt)
		return secretObj.GetSecret(retrievalPath, p.separator)
	}
	fetch := unmanagedFetch
	if managedAccountType {
		fetch = managedFetch
	}
	returnSecret, err := fetch()
	if err != nil {
		if serr := p.authenticate.SignOut(); serr != nil {
			return nil, errors.Join(err, serr)
		}
		return nil, fmt.Errorf("error getting secret/managed account: %w", err)
	}
	return []byte(returnSecret), nil
}

// ValidateStore validates the store configuration to prevent unexpected errors.
func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
	if store == nil {
		return nil, errors.New(errNilStore)
	}

	spec := store.GetSpec()

	if spec == nil {
		return nil, errors.New(errMissingStoreSpec)
	}

	if spec.Provider == nil {
		return nil, errors.New(errMissingProvider)
	}

	provider := spec.Provider.Beyondtrust
	if provider == nil {
		return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
	}

	if provider.Server == nil {
		return nil, errors.New(errInvalidHostURL)
	}

	apiURL, err := url.Parse(provider.Server.APIURL)
	if err != nil || apiURL.Host == "" {
		return nil, errors.New(errInvalidHostURL)
	}

	if provider.Auth == nil {
		return nil, errors.New(errMissingAuthMethod)
	}

	// Auth method requirements must stay in sync with loadCredentialsFromConfig.
	switch {
	case provider.Auth.APIKey != nil:
		if err := validateAuthRef(store, "apiKey", provider.Auth.APIKey); err != nil {
			return nil, err
		}
	case provider.Auth.ClientID != nil && provider.Auth.ClientSecret != nil:
		if err := validateAuthRef(store, "clientId", provider.Auth.ClientID); err != nil {
			return nil, err
		}
		if err := validateAuthRef(store, "clientSecret", provider.Auth.ClientSecret); err != nil {
			return nil, err
		}
	case provider.Auth.ClientID != nil || provider.Auth.ClientSecret != nil:
		return nil, errors.New(errIncompleteOAuth)
	default:
		return nil, errors.New(errMissingAuthMethod)
	}

	// loadCertificateFromConfig silently ignores an incomplete certificate pair,
	// so warn instead of rejecting the store.
	var warnings admission.Warnings
	hasCertificate := provider.Auth.Certificate != nil
	hasCertificateKey := provider.Auth.CertificateKey != nil
	if hasCertificate != hasCertificateKey {
		warnings = append(warnings, "certificate and certificateKey must both be set for client-certificate authentication; the incomplete pair is ignored")
	}
	if hasCertificate {
		if err := validateAuthRef(store, "certificate", provider.Auth.Certificate); err != nil {
			return warnings, err
		}
	}
	if hasCertificateKey {
		if err := validateAuthRef(store, "certificateKey", provider.Auth.CertificateKey); err != nil {
			return warnings, err
		}
	}

	return warnings, nil
}

func validateAuthRef(store esv1.GenericStore, field string, ref *esv1.BeyondTrustProviderSecretRef) error {
	if ref.SecretRef == nil && ref.Value == "" {
		return fmt.Errorf("%s: either secretRef or value must be set", field)
	}
	if err := validateSecretRef(ref); err != nil {
		return fmt.Errorf("%s: %w", field, err)
	}
	if ref.SecretRef != nil {
		if err := esutils.ValidateReferentSecretSelector(store, *ref.SecretRef); err != nil {
			return fmt.Errorf("%s: %w", field, err)
		}
	}
	return nil
}

// NewProvider creates a new Provider instance.
func NewProvider() esv1.Provider {
	return &Provider{}
}

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

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

// PushSecret implements v1beta1.SecretsClient.
func (p *Provider) PushSecret(_ context.Context, secret *v1.Secret, psd esv1.PushSecretData) error {
	ESOLogger.Info("Pushing secret to BeyondTrust Password Safe")
	value, err := esutils.ExtractSecretData(psd, secret)

	if err != nil {
		return fmt.Errorf("extract secret data failed: %w", err)
	}

	secretValue := string(value)

	metadata := psd.GetMetadata()
	data, err := json.Marshal(metadata)

	if err != nil {
		return fmt.Errorf("Error getting metadata: %w", err)
	}

	var metaDataObject map[string]any
	err = json.Unmarshal(data, &metaDataObject)
	if err != nil {
		return fmt.Errorf("Error in parameters: %w", err)
	}

	signAppinResponse, err := p.authenticate.GetPasswordSafeAuthentication()
	if err != nil {
		return fmt.Errorf("Error in authentication: %w", err)
	}

	err = p.CreateSecret(secretValue, metaDataObject, signAppinResponse)

	if err != nil {
		return fmt.Errorf("Error in creating the secret: %w", err)
	}

	return nil
}

// CreateSecret creates a secret in BeyondTrust Password Safe.
func (p *Provider) CreateSecret(secret string, data map[string]any, signAppinResponse entities.SignAppinResponse) error {
	logger := logging.NewLogrLogger(&ESOLogger)
	secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes, false)

	if err != nil {
		return err
	}

	username := utils.GetStringField(data, usernameFieldName, "")
	folderName := utils.GetStringField(data, folderNameFieldName, "")
	fileName := utils.GetStringField(data, fileNameFieldName, "")
	title := utils.GetStringField(data, titleFieldName, "")
	description := utils.GetStringField(data, descriptionFieldName, "")
	ownerID := utils.GetIntField(data, ownerIDFieldName, 0)
	groupID := utils.GetIntField(data, groupIDFieldName, 0)
	ownerType := utils.GetStringField(data, ownerTypeFieldName, "")
	secretType := utils.GetStringField(data, secretTypeFieldName, secretTypeCredential)

	var notes string
	var urls []entities.UrlDetails
	var ownerDetailsOwnerID []entities.OwnerDetailsOwnerId
	var ownerDetailsGroupID []entities.OwnerDetailsGroupId

	_, ok := data["notes"]
	if ok {
		notes = data["notes"].(string)
	}

	_, ok = data["urls"]
	if ok {
		urls = utils.GetUrlsDetailsList(data)
	}

	ownerDetailsOwnerID = utils.GetOwnerDetailsOwnerIdList(data, signAppinResponse)
	ownerDetailsGroupID = utils.GetOwnerDetailsGroupIdList(data, groupID, signAppinResponse)

	secretDetailsConfig := entities.SecretDetailsBaseConfig{
		Title:       title,
		Description: description,
		Urls:        urls,
		Notes:       notes,
	}

	// Build a version-neutral input per secret type; CreateSecretFlow
	// selects Config30/Config31 from p.authenticate.ApiVersion internally.
	var secretInput any
	switch strings.ToUpper(secretType) {
	case secretTypeCredential:
		secretInput = entities.SecretCredentialInput{
			SecretDetailsBaseConfig: secretDetailsConfig,
			Username:                username,
			Password:                secret,
			OwnerId:                 ownerID,
			OwnerType:               ownerType,
			OwnersByOwnerId:         ownerDetailsOwnerID,
			OwnersByGroupId:         ownerDetailsGroupID,
		}

	case "FILE":
		secretInput = entities.SecretFileInput{
			SecretDetailsBaseConfig: secretDetailsConfig,
			FileName:                fileName,
			FileContent:             secret,
			OwnerId:                 ownerID,
			OwnerType:               ownerType,
			OwnersByOwnerId:         ownerDetailsOwnerID,
			OwnersByGroupId:         ownerDetailsGroupID,
		}

	case "TEXT":
		secretInput = entities.SecretTextInput{
			SecretDetailsBaseConfig: secretDetailsConfig,
			Text:                    secret,
			OwnerId:                 ownerID,
			OwnerType:               ownerType,
			OwnersByOwnerId:         ownerDetailsOwnerID,
			OwnersByGroupId:         ownerDetailsGroupID,
		}

	default:
		return fmt.Errorf("Unknown secret type")
	}

	_, err = secretObj.CreateSecretFlow(folderName, secretInput)

	if err != nil {
		return err
	}

	ESOLogger.Info("Secret pushed to BeyondTrust Password Safe")

	return nil
}
