// Package metadata provides an API that allows plugins to add metadata to the context.
// Each metadata is stored under a label that has the form <plugin>/<name>. Each metadata
// is returned as a Func. When Func is called the metadata is returned. If Func is expensive to
// execute it is its responsibility to provide some form of caching. During the handling of a
// query it is expected the metadata stays constant.
//
// Basic example:
//
// Implement the Provider interface for a plugin p:
//
//	   func (p P) Metadata(ctx context.Context, state request.Request) context.Context {
//	      metadata.SetValueFunc(ctx, "test/something", func() string { return "myvalue" })
//		 return ctx
//	   }
//
// Basic example with caching:
//
//	   func (p P) Metadata(ctx context.Context, state request.Request) context.Context {
//	      cached := ""
//	      f := func() string {
//			if cached != "" {
//	                return cached
//	            }
//	            cached = expensiveFunc()
//	            return cached
//	      }
//	      metadata.SetValueFunc(ctx, "test/something", f)
//		 return ctx
//	   }
//
// If you need access to this metadata from another plugin:
//
//	// ...
//	valueFunc := metadata.ValueFunc(ctx, "test/something")
//	value := valueFunc()
//	// use 'value'
package metadata

import (
	"context"
	"strings"

	"github.com/coredns/coredns/request"
)

// Provider interface needs to be implemented by each plugin willing to provide
// metadata information for other plugins.
type Provider interface {
	// Metadata adds metadata to the context and returns a (potentially) new context.
	// Note: this method should work quickly, because it is called for every request
	// from the metadata plugin.
	Metadata(ctx context.Context, state request.Request) context.Context
}

// Func is the type of function in the metadata, when called they return the value of the label.
type Func func() string

// IsLabel checks that the provided name is a valid label name, i.e. two or more words separated by a slash.
func IsLabel(label string) bool {
	p := strings.Index(label, "/")
	if p <= 0 || p >= len(label)-1 {
		// cannot accept namespace empty nor label empty
		return false
	}
	return true
}

// Labels returns all metadata keys stored in the context. These label names should be named
// as: plugin/NAME, where NAME is something descriptive.
func Labels(ctx context.Context) []string {
	if metadata := ctx.Value(key{}); metadata != nil {
		if m, ok := metadata.(md); ok {
			return keys(m)
		}
	}
	return nil
}

// ValueFuncs returns the map[string]Func from the context, or nil if it does not exist.
func ValueFuncs(ctx context.Context) map[string]Func {
	if metadata := ctx.Value(key{}); metadata != nil {
		if m, ok := metadata.(md); ok {
			return m
		}
	}
	return nil
}

// ValueFunc returns the value function of label. If none can be found nil is returned. Calling the
// function returns the value of the label.
func ValueFunc(ctx context.Context, label string) Func {
	if metadata := ctx.Value(key{}); metadata != nil {
		if m, ok := metadata.(md); ok {
			return m[label]
		}
	}
	return nil
}

// SetValueFunc set the metadata label to the value function. If no metadata can be found this is a noop and
// false is returned. Any existing value is overwritten.
func SetValueFunc(ctx context.Context, label string, f Func) bool {
	if metadata := ctx.Value(key{}); metadata != nil {
		if m, ok := metadata.(md); ok {
			m[label] = f
			return true
		}
	}
	return false
}

// md is metadata information storage.
type md map[string]Func

// key defines the type of key that is used to save metadata into the context.
type key struct{}

func keys(m map[string]Func) []string {
	s := make([]string, len(m))
	i := 0
	for k := range m {
		s[i] = k
		i++
	}
	return s
}
