package utils

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"

	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)

// LabelKeyGetHistory is used to select object history for an given resource
const LabelKeyGetHistory = "grafana.app/get-history"

// LabelKeyGetTrash is used to list objects that have been (soft) deleted
const LabelKeyGetTrash = "grafana.app/get-trash"

// AnnoKeyKubectlLastAppliedConfig is the annotation kubectl writes with the entire previous config
const AnnoKeyKubectlLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration"

// AnnoKeyGrantPermissions allows users to explicitly grant themself permissions when creating
// resoures in the "root" folder.  This annotation is not saved and invalud for update.
const AnnoKeyGrantPermissions = "grafana.app/grant-permissions"

// AnnoGrantPermissionsDefault is the value that should be sent with AnnoKeyGrantPermissions
const AnnoGrantPermissionsDefault = "default"

// DeletedGeneration is set on Resources that have been (soft) deleted
const DeletedGeneration = int64(-999)

// Annotation keys

const AnnoKeyCreatedBy = "grafana.app/createdBy"
const AnnoKeyUpdatedTimestamp = "grafana.app/updatedTimestamp"
const AnnoKeyUpdatedBy = "grafana.app/updatedBy"
const AnnoKeyFolder = "grafana.app/folder"
const AnnoKeyBlob = "grafana.app/blob"
const AnnoKeyMessage = "grafana.app/message"

// Identify where values came from

const oldAnnoKeyRepoName = "grafana.app/repoName"
const oldAnnoKeyRepoPath = "grafana.app/repoPath"
const oldAnnoKeyRepoHash = "grafana.app/repoHash"
const oldAnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"

// Annotations used to store manager properties

const AnnoKeyManagerKind = "grafana.app/managedBy"
const AnnoKeyManagerIdentity = "grafana.app/managerId"
const AnnoKeyManagerAllowsEdits = "grafana.app/managerAllowsEdits"
const AnnoKeyManagerSuspended = "grafana.app/managerSuspended"

// Annotations used to store source properties

const AnnoKeySourcePath = "grafana.app/sourcePath"
const AnnoKeySourceChecksum = "grafana.app/sourceChecksum"
const AnnoKeySourceTimestamp = "grafana.app/sourceTimestamp"

// LabelKeyDeprecatedInternalID gives the deprecated internal ID of a resource
// Deprecated: will be removed in grafana 13
const LabelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID"

// Accessor functions for k8s objects
//
//go:generate mockery --name GrafanaMetaAccessor --structname MockGrafanaMetaAccessor --inpackage --filename meta_mock.go --with-expecter
type GrafanaMetaAccessor interface {
	metav1.Object

	GetGroupVersionKind() schema.GroupVersionKind
	GetRuntimeObject() (runtime.Object, bool)

	// Helper to get resource versions as int64, however this is not required
	// See: https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions
	GetResourceVersionInt64() (int64, error)
	SetResourceVersionInt64(int64)

	GetUpdatedTimestamp() (*time.Time, error)
	SetUpdatedTimestamp(v *time.Time)
	SetUpdatedTimestampMillis(unix int64)
	GetCreatedBy() string
	SetCreatedBy(user string)
	GetUpdatedBy() string
	SetUpdatedBy(user string)
	GetFolder() string
	SetFolder(uid string)
	GetMessage() string
	SetMessage(msg string)
	SetAnnotation(key string, val string)
	GetAnnotation(key string) string

	SetBlob(v *BlobInfo)
	GetBlob() *BlobInfo

	// Deprecated: This should only be used to support legacy /api endpoints that need to return internal IDs. Those endpoints should be marked as deprecated and once they are removed, this will be too
	GetDeprecatedInternalID() int64

	// Deprecated: This should only be used to support legacy /api endpoints that need to return internal IDs. Those endpoints should be marked as deprecated and once they are removed, this will be too
	SetDeprecatedInternalID(id int64)

	GetSpec() (any, error)
	SetSpec(any) error

	GetStatus() (any, error)

	// Used by the generic strategy to keep the status value unchanged on an update
	// NOTE the type must match the existing value, or an error will be thrown
	SetStatus(any) error

	// Find a title in the object
	// This will reflect the object and try to get:
	//  * spec.title
	//  * spec.name
	//  * title
	// and return an empty string if nothing was found
	FindTitle(defaultTitle string) string

	// GetManagerProperties returns the identity of the tool,
	// which is responsible for managing the resource.
	//
	// If the identity is not known, the second return value will be false.
	GetManagerProperties() (ManagerProperties, bool)

	// SetManagerProperties sets the identity of the tool,
	// which is responsible for managing the resource.
	SetManagerProperties(ManagerProperties)

	// GetSourceProperties returns the source properties of the resource.
	GetSourceProperties() (SourceProperties, bool)

	// SetSourceProperties sets the source properties of the resource.
	SetSourceProperties(SourceProperties)

	// GetSecureValues reads the "secure" property on a resource
	GetSecureValues() (common.InlineSecureValues, error)

	// SetSourceProperties sets the source properties of the resource.
	// For write commands, this may include inline secrets; read will only have references
	SetSecureValues(common.InlineSecureValues) error
}

var _ GrafanaMetaAccessor = (*grafanaMetaAccessor)(nil)

type grafanaMetaAccessor struct {
	raw interface{} // the original object (it implements metav1.Object)
	obj metav1.Object
	r   reflect.Value
}

// Accessor takes an arbitrary object pointer and returns meta.Interface.
// obj must be a pointer to an API type. An error is returned if the minimum
// required fields are missing. Fields that are not required return the default
// value and are a no-op if set.
func MetaAccessor(raw interface{}) (GrafanaMetaAccessor, error) {
	if raw == nil {
		return nil, fmt.Errorf("unable to read metadata from nil object")
	}

	obj, err := meta.Accessor(raw)
	if err != nil {
		return nil, fmt.Errorf("unable to read metadata from: %T, %s", raw, err)
	}

	// reflection to find title and other non object properties
	r := reflect.ValueOf(raw)
	if r.Kind() == reflect.Ptr || r.Kind() == reflect.Interface {
		r = r.Elem()
	}
	return &grafanaMetaAccessor{raw, obj, r}, nil
}

func (m *grafanaMetaAccessor) GetResourceVersionInt64() (int64, error) {
	v := m.obj.GetResourceVersion()
	if v == "" {
		return 0, nil
	}
	return strconv.ParseInt(v, 10, 64)
}

func (m *grafanaMetaAccessor) GetRuntimeObject() (runtime.Object, bool) {
	obj, ok := m.raw.(runtime.Object)
	return obj, ok
}

func (m *grafanaMetaAccessor) SetResourceVersionInt64(rv int64) {
	m.obj.SetResourceVersion(strconv.FormatInt(rv, 10))
}

func (m *grafanaMetaAccessor) SetAnnotation(key string, val string) {
	anno := m.obj.GetAnnotations()
	if val == "" {
		if anno != nil {
			delete(anno, key)
		}
	} else {
		if anno == nil {
			anno = make(map[string]string)
		}
		anno[key] = val
	}
	m.obj.SetAnnotations(anno)
}

func (m *grafanaMetaAccessor) GetAnnotation(key string) string {
	anno := m.obj.GetAnnotations()
	if anno == nil {
		return ""
	}
	return anno[key]
}

func (m *grafanaMetaAccessor) GetUpdatedTimestamp() (*time.Time, error) {
	v, ok := m.obj.GetAnnotations()[AnnoKeyUpdatedTimestamp]
	if !ok || v == "" {
		return nil, nil
	}
	t, err := time.Parse(time.RFC3339, v)
	if err != nil {
		return nil, fmt.Errorf("invalid updated timestamp: %s", err.Error())
	}
	t = t.UTC()
	return &t, nil
}

func (m *grafanaMetaAccessor) SetUpdatedTimestampMillis(v int64) {
	if v > 0 {
		t := time.UnixMilli(v)
		m.SetUpdatedTimestamp(&t)
	} else {
		m.SetAnnotation(AnnoKeyUpdatedTimestamp, "") // will clear the annotation
	}
}

func (m *grafanaMetaAccessor) SetUpdatedTimestamp(v *time.Time) {
	txt := ""
	if v != nil && v.Unix() != 0 {
		txt = v.UTC().Format(time.RFC3339)
	}
	m.SetAnnotation(AnnoKeyUpdatedTimestamp, txt)
}

func (m *grafanaMetaAccessor) GetCreatedBy() string {
	return m.GetAnnotation(AnnoKeyCreatedBy)
}

func (m *grafanaMetaAccessor) SetCreatedBy(user string) {
	m.SetAnnotation(AnnoKeyCreatedBy, user)
}

func (m *grafanaMetaAccessor) GetUpdatedBy() string {
	return m.GetAnnotation(AnnoKeyUpdatedBy)
}

func (m *grafanaMetaAccessor) SetUpdatedBy(user string) {
	m.SetAnnotation(AnnoKeyUpdatedBy, user)
}

func (m *grafanaMetaAccessor) GetBlob() *BlobInfo {
	return ParseBlobInfo(m.GetAnnotation(AnnoKeyBlob))
}

func (m *grafanaMetaAccessor) SetBlob(info *BlobInfo) {
	if info == nil {
		m.SetAnnotation(AnnoKeyBlob, "") // delete
	} else {
		m.SetAnnotation(AnnoKeyBlob, info.String())
	}
}

func (m *grafanaMetaAccessor) GetFolder() string {
	return m.GetAnnotation(AnnoKeyFolder)
}

func (m *grafanaMetaAccessor) SetFolder(uid string) {
	m.SetAnnotation(AnnoKeyFolder, uid)
}

func (m *grafanaMetaAccessor) GetMessage() string {
	return m.GetAnnotation(AnnoKeyMessage)
}

func (m *grafanaMetaAccessor) SetMessage(uid string) {
	m.SetAnnotation(AnnoKeyMessage, uid)
}

// This will be removed in Grafana 13. Do not add any new usage of it.
func (m *grafanaMetaAccessor) GetDeprecatedInternalID() int64 {
	labels := m.obj.GetLabels()
	if labels == nil {
		return 0
	}

	if internalID, ok := labels[LabelKeyDeprecatedInternalID]; ok {
		id, err := strconv.ParseInt(internalID, 10, 64)
		if err == nil {
			return id
		}
	}

	return 0
}

// This will be removed in Grafana 13. Do not add any new usage of it.
func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) {
	labels := m.obj.GetLabels()

	// disallow setting it to 0
	if id == 0 {
		if labels != nil {
			delete(labels, LabelKeyDeprecatedInternalID)
			m.obj.SetLabels(labels)
		}
		return
	}

	if labels == nil {
		labels = make(map[string]string)
	}

	labels[LabelKeyDeprecatedInternalID] = strconv.FormatInt(id, 10)
	m.obj.SetLabels(labels)
}

// GetAnnotations implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetAnnotations() map[string]string {
	return m.obj.GetAnnotations()
}

// GetCreationTimestamp implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetCreationTimestamp() metav1.Time {
	return m.obj.GetCreationTimestamp()
}

// GetDeletionGracePeriodSeconds implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetDeletionGracePeriodSeconds() *int64 {
	return m.obj.GetDeletionGracePeriodSeconds()
}

// GetDeletionTimestamp implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetDeletionTimestamp() *metav1.Time {
	return m.obj.GetDeletionTimestamp()
}

// GetFinalizers implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetFinalizers() []string {
	return m.obj.GetFinalizers()
}

// GetGenerateName implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetGenerateName() string {
	return m.obj.GetGenerateName()
}

// GetGeneration implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetGeneration() int64 {
	return m.obj.GetGeneration()
}

// GetLabels implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetLabels() map[string]string {
	return m.obj.GetLabels()
}

// GetManagedFields implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetManagedFields() []metav1.ManagedFieldsEntry {
	return m.obj.GetManagedFields()
}

// GetName implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetName() string {
	return m.obj.GetName()
}

// GetNamespace implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetNamespace() string {
	return m.obj.GetNamespace()
}

// GetOwnerReferences implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetOwnerReferences() []metav1.OwnerReference {
	return m.obj.GetOwnerReferences()
}

// GetResourceVersion implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetResourceVersion() string {
	return m.obj.GetResourceVersion()
}

// GetSelfLink implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetSelfLink() string {
	return m.obj.GetSelfLink()
}

// GetUID implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetUID() types.UID {
	return m.obj.GetUID()
}

// SetAnnotations implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetAnnotations(annotations map[string]string) {
	m.obj.SetAnnotations(annotations)
}

// SetCreationTimestamp implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetCreationTimestamp(timestamp metav1.Time) {
	m.obj.SetCreationTimestamp(timestamp)
}

// SetDeletionGracePeriodSeconds implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetDeletionGracePeriodSeconds(v *int64) {
	m.obj.SetDeletionGracePeriodSeconds(v)
}

// SetDeletionTimestamp implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetDeletionTimestamp(timestamp *metav1.Time) {
	m.obj.SetDeletionTimestamp(timestamp)
}

// SetFinalizers implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetFinalizers(finalizers []string) {
	m.obj.SetFinalizers(finalizers)
}

// SetGenerateName implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetGenerateName(name string) {
	m.obj.SetGenerateName(name)
}

// SetGeneration implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetGeneration(generation int64) {
	m.obj.SetGeneration(generation)
}

// SetLabels implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetLabels(labels map[string]string) {
	m.obj.SetLabels(labels)
}

// SetManagedFields implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) {
	m.obj.SetManagedFields(managedFields)
}

// SetName implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetName(name string) {
	m.obj.SetName(name)
}

// SetNamespace implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetNamespace(namespace string) {
	m.obj.SetNamespace(namespace)
}

// SetOwnerReferences implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetOwnerReferences(v []metav1.OwnerReference) {
	m.obj.SetOwnerReferences(v)
}

// SetResourceVersion implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetResourceVersion(version string) {
	m.obj.SetResourceVersion(version)
}

// SetSelfLink implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetSelfLink(selfLink string) {
	m.obj.SetSelfLink(selfLink)
}

// SetUID implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetUID(uid types.UID) {
	m.obj.SetUID(uid)
}

func (m *grafanaMetaAccessor) GetGroupVersionKind() schema.GroupVersionKind {
	obj, ok := m.raw.(runtime.Object)
	if ok {
		return obj.GetObjectKind().GroupVersionKind()
	}

	gvk := schema.GroupVersionKind{}
	apiVersion := ""

	typ, ok := m.raw.(metav1.Type)
	if ok {
		apiVersion = typ.GetAPIVersion()
		gvk.Kind = typ.GetKind()
	} else {
		val := m.r.FieldByName("APIVersion")
		if val.IsValid() && val.Kind() == reflect.String {
			apiVersion = val.String()
		}
		val = m.r.FieldByName("Kind")
		if val.IsValid() && val.Kind() == reflect.String {
			gvk.Kind = val.String()
		}
	}
	if apiVersion != "" {
		gv, err := schema.ParseGroupVersion(apiVersion)
		if err == nil {
			gvk.Group = gv.Group
			gvk.Version = gv.Version
		}
	}
	return gvk
}

func (m *grafanaMetaAccessor) GetSpec() (spec any, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("error reading spec")
		}
	}()

	f := m.r.FieldByName("Spec")
	if f.IsValid() {
		spec = f.Interface()
		return
	}

	// Unstructured
	u, ok := m.raw.(*unstructured.Unstructured)
	if ok {
		spec, ok = u.Object["spec"]
		if ok {
			return // no error
		}
	}
	err = fmt.Errorf("unable to read spec")
	return
}

func (m *grafanaMetaAccessor) SetSpec(s any) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("error setting spec")
		}
	}()

	f := m.r.FieldByName("Spec")
	if f.IsValid() {
		f.Set(reflect.ValueOf(s))
		return
	}

	// Unstructured
	u, ok := m.raw.(*unstructured.Unstructured)
	if ok {
		u.Object["spec"] = s
	} else {
		err = fmt.Errorf("unable to set spec")
	}
	return
}

func (m *grafanaMetaAccessor) GetStatus() (status any, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("error reading status")
		}
	}()

	f := m.r.FieldByName("Status")
	if f.IsValid() {
		status = f.Interface()
		return
	}

	// Unstructured
	u, ok := m.raw.(*unstructured.Unstructured)
	if ok {
		status, ok = u.Object["status"]
		if ok {
			return // no error
		}
	}
	err = fmt.Errorf("unable to read status")
	return
}

func (m *grafanaMetaAccessor) SetStatus(s any) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("error setting status")
		}
	}()

	f := m.r.FieldByName("Status")
	if f.IsValid() {
		f.Set(reflect.ValueOf(s))
		return
	}

	// Unstructured
	u, ok := m.raw.(*unstructured.Unstructured)
	if ok {
		u.Object["status"] = s
	} else {
		err = fmt.Errorf("unable to read status")
	}
	return
}

func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string {
	// look for Spec.Title or Spec.Name
	spec := m.r.FieldByName("Spec")
	if spec.Kind() == reflect.Struct {
		title := spec.FieldByName("Title")
		if title.IsValid() && title.Kind() == reflect.String {
			return title.String()
		}
		name := spec.FieldByName("Name")
		if name.IsValid() && name.Kind() == reflect.String {
			return name.String()
		}

		// Unstructured uses Object subtype
		object := spec.FieldByName("Object")
		if object.IsValid() && object.Kind() == reflect.Map {
			key := reflect.ValueOf("title")
			value := object.MapIndex(key)
			if value.IsValid() {
				if value.CanInterface() {
					v := value.Interface()
					t, ok := v.(string)
					if ok {
						return t
					}
				}
			}
		}
	}

	obj, ok := m.obj.(*unstructured.Unstructured)
	if ok {
		title, ok, _ := unstructured.NestedString(obj.Object, "spec", "title")
		if ok && title != "" {
			return title
		}
		title, ok, _ = unstructured.NestedString(obj.Object, "spec", "name")
		if ok && title != "" {
			return title
		}
	}

	title := m.r.FieldByName("Title")
	if title.IsValid() && title.Kind() == reflect.String {
		return title.String()
	}
	return defaultTitle
}

func (m *grafanaMetaAccessor) GetManagerProperties() (ManagerProperties, bool) {
	res := ManagerProperties{
		Identity:    "",
		Kind:        ManagerKindUnknown,
		AllowsEdits: false,
		Suspended:   false,
	}

	annot := m.obj.GetAnnotations()

	id, ok := annot[AnnoKeyManagerIdentity]
	if !ok || id == "" {
		// Temporarily support the repo name annotation
		repo := annot[oldAnnoKeyRepoName]
		if repo != "" {
			return ManagerProperties{
				Kind:     ManagerKindRepo,
				Identity: repo,
			}, true
		}

		// If the identity is not set, we should ignore the other annotations and return the default values.
		//
		// This is to prevent inadvertently marking resources as managed,
		// since that can potentially block updates from other sources.
		return res, false
	}
	res.Identity = id

	if v, ok := annot[AnnoKeyManagerKind]; ok {
		res.Kind = ParseManagerKindString(v)
	}

	if v, ok := annot[AnnoKeyManagerAllowsEdits]; ok {
		res.AllowsEdits = v == "true"
	}

	if v, ok := annot[AnnoKeyManagerSuspended]; ok {
		res.Suspended = v == "true"
	}

	return res, true
}

func (m *grafanaMetaAccessor) SetManagerProperties(v ManagerProperties) {
	annot := m.obj.GetAnnotations()
	if annot == nil {
		annot = make(map[string]string, 4)
	}

	if v.Identity != "" {
		annot[AnnoKeyManagerIdentity] = v.Identity
	} else {
		delete(annot, AnnoKeyManagerIdentity)
	}

	if string(v.Kind) != "" {
		annot[AnnoKeyManagerKind] = string(v.Kind)
	} else {
		delete(annot, AnnoKeyManagerKind)
	}

	if v.AllowsEdits {
		annot[AnnoKeyManagerAllowsEdits] = strconv.FormatBool(v.AllowsEdits)
	} else {
		delete(annot, AnnoKeyManagerAllowsEdits)
	}
	if v.Suspended {
		annot[AnnoKeyManagerSuspended] = strconv.FormatBool(v.Suspended)
	} else {
		delete(annot, AnnoKeyManagerSuspended)
	}

	// Clean up old annotation access
	delete(annot, oldAnnoKeyRepoName)

	m.obj.SetAnnotations(annot)
}

func (m *grafanaMetaAccessor) GetSourceProperties() (SourceProperties, bool) {
	var (
		res   SourceProperties
		found bool
	)

	annot := m.obj.GetAnnotations()
	if annot == nil {
		return res, false
	}

	if path, ok := annot[AnnoKeySourcePath]; ok && path != "" {
		res.Path = path
		found = true
	} else if path, ok := annot[oldAnnoKeyRepoPath]; ok && path != "" {
		res.Path = path
		found = true
	}

	if hash, ok := annot[AnnoKeySourceChecksum]; ok && hash != "" {
		res.Checksum = hash
		found = true
	} else if hash, ok := annot[oldAnnoKeyRepoHash]; ok && hash != "" {
		res.Checksum = hash
		found = true
	}

	t, ok := annot[AnnoKeySourceTimestamp]
	if !ok {
		t, ok = annot[oldAnnoKeyRepoTimestamp]
	}
	if ok && t != "" {
		var err error
		res.TimestampMillis, err = strconv.ParseInt(t, 10, 64)
		if err != nil {
			found = true
		}
	}

	return res, found
}

func (m *grafanaMetaAccessor) SetSourceProperties(v SourceProperties) {
	annot := m.obj.GetAnnotations()
	if annot == nil {
		annot = make(map[string]string, 3)
	}

	if v.Path != "" {
		annot[AnnoKeySourcePath] = v.Path
	} else {
		delete(annot, AnnoKeySourcePath)
	}

	if v.Checksum != "" {
		annot[AnnoKeySourceChecksum] = v.Checksum
	} else {
		delete(annot, AnnoKeySourceChecksum)
	}

	if v.TimestampMillis > 0 {
		annot[AnnoKeySourceTimestamp] = strconv.FormatInt(v.TimestampMillis, 10)
	} else {
		delete(annot, AnnoKeySourceTimestamp)
	}

	m.obj.SetAnnotations(annot)
}

// GetSecureValues implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetSecureValues() (vals common.InlineSecureValues, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("error reading secure values")
		}
	}()

	var property any // may be map or struct

	f := m.r.FieldByName("Secure")
	if f.IsValid() {
		property = f.Interface()
	} else {
		// Unstructured
		u, ok := m.raw.(*unstructured.Unstructured)
		if ok {
			property = u.Object["secure"]
		}
	}

	// Not found (and no error)
	if property == nil {
		return nil, nil
	}

	// Try directly casting the property
	vals, ok := property.(common.InlineSecureValues)
	if ok {
		return vals, nil
	}

	// Generic map
	u, ok := property.(map[string]any)
	if ok {
		vals = make(common.InlineSecureValues, len(u))
		for k, v := range u {
			inline, ok := v.(common.InlineSecureValue)
			if !ok {
				sv, ok := v.(map[string]any)
				if !ok {
					return nil, fmt.Errorf("unsupported nested secure value: %t", v)
				}
				inline.Name, _, _ = unstructured.NestedString(sv, "name")
				inline.Remove, _, _ = unstructured.NestedBool(sv, "remove")
				create, _, _ := unstructured.NestedString(sv, "create")
				if create != "" {
					inline.Create = common.NewSecretValue(create)

					if rawDesc, _, _ := unstructured.NestedFieldNoCopy(sv, "description"); rawDesc != nil {
						if desc, ok := rawDesc.(*string); ok && desc != nil && *desc != "" {
							inline.Description = desc
						}
					}
				}
			}
			vals[k] = inline
		}
		return vals, nil
	}

	if f.Kind() == reflect.Struct {
		num := f.NumField()
		vals = make(common.InlineSecureValues, num)
		for i := range num {
			val := f.Field(i)
			if val.IsValid() && val.CanInterface() {
				property = val.Interface()
				inline, ok := property.(common.InlineSecureValue)
				if !ok {
					return nil, fmt.Errorf("secure property must be InlineSecureValue (found: %T)", property)
				}

				if inline.IsZero() {
					continue // nothing
				}

				vals[getJSONFieldName(f, i)] = inline
				continue
			}
			return nil, fmt.Errorf("value not an interface")
		}
		return vals, nil
	}

	return nil, fmt.Errorf("secure value saved in unsupported type: %T", property)
}

// SetSecureValues implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetSecureValues(vals common.InlineSecureValues) (err error) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("ERR: %v", r)
			err = fmt.Errorf("error writing secure values")
		}
	}()

	f := m.r.FieldByName("Secure")
	if f.IsValid() && f.CanSet() {
		if f.Kind() == reflect.Struct {
			keys := make(map[string]bool, len(vals))
			for k := range vals {
				keys[k] = true
			}
			for i := 0; i < f.NumField(); i++ {
				val := f.Field(i)
				if val.IsValid() && val.CanInterface() && val.CanSet() {
					k := getJSONFieldName(f, i)
					sv := vals[k]
					val.Set(reflect.ValueOf(sv))
					delete(keys, k)
				} else {
					return fmt.Errorf("invalid secure value: %v", val)
				}
			}
			if len(keys) > 0 {
				return fmt.Errorf("invalid secure value key: %v", keys)
			}
			return
		}

		// It should be a generic map
		f.Set(reflect.ValueOf(vals))
		return
	}

	// Unstructured object
	u, ok := m.raw.(*unstructured.Unstructured)
	if ok {
		u.Object["secure"] = vals
		return
	}

	return fmt.Errorf("unable to set secure values on (%T)", m.raw)
}

func getJSONFieldName(f reflect.Value, idx int) string {
	field := f.Type().Field(idx)
	fname := field.Tag.Get("json")
	if fname == "" {
		return field.Name
	}
	fname, _ = strings.CutSuffix(fname, ",omitempty")
	return fname
}

func ToObjectReference(obj GrafanaMetaAccessor) common.ObjectReference {
	gvk := obj.GetGroupVersionKind()
	return common.ObjectReference{
		APIGroup:   gvk.Group,
		APIVersion: gvk.Version,
		Kind:       gvk.Kind,
		Namespace:  obj.GetNamespace(),
		Name:       obj.GetName(),
		UID:        obj.GetUID(),
	}
}
