/*
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 provides a client for BeyondTrust Workload Credentials.
package beyondtrustworkloadcredentials

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"path"
	"regexp"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"

	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"
)

const (
	// ErrMsgNotImplemented is the error message for unimplemented methods.
	ErrMsgNotImplemented = "not implemented: %s"

	// validationTimeout is the timeout for SecretStore validation operations (network check and session validation).
	// Set to 15 seconds to balance between allowing sufficient time for API responses and failing fast on connectivity issues.
	validationTimeout = 15 * time.Second
)

// Client implements the SecretsClient interface for BeyondTrust Secrets.
type Client struct {
	beyondtrustWorkloadCredentialsClient btwcutil.Client
	store                                *esv1.BeyondtrustWorkloadCredentialsProvider
}

// Validate checks if the client is configured correctly
// and is able to retrieve secrets from the BeyondTrust Secrets provider.
// If the validation result is unknown it will be ignored.
func (c *Client) Validate() (esv1.ValidationResult, error) {
	// Check for nil receiver
	if c == nil {
		return esv1.ValidationResultError, fmt.Errorf("client is nil")
	}

	// Check for nil beyondtrustWorkloadCredentialsClient
	if c.beyondtrustWorkloadCredentialsClient == nil {
		return esv1.ValidationResultError, fmt.Errorf("beyondtrustWorkloadCredentialsClient is not initialized")
	}

	// Check for nil BaseURL
	baseURL := c.beyondtrustWorkloadCredentialsClient.BaseURL()
	if baseURL == nil {
		return esv1.ValidationResultError, fmt.Errorf("base URL is not configured")
	}

	clientURL := baseURL.String()
	if err := esutils.NetworkValidate(clientURL, validationTimeout); err != nil {
		return esv1.ValidationResultError, err
	}

	// Validate authentication by checking session
	ctx, cancel := context.WithTimeout(context.Background(), validationTimeout)
	defer cancel()

	if err := c.beyondtrustWorkloadCredentialsClient.CheckSession(ctx); err != nil {
		return esv1.ValidationResultError, fmt.Errorf("authentication validation failed: %w", err)
	}

	return esv1.ValidationResultReady, nil
}

// GetSecret returns a single secret from the BeyondTrust Secrets provider
//
//	if GetSecret returns an error with type NoSecretError
//	then the secret entry will be deleted depending on the deletionPolicy.
func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
	folderPath := c.store.FolderPath

	secret, err := c.beyondtrustWorkloadCredentialsClient.GetSecret(ctx, ref.Key, &folderPath)
	if err != nil {
		// Wrap 404s as NoSecretError to allow ESO deletionPolicy handling
		var apiErr *httpclient.APIError
		if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
			return nil, esv1.NoSecretError{}
		}
		return nil, fmt.Errorf("failed to get secret: %w", err)
	}

	// Extract value from map
	if secret.Secret == nil {
		return nil, fmt.Errorf("secret value is nil")
	}

	// If there's a property key in the remote reference, use it
	if ref.Property != "" {
		value, ok := secret.Secret[ref.Property]
		if !ok {
			return nil, fmt.Errorf("property %s not found in secret", ref.Property)
		}

		// Handle different value types to preserve binary and object data
		switch val := value.(type) {
		case string:
			return []byte(val), nil
		case []byte:
			return val, nil
		default:
			// non-string: marshal to JSON to preserve structure
			b, err := json.Marshal(val)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal secret value for property %q: %w", ref.Property, err)
			}
			return b, nil
		}
	}

	// If no property specified, return the entire secret as JSON
	secretBytes, err := json.Marshal(secret.Secret)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal secret: %w", err)
	}

	return secretBytes, nil
}

// GetAllSecrets retrieves all secrets from BeyondTrust Secrets that match the given criteria.
func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
	// Determine folder path: use ref.Path as folder scope if provided, otherwise use store default
	folderPath := c.store.FolderPath
	if ref.Path != nil {
		folderPath = strings.TrimSuffix(*ref.Path, "/")
	}

	result := map[string][]byte{}

	// List all secrets in the folder (recursive to get all secrets under the path)
	secretsList, err := c.beyondtrustWorkloadCredentialsClient.GetSecrets(ctx, &folderPath, true)
	if err != nil {
		// Treat 404 from listing API as NoSecretError (folder not found or empty)
		var apiErr *httpclient.APIError
		if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
			return nil, esv1.NoSecretError{}
		}
		return nil, fmt.Errorf("failed to list secrets: %w", err)
	}

	// If no regexp provided, include everything. If regexp provided, filter names.
	var nameRe *regexp.Regexp
	if ref.Name != nil && ref.Name.RegExp != "" {
		nameRe, err = regexp.Compile(ref.Name.RegExp)
		if err != nil {
			return nil, fmt.Errorf("invalid name regexp %q: %w", ref.Name.RegExp, err)
		}
	}

	for _, item := range secretsList {
		// item.Path may be a full path; split to derive folder/name as the API expects
		dir, itemName := path.Split(item.Path)
		dir = strings.TrimSuffix(dir, "/")
		itemFolderPath := folderPath
		if dir != "" {
			itemFolderPath = dir
		}

		if nameRe != nil {
			if !nameRe.MatchString(itemName) {
				continue
			}
		}

		// Fetch the full secret for this matched item
		fullSecret, err := c.beyondtrustWorkloadCredentialsClient.GetSecret(ctx, itemName, &itemFolderPath)
		if err != nil {
			// In name-regex listing, skip missing items instead of failing the entire operation
			var apiErr *httpclient.APIError
			if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
				continue
			}
			return nil, fmt.Errorf("failed to get secret at path %q: %w", path.Join(itemFolderPath, itemName), err)
		}
		if fullSecret == nil || fullSecret.Secret == nil {
			// Skip empty/missing entries in list mode
			continue
		}

		// Filter by tags if specified (all tags must match)
		if ref.Tags != nil && len(ref.Tags) > 0 {
			if fullSecret.Metadata == nil || !matchTags(fullSecret.Metadata.Tags, ref.Tags) {
				continue
			}
		}

		// Add each property in the secret namespaced by the secret path to avoid key collisions
		// across secrets that share inner property names.
		for k, v := range fullSecret.Secret {
			key := item.Path + "/" + k
			switch val := v.(type) {
			case string:
				result[key] = []byte(val)
			case []byte:
				result[key] = val
			default:
				// non-string: marshal to JSON to preserve structure
				b, err := json.Marshal(val)
				if err != nil {
					return nil, fmt.Errorf("failed to marshal secret value for key %q: %w", key, err)
				}
				result[key] = b
			}
		}
	}

	// If no secrets matched the criteria, return NoSecretError
	if len(result) == 0 {
		return nil, esv1.NoSecretError{}
	}

	return result, nil
}

// GetSecretMap returns multiple k/v pairs from the BeyondTrust Secrets provider as separate keys.
// Each key-value pair in the secret becomes a separate entry in the returned map.
func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
	folderPath := c.store.FolderPath

	secret, err := c.beyondtrustWorkloadCredentialsClient.GetSecret(ctx, ref.Key, &folderPath)
	if err != nil {
		// Wrap 404s as NoSecretError to allow ESO deletionPolicy handling
		var apiErr *httpclient.APIError
		if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
			return nil, esv1.NoSecretError{}
		}
		return nil, fmt.Errorf("failed to get secret: %w", err)
	}

	if secret == nil || secret.Secret == nil {
		return nil, fmt.Errorf("secret value is nil")
	}

	// Convert all k/v pairs to []byte, preserving structure for non-string values
	result := make(map[string][]byte)
	for k, v := range secret.Secret {
		switch val := v.(type) {
		case string:
			result[k] = []byte(val)
		case []byte:
			result[k] = val
		default:
			// non-string: marshal to JSON to preserve structure
			b, err := json.Marshal(val)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal secret value for key %q: %w", k, err)
			}
			result[k] = b
		}
	}

	return result, nil
}

// matchTags checks if all required tags (filter) are present in the secret's tags
// with matching values. Returns true if all filter tags match, false otherwise.
func matchTags(secretTags, filterTags map[string]string) bool {
	if len(filterTags) == 0 {
		return true
	}
	if len(secretTags) == 0 {
		return false
	}

	// All filter tags must be present and match
	for filterKey, filterValue := range filterTags {
		secretValue, exists := secretTags[filterKey]
		if !exists || secretValue != filterValue {
			return false
		}
	}
	return true
}

/////////////////////////
// NOT YET IMPLEMENTED //
/////////////////////////

// PushSecret will write a single secret into the BeyondTrust Secrets provider.
func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
	return fmt.Errorf(ErrMsgNotImplemented, "PushSecret")
}

// DeleteSecret will delete the secret from the BeyondTrust Secrets provider.
func (c *Client) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
	return fmt.Errorf(ErrMsgNotImplemented, "DeleteSecret")
}

// SecretExists checks if a secret is already present in the BeyondTrust Secrets provider at the given location.
func (c *Client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
	return false, fmt.Errorf(ErrMsgNotImplemented, "SecretExists")
}

// Close implements cleanup operations for the BeyondTrust Secrets client.
func (c *Client) Close(_ context.Context) error {
	return nil
}
