package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"time"

	"k8s.io/client-go/tools/clientcmd"

	"github.com/grafana/grafana-app-sdk/k8s"
	"github.com/grafana/grafana-app-sdk/resource"
)

// Schemas are defined here for clarity
var (
	obj1Kind = resource.Kind{
		Schema: resource.NewSimpleSchema("org.example.obj", "v1", &Obj1{}, &resource.UntypedList{}),
		Codecs: map[resource.KindEncoding]resource.Codec{resource.KindEncodingJSON: resource.NewJSONCodec()},
	}
	obj2Kind = resource.Kind{
		Schema: resource.NewSimpleSchema("org.example.obj", "v1", &Obj2{}, &resource.UntypedList{}),
		Codecs: map[resource.KindEncoding]resource.Codec{resource.KindEncodingJSON: resource.NewJSONCodec()},
	}
	def1Kind = resource.Kind{
		Schema: resource.NewSimpleSchema("org.example.def", "v1", &Def1{}, &resource.UntypedList{}),
		Codecs: map[resource.KindEncoding]resource.Codec{resource.KindEncodingJSON: resource.NewJSONCodec()},
	}

	group1 = &group{[]resource.Kind{obj1Kind, obj2Kind}}
	group2 = &group{[]resource.Kind{def1Kind}}

	allSchemas = []resource.Schema{
		obj1Kind,
		obj2Kind,
		def1Kind,
	}
)

type group struct {
	kinds []resource.Kind
}

func (g *group) Kinds() []resource.Kind {
	return g.kinds
}

func main() {
	// We're going to use kubernetes for our storage system, so we need to get a kubernetes config
	// In this example, we load a kube config from a kube config file on-disk at the path specified by the kubecfg flag
	kubeCfgFile := flag.String("kubecfg", "", "kube config path")
	flag.Parse()
	if kubeCfgFile == nil || *kubeCfgFile == "" {
		_, _ = fmt.Println("--kubecfg must be set to the path of your kubernetes config file")
		os.Exit(1)
	}
	kubeConfig, err := clientcmd.BuildConfigFromFlags("", *kubeCfgFile)
	if err != nil {
		panic(err)
	}
	kubeConfig.APIPath = "/apis" // Don't know why this isn't set correctly by default, but it isn't

	// Now, let's make our Manager and ClientRegistry from our kube config.
	// After this, we don't have to think about kubernetes anymore
	clientGenerator := k8s.NewClientRegistry(*kubeConfig, k8s.ClientConfig{})
	manager, err := k8s.NewManager(*kubeConfig)
	if err != nil {
		panic(err)
	}

	// Register all our schemas with the Manager, to ensure they exist in our storage layer
	// This can also be done outside of the application with source-controlled schema
	// (for example, Custom Resource Definitions in kubernetes) files generated by codegen
	for _, schema := range allSchemas {
		ctx, cancel := context.WithTimeout(context.TODO(), time.Second*5)
		defer cancel() //nolint:revive
		err = manager.RegisterSchema(ctx, schema, resource.RegisterSchemaOptions{
			NoErrorOnConflict:   true, // If the schema already exists, don't throw an error, just return
			WaitForAvailability: true, // Wait for the schema to be available in the system (creates can be async), or until the context is canceled
			// We could also call manager.WaitForAvailability, but this just adds a convenience layer
		})
		if err != nil {
			panic(fmt.Errorf("error registering schema '%s': %w", schema.Kind(), err))
		}
	}

	// Do some stuff using resource.Store:
	useStore(clientGenerator)

	// Do some stuff using resource.SimpleStore:
	useSimpleStore(clientGenerator)
}

// useStore creates a resource.Store and manipulates resources using it.
// When using a resource.Store, Schemas must be registered with the store, either by providing a SchemaGroup
// in the resource.NewStore method, or with later resource.Store.Register() calls.
// Once a Schema is registered in the store, it is referenced by its Kind() value in Store calls.
func useStore(generator resource.ClientGenerator) {
	// Store usage for all managed resources
	store := resource.NewStore(generator, group1, group2)

	// Create an example of each of our CRD types
	obj1 := Obj1{}
	obj1.SetNamespace("default")
	obj1.SetName("example-1")
	obj1.Kind = obj1Kind.Kind()
	obj1Added, err := store.Add(context.TODO(), &obj1)
	if err != nil {
		panic(fmt.Errorf("error adding obj1: %s", err))
	}
	logObj("Added Obj1 using Store", obj1Added)

	obj2 := Obj2{}
	obj2.Spec = Obj2Spec{
		Simple1: "foo",
		Simple2: "bar",
	}
	obj2.Namespace = "default"
	obj2.Name = "example-1" // We can use the same name, because it's a different resource kind
	obj2.Kind = obj1Kind.Kind()
	obj2Added, err := store.Add(context.TODO(), &obj2)
	if err != nil {
		panic(err)
	}
	logObj("Added Obj2 using Store", obj2Added)

	statusUpdate := Obj2{}
	statusUpdate.Status.State = "Updated"
	obj2Updated, _ := store.UpdateSubresource(context.TODO(), obj2Kind.Kind(), resource.Identifier{
		Namespace: "default",
		Name:      "example-1",
	}, resource.SubresourceStatus, &statusUpdate)
	logObj("Updated Obj2 using Store", obj2Updated)

	def1 := Def1{}
	def1.Spec.Param1 = "param"
	def1Added, err := store.SimpleAdd(context.TODO(), def1Kind.Kind(), resource.Identifier{
		Namespace: "default",
		Name:      "example-1",
	}, &def1)
	if err != nil {
		panic(err)
	}
	logObj("Added Def1 using Store", def1Added)

	// Delete everything we made so far
	err = store.Delete(context.TODO(), obj1Kind.Kind(), resource.Identifier{
		Namespace: "default",
		Name:      "example-1",
	})
	if err != nil {
		panic(err)
	}
	err = store.Delete(context.TODO(), obj2Kind.Kind(), resource.Identifier{
		Namespace: "default",
		Name:      "example-1",
	})
	if err != nil {
		panic(err)
	}
	err = store.Delete(context.TODO(), def1Kind.Kind(), resource.Identifier{
		Namespace: "default",
		Name:      "example-1",
	})
	if err != nil {
		panic(err)
	}
}

// useSimpleStore uses a resource.SimpleStore to manipulate a resources for a single Schema.
// A resource.SimpleStore is created for one specific Schema, and has a type parameter of the Schema.ZeroValue()'s
// Object's Spec. This is intended to make it easier to work directly with the Spec objects themselves,
// rather than dealing with the extra associated data and doing type conversions.
// The trade-off is that the SimpleStore can only be tied to one specific Schema.
func useSimpleStore(generator resource.ClientGenerator) {
	// SimpleStore can only manipulate one Custom Resource type, but allows for direct manipulation of the Spec object
	simpleStore, _ := resource.NewSimpleStore[Obj2Spec](obj2Kind, generator) //nolint:staticcheck
	added, err := simpleStore.Add(context.TODO(), resource.Identifier{
		Namespace: "default",
		Name:      "example-2",
	}, Obj2Spec{})
	if err != nil {
		panic(err)
	}
	logObj("Added using SimpleStore", added)
	added.Spec.Simple2 = "updated value"
	updated, err := simpleStore.Update(context.TODO(), resource.Identifier{
		Namespace: "default",
		Name:      "example-2",
	}, added.Spec)
	if err != nil {
		panic(err)
	}
	logObj("Updated using SimpleStore", updated)
	// Update the status subresource
	_, _ = simpleStore.UpdateSubresource(context.TODO(), resource.Identifier{
		Namespace: "default",
		Name:      "example-2",
	}, resource.SubresourceStatus, Obj2Status{
		State: "foo",
	})
	_ = simpleStore.Delete(context.TODO(), resource.Identifier{
		Namespace: "default",
		Name:      "example-2",
	})
}

func logObj(msg string, obj any) {
	j, _ := json.Marshal(obj)
	log.Printf("\u001B[1;32m%s:\u001B[0m %s", msg, string(j))
}

/*
 * OBJECT STRUCTS
 * Typically, these are generated. Here, to keep this example self-contained, we define them
 * TODO: Maybe generate these and commit generated code?
 * TODO: Will need codegen to work with subresources
 *
 */

type Obj1 struct {
	resource.TypedSpecObject[Obj1Spec]
}

func (o1 *Obj1) Copy() resource.Object {
	return resource.CopyObject(o1)
}

type Obj1Spec struct {
	Field1 string `json:"field1"`
	Field2 int    `json:"field2"`
}

type Obj2 struct {
	resource.TypedSpecStatusObject[Obj2Spec, Obj2Status]
}

func (o2 *Obj2) Copy() resource.Object {
	return resource.CopyObject(o2)
}

type Obj2Spec struct {
	Simple1 string `json:"simple1"`
	Simple2 string `json:"simple2"`
}

type Obj2Status struct {
	State string `json:"state"`
}

type Def1 struct {
	resource.TypedSpecObject[Def1Spec]
}

func (d *Def1) Copy() resource.Object {
	return resource.CopyObject(d)
}

type Def1Spec struct {
	Param1 string `json:"param1"`
	Param2 bool   `json:"param2"`
}
