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

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"

	"github.com/google/uuid"
	"github.com/ovh/okms-sdk-go/types"
	corev1 "k8s.io/api/core/v1"

	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
)

const pushSecretError = "failed to push secret at path"

// PushSecret pushes a secret to the Secret Manager according to the updatePolicy
// defined in the PushSecret (create or update).
func (cl *ovhClient) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
	remoteKey := data.GetRemoteKey()

	if secret == nil {
		return newPushSecretValidationError(remoteKey, "provided secret is nil")
	}
	if len(secret.Data) == 0 {
		return newPushSecretValidationError(remoteKey, "provided secret is empty")
	}

	// Check if the secret already exists.
	// This determines which method to use: create or update.
	remoteSecret, currentVersion, err := cl.getSecretWithOvhSDK(ctx, cl.okmsID, esv1.ExternalSecretDataRemoteRef{
		Key: remoteKey,
	})
	noSecretErr := errors.Is(err, esv1.NoSecretErr)
	if err != nil && !noSecretErr {
		return wrapPushSecretError(remoteKey, err)
	}
	secretExists := !noSecretErr

	// Build the secret to be pushed.
	secretToPush, err := buildSecretToPush(secret, data)
	if err != nil {
		return wrapPushSecretError(remoteKey, err)
	}

	// Compare the data of secretToPush with that of remoteSecret.
	equal, err := compareSecretsData(secretToPush, remoteSecret)
	if err != nil {
		return wrapPushSecretError(remoteKey, err)
	}
	if equal {
		return nil
	}

	// Set cas according to client configuration
	if !cl.cas {
		currentVersion = nil
	}

	// Push the secret.
	err = pushNewSecret(ctx, cl.okmsClient, cl.okmsID, secretToPush, remoteKey, currentVersion, secretExists)
	if err != nil {
		return wrapPushSecretError(remoteKey, err)
	}
	return nil
}

func wrapPushSecretError(remoteKey string, err error) error {
	return fmt.Errorf("%s %q: %w", pushSecretError, remoteKey, err)
}

func newPushSecretValidationError(remoteKey, msg string) error {
	return fmt.Errorf("%s %q: %s", pushSecretError, remoteKey, msg)
}

// Compare the secret to push with the remote secret.
// If they are equal, do not push the secret.
func compareSecretsData(secretToPush map[string]any, remoteSecret []byte) (bool, error) {
	if len(remoteSecret) == 0 {
		return false, nil
	}

	localBytes, err := json.Marshal(secretToPush)
	if err != nil {
		return false, fmt.Errorf("could not compare remote secret with secret to push: %w", err)
	}

	var localSecretMap, remoteSecretMap any
	if err := json.Unmarshal(localBytes, &localSecretMap); err != nil {
		return false, fmt.Errorf("could not normalize local secret for comparison: %w", err)
	}
	if err := json.Unmarshal(remoteSecret, &remoteSecretMap); err != nil {
		return false, fmt.Errorf("could not normalize remote secret for comparison: %w", err)
	}

	return reflect.DeepEqual(localSecretMap, remoteSecretMap), nil
}

// Build the secret to be pushed.
//
// If remoteProperty is defined, it will be used as the key to store the secret value.
// If secretKey is not defined, the entire secret value will be pushed.
// Otherwise, only the value of the specified secretKey will be pushed.
func buildSecretToPush(secret *corev1.Secret, data esv1.PushSecretData) (map[string]any, error) {
	// Retrieve the secret value to push based on secretKey.
	var secretValueToPush map[string]any
	var err error

	secretValueToPush, err = extractSecretValue(secret.Data, data.GetSecretKey())

	if err != nil {
		return map[string]any{}, err
	}

	// Build the secret to push using remoteProperty.
	secretToPush := make(map[string]any)
	property := data.GetProperty()

	if property == "" {
		secretToPush = secretValueToPush
	} else {
		secretToPush[property] = secretValueToPush
	}

	return secretToPush, nil
}

func extractSecretValue(data map[string][]byte, secretKey string) (map[string]any, error) {
	var err error
	secretValueToPush := make(map[string]any)

	if secretKey != "" {
		err = extractSecretKeyValue(data, secretValueToPush, secretKey)
		return secretValueToPush, err
	}

	for key := range data {
		err = extractSecretKeyValue(data, secretValueToPush, key)
		if err != nil {
			return nil, err
		}
	}

	return secretValueToPush, nil
}

func extractSecretKeyValue(data map[string][]byte, secretValueToPush map[string]any, secretKey string) error {
	value, ok := data[secretKey]
	if !ok {
		return fmt.Errorf(
			"could not extract secret key value to push: secretKey %q not found in secret data", secretKey,
		)
	}
	var decoded any
	if json.Unmarshal(value, &decoded) != nil {
		secretValueToPush[secretKey] = string(value)
	} else {
		secretValueToPush[secretKey] = json.RawMessage(value)
	}

	return nil
}

// This pushes the created/updated secret.
func pushNewSecret(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, secretToPush map[string]any, path string, cas *uint32, secretExists bool) error {
	var err error

	if !secretExists {
		// Create a secret.
		_, err = okmsClient.PostSecretV2(ctx, okmsID, types.PostSecretV2Request{
			Path: path,
			Version: types.SecretV2VersionShort{
				Data: &secretToPush,
			},
		})
		if err != nil {
			return fmt.Errorf("could not create remote secret: %w", err)
		}
		return nil
	}

	// Update a secret.
	_, err = okmsClient.PutSecretV2(ctx, okmsID, path, cas, types.PutSecretV2Request{
		Version: &types.SecretV2VersionShort{
			Data: &secretToPush,
		},
	})
	if err != nil {
		return fmt.Errorf("could not update remote secret: %w", err)
	}
	return nil
}
