package provider

import (
	"context"
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
	"github.com/aws/secrets-store-csi-driver-provider-aws/utils"
	"k8s.io/klog/v2"

	"sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1"
)

// Implements the provider interface for Secrets Manager.
//
// Unlike the ParameterStoreProvider, this implementation is optimized for
// latency and not reduced API call rates becuase Secrets Manager provides
// higher API limits.
//
// When there are no existing versions of the secret (first mount), this
// provider will just call GetSecretValue, update the current version map
// (curMap), and return the secret in the results. When there are existing
// versions (rotation reconciler case), this implementation will use the lower
// latency DescribeSecret call to first determine if the secret has been
// updated.
type SecretsManagerProvider struct {
	clients []SecretsManagerClient
}

type SecretsManagerGetDescriber interface {
	GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error)
	DescribeSecret(ctx context.Context, params *secretsmanager.DescribeSecretInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.DescribeSecretOutput, error)
}

// SecretsManager client with region
type SecretsManagerClient struct {
	Region     string
	Client     SecretsManagerGetDescriber
	IsFailover bool
}

// Get the secret from SecretsManager.
//
// This method iterates over all descriptors and requests a fetch. When
// sucessfully fetched, then it continues until all descriptors have been fetched.
// Once an error happens, it immediately returns the error.
func (p *SecretsManagerProvider) GetSecretValues(
	ctx context.Context,
	descriptors []*SecretDescriptor,
	curMap map[string]*v1alpha1.ObjectVersion,
) (v []*SecretValue, errs error) {

	// Fetch each secret in order. If any secret fails we will return that secret's errors
	for _, descriptor := range descriptors {
		values, errs := p.fetchSecretManagerValue(ctx, descriptor, curMap)
		if values == nil {
			return nil, errs
		}
		v = append(v, values...)
	}
	return v, nil
}

// Private helper function to fetch a single secret.
//
// This method iterates over all available clients in the SecretsManagerProvider.
// It requests a fetch from each of them.  Once a fetch succeeds it returns the
//
//	value. If a fetch fails all clients it returns all errors.
func (p *SecretsManagerProvider) fetchSecretManagerValue(
	ctx context.Context,
	descriptor *SecretDescriptor,
	curMap map[string]*v1alpha1.ObjectVersion,
) (value []*SecretValue, err error) {

	for _, client := range p.clients {
		secretVal, err := p.fetchSecretManagerValueWithClient(ctx, client, descriptor, curMap)

		//check if fatal(4XX status error) exist to error out the mount
		if utils.IsFatalError(err) {
			return nil, err
		} else if err != nil {
			klog.Warning(err)
		}

		if len(secretVal) > 0 && len(value) == 0 {
			value = secretVal
		}
	}
	if len(value) == 0 {
		return nil, fmt.Errorf("Failed to fetch secret from all regions. Verify secret exists and required permissions are granted for: %s", descriptor.ObjectName)
	}

	return value, nil
}

// Private helper function to fetch a single secret from a single region
//
// This method checks if the secret is current. If a secret is not current
// (or this is the first time), the secret is fetched, added to the list of
// secrets, and the version information is updated in the current version map.
func (p *SecretsManagerProvider) fetchSecretManagerValueWithClient(
	ctx context.Context,
	client SecretsManagerClient,
	descriptor *SecretDescriptor,
	curMap map[string]*v1alpha1.ObjectVersion,
) (v []*SecretValue, e error) {

	var values []*SecretValue

	// Don't re-fetch if we already have the current version.
	isCurrent, version, err := p.isCurrent(ctx, client, descriptor, curMap)
	if err != nil {
		return nil, err
	}

	// If version is current, read it back in, otherwise pull it down
	var secret *SecretValue
	if isCurrent {
		secret, err = p.reloadSecret(descriptor)
		if err != nil {
			return nil, err
		}
	} else { // Fetch the latest version.
		version, secret, err = p.fetchSecret(ctx, client, descriptor)
		if err != nil {
			return nil, err
		}
	}
	values = append(values, secret) // Build up the slice of values

	//Fetch individual json key value pairs based on jmesPath
	jsonSecrets, jsonError := secret.getJsonSecrets()
	if jsonError != nil {
		return nil, jsonError
	}

	values = append(values, jsonSecrets...)

	// Update the version in the current version map.
	for _, jsonSecret := range jsonSecrets {
		jsonDescriptor := jsonSecret.Descriptor
		curMap[jsonDescriptor.GetFileName()] = &v1alpha1.ObjectVersion{
			Id:      jsonDescriptor.GetFileName(),
			Version: version,
		}
	}

	// Update the version in the current version map.
	curMap[descriptor.GetFileName()] = &v1alpha1.ObjectVersion{
		Id:      descriptor.GetFileName(),
		Version: version,
	}

	return values, nil
}

// Private helper to check if a secret is current.
//
// This method looks for the given secret in the current version map, if it
// does not exist (first time) it is not current. If the requsted secret uses
// the objectVersion parameter, the current version is compared to the required
// version to determine if it is current. Otherwise, the current vesion
// information is fetched using DescribeSecret and this method checks if the
// current version is labeled as current (AWSCURRENT) or has the label
// sepecified via objectVersionLable (if any).
func (p *SecretsManagerProvider) isCurrent(
	ctx context.Context,
	client SecretsManagerClient,
	descriptor *SecretDescriptor,
	curMap map[string]*v1alpha1.ObjectVersion,
) (cur bool, ver string, err error) {

	// If we don't have this version, it is not current.
	curVer := curMap[descriptor.GetFileName()]
	if curVer == nil {
		return false, "", nil
	}

	// If the secret is pinned to a version see if that is what we have.
	if len(descriptor.GetObjectVersion(client.IsFailover)) > 0 {
		return curVer.Version == descriptor.GetObjectVersion(client.IsFailover), curVer.Version, nil
	}

	// Lookup the current version information.
	rsp, err := client.Client.DescribeSecret(ctx, &secretsmanager.DescribeSecretInput{SecretId: aws.String(descriptor.GetSecretName(client.IsFailover))})
	if err != nil {
		return false, curVer.Version, fmt.Errorf("%s: Failed to describe secret %s: %w", client.Region, descriptor.ObjectName, err)
	}

	// If no label is specified use current, otherwise use the specified label.
	label := "AWSCURRENT"
	if len(descriptor.GetObjectVersionLabel(client.IsFailover)) > 0 {
		label = descriptor.GetObjectVersionLabel(client.IsFailover)
	}

	// Linear search for desired label in the list of labels on current version.
	stages := rsp.VersionIdsToStages[curVer.Version]
	hasLabel := false
	for i := 0; i < len(stages) && !hasLabel; i++ {
		hasLabel = stages[i] == label
	}

	return hasLabel, curVer.Version, nil // If the current version has the desired label, it is current.
}

// Private helper to fetch a given secret.
//
// This method builds up the GetSecretValue request using the objectName from
// the request and any objectVersion or objectVersionLabel parameters.
func (p *SecretsManagerProvider) fetchSecret(
	ctx context.Context,
	client SecretsManagerClient,
	descriptor *SecretDescriptor,
) (ver string, val *SecretValue, err error) {

	input := &secretsmanager.GetSecretValueInput{SecretId: aws.String(descriptor.GetSecretName(client.IsFailover))}

	// Use explicit version if specified
	if len(descriptor.GetObjectVersion(client.IsFailover)) != 0 {
		input.VersionId = aws.String(descriptor.GetObjectVersion(client.IsFailover))
	}

	// Use stage label if specified
	if len(descriptor.GetObjectVersionLabel(client.IsFailover)) != 0 {
		input.VersionStage = aws.String(descriptor.GetObjectVersionLabel(client.IsFailover))
	}

	rsp, err := client.Client.GetSecretValue(ctx, input)
	if err != nil {
		return "", nil, fmt.Errorf("%s: Failed fetching secret %s: %w", client.Region, descriptor.ObjectName, err)
	}

	// Use either secret string or secret binary.
	var sValue []byte
	if rsp.SecretString != nil {
		sValue = []byte(*rsp.SecretString)
	} else {
		sValue = rsp.SecretBinary
	}

	return *rsp.VersionId, &SecretValue{Value: sValue, Descriptor: *descriptor}, nil
}

// Private helper to refesh a secret from its previously stored value.
//
// Reads a secret back in from the file system.
func (p *SecretsManagerProvider) reloadSecret(descriptor *SecretDescriptor) (val *SecretValue, e error) {
	sValue, err := os.ReadFile(descriptor.GetMountPath())
	if err != nil {
		return nil, fmt.Errorf("failed to read secret %s: %w", descriptor.ObjectName, err)
	}
	return &SecretValue{Value: sValue, Descriptor: *descriptor}, nil
}

// Factory methods to build a new SecretsManagerProvider
func NewSecretsManagerProviderWithClients(clients ...SecretsManagerClient) *SecretsManagerProvider {
	return &SecretsManagerProvider{
		clients: clients,
	}
}

func NewSecretsManagerProvider(configs []aws.Config, regions []string) *SecretsManagerProvider {
	var clients []SecretsManagerClient
	for i, cfg := range configs {
		client := SecretsManagerClient{
			Region:     regions[i],
			Client:     secretsmanager.NewFromConfig(cfg),
			IsFailover: i > 0,
		}
		clients = append(clients, client)
	}
	return NewSecretsManagerProviderWithClients(clients...)
}
