// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package generator

import (
	"fmt"
	"io"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"

	configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
)

// SecretHelper describes an object that can Generate() a Secret and determine whether a Secret
// IsValid(). It can also be Notify()'d about a Secret being persisted.
//
// A SecretHelper has a NamePrefix() that can be used to identify it from other SecretHelper instances.
type SecretHelper interface {
	NamePrefix() string
	Generate(*configv1alpha1.FederationDomain) (*corev1.Secret, error)
	IsValid(*configv1alpha1.FederationDomain, *corev1.Secret) bool
	ObserveActiveSecretAndUpdateParentFederationDomain(*configv1alpha1.FederationDomain, *corev1.Secret) *configv1alpha1.FederationDomain
	Handles(metav1.Object) bool
}

const (
	// SupervisorCSRFSigningKeySecretType for the Secret storing the CSRF signing key.
	SupervisorCSRFSigningKeySecretType corev1.SecretType = "secrets.pinniped.dev/supervisor-csrf-signing-key"

	// FederationDomainTokenSigningKeyType for the Secret storing the FederationDomain token signing key.
	FederationDomainTokenSigningKeyType corev1.SecretType = "secrets.pinniped.dev/federation-domain-token-signing-key"

	// FederationDomainStateSigningKeyType for the Secret storing the FederationDomain state signing key.
	FederationDomainStateSigningKeyType corev1.SecretType = "secrets.pinniped.dev/federation-domain-state-signing-key"

	// FederationDomainStateEncryptionKeyType for the Secret storing the FederationDomain state encryption key.
	FederationDomainStateEncryptionKeyType corev1.SecretType = "secrets.pinniped.dev/federation-domain-state-encryption-key"

	federationDomainKind = "FederationDomain"

	// symmetricSecretDataKey is the corev1.Secret.Data key for the symmetric key value generated by this helper.
	symmetricSecretDataKey = "key"

	// symmetricKeySize is the default length, in bytes, of generated keys. It is set to 32 since this
	// seems like reasonable entropy for our keys, and a 32-byte key will allow for AES-256
	// to be used in our codecs (see dynamiccodec.Codec).
	symmetricKeySize = 32
)

// SecretUsage describes how a cryptographic secret is going to be used. It is currently used to
// indicate to a SecretHelper which status field to set on the parent FederationDomain for a Secret.
type SecretUsage int

const (
	SecretUsageTokenSigningKey SecretUsage = iota
	SecretUsageStateSigningKey
	SecretUsageStateEncryptionKey
)

// New returns a SecretHelper that has been parameterized with common symmetric secret generation
// knobs.
func NewSymmetricSecretHelper(
	namePrefix string,
	labels map[string]string,
	rand io.Reader,
	secretUsage SecretUsage,
	updateCacheFunc func(cacheKey string, cacheValue []byte),
) SecretHelper {
	return &symmetricSecretHelper{
		namePrefix:      namePrefix,
		labels:          labels,
		rand:            rand,
		secretUsage:     secretUsage,
		updateCacheFunc: updateCacheFunc,
	}
}

type symmetricSecretHelper struct {
	namePrefix      string
	labels          map[string]string
	rand            io.Reader
	secretUsage     SecretUsage
	updateCacheFunc func(cacheKey string, cacheValue []byte)
}

func (s *symmetricSecretHelper) NamePrefix() string { return s.namePrefix }

// Generate implements SecretHelper.Generate().
func (s *symmetricSecretHelper) Generate(parent *configv1alpha1.FederationDomain) (*corev1.Secret, error) {
	key := make([]byte, symmetricKeySize)
	if _, err := s.rand.Read(key); err != nil {
		return nil, err
	}

	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("%s%s", s.namePrefix, parent.UID),
			Namespace: parent.Namespace,
			Labels:    s.labels,
			OwnerReferences: []metav1.OwnerReference{
				*metav1.NewControllerRef(parent, schema.GroupVersionKind{
					Group:   configv1alpha1.SchemeGroupVersion.Group,
					Version: configv1alpha1.SchemeGroupVersion.Version,
					Kind:    federationDomainKind,
				}),
			},
		},
		Type: s.secretType(),
		Data: map[string][]byte{
			symmetricSecretDataKey: key,
		},
	}, nil
}

// IsValid implements SecretHelper.IsValid().
func (s *symmetricSecretHelper) IsValid(parent *configv1alpha1.FederationDomain, secret *corev1.Secret) bool {
	if !metav1.IsControlledBy(secret, parent) {
		return false
	}

	if secret.Type != s.secretType() {
		return false
	}

	key, ok := secret.Data[symmetricSecretDataKey]
	if !ok {
		return false
	}
	if len(key) != symmetricKeySize {
		return false
	}

	return true
}

// ObserveActiveSecretAndUpdateParentFederationDomain implements SecretHelper.ObserveActiveSecretAndUpdateParentFederationDomain().
func (s *symmetricSecretHelper) ObserveActiveSecretAndUpdateParentFederationDomain(
	federationDomain *configv1alpha1.FederationDomain,
	secret *corev1.Secret,
) *configv1alpha1.FederationDomain {
	s.updateCacheFunc(federationDomain.Spec.Issuer, secret.Data[symmetricSecretDataKey])

	switch s.secretUsage {
	case SecretUsageTokenSigningKey:
		federationDomain.Status.Secrets.TokenSigningKey.Name = secret.Name
	case SecretUsageStateSigningKey:
		federationDomain.Status.Secrets.StateSigningKey.Name = secret.Name
	case SecretUsageStateEncryptionKey:
		federationDomain.Status.Secrets.StateEncryptionKey.Name = secret.Name
	default:
		panic(fmt.Sprintf("unknown secret usage enum value: %d", s.secretUsage))
	}

	return federationDomain
}

func (s *symmetricSecretHelper) secretType() corev1.SecretType {
	switch s.secretUsage {
	case SecretUsageTokenSigningKey:
		return FederationDomainTokenSigningKeyType
	case SecretUsageStateSigningKey:
		return FederationDomainStateSigningKeyType
	case SecretUsageStateEncryptionKey:
		return FederationDomainStateEncryptionKeyType
	default:
		panic(fmt.Sprintf("unknown secret usage enum value: %d", s.secretUsage))
	}
}

func (s *symmetricSecretHelper) Handles(obj metav1.Object) bool {
	return IsFederationDomainSecretOfType(obj, s.secretType())
}

func IsFederationDomainSecretOfType(obj metav1.Object, secretType corev1.SecretType) bool {
	secret, ok := obj.(*corev1.Secret)
	if !ok {
		return false
	}
	if secret.Type != secretType {
		return false
	}
	return isFederationDomainControllee(secret)
}

// isFederationDomainControllee returns whether the provided obj is controlled by an FederationDomain.
func isFederationDomainControllee(obj metav1.Object) bool {
	controller := metav1.GetControllerOf(obj)
	return controller != nil &&
		controller.APIVersion == configv1alpha1.SchemeGroupVersion.String() &&
		controller.Kind == federationDomainKind
}