package resources

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer/yaml"

	"github.com/grafana/grafana-app-sdk/logging"
	dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository"
)

var (
	ErrUnableToReadResourceBytes        = errors.New("unable to read bytes as a resource")
	ErrUnableToReadPanelsMissing        = errors.New("panels property is required")
	ErrUnableToReadSchemaVersionMissing = errors.New("schemaVersion property is required")
	ErrUnableToReadTagsMissing          = errors.New("tags property is required")
	ErrClassicResourceIsAlreadyK8sForm  = errors.New("classic resource is already structured with apiVersion and kind")
)

// This reads a "classic" file format and will convert it to an unstructured k8s resource
// The file path may determine how the resource is parsed
//
// The context and logger are both only used for logging purposes. They do not control any logic.
func ReadClassicResource(ctx context.Context, info *repository.FileInfo) (*unstructured.Unstructured, *schema.GroupVersionKind, provisioning.ClassicFileType, error) {
	var value map[string]any

	// Try parsing as JSON
	if info.Data[0] == '{' {
		err := json.Unmarshal(info.Data, &value)
		if err != nil {
			return nil, nil, "", err
		}
	} else {
		return nil, nil, "", fmt.Errorf("unable to read file")
	}

	// regular version headers exist
	// TODO: do we intend on this checking Kind or kind? document reasoning.
	if value["apiVersion"] != nil {
		if value["kind"] != nil {
			return nil, nil, "", ErrClassicResourceIsAlreadyK8sForm
		}

		logging.FromContext(ctx).Debug("TODO... likely a provisioning",
			"apiVersion", value["apiVersion"],
			"kind", value["Kind"])
		gv, err := schema.ParseGroupVersion(value["apiVersion"].(string))
		if err != nil {
			return nil, nil, "", fmt.Errorf("invalid apiVersion")
		}
		gvk := gv.WithKind(value["Kind"].(string))
		return &unstructured.Unstructured{Object: value}, &gvk, "", nil
	}

	// If this is a dashboard, convert it
	if value["panels"] != nil &&
		value["schemaVersion"] != nil &&
		value["tags"] != nil {
		gvk := &schema.GroupVersionKind{
			Group:   dashboard.GROUP,
			Version: "v0alpha1", // no schema
			Kind:    "Dashboard"}
		return &unstructured.Unstructured{
			Object: map[string]interface{}{
				"apiVersion": gvk.GroupVersion().String(),
				"kind":       gvk.Kind,
				"metadata": map[string]any{
					"name": value["uid"],
				},
				"spec": value,
			},
		}, gvk, provisioning.ClassicDashboard, nil
	}

	return nil, nil, "", ErrUnableToReadResourceBytes
}

// DecodeYAMLObject reads the input as YAML and outputs its Kubernetes resource, if it is one.
// Note that all JSON is also valid YAML, so this can also be used for JSON data.
func DecodeYAMLObject(input io.Reader) (*unstructured.Unstructured, *schema.GroupVersionKind, error) {
	data, err := io.ReadAll(input)
	if err != nil {
		return nil, nil, err
	}

	obj, gvk, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).
		Decode(data, nil, nil)
	if err != nil {
		return nil, gvk, err
	}

	// The decoder should put it directly into an unstructured object
	val, ok := obj.(*unstructured.Unstructured)
	if ok {
		return val, gvk, err
	}

	unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
	if err != nil {
		return nil, gvk, err
	}
	return &unstructured.Unstructured{Object: unstructuredMap}, gvk, err
}
