package codec

import (
	"encoding/json"
	"fmt"

	"cosmossdk.io/schema"
)

// HasSchemaCodec is an interface that all codec's should implement in order
// to properly support indexing. It is not required by KeyCodec or ValueCodec
// in order to preserve backwards compatibility, but a future version of collections
// may make it required and all codec's should aim to implement it. If it is not
// implemented, fallback defaults will be used for indexing that may be sub-optimal.
//
// Implementations of HasSchemaCodec should test that they are conformant using
// schema.ValidateObjectKey or schema.ValidateObjectValue depending on whether
// the codec is a KeyCodec or ValueCodec respectively.
type HasSchemaCodec[T any] interface {
	// SchemaCodec returns the schema codec for the collections codec.
	SchemaCodec() (SchemaCodec[T], error)
}

// SchemaCodec is a codec that supports converting collection codec values to and
// from schema codec values.
type SchemaCodec[T any] struct {
	// Fields are the schema fields that the codec represents. If this is empty,
	// it will be assumed that this codec represents no value (such as an item key
	// or key set value).
	Fields []schema.Field

	// ToSchemaType converts a codec value of type T to a value corresponding to
	// a schema object key or value (depending on whether this is a key or value
	// codec). The returned value should pass validation with schema.ValidateObjectKey
	// or schema.ValidateObjectValue with the fields specified in Fields.
	// If this function is nil, it will be assumed that T already represents a
	// value that conforms to a schema value without any further conversion.
	ToSchemaType func(T) (any, error)

	// FromSchemaType converts a schema object key or value to T.
	// If this function is nil, it will be assumed that T already represents a
	// value that conforms to a schema value without any further conversion.
	FromSchemaType func(any) (T, error)
}

// KeySchemaCodec gets the schema codec for the provided KeyCodec either
// by casting to HasSchemaCodec or returning a fallback codec.
func KeySchemaCodec[K any](cdc KeyCodec[K]) (SchemaCodec[K], error) {
	if indexable, ok := cdc.(HasSchemaCodec[K]); ok {
		return indexable.SchemaCodec()
	} else {
		return FallbackSchemaCodec[K](), nil
	}
}

// ValueSchemaCodec gets the schema codec for the provided ValueCodec either
// by casting to HasSchemaCodec or returning a fallback codec.
func ValueSchemaCodec[V any](cdc ValueCodec[V]) (SchemaCodec[V], error) {
	if indexable, ok := cdc.(HasSchemaCodec[V]); ok {
		return indexable.SchemaCodec()
	} else {
		return FallbackSchemaCodec[V](), nil
	}
}

// FallbackSchemaCodec returns a fallback schema codec for T when one isn't explicitly
// specified with HasSchemaCodec. It maps all simple types directly to schema kinds
// and converts everything else to JSON String.
func FallbackSchemaCodec[T any]() SchemaCodec[T] {
	var t T
	kind := schema.KindForGoValue(t)
	if err := kind.Validate(); err == nil {
		return SchemaCodec[T]{
			Fields: []schema.Field{{
				// we don't set any name so that this can be set to a good default by the caller
				Name: "",
				Kind: kind,
			}},
			// these can be nil because T maps directly to a schema value for this kind
			ToSchemaType:   nil,
			FromSchemaType: nil,
		}
	} else {
		// we default to encoding everything to JSON String
		return SchemaCodec[T]{
			Fields: []schema.Field{{Kind: schema.StringKind}},
			ToSchemaType: func(t T) (any, error) {
				bz, err := json.Marshal(t)
				return string(json.RawMessage(bz)), err
			},
			FromSchemaType: func(a any) (T, error) {
				var t T
				sz, ok := a.(string)
				if !ok {
					return t, fmt.Errorf("expected string, got %T", a)
				}
				err := json.Unmarshal([]byte(sz), &t)
				return t, err
			},
		}
	}
}
