package service

import (
	"context"
	"fmt"
	"strconv"
	"time"

	claims "github.com/grafana/authlib/types"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"k8s.io/apiserver/pkg/admission"

	"github.com/prometheus/client_golang/prometheus"
	"go.opentelemetry.io/otel/codes"

	"github.com/grafana/grafana-app-sdk/logging"
	secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
	"github.com/grafana/grafana/pkg/registry/apis/secret/service/metrics"
	"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
)

var _ contracts.SecureValueService = (*SecureValueService)(nil)

type SecureValueService struct {
	tracer                     trace.Tracer
	accessClient               claims.AccessClient
	secureValueMetadataStorage contracts.SecureValueMetadataStorage
	secureValueValidator       contracts.SecureValueValidator
	secureValueMutator         contracts.SecureValueMutator
	keeperMetadataStorage      contracts.KeeperMetadataStorage
	keeperService              contracts.KeeperService
	metrics                    *metrics.SecureValueServiceMetrics
}

func ProvideSecureValueService(
	tracer trace.Tracer,
	accessClient claims.AccessClient,
	secureValueMetadataStorage contracts.SecureValueMetadataStorage,
	secureValueValidator contracts.SecureValueValidator,
	secureValueMutator contracts.SecureValueMutator,
	keeperMetadataStorage contracts.KeeperMetadataStorage,
	keeperService contracts.KeeperService,
	reg prometheus.Registerer,
) contracts.SecureValueService {
	return &SecureValueService{
		tracer:                     tracer,
		accessClient:               accessClient,
		secureValueMetadataStorage: secureValueMetadataStorage,
		secureValueValidator:       secureValueValidator,
		secureValueMutator:         secureValueMutator,
		keeperMetadataStorage:      keeperMetadataStorage,
		keeperService:              keeperService,
		metrics:                    metrics.NewSecureValueServiceMetrics(reg),
	}
}

func (s *SecureValueService) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (createdSv *secretv1beta1.SecureValue, createErr error) {
	start := time.Now()

	ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes(
		attribute.String("namespace", sv.GetNamespace()),
		attribute.String("actor", actorUID),
	))
	defer span.End()

	defer func() {
		args := []any{
			"namespace", sv.GetNamespace(),
			"actorUID", actorUID,
		}

		if createdSv != nil {
			args = append(args, "name", createdSv.GetName())
			span.SetAttributes(attribute.String("name", createdSv.GetName()))
		}

		success := createErr == nil
		args = append(args, "success", success)
		if !success {
			span.SetStatus(codes.Error, "SecureValueService.Create failed")
			span.RecordError(createErr)
			args = append(args, "error", createErr)
		}

		logging.FromContext(ctx).Info("SecureValueService.Create finished", args...)

		s.metrics.SecureValueCreateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
	}()

	// Secure value creation uses the active keeper
	keeperName, keeperCfg, err := s.keeperMetadataStorage.GetActiveKeeperConfig(ctx, sv.Namespace)
	if err != nil {
		return nil, fmt.Errorf("fetching active keeper config: namespace=%+v %w", sv.Namespace, err)
	}

	return s.createNewVersion(ctx, keeperName, keeperCfg, sv, actorUID)
}

func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, sync bool, updateErr error) {
	start := time.Now()
	name, namespace := newSecureValue.GetName(), newSecureValue.GetNamespace()

	ctx, span := s.tracer.Start(ctx, "SecureValueService.Update", trace.WithAttributes(
		attribute.String("name", name),
		attribute.String("namespace", namespace),
		attribute.String("actor", actorUID),
	))
	defer span.End()

	defer func() {
		args := []any{
			"name", name,
			"namespace", namespace,
			"actorUID", actorUID,
			"sync", sync,
		}

		success := updateErr == nil
		args = append(args, "success", success)
		if !success {
			span.SetStatus(codes.Error, "SecureValueService.Update failed")
			span.RecordError(updateErr)
			args = append(args, "error", updateErr)
		}

		logging.FromContext(ctx).Info("SecureValueService.Update finished", args...)

		s.metrics.SecureValueUpdateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
	}()

	currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{})
	if err != nil {
		return nil, false, fmt.Errorf("reading secure value secret: %+w", err)
	}

	keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, currentVersion.Namespace, currentVersion.Status.Keeper, contracts.ReadOpts{})
	if err != nil {
		return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeper: %q %w", newSecureValue.Namespace, currentVersion.Status.Keeper, err)
	}

	if newSecureValue.Spec.Value == nil {
		keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
		if err != nil {
			return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Status.Keeper, err)
		}
		logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "type", keeperCfg.Type())

		secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version)
		if err != nil {
			return nil, false, fmt.Errorf("reading secret value from keeper: %w", err)
		}

		newSecureValue.Spec.Value = &secret
	}

	// Secure value updates use the keeper used to create the secure value
	const updateIsSync = true
	createdSv, err := s.createNewVersion(ctx, currentVersion.Status.Keeper, keeperCfg, newSecureValue, actorUID)
	return createdSv, updateIsSync, err
}

func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName string, keeperCfg secretv1beta1.KeeperConfig, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) {
	if keeperName == "" {
		return nil, fmt.Errorf("keeper name is required, got empty string")
	}
	if err := s.secureValueMutator.Mutate(sv, admission.Create); err != nil {
		return nil, err
	}

	if errorList := s.secureValueValidator.Validate(sv, nil, admission.Create); len(errorList) > 0 {
		return nil, contracts.NewErrValidateSecureValue(errorList)
	}

	createdSv, err := s.secureValueMetadataStorage.Create(ctx, keeperName, sv, actorUID)
	if err != nil {
		return nil, fmt.Errorf("creating secure value: %w", err)
	}

	createdSv.Status = secretv1beta1.SecureValueStatus{
		Version: createdSv.Status.Version,
		Keeper:  keeperName,
	}

	keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
	if err != nil {
		return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, keeperName, err)
	}
	logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "type", keeperCfg.Type())

	// TODO: can we stop using external id?
	// TODO: store uses only the namespace and returns and id. It could be a kv instead.
	// TODO: check that the encrypted store works with multiple versions
	externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
	if err != nil {
		return nil, fmt.Errorf("storing secure value in keeper: %w", err)
	}
	createdSv.Status.ExternalID = string(externalID)

	if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil {
		return nil, fmt.Errorf("setting secure value external id: %w", err)
	}

	if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version); err != nil {
		return nil, fmt.Errorf("marking secure value version as active: %w", err)
	}

	// In a single query:
	// TODO: set external id
	// TODO: set to active

	return createdSv, nil
}

func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (_ *secretv1beta1.SecureValue, readErr error) {
	if namespace == "" {
		return nil, fmt.Errorf("namespace cannot be empty")
	}
	if name == "" {
		return nil, fmt.Errorf("name cannot be empty")
	}

	start := time.Now()

	ctx, span := s.tracer.Start(ctx, "SecureValueService.Read", trace.WithAttributes(
		attribute.String("name", name),
		attribute.String("namespace", namespace.String()),
	))

	defer func() {
		args := []any{
			"name", name,
			"namespace", namespace.String(),
		}

		success := readErr == nil
		args = append(args, "success", success)
		if !success {
			span.SetStatus(codes.Error, "SecureValueService.Read failed")
			span.RecordError(readErr)
			args = append(args, "error", readErr)
		}

		logging.FromContext(ctx).Info("SecureValueService.Read finished", args...)

		s.metrics.SecureValueReadDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
	}()

	defer span.End()

	return s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: false})
}

func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace) (_ *secretv1beta1.SecureValueList, listErr error) {
	if namespace == "" {
		return nil, fmt.Errorf("namespace cannot be empty")
	}

	start := time.Now()

	ctx, span := s.tracer.Start(ctx, "SecureValueService.List", trace.WithAttributes(
		attribute.String("namespace", namespace.String()),
	))
	defer span.End()

	defer func() {
		args := []any{
			"namespace", namespace,
		}

		success := listErr == nil
		args = append(args, "success", success)
		if !success {
			span.SetStatus(codes.Error, "SecureValueService.List failed")
			span.RecordError(listErr)
			args = append(args, "error", listErr)
		}

		logging.FromContext(ctx).Info("SecureValueService.List finished", args...)

		s.metrics.SecureValueListDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
	}()

	user, ok := claims.AuthInfoFrom(ctx)
	if !ok {
		return nil, fmt.Errorf("missing auth info in context")
	}

	hasPermissionFor, _, err := s.accessClient.Compile(ctx, user, claims.ListRequest{
		Group:     secretv1beta1.APIGroup,
		Resource:  secretv1beta1.SecureValuesResourceInfo.GetName(),
		Namespace: namespace.String(),
		Verb:      utils.VerbGet, // Why not VerbList?
	})
	if err != nil {
		return nil, fmt.Errorf("failed to compile checker: %w", err)
	}

	secureValuesMetadata, err := s.secureValueMetadataStorage.List(ctx, namespace)
	if err != nil {
		return nil, fmt.Errorf("fetching secure values from storage: %+w", err)
	}

	out := make([]secretv1beta1.SecureValue, 0)

	for _, metadata := range secureValuesMetadata {
		// Check whether the user has permission to access this specific SecureValue in the namespace.
		if !hasPermissionFor(metadata.Name, "") {
			continue
		}

		out = append(out, metadata)
	}

	return &secretv1beta1.SecureValueList{
		Items: out,
	}, nil
}

func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (_ *secretv1beta1.SecureValue, deleteErr error) {
	if namespace == "" {
		return nil, fmt.Errorf("namespace cannot be empty")
	}
	if name == "" {
		return nil, fmt.Errorf("name cannot be empty")
	}

	start := time.Now()

	ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes(
		attribute.String("name", name),
		attribute.String("namespace", namespace.String()),
	))
	defer span.End()

	defer func() {
		args := []any{
			"name", name,
			"namespace", namespace,
		}

		success := deleteErr == nil
		args = append(args, "success", success)
		if !success {
			span.SetStatus(codes.Error, "SecureValueService.Delete failed")
			span.RecordError(deleteErr)
			args = append(args, "error", deleteErr)
		}

		logging.FromContext(ctx).Info("SecureValueService.Delete finished", args...)

		s.metrics.SecureValueDeleteDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
	}()

	// TODO: does this need to be for update?
	sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true})
	if err != nil {
		return nil, fmt.Errorf("fetching secure value: %+w", err)
	}

	if err := s.secureValueMetadataStorage.SetVersionToInactive(ctx, namespace, name, sv.Status.Version); err != nil {
		return nil, fmt.Errorf("setting secure value version to inactive: %+w", err)
	}

	return sv, nil
}
