/*
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 kubernetes implements a provider for Kubernetes secrets, allowing
// External Secrets to read from and write to Kubernetes Secrets.
package kubernetes

import (
	"fmt"
	"maps"

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

	"github.com/external-secrets/external-secrets/runtime/esutils/metadata"
)

// PushSecretMetadataSpec defines the metadata configuration for pushing secrets.
type PushSecretMetadataSpec struct {
	TargetMergePolicy targetMergePolicy `json:"targetMergePolicy,omitempty"`
	SourceMergePolicy sourceMergePolicy `json:"sourceMergePolicy,omitempty"`

	Labels          map[string]string `json:"labels,omitempty"`
	Annotations     map[string]string `json:"annotations,omitempty"`
	RemoteNamespace string            `json:"remoteNamespace,omitempty"`
}

type targetMergePolicy string

// Target merge policy constants.
const (
	targetMergePolicyMerge   targetMergePolicy = "Merge"
	targetMergePolicyReplace targetMergePolicy = "Replace"
	targetMergePolicyIgnore  targetMergePolicy = "Ignore"
)

type sourceMergePolicy string

// Source merge policy constants.
const (
	sourceMergePolicyMerge   sourceMergePolicy = "Merge"
	sourceMergePolicyReplace sourceMergePolicy = "Replace"
)

// Takes the local secret metadata and merges it with the push metadata.
// The push metadata takes precedence.
// Depending on the policy, we either merge or overwrite the metadata from the local secret.
func mergeSourceMetadata(localSecret *v1.Secret, pushMeta *metadata.PushSecretMetadata[PushSecretMetadataSpec]) (map[string]string, map[string]string, error) {
	labels := localSecret.ObjectMeta.Labels
	annotations := localSecret.ObjectMeta.Annotations
	if pushMeta == nil {
		return labels, annotations, nil
	}
	if labels == nil {
		labels = make(map[string]string)
	}
	if annotations == nil {
		annotations = make(map[string]string)
	}

	switch pushMeta.Spec.SourceMergePolicy {
	case "", sourceMergePolicyMerge:
		maps.Copy(labels, pushMeta.Spec.Labels)
		maps.Copy(annotations, pushMeta.Spec.Annotations)
	case sourceMergePolicyReplace:
		labels = pushMeta.Spec.Labels
		annotations = pushMeta.Spec.Annotations
	default:
		return nil, nil, fmt.Errorf("unexpected source merge policy %q", pushMeta.Spec.SourceMergePolicy)
	}
	return labels, annotations, nil
}

// Takes the remote secret metadata and merges it with the source metadata.
// The source metadata may replace the existing labels/annotations
// or merge into it depending on policy.
func mergeTargetMetadata(
	remoteSecret *v1.Secret,
	pushMeta *metadata.PushSecretMetadata[PushSecretMetadataSpec],
	sourceLabels, sourceAnnotations map[string]string,
) (map[string]string, map[string]string, error) {
	labels := remoteSecret.ObjectMeta.Labels
	annotations := remoteSecret.ObjectMeta.Annotations
	if labels == nil {
		labels = make(map[string]string)
	}
	if annotations == nil {
		annotations = make(map[string]string)
	}
	var targetMergePolicy targetMergePolicy
	if pushMeta != nil {
		targetMergePolicy = pushMeta.Spec.TargetMergePolicy
	}

	switch targetMergePolicy {
	case "", targetMergePolicyMerge:
		maps.Copy(labels, sourceLabels)
		maps.Copy(annotations, sourceAnnotations)
	case targetMergePolicyReplace:
		labels = sourceLabels
		annotations = sourceAnnotations
	case targetMergePolicyIgnore:
		// leave the target metadata as is
		// this is useful when we only want to push data
		// and the user does not want to touch the metadata
	default:
		return nil, nil, fmt.Errorf("unexpected target merge policy %q", targetMergePolicy)
	}
	return labels, annotations, nil
}
