package metadata

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"time"

	"github.com/google/uuid"
	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/xkube"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
)

type secureValueDB struct {
	// Kubernetes Metadata
	GUID                     string
	Name                     string
	Namespace                string
	Annotations              string // map[string]string
	Labels                   string // map[string]string
	Created                  int64
	CreatedBy                string
	Updated                  int64
	UpdatedBy                string
	OwnerReferenceAPIGroup   sql.NullString
	OwnerReferenceAPIVersion sql.NullString
	OwnerReferenceKind       sql.NullString
	OwnerReferenceName       sql.NullString

	// Kubernetes Status
	Active  bool
	Version int64

	// Spec
	Description string
	Keeper      sql.NullString
	Decrypters  sql.NullString
	Ref         sql.NullString
	ExternalID  string
}

// toKubernetes maps a DB row into a Kubernetes resource (metadata + spec).
func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) {
	annotations := make(map[string]string, 0)
	if sv.Annotations != "" {
		if err := json.Unmarshal([]byte(sv.Annotations), &annotations); err != nil {
			return nil, fmt.Errorf("failed to unmarshal annotations: %w", err)
		}
	}

	labels := make(map[string]string, 0)
	if sv.Labels != "" {
		if err := json.Unmarshal([]byte(sv.Labels), &labels); err != nil {
			return nil, fmt.Errorf("failed to unmarshal labels: %w", err)
		}
	}

	decrypters := make([]string, 0)

	if sv.Decrypters.Valid && sv.Decrypters.String != "" {
		if err := json.Unmarshal([]byte(sv.Decrypters.String), &decrypters); err != nil {
			return nil, fmt.Errorf("failed to unmarshal decrypters: %w", err)
		}
	}

	resource := &secretv1beta1.SecureValue{
		Spec: secretv1beta1.SecureValueSpec{
			Description: sv.Description,
			Decrypters:  decrypters,
		},
		Status: secretv1beta1.SecureValueStatus{
			ExternalID: sv.ExternalID,
			Version:    sv.Version,
		},
	}

	if sv.Keeper.Valid {
		resource.Status.Keeper = sv.Keeper.String
	}
	if sv.Ref.Valid {
		resource.Spec.Ref = &sv.Ref.String
	}

	// Set all meta fields here for consistency.
	meta, err := utils.MetaAccessor(resource)
	if err != nil {
		return nil, fmt.Errorf("failed to get meta accessor: %w", err)
	}

	updated := time.Unix(sv.Updated, 0).UTC()

	meta.SetUID(types.UID(sv.GUID))
	meta.SetName(sv.Name)
	meta.SetNamespace(sv.Namespace)
	meta.SetAnnotations(annotations)
	meta.SetLabels(labels)
	meta.SetCreatedBy(sv.CreatedBy)
	meta.SetCreationTimestamp(metav1.NewTime(time.Unix(sv.Created, 0).UTC()))
	meta.SetUpdatedBy(sv.UpdatedBy)
	meta.SetUpdatedTimestamp(&updated)
	meta.SetResourceVersionInt64(sv.Updated)

	hasOwnerReference := sv.OwnerReferenceAPIGroup.Valid && sv.OwnerReferenceAPIGroup.String != "" &&
		sv.OwnerReferenceAPIVersion.Valid && sv.OwnerReferenceAPIVersion.String != "" &&
		sv.OwnerReferenceKind.Valid && sv.OwnerReferenceKind.String != "" &&
		sv.OwnerReferenceName.Valid && sv.OwnerReferenceName.String != ""
	if hasOwnerReference {
		meta.SetOwnerReferences([]metav1.OwnerReference{
			{
				APIVersion: schema.GroupVersion{Group: sv.OwnerReferenceAPIGroup.String, Version: sv.OwnerReferenceAPIVersion.String}.String(),
				Kind:       sv.OwnerReferenceKind.String,
				Name:       sv.OwnerReferenceName.String,
			},
		})
	}

	return resource, nil
}

// toCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted.
func toCreateRow(createdAt, updatedAt int64, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) {
	row, err := toRow(keeper, sv, "")
	if err != nil {
		return nil, fmt.Errorf("failed to convert SecureValue to secureValueDB: %w", err)
	}

	row.GUID = uuid.New().String()
	row.Created = createdAt
	row.CreatedBy = actorUID
	row.Updated = updatedAt
	row.UpdatedBy = actorUID

	return row, nil
}

// toRow maps a Kubernetes resource into a DB row.
func toRow(keeper string, sv *secretv1beta1.SecureValue, externalID string) (*secureValueDB, error) {
	var annotations string
	if len(sv.Annotations) > 0 {
		cleanedAnnotations := xkube.CleanAnnotations(sv.Annotations)
		if len(cleanedAnnotations) > 0 {
			sv.Annotations = make(map[string]string) // Safety: reset to prohibit use of sv.Annotations further.

			encodedAnnotations, err := json.Marshal(cleanedAnnotations)
			if err != nil {
				return nil, fmt.Errorf("failed to encode annotations: %w", err)
			}

			annotations = string(encodedAnnotations)
		}
	}

	var labels string
	if len(sv.Labels) > 0 {
		encodedLabels, err := json.Marshal(sv.Labels)
		if err != nil {
			return nil, fmt.Errorf("failed to encode labels: %w", err)
		}

		labels = string(encodedLabels)
	}

	var decrypters *string
	if len(sv.Spec.Decrypters) > 0 {
		encodedDecrypters, err := json.Marshal(sv.Spec.Decrypters)
		if err != nil {
			return nil, fmt.Errorf("failed to encode decrypters: %w", err)
		}

		rawDecrypters := string(encodedDecrypters)
		decrypters = &rawDecrypters
	}

	meta, err := utils.MetaAccessor(sv)
	if err != nil {
		return nil, fmt.Errorf("failed to get meta accessor: %w", err)
	}

	if meta.GetFolder() != "" {
		return nil, fmt.Errorf("folders are not supported")
	}

	updatedTimestamp, err := meta.GetResourceVersionInt64()
	if err != nil {
		return nil, fmt.Errorf("failed to get resource version: %w", err)
	}

	var (
		ownerReferenceAPIGroup   sql.NullString
		ownerReferenceAPIVersion sql.NullString
		ownerReferenceKind       sql.NullString
		ownerReferenceName       sql.NullString
	)

	ownerReferences := meta.GetOwnerReferences()
	if len(ownerReferences) > 1 {
		return nil, fmt.Errorf("only one owner reference is supported, found %d", len(ownerReferences))
	}
	if len(ownerReferences) == 1 {
		ownerReference := ownerReferences[0]

		gv, err := schema.ParseGroupVersion(ownerReference.APIVersion)
		if err != nil {
			return nil, fmt.Errorf("failed to parse owner reference API version %s: %w", ownerReference.APIVersion, err)
		}
		if gv.Group == "" {
			return nil, fmt.Errorf("malformed api version %s requires <group>/<version> format", ownerReference.APIVersion)
		}

		ownerReferenceAPIGroup = toNullString(&gv.Group)
		ownerReferenceAPIVersion = toNullString(&gv.Version)
		ownerReferenceKind = toNullString(&ownerReference.Kind)
		ownerReferenceName = toNullString(&ownerReference.Name)
	}

	return &secureValueDB{
		GUID:                     string(sv.UID),
		Name:                     sv.Name,
		Namespace:                sv.Namespace,
		Annotations:              annotations,
		Labels:                   labels,
		Created:                  meta.GetCreationTimestamp().UnixMilli(),
		CreatedBy:                meta.GetCreatedBy(),
		Updated:                  updatedTimestamp,
		UpdatedBy:                meta.GetUpdatedBy(),
		OwnerReferenceAPIGroup:   ownerReferenceAPIGroup,
		OwnerReferenceAPIVersion: ownerReferenceAPIVersion,
		OwnerReferenceKind:       ownerReferenceKind,
		OwnerReferenceName:       ownerReferenceName,

		Version: sv.Status.Version,

		Description: sv.Spec.Description,
		Keeper:      toNullString(&keeper),
		Decrypters:  toNullString(decrypters),
		Ref:         toNullString(sv.Spec.Ref),
		ExternalID:  externalID,
	}, nil
}

// toNullString returns a sql.NullString struct given a *string
// assumes that "" (empty string) is a valid string
func toNullString(s *string) sql.NullString {
	if s == nil {
		return sql.NullString{
			String: "",
			Valid:  false,
		}
	}

	return sql.NullString{
		String: *s,
		Valid:  true,
	}
}
