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

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"strings"

	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/beyondtrustworkloadcredentials/httpclient"
	btwcutil "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/util"
	"github.com/external-secrets/external-secrets/runtime/esutils"
	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
)

var (
	// ErrNoStore is returned when the BeyondtrustWorkloadCredentials SecretStore is missing or invalid.
	ErrNoStore = errors.New("missing or invalid BeyondtrustWorkloadCredentials SecretStore")
	// ErrNoAPIKey is returned when the API Token is missing or invalid.
	ErrNoAPIKey = errors.New("missing or invalid BeyondtrustWorkloadCredentials API Token in BeyondtrustWorkloadCredentials SecretStore")
	// ErrNoTokenName is returned when the API Token name is missing or invalid.
	ErrNoTokenName = errors.New("missing or invalid BeyondtrustWorkloadCredentials API Token name in BeyondtrustWorkloadCredentials SecretStore")
	// ErrNoTokenKey is returned when the API Token key is missing or invalid.
	ErrNoTokenKey = errors.New("missing or invalid BeyondtrustWorkloadCredentials API Token key in BeyondtrustWorkloadCredentials SecretStore")
	// ErrNoServer is returned when the BeyondtrustWorkloadCredentials Server is missing or invalid.
	ErrNoServer = errors.New("missing or invalid BeyondtrustWorkloadCredentials Server in BeyondtrustWorkloadCredentials SecretStore")
	// ErrNoAPIURL is returned when the Server API URL is missing or invalid.
	ErrNoAPIURL = errors.New("missing or invalid BeyondtrustWorkloadCredentials Server API URL in BeyondtrustWorkloadCredentials SecretStore")
	// ErrNoSiteID is returned when the Server site ID is missing or invalid.
	ErrNoSiteID = errors.New("missing or invalid BeyondtrustWorkloadCredentials Server site ID in BeyondtrustWorkloadCredentials SecretStore")
)

// Provider is a BeyondtrustWorkloadCredentials provider implementing NewClient and ValidateStore for the esv1.Provider interface.
type Provider struct {
	// NewBeyondtrustWorkloadCredentialsClient is a function that returns a new BeyondTrust Secrets client.
	// This is used for testing to inject a fake client.
	NewBeyondtrustWorkloadCredentialsClient func(server, token string) (btwcutil.Client, error)
}

// https://github.com/external-secrets/external-secrets/issues/644
var _ esv1.SecretsClient = &Client{}
var _ esv1.Provider = &Provider{}

// NewClient constructs a BeyondtrustWorkloadCredentials SecretsManager Provider.
func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
	storeSpec := store.GetSpec()
	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.BeyondtrustWorkloadCredentials == nil {
		return nil, ErrNoStore
	}

	BeyondtrustWorkloadCredentialsStoreSpec := storeSpec.Provider.BeyondtrustWorkloadCredentials
	storeKind := store.GetKind()

	// fetch server values from spec
	serverURL, apiKey, err := fetchServerValuesFromSpec(ctx, BeyondtrustWorkloadCredentialsStoreSpec, kube, namespace, storeKind)
	if err != nil {
		return nil, err
	}

	// create BeyondtrustWorkloadCredentials client
	BeyondtrustWorkloadCredentialsClient, err := p.newClient(ctx, serverURL, apiKey, BeyondtrustWorkloadCredentialsStoreSpec, kube, namespace, storeKind)
	if err != nil {
		return nil, fmt.Errorf("failed to create BeyondtrustWorkloadCredentials client: %w", err)
	}

	client := &Client{
		beyondtrustWorkloadCredentialsClient: BeyondtrustWorkloadCredentialsClient,
		store:                                BeyondtrustWorkloadCredentialsStoreSpec,
	}

	return client, nil
}

// newClient is a shared helper creates the appropriate BeyondtrustWorkloadCredentials client based on the provided spec.
func (p *Provider) newClient(
	ctx context.Context,
	serverURL, apiKey string,
	btSpec *esv1.BeyondtrustWorkloadCredentialsProvider,
	kube kclient.Client,
	namespace, storeKind string,
) (btwcutil.Client, error) {
	// Fetch CA from CABundle/CAProvider using ESO helper
	var caCert []byte
	var err error
	if btSpec != nil {
		caCert, err = esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
			StoreKind:  storeKind,
			Client:     kube,
			Namespace:  namespace,
			CABundle:   btSpec.CABundle,
			CAProvider: btSpec.CAProvider,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to fetch CA certificate: %w", err)
		}
	}

	if len(caCert) > 0 {
		return httpclient.NewBeyondtrustWorkloadCredentialsClientWithCustomCA(serverURL, apiKey, caCert)
	}
	if p.NewBeyondtrustWorkloadCredentialsClient != nil {
		return p.NewBeyondtrustWorkloadCredentialsClient(serverURL, apiKey)
	}
	return httpclient.NewBeyondtrustWorkloadCredentialsClient(serverURL, apiKey)
}

// NewGeneratorClient creates a new BeyondtrustWorkloadCredentials client for the generator controller.
func (p *Provider) NewGeneratorClient(ctx context.Context, kube kclient.Client, btSpec *esv1.BeyondtrustWorkloadCredentialsProvider, namespace string) (btwcutil.Client, error) {
	if btSpec == nil {
		return nil, ErrNoStore
	}

	serverURL, apiKey, err := fetchServerValuesFromSpec(ctx, btSpec, kube, namespace, "")
	if err != nil {
		return nil, err
	}

	client, err := p.newClient(ctx, serverURL, apiKey, btSpec, kube, namespace, "")
	if err != nil {
		return nil, fmt.Errorf("failed to create BeyondtrustWorkloadCredentials client: %w", err)
	}

	return client, nil
}

// ValidateStore checks if the BeyondtrustWorkloadCredentials store is valid.
// The provider may return a warning and an error.
// The intended use of the warning to indicate a deprecation of behavior
// or other type of message that is NOT a validation failure but should be noticed by the user.
func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
	storeSpec := store.GetSpec()
	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.BeyondtrustWorkloadCredentials == nil {
		return nil, ErrNoStore
	}

	BeyondtrustWorkloadCredentialsStoreSpec := storeSpec.Provider.BeyondtrustWorkloadCredentials

	// validate token selector
	if BeyondtrustWorkloadCredentialsStoreSpec.Auth == nil {
		return nil, ErrNoAPIKey
	}
	tokenRef := BeyondtrustWorkloadCredentialsStoreSpec.Auth.APIKey.Token
	if err := esutils.ValidateSecretSelector(store, tokenRef); err != nil {
		return nil, err
	}
	if tokenRef.Name == "" {
		return nil, ErrNoTokenName
	}

	// validate server config is present and contains required fields
	if BeyondtrustWorkloadCredentialsStoreSpec.Server == nil {
		return nil, ErrNoServer
	}
	if BeyondtrustWorkloadCredentialsStoreSpec.Server.APIURL == "" {
		return nil, ErrNoAPIURL
	}

	// Validate APIURL format
	if err := validateAPIURL(BeyondtrustWorkloadCredentialsStoreSpec.Server.APIURL); err != nil {
		return nil, fmt.Errorf("invalid apiUrl: %w", err)
	}

	if BeyondtrustWorkloadCredentialsStoreSpec.Server.SiteID == "" {
		return nil, ErrNoSiteID
	}

	// Validate SiteID format (should be UUID)
	if err := validateSiteID(BeyondtrustWorkloadCredentialsStoreSpec.Server.SiteID); err != nil {
		return nil, fmt.Errorf("invalid siteId: %w", err)
	}

	return nil, nil
}

// Capabilities returns the BeyondtrustWorkloadCredentials provider Capabilities (Read, Write, ReadWrite).
func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
	return esv1.SecretStoreReadOnly
}

func loadAPIKeyFromSpec(ctx context.Context, spec *esv1.BeyondtrustWorkloadCredentialsProvider, kube kclient.Client, namespace, storeKind string) (string, error) {
	if spec == nil {
		return "", ErrNoStore
	}
	if spec.Auth == nil {
		return "", ErrNoAPIKey
	}

	tokenRef := spec.Auth.APIKey.Token
	if tokenRef.Name == "" {
		return "", ErrNoTokenName
	}
	if tokenRef.Key == "" {
		return "", ErrNoTokenKey
	}

	return resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &tokenRef)
}

func loadURLFromSpec(spec *esv1.BeyondtrustWorkloadCredentialsProvider) (string, string, error) {
	if spec == nil {
		return "", "", ErrNoStore
	}
	if spec.Server == nil {
		return "", "", ErrNoServer
	}

	if spec.Server.APIURL == "" {
		return "", "", ErrNoAPIURL
	}

	// Validate APIURL format
	if err := validateAPIURL(spec.Server.APIURL); err != nil {
		return "", "", fmt.Errorf("invalid apiUrl: %w", err)
	}

	if spec.Server.SiteID == "" {
		return "", "", ErrNoSiteID
	}

	// Validate SiteID format
	if err := validateSiteID(spec.Server.SiteID); err != nil {
		return "", "", fmt.Errorf("invalid siteId: %w", err)
	}

	return spec.Server.APIURL, spec.Server.SiteID, nil
}

func fetchServerValuesFromSpec(ctx context.Context, spec *esv1.BeyondtrustWorkloadCredentialsProvider, kube kclient.Client, namespace, storeKind string) (string, string, error) {
	if spec == nil {
		return "", "", ErrNoStore
	}

	apiKey, err := loadAPIKeyFromSpec(ctx, spec, kube, namespace, storeKind)
	if err != nil {
		return "", "", fmt.Errorf("failed to load credentials: %w", err)
	}

	baseURL, siteID, err := loadURLFromSpec(spec)
	if err != nil {
		return "", "", fmt.Errorf("failed to load server URL configuration: %w", err)
	}

	// Normalize baseURL by removing trailing slash to prevent double slashes
	baseURL = strings.TrimRight(baseURL, "/")
	serverURL := fmt.Sprintf("%s/%s/secrets", baseURL, siteID)

	return serverURL, apiKey, nil
}

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

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

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

// validateAPIURL validates the BeyondTrust API URL format.
func validateAPIURL(apiURL string) error {
	if apiURL == "" {
		return fmt.Errorf("apiUrl cannot be empty")
	}

	parsedURL, err := url.Parse(apiURL)
	if err != nil {
		return fmt.Errorf("failed to parse apiUrl: %w", err)
	}

	if parsedURL.Scheme == "" {
		return fmt.Errorf("apiUrl must include a scheme (https)")
	}

	if parsedURL.Scheme != "https" {
		return fmt.Errorf("apiUrl must use https scheme, got %q", parsedURL.Scheme)
	}

	if parsedURL.Host == "" {
		return fmt.Errorf("apiUrl must include a host")
	}

	return nil
}

// validateSiteID validates the BeyondTrust site ID format (must be a valid UUID).
func validateSiteID(siteID string) error {
	if siteID == "" {
		return fmt.Errorf("siteId cannot be empty")
	}

	// Validate UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
	// Where y is one of [8, 9, a, b] (RFC 4122 variant bits)
	uuidPattern := `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$`
	matched, err := regexp.MatchString(uuidPattern, siteID)
	if err != nil {
		return fmt.Errorf("failed to validate siteId format: %w", err)
	}

	if !matched {
		return fmt.Errorf("siteId must be a valid UUID format, got %q", siteID)
	}

	return nil
}
