package metadata

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/grpc/metadata"

	"github.com/grafana/authlib/authn"
	claims "github.com/grafana/authlib/types"
	"github.com/grafana/grafana-app-sdk/logging"

	secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
	"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
	"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
	"github.com/grafana/grafana/pkg/storage/secret/metadata/metrics"
)

func ProvideDecryptStorage(
	tracer trace.Tracer,
	keeperService contracts.KeeperService,
	keeperMetadataStorage contracts.KeeperMetadataStorage,
	secureValueMetadataStorage contracts.SecureValueMetadataStorage,
	decryptAuthorizer contracts.DecryptAuthorizer,
	reg prometheus.Registerer,
) (contracts.DecryptStorage, error) {
	if decryptAuthorizer == nil {
		return nil, fmt.Errorf("a decrypt authorizer is required")
	}

	return &decryptStorage{
		tracer:                     tracer,
		keeperMetadataStorage:      keeperMetadataStorage,
		keeperService:              keeperService,
		secureValueMetadataStorage: secureValueMetadataStorage,
		decryptAuthorizer:          decryptAuthorizer,
		metrics:                    metrics.NewStorageMetrics(reg),
	}, nil
}

// decryptStorage is the actual implementation of the decrypt storage.
type decryptStorage struct {
	tracer                     trace.Tracer
	keeperMetadataStorage      contracts.KeeperMetadataStorage
	keeperService              contracts.KeeperService
	secureValueMetadataStorage contracts.SecureValueMetadataStorage
	decryptAuthorizer          contracts.DecryptAuthorizer
	metrics                    *metrics.StorageMetrics
}

// Decrypt decrypts a secure value from the keeper.
func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (_ secretv1beta1.ExposedSecureValue, decryptErr error) {
	ctx, span := s.tracer.Start(ctx, "DecryptStorage.Decrypt", trace.WithAttributes(
		attribute.String("namespace", namespace.String()),
		attribute.String("name", name),
	))
	defer span.End()

	var decrypterIdentity string
	start := time.Now()
	// TEMPORARY: While we evaluate all of our auditing needs, provide one for decrypt operations.
	defer func() {
		// If at this point the identity is still empty, try to get it from the auth info in context.
		if decrypterIdentity == "" {
			if authInfo, ok := claims.AuthInfoFrom(ctx); authInfo != nil && ok {
				if serviceIdentityList, ok := authInfo.GetExtra()[authn.ServiceIdentityKey]; ok && len(serviceIdentityList) > 0 {
					decrypterIdentity = strings.TrimSpace(serviceIdentityList[0])
				}
			}
		}

		span.SetAttributes(attribute.String("decrypter.identity", decrypterIdentity))

		args := []any{
			"namespace", namespace.String(),
			"secret_name", name,
			"decrypter_identity", decrypterIdentity,
		}

		// The service identity used for decryption is always what is from the signed token, but if the request is
		// coming from grafana, the service identity will be grafana, but the request metadata will contain
		// additional service identity information (such as coming from the provisioning service in grafana).
		// we do this for auditing purposes.
		if md, ok := metadata.FromIncomingContext(ctx); ok {
			if svcIdentities := md.Get(contracts.HeaderGrafanaServiceIdentityName); len(svcIdentities) > 0 {
				args = append(args, "grafana_decrypter_identity", svcIdentities[0])
				span.SetAttributes(attribute.String("grafana_decrypter.identity", svcIdentities[0]))
			}
		}

		decryptResultLabel := metrics.DecryptResultLabel(decryptErr)

		if decryptErr == nil {
			span.SetStatus(codes.Ok, "Decrypt succeeded")
			args = append(args, "operation", "decrypt_secret_success")
		} else {
			span.SetStatus(codes.Error, "Decrypt failed")
			span.RecordError(decryptErr)
			args = append(args, "operation", "decrypt_secret_error", "error", decryptErr.Error(), "result", decryptResultLabel)
		}

		logging.FromContext(ctx).Info("Secrets Audit Log", args...)

		s.metrics.DecryptDuration.WithLabelValues(decryptResultLabel, cmp.Or(decrypterIdentity, "unknown")).Observe(time.Since(start).Seconds())

		// Do not leak error details to caller, return only the wrapped domain errors.
		if decryptErr != nil {
			decryptErr = cmp.Or(errors.Unwrap(decryptErr), contracts.ErrDecryptFailed)
		}
	}()

	// Basic authn check before reading a secure value metadata, it is here on purpose.
	if _, ok := claims.AuthInfoFrom(ctx); !ok {
		return "", fmt.Errorf("no auth info in context (%w)", contracts.ErrDecryptNotAuthorized)
	}

	// The auth token will not necessarily have the permission to read the secure value metadata,
	// but we still need to do it to inspect the `decrypters` field, hence the actual `authorize`
	// function call happens after this.
	sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{})
	if err != nil {
		return "", fmt.Errorf("failed to read secure value metadata storage: %v (%w)", err, contracts.ErrDecryptNotFound)
	}

	decrypterIdentity, authorized, reason := s.decryptAuthorizer.Authorize(ctx, namespace, name, sv.Spec.Decrypters, sv.OwnerReferences)
	if !authorized {
		return "", fmt.Errorf("failed to authorize decryption with reason %v (%w)", reason, contracts.ErrDecryptNotAuthorized)
	}

	keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Status.Keeper, contracts.ReadOpts{})
	if err != nil {
		return "", fmt.Errorf("failed to read keeper config metadata storage: %v (%w)", err, contracts.ErrDecryptFailed)
	}

	keeper, err := s.keeperService.KeeperForConfig(keeperConfig)
	if err != nil {
		return "", fmt.Errorf("failed to get keeper for config: %v (%w)", err, contracts.ErrDecryptFailed)
	}

	exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace, name, sv.Status.Version)
	if err != nil {
		return "", fmt.Errorf("failed to expose secret: %v (%w)", err, contracts.ErrDecryptFailed)
	}

	return exposedValue, nil
}
