package apistore

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/google/uuid"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/klog/v2"

	authlib "github.com/grafana/authlib/types"
	"github.com/grafana/grafana-app-sdk/logging"
	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)

type objectForStorage struct {
	// The value to save in unistore
	raw bytes.Buffer

	// Reference to the owner object
	ref common.ObjectReference

	// apply permissions after create (defined in the resource body)
	grantPermissions string

	// Synchronous AfterCreate permissions -- allows users to become "admin" of the thing they made
	permissionCreator permissionCreatorFunc

	// These secrets where created, should be cleaned up if storage fails
	createdSecureValues []string

	// These should be deleted if storage succeeds
	deleteSecureValues []string

	// We know something changed
	// This will ensure that the generation increments
	hasChanged bool
}

func (v *objectForStorage) finish(ctx context.Context, err error, secrets secrets.InlineSecureValueSupport) error {
	if err != nil {
		// Remove the secure values that were created
		for _, s := range v.createdSecureValues {
			if e := secrets.DeleteWhenOwnedByResource(ctx, v.ref, s); e != nil {
				logging.FromContext(ctx).Warn("unable to clean up new secure value", "name", s, "err", e)
			}
		}
		return err
	}

	// Delete secure values after successfully saving the object
	if len(v.deleteSecureValues) > 0 {
		for _, s := range v.deleteSecureValues {
			if e := secrets.DeleteWhenOwnedByResource(ctx, v.ref, s); e != nil {
				logging.FromContext(ctx).Warn("unable to clean up new secure value", "name", s, "err", e)
			}
		}
	}

	// Create permissions
	if v.permissionCreator != nil {
		return v.permissionCreator(ctx)
	}

	return nil
}

// Called on create
func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime.Object) (objectForStorage, error) {
	v := objectForStorage{}
	info, ok := authlib.AuthInfoFrom(ctx)
	if !ok {
		return v, errors.New("missing auth info")
	}

	obj, err := utils.MetaAccessor(newObject)
	if err != nil {
		return v, err
	}
	if obj.GetName() == "" {
		return v, storage.NewInvalidObjError("", "missing name")
	}
	if obj.GetResourceVersion() != "" {
		return v, storage.ErrResourceVersionSetOnCreate
	}
	if obj.GetUID() == "" {
		obj.SetUID(types.UID(uuid.NewString()))
	}
	if obj.GetFolder() != "" && !s.opts.EnableFolderSupport {
		return v, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
	}
	if s.opts.MaximumNameLength > 0 && len(obj.GetName()) > s.opts.MaximumNameLength {
		return v, apierrors.NewBadRequest(fmt.Sprintf("name exceeds maximum length (%d)", s.opts.MaximumNameLength))
	}

	v.grantPermissions = obj.GetAnnotation(utils.AnnoKeyGrantPermissions)
	if v.grantPermissions != "" {
		obj.SetAnnotation(utils.AnnoKeyGrantPermissions, "") // remove the annotation
	}
	if err := checkManagerPropertiesOnCreate(info, obj); err != nil {
		return v, err
	}

	if s.opts.RequireDeprecatedInternalID {
		// nolint:staticcheck
		id := obj.GetDeprecatedInternalID()
		if id < 1 {
			// the ID must be smaller than 9007199254740991, otherwise we will lose prescision
			// on the frontend, which uses the number type to store ids. The largest safe number in
			// javascript is 9007199254740991, compared to 9223372036854775807 as the max int64
			// nolint:staticcheck
			obj.SetDeprecatedInternalID(s.snowflake.Generate().Int64() & ((1 << 52) - 1))
		}
	}

	obj.SetGenerateName("") // Clear the random name field
	obj.SetResourceVersion("")
	obj.SetSelfLink("")

	obj.SetUpdatedBy("")
	obj.SetUpdatedTimestamp(nil)
	obj.SetCreatedBy(info.GetUID())
	obj.SetGeneration(1) // the first time we write

	err = prepareSecureValues(ctx, s.opts.SecureValues, obj, nil, &v)
	if err != nil {
		return v, err
	}

	err = s.codec.Encode(newObject, &v.raw)
	if err == nil {
		err = s.handleLargeResources(ctx, obj, &v.raw)
	}
	return v, err
}

// Called on update
func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runtime.Object, previousObject runtime.Object) (objectForStorage, error) {
	v := objectForStorage{}
	info, ok := authlib.AuthInfoFrom(ctx)
	if !ok {
		return v, errors.New("missing auth info")
	}

	obj, err := utils.MetaAccessor(updateObject)
	if err != nil {
		return v, err
	}
	if obj.GetName() == "" {
		return v, fmt.Errorf("updated object must have a name")
	}

	previous, err := utils.MetaAccessor(previousObject)
	if err != nil {
		return v, err
	}

	if previous.GetUID() == "" {
		klog.Errorf("object is missing UID: %s, %s", obj.GetGroupVersionKind().String(), obj.GetName())
	} else if obj.GetUID() != previous.GetUID() {
		// Eventually this should be a real error or logged
		// However the dashboard dual write behavior hits this every time, so we will ignore it
		// if obj.GetUID() != "" {
		// 	klog.Errorf("object UID mismatch: %s, was:%s, now: %s", obj.GetGroupVersionKind().String(), previous.GetName(), obj.GetUID())
		// }
		obj.SetUID(previous.GetUID())
	}

	if obj.GetName() != previous.GetName() {
		return v, fmt.Errorf("name mismatch between existing and updated object")
	}

	obj.SetCreatedBy(previous.GetCreatedBy())
	obj.SetCreationTimestamp(previous.GetCreationTimestamp())
	obj.SetResourceVersion("")                           // removed from saved JSON because the RV is not yet calculated
	obj.SetAnnotation(utils.AnnoKeyGrantPermissions, "") // Grant is ignored for update requests

	// for dashboards, a mutation hook will set it if it didn't exist on the previous obj
	// avoid setting it back to 0
	previousInternalID := previous.GetDeprecatedInternalID() // nolint:staticcheck
	if previousInternalID != 0 {
		obj.SetDeprecatedInternalID(previousInternalID) // nolint:staticcheck
	}

	err = prepareSecureValues(ctx, s.opts.SecureValues, obj, previous, &v)
	if err != nil {
		return v, err
	}

	// Check if we should bump the generation
	if obj.GetFolder() != previous.GetFolder() {
		if !s.opts.EnableFolderSupport {
			return v, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
		}
		// TODO: check that we can move the folder?
		v.hasChanged = true
	} else if obj.GetDeletionTimestamp() != nil && previous.GetDeletionTimestamp() == nil {
		v.hasChanged = true // bump generation when deleted
	} else if !v.hasChanged {
		spec, e1 := obj.GetSpec()
		oldSpec, e2 := previous.GetSpec()
		if e1 == nil && e2 == nil {
			if !apiequality.Semantic.DeepEqual(spec, oldSpec) {
				v.hasChanged = true
			}
		}
	}

	// Mark the resource as changed
	if v.hasChanged {
		obj.SetGeneration(previous.GetGeneration() + 1)
		obj.SetUpdatedBy(info.GetUID())
		obj.SetUpdatedTimestampMillis(time.Now().UnixMilli())

		// Only validate when the generation has changed
		if err := checkManagerPropertiesOnUpdateSpec(info, obj, previous); err != nil {
			return v, err
		}
	} else {
		obj.SetGeneration(previous.GetGeneration())
		obj.SetAnnotation(utils.AnnoKeyUpdatedBy, previous.GetAnnotation(utils.AnnoKeyUpdatedBy))
		obj.SetAnnotation(utils.AnnoKeyUpdatedTimestamp, previous.GetAnnotation(utils.AnnoKeyUpdatedTimestamp))
	}

	err = s.codec.Encode(updateObject, &v.raw)
	if err == nil {
		err = s.handleLargeResources(ctx, obj, &v.raw)
	}
	return v, err
}

// The bytes buffer will be reset with the proper value
func (s *Storage) handleLargeResources(ctx context.Context, obj utils.GrafanaMetaAccessor, buf *bytes.Buffer) error {
	support := s.opts.LargeObjectSupport
	size := buf.Len()
	if support != nil && size > support.Threshold() {
		if support.MaxSize() > 0 && size > support.MaxSize() {
			return fmt.Errorf("request object is too big (%s > %s)", humanize.Bytes(uint64(size)), humanize.Bytes(uint64(support.MaxSize())))
		}

		key := &resourcepb.ResourceKey{
			Group:     s.gr.Group,
			Resource:  s.gr.Resource,
			Namespace: obj.GetNamespace(),
			Name:      obj.GetName(),
		}

		err := support.Deconstruct(ctx, key, s.store, obj, buf.Bytes())
		if err != nil {
			return err
		}

		buf.Reset()
		orig, ok := obj.GetRuntimeObject()
		if !ok {
			return fmt.Errorf("error using object as runtime object")
		}

		// Now encode the smaller version
		return s.codec.Encode(orig, buf)
	}
	return nil
}
