package controller

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"time"

	cephv1 "git.unkin.net/unkin/cephrgw-operator/api/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	annPrefix      = "backups.unkin.net/"
	annSchedule    = annPrefix + "schedule"
	annDestination = annPrefix + "destination"
	annPurge       = annPrefix + "purge-on-delete"

	// requeueSteady re-syncs healthy objects so drift heals without waiting for
	// an event.
	requeueSteady = 10 * time.Minute
	// requeueShort backs off while waiting for a dependency (e.g. the
	// BucketAccess to become Ready or a destination to appear).
	requeueShort = 30 * time.Second
)

// baseReconciler holds the shared client, scheme, recorder and destinations
// ConfigMap location used by both the PVC and CNPG Cluster controllers.
type baseReconciler struct {
	client.Client
	Scheme        *runtime.Scheme
	Recorder      record.EventRecorder
	DestNamespace string
	DestConfigMap string
}

func (b *baseReconciler) resolveDest(ctx context.Context, dest string) (Destination, error) {
	return resolveDestination(ctx, b.Client, b.DestNamespace, b.DestConfigMap, dest)
}

// ensureBucketStack provisions the ObjectStoreUser, Bucket and BucketAccess for
// an annotated object (all owner-referenced to it) and reports whether the
// BucketAccess is Ready together with the name of its credential Secret.
func (b *baseReconciler) ensureBucketStack(ctx context.Context, owner client.Object, dest Destination) (ready bool, credSecret string, err error) {
	ns := owner.GetNamespace()
	obj := owner.GetName()
	bName := bucketName(ns, obj)

	user := &cephv1.ObjectStoreUser{ObjectMeta: metav1.ObjectMeta{Name: userName(obj), Namespace: ns}}
	if _, err = controllerutil.CreateOrUpdate(ctx, b.Client, user, func() error {
		user.Spec.DisplayName = fmt.Sprintf("autobackup %s/%s", ns, obj)
		return controllerutil.SetControllerReference(owner, user, b.Scheme)
	}); err != nil {
		return false, "", err
	}

	bucket := &cephv1.Bucket{ObjectMeta: metav1.ObjectMeta{Name: bName, Namespace: ns}}
	if _, err = controllerutil.CreateOrUpdate(ctx, b.Client, bucket, func() error {
		bucket.Spec.OwnerRef = user.Name
		bucket.Spec.BucketName = bName
		bucket.Spec.PlacementTarget = dest.PlacementTarget
		bucket.Spec.Zonegroup = dest.Zonegroup
		bucket.Spec.Versioning = true
		// Keep bucket data by default; deleting the owner GCs the CRs but the
		// underlying objects are retained for safety.
		bucket.Spec.RetainOnDelete = true
		return controllerutil.SetControllerReference(owner, bucket, b.Scheme)
	}); err != nil {
		return false, "", err
	}

	access := &cephv1.BucketAccess{ObjectMeta: metav1.ObjectMeta{Name: accessName(obj), Namespace: ns}}
	if _, err = controllerutil.CreateOrUpdate(ctx, b.Client, access, func() error {
		access.Spec.BucketRef = bName
		access.Spec.Level = cephv1.AccessReadWrite
		access.Spec.SecretName = credSecretName(obj)
		return controllerutil.SetControllerReference(owner, access, b.Scheme)
	}); err != nil {
		return false, "", err
	}

	if access.Status.Phase != "Ready" || access.Status.SecretName == "" {
		return false, "", nil
	}
	return true, access.Status.SecretName, nil
}

// ensureResticSecret creates (once) an owner-referenced Secret holding a random
// restic repository password under key "password".
func (b *baseReconciler) ensureResticSecret(ctx context.Context, owner client.Object) (string, error) {
	name := resticSecretName(owner.GetName())
	sec := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: owner.GetNamespace()}}
	_, err := controllerutil.CreateOrUpdate(ctx, b.Client, sec, func() error {
		sec.Type = corev1.SecretTypeOpaque
		if _, ok := sec.Data["password"]; !ok {
			pw, err := randomPassword(32)
			if err != nil {
				return err
			}
			if sec.Data == nil {
				sec.Data = map[string][]byte{}
			}
			sec.Data["password"] = []byte(pw)
		}
		return controllerutil.SetControllerReference(owner, sec, b.Scheme)
	})
	return name, err
}

func randomPassword(n int) (string, error) {
	buf := make([]byte, n)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return hex.EncodeToString(buf), nil
}

// deleteIfExists best-effort deletes obj, ignoring a NotFound result.
func deleteIfExists(ctx context.Context, c client.Client, obj client.Object) error {
	return client.IgnoreNotFound(c.Delete(ctx, obj))
}

// bucketStackObjects lists the cephrgw CRs and credential Secret created for an
// object, for explicit purge-on-delete teardown.
func bucketStackObjects(ns, obj string) []client.Object {
	meta := func(name string) metav1.ObjectMeta { return metav1.ObjectMeta{Name: name, Namespace: ns} }
	return []client.Object{
		&cephv1.BucketAccess{ObjectMeta: meta(accessName(obj))},
		&cephv1.Bucket{ObjectMeta: meta(bucketName(ns, obj))},
		&cephv1.ObjectStoreUser{ObjectMeta: meta(userName(obj))},
		&corev1.Secret{ObjectMeta: meta(credSecretName(obj))},
		&corev1.Secret{ObjectMeta: meta(resticSecretName(obj))},
	}
}
