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

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/cyberark/conjur-api-go/conjurapi"
	"github.com/tidwall/gjson"

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

type conjurResource map[string]any

// resourceFilterFunc is a function that filters resources.
// It takes a resource as input and returns the name of the resource if it should be included.
// If the resource should not be included, it returns an empty string.
// If an error occurs, it returns an empty string and the error.
type resourceFilterFunc func(candidate conjurResource) (name string, err error)

// GetSecret returns a single secret from the provider.
func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
	conjurClient, getConjurClientError := c.GetConjurClient(ctx)
	if getConjurClientError != nil {
		return nil, getConjurClientError
	}
	secretValue, err := conjurClient.RetrieveSecret(ref.Key)
	if err != nil {
		return nil, err
	}

	// If no property is specified, return the secret value as is
	if ref.Property == "" {
		return secretValue, nil
	}

	// If a property is specified, parse the secret value as JSON and return the property value
	val := gjson.Get(string(secretValue), ref.Property)
	if !val.Exists() {
		return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
	}
	return []byte(val.String()), nil
}

// GetSecretMap returns multiple k/v pairs from the provider.
func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
	// Gets a secret as normal, expecting secret value to be a json object
	data, err := c.GetSecret(ctx, ref)
	if err != nil {
		return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
	}

	// Maps the json data to a string:string map
	kv := make(map[string]string)
	err = json.Unmarshal(data, &kv)
	if err != nil {
		return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
	}

	// Converts values in K:V pairs into bytes, while leaving keys as strings
	secretData := make(map[string][]byte)
	for k, v := range kv {
		secretData[k] = []byte(v)
	}
	return secretData, nil
}

// GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
// First load all secrets from secretStore path configuration
// Then, gets secrets from a matching name or matching custom_metadata.
func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
	if ref.Name != nil {
		return c.findSecretsFromName(ctx, *ref.Name)
	}
	return c.findSecretsFromTags(ctx, ref.Tags)
}

func (c *Client) findSecretsFromName(ctx context.Context, ref esv1.FindName) (map[string][]byte, error) {
	matcher, err := find.New(ref)
	if err != nil {
		return nil, err
	}

	var resourceFilterFunc = func(candidate conjurResource) (string, error) {
		name := trimConjurResourceName(candidate["id"].(string))
		isMatch := matcher.MatchName(name)
		if !isMatch {
			return "", nil
		}
		return name, nil
	}

	return c.listSecrets(ctx, resourceFilterFunc)
}

func (c *Client) findSecretsFromTags(ctx context.Context, tags map[string]string) (map[string][]byte, error) {
	var resourceFilterFunc = func(candidate conjurResource) (string, error) {
		name := trimConjurResourceName(candidate["id"].(string))
		annotations, ok := candidate["annotations"].([]any)
		if !ok {
			// No annotations, skip
			return "", nil
		}

		formattedAnnotations, err := formatAnnotations(annotations)
		if err != nil {
			return "", err
		}

		// Check if all tags match
		for tk, tv := range tags {
			p, ok := formattedAnnotations[tk]
			if !ok || p != tv {
				return "", nil
			}
		}

		return name, nil
	}

	return c.listSecrets(ctx, resourceFilterFunc)
}

func (c *Client) listSecrets(ctx context.Context, filterFunc resourceFilterFunc) (map[string][]byte, error) {
	conjurClient, getConjurClientError := c.GetConjurClient(ctx)
	if getConjurClientError != nil {
		return nil, getConjurClientError
	}

	filteredResourceNames := []string{}

	// Loop through all secrets in the Conjur account.
	// Ideally this will be only a small list, but we need to handle pagination in the
	// case that there are a lot of secrets. To limit load on Conjur and memory usage
	// in ESO, we will only load 100 secrets at a time. We will then filter these secrets,
	// discarding any that do not match the filterFunc. We will then repeat this process
	// until we have loaded all secrets.
	for offset := 0; ; offset += 100 {
		resFilter := &conjurapi.ResourceFilter{
			Kind:   "variable",
			Limit:  100,
			Offset: offset,
		}
		resources, err := conjurClient.Resources(resFilter)
		if err != nil {
			return nil, err
		}

		for _, candidate := range resources {
			name, err := filterFunc(candidate)
			if err != nil {
				return nil, err
			}
			if name != "" {
				filteredResourceNames = append(filteredResourceNames, name)
			}
		}

		// If we have less than 100 resources, we reached the last page
		if len(resources) < 100 {
			break
		}
	}

	filteredResources, err := c.client.RetrieveBatchSecrets(filteredResourceNames)
	if err != nil {
		return nil, err
	}

	// Trim the resource names to just the last part of the ID
	return trimConjurResourceNames(filteredResources), nil
}

// trimConjurResourceNames trims the Conjur resource names to the last part of the ID.
// It iterates over a map of secrets and returns a new map with the trimmed names.
func trimConjurResourceNames(resources map[string][]byte) map[string][]byte {
	trimmedResources := make(map[string][]byte)
	for k, v := range resources {
		trimmedResources[trimConjurResourceName(k)] = v
	}
	return trimmedResources
}

// trimConjurResourceName trims the Conjur resource name to the last part of the ID.
// For example, if the ID is "account:variable:secret", the function will return
// "secret".
func trimConjurResourceName(id string) string {
	tokens := strings.SplitN(id, ":", 3)
	return tokens[len(tokens)-1]
}

// Convert annotations from objects with "name", "policy", "value" keys (as returned by the Conjur API)
// to a key/value map for easier comparison in code.
func formatAnnotations(annotations []any) (map[string]string, error) {
	formattedAnnotations := make(map[string]string)
	for _, annot := range annotations {
		annot, ok := annot.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("could not parse annotation: %v", annot)
		}
		name := annot["name"].(string)
		value := annot["value"].(string)
		formattedAnnotations[name] = value
	}
	return formattedAnnotations, nil
}
