package resource

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"github.com/grafana/grafana-app-sdk/app"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)

// Convert raw resource bytes into an IndexableDocument
type DocumentBuilder interface {
	// Convert raw bytes into an document that can be written
	BuildDocument(ctx context.Context, key *resourcepb.ResourceKey, rv int64, value []byte) (*IndexableDocument, error)
}

// Registry of the searchable document fields
type SearchableDocumentFields interface {
	Fields() []string
	Field(name string) *resourcepb.ResourceTableColumnDefinition
}

// Some kinds will require special processing for their namespace
type NamespacedDocumentSupplier = func(ctx context.Context, namespace string, blob BlobSupport) (DocumentBuilder, error)

// Register how documents can be built for a resource
type DocumentBuilderInfo struct {
	// The target resource (empty will be used to match anything)
	GroupResource schema.GroupResource

	// Defines the searchable fields
	// NOTE: this does not include the root/common fields, only values specific to the the builder
	Fields SearchableDocumentFields

	// simple/static builders that do not depend on the environment can be declared once
	Builder DocumentBuilder

	// Complicated builders (eg dashboards!) will be declared dynamically and managed by the ResourceServer
	Namespaced NamespacedDocumentSupplier
}

type DocumentBuilderSupplier interface {
	GetDocumentBuilders() ([]DocumentBuilderInfo, error)
}

// IndexableDocument can be written to a ResourceIndex
// Although public, this is *NOT* an end user interface
type IndexableDocument struct {
	// The resource key
	Key *resourcepb.ResourceKey `json:"key"`

	// The k8s name
	Name string `json:"name,omitempty"`

	// Resource version for the resource (if known)
	RV int64 `json:"rv,omitempty"`

	// The generic display name
	Title string `json:"title,omitempty"`

	// internal field for searching title with ngrams
	TitleNgram string `json:"title_ngram,omitempty"`

	// internal sort field for title ( don't set this directly )
	TitlePhrase string `json:"title_phrase,omitempty"`

	// A generic description -- helpful in global search
	Description string `json:"description,omitempty"`

	// Like dashboard tags
	Tags []string `json:"tags,omitempty"`

	// Generic metadata labels
	Labels map[string]string `json:"labels,omitempty"`

	// The folder (K8s name)
	Folder string `json:"folder,omitempty"`

	// The first time this resource was saved
	Created int64 `json:"created,omitempty"`

	// Who created the resource (will be in the form `user:uid`)
	CreatedBy string `json:"createdBy,omitempty"`

	// The last time a user updated the spec
	Updated int64 `json:"updated,omitempty"`

	// Who updated the resource (will be in the form `user:uid`)
	UpdatedBy string `json:"updatedBy,omitempty"`

	// Searchable nested keys
	// The key should exist from the fields defined in DocumentBuilderInfo
	// This should not contain duplicate information from the results above
	// The meaning of these fields changes depending on the field type
	// These values typically come from the Spec, but may also come from status
	// metadata, annotations, or external data linked at index time
	Fields map[string]any `json:"fields,omitempty"`

	// Automatically indexed selectable fields, used for field-based filtering when listing.
	SelectableFields map[string]string `json:"selectable_fields,omitempty"`

	// Maintain a list of resource references.
	// Someday this will likely be part of https://github.com/grafana/gamma
	References ResourceReferences `json:"references,omitempty"`

	// internal field for mapping references to kind ( don't set this directly )
	Reference map[string][]string `json:"reference,omitempty"` // map of kind to list of names

	// When the resource is managed by an upstream repository
	Manager *utils.ManagerProperties `json:"manager,omitempty"`

	// indexed only field for faceting manager info
	ManagedBy string `json:"managedBy,omitempty"`

	// When the manager knows about file paths
	Source *utils.SourceProperties `json:"source,omitempty"`
}

func (m *IndexableDocument) UpdateCopyFields() *IndexableDocument {
	m.TitleNgram = m.Title
	m.TitlePhrase = strings.ToLower(m.Title) // Lowercase for case-insensitive sorting ?? in the analyzer?
	if m.Manager != nil {
		m.ManagedBy = fmt.Sprintf("%s:%s", m.Manager.Kind, m.Manager.Identity)
	}

	m.Reference = make(map[string][]string)
	for _, ref := range m.References {
		// Group and Version are ignored for now. This could be revisited.
		m.Reference[ref.Kind] = append(m.Reference[ref.Kind], ref.Name)
	}
	return m
}

func (m *IndexableDocument) Type() string {
	return m.Key.Resource
}

type ResourceReference struct {
	Relation string `json:"relation"`          // eg: depends-on
	Group    string `json:"group,omitempty"`   // the api group
	Version  string `json:"version,omitempty"` // the api version
	Kind     string `json:"kind,omitempty"`    // panel, data source (for now)
	Name     string `json:"name"`              // the UID / panel name
}

func (m ResourceReference) String() string {
	var sb strings.Builder
	sb.WriteString(m.Relation)
	sb.WriteString(">>")
	sb.WriteString(m.Group)
	if m.Version != "" {
		sb.WriteString("/")
		sb.WriteString(m.Version)
	}
	if m.Kind != "" {
		sb.WriteString("/")
		sb.WriteString(m.Kind)
	}
	sb.WriteString("/")
	sb.WriteString(m.Name)
	return sb.String()
}

// Sortable list of references
type ResourceReferences []ResourceReference

func (m ResourceReferences) Len() int      { return len(m) }
func (m ResourceReferences) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m ResourceReferences) Less(i, j int) bool {
	a := m[i].String()
	b := m[j].String()
	return strings.Compare(a, b) > 0
}

// Create a new indexable document based on a generic k8s resource
func NewIndexableDocument(key *resourcepb.ResourceKey, rv int64, obj utils.GrafanaMetaAccessor, selectableFields map[string]string) *IndexableDocument {
	title := obj.FindTitle(key.Name)
	if title == key.Name {
		// TODO: something wrong with FindTitle
		spec, err := obj.GetSpec()
		if err == nil {
			specValue, ok := spec.(map[string]any)
			if ok {
				specTitle, ok := specValue["title"].(string)
				if ok {
					title = specTitle
				}
			}
		}
	}
	doc := &IndexableDocument{
		Key:              key,
		RV:               rv,
		Name:             key.Name,
		Title:            title, // We always want *something* to display
		Labels:           obj.GetLabels(),
		Folder:           obj.GetFolder(),
		CreatedBy:        obj.GetCreatedBy(),
		UpdatedBy:        obj.GetUpdatedBy(),
		SelectableFields: selectableFields,
	}
	m, ok := obj.GetManagerProperties()
	if ok {
		doc.Manager = &m
		doc.ManagedBy = fmt.Sprintf("%s:%s", m.Kind, m.Identity)
	}
	s, ok := obj.GetSourceProperties()
	if ok {
		doc.Source = &s
	}
	ts := obj.GetCreationTimestamp()
	if !ts.Time.IsZero() {
		doc.Created = ts.UnixMilli()
	}
	tt, err := obj.GetUpdatedTimestamp()
	if err != nil && tt != nil {
		doc.Updated = tt.UnixMilli()
	}
	return doc.UpdateCopyFields()
}

func StandardDocumentBuilder(manifests []app.Manifest) DocumentBuilder {
	return &standardDocumentBuilder{selectableFields: SelectableFieldsForManifests(manifests)}
}

type standardDocumentBuilder struct {
	// Maps "group/resource" (in lowercase) to list of selectable fields.
	selectableFields map[string][]string
}

func (s *standardDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb.ResourceKey, rv int64, value []byte) (*IndexableDocument, error) {
	tmp := &unstructured.Unstructured{}
	err := tmp.UnmarshalJSON(value)
	if err != nil {
		return nil, err
	}

	obj, err := utils.MetaAccessor(tmp)
	if err != nil {
		return nil, err
	}

	sfKey := strings.ToLower(key.GetGroup() + "/" + key.GetResource())
	selectableFields := buildSelectableFields(tmp, s.selectableFields[sfKey])

	doc := NewIndexableDocument(key, rv, obj, selectableFields)
	return doc, nil
}

func buildSelectableFields(tmp *unstructured.Unstructured, fields []string) map[string]string {
	result := map[string]string{}

	for _, field := range fields {
		path := strings.Split(field, ".")
		val, ok, err := unstructured.NestedFieldNoCopy(tmp.Object, path...)
		if err != nil || !ok {
			continue
		}

		switch v := val.(type) {
		case string:
			result[field] = v
		case bool:
			result[field] = strconv.FormatBool(v)
		case int, float64:
			result[field] = fmt.Sprintf("%v", v)
		}
	}

	return result
}

type searchableDocumentFields struct {
	names  []string
	fields map[string]*resourceTableColumn
}

// This requires unique names
func NewSearchableDocumentFields(columns []*resourcepb.ResourceTableColumnDefinition) (SearchableDocumentFields, error) {
	f := &searchableDocumentFields{
		names:  make([]string, len(columns)),
		fields: make(map[string]*resourceTableColumn),
	}
	for i, c := range columns {
		if f.fields[c.Name] != nil {
			return nil, fmt.Errorf("duplicate name")
		}
		col, err := newResourceTableColumn(c, i)
		if err != nil {
			return nil, err
		}
		f.names[i] = c.Name
		f.fields[c.Name] = col
	}
	return f, nil
}

func (x *searchableDocumentFields) Fields() []string {
	return x.names
}

func (x *searchableDocumentFields) Field(name string) *resourcepb.ResourceTableColumnDefinition {
	name = strings.TrimPrefix(name, SEARCH_FIELD_PREFIX)

	f, ok := x.fields[name]
	if ok {
		return f.def
	}
	return nil
}

const SEARCH_FIELD_PREFIX = "fields."
const SEARCH_FIELD_ID = "_id" // {namespace}/{group}/{resource}/{name}
const SEARCH_FIELD_LEGACY_ID = utils.LabelKeyDeprecatedInternalID
const SEARCH_FIELD_KIND = "kind"         // resource ( for federated index filtering )
const SEARCH_FIELD_GROUP_RESOURCE = "gr" // group/resource
const SEARCH_FIELD_NAMESPACE = "namespace"
const SEARCH_FIELD_NAME = "name"
const SEARCH_FIELD_RV = "rv"
const SEARCH_FIELD_TITLE = "title"
const SEARCH_FIELD_TITLE_NGRAM = "title_ngram"
const SEARCH_FIELD_TITLE_PHRASE = "title_phrase" // filtering/sorting on title by full phrase
const SEARCH_FIELD_DESCRIPTION = "description"
const SEARCH_FIELD_TAGS = "tags"
const SEARCH_FIELD_LABELS = "labels" // All labels, not a specific one

const SEARCH_FIELD_FOLDER = "folder"
const SEARCH_FIELD_CREATED = "created"
const SEARCH_FIELD_CREATED_BY = "createdBy"
const SEARCH_FIELD_UPDATED = "updated"
const SEARCH_FIELD_UPDATED_BY = "updatedBy"

const SEARCH_FIELD_MANAGED_BY = "managedBy" // {kind}:{id}
const SEARCH_FIELD_MANAGER_KIND = "manager.kind"
const SEARCH_FIELD_MANAGER_ID = "manager.id"
const SEARCH_FIELD_SOURCE_PATH = "source.path"
const SEARCH_FIELD_SOURCE_CHECKSUM = "source.checksum"
const SEARCH_FIELD_SOURCE_TIME = "source.timestampMillis"

const SEARCH_FIELD_SCORE = "_score"     // the match score
const SEARCH_FIELD_EXPLAIN = "_explain" // score explanation as JSON object

var standardSearchFieldsInit sync.Once
var standardSearchFields SearchableDocumentFields

func StandardSearchFields() SearchableDocumentFields {
	standardSearchFieldsInit.Do(func() {
		var err error
		standardSearchFields, err = NewSearchableDocumentFields([]*resourcepb.ResourceTableColumnDefinition{
			{
				Name:        SEARCH_FIELD_ID,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "Unique Identifier. {namespace}/{group}/{resource}/{name}",
				Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
					NotNull: true,
				},
			},
			{
				Name:        SEARCH_FIELD_GROUP_RESOURCE,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "The resource kind: {group}/{resource}",
				Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
					NotNull: true,
				},
			},
			{
				Name:        SEARCH_FIELD_NAMESPACE,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "Tenant isolation",
				Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
					NotNull: true,
				},
			},
			{
				Name:        SEARCH_FIELD_NAME,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "Kubernetes name.  Unique identifier within a namespace+group+resource",
				Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
					NotNull: true,
				},
			},
			{
				Name:        SEARCH_FIELD_TITLE,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "Display name for the resource",
			},
			{
				Name:        SEARCH_FIELD_DESCRIPTION,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "An account of the resource.",
				Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
					FreeText: true,
				},
			},
			{
				Name:        SEARCH_FIELD_TAGS,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				IsArray:     true,
				Description: "Unique tags",
				Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
					Filterable: true,
				},
			},
			{
				Name:        SEARCH_FIELD_FOLDER,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "Kubernetes name for the folder",
			},
			{
				Name:        SEARCH_FIELD_RV,
				Type:        resourcepb.ResourceTableColumnDefinition_INT64,
				Description: "resource version",
			},
			{
				Name:        SEARCH_FIELD_CREATED,
				Type:        resourcepb.ResourceTableColumnDefinition_INT64,
				Description: "created timestamp", // date?
			},
			{
				Name:        SEARCH_FIELD_EXPLAIN,
				Type:        resourcepb.ResourceTableColumnDefinition_OBJECT,
				Description: "Explain why this result matches (depends on the engine)",
			},
			{
				Name:        SEARCH_FIELD_SCORE,
				Type:        resourcepb.ResourceTableColumnDefinition_DOUBLE,
				Description: "The search score",
			},
			{
				Name:        SEARCH_FIELD_LEGACY_ID,
				Type:        resourcepb.ResourceTableColumnDefinition_INT64,
				Description: "Deprecated legacy id of the resource",
			},
			{
				Name:        SEARCH_FIELD_MANAGER_KIND,
				Type:        resourcepb.ResourceTableColumnDefinition_STRING,
				Description: "Type of manager, which is responsible for managing the resource",
			},
			// TODO: below fields only need to be returned from search, but do not need to be searchable
			{
				Name: SEARCH_FIELD_MANAGER_ID,
				Type: resourcepb.ResourceTableColumnDefinition_STRING,
			},
			{
				Name: SEARCH_FIELD_SOURCE_TIME,
				Type: resourcepb.ResourceTableColumnDefinition_INT64,
			},
			{
				Name: SEARCH_FIELD_SOURCE_PATH,
				Type: resourcepb.ResourceTableColumnDefinition_STRING,
			},
			{
				Name: SEARCH_FIELD_SOURCE_CHECKSUM,
				Type: resourcepb.ResourceTableColumnDefinition_STRING,
			},
		})

		if err != nil {
			panic("failed to initialize standard search fields")
		}
	})
	return standardSearchFields
}

// // Helper function to convert everything except the "Fields" property to values
// // NOTE: this is really to help testing things absent real backend index
// func IndexableDocumentStandardFields(doc *IndexableDocument) map[string]any {
// 	fields := make(map[string]any)

// 	// These should always exist
// 	fields[SEARCH_FIELD_ID] = doc.Key.SearchID()
// 	fields[SEARCH_FIELD_NAMESPACE] = doc.Key.Namespace
// 	fields[SEARCH_FIELD_NAME] = doc.Key.Name
// 	fields[SEARCH_FIELD_GROUP_RESOURCE] = fmt.Sprintf("%s/%s", doc.Key.Group, doc.Key.Resource)

// 	fields[SEARCH_FIELD_TITLE] = doc.Title

// 	return fields
// }
