package cuekind

import (
	"encoding/json"
	"path/filepath"
	"strings"

	"github.com/grafana/codejen"
	"sigs.k8s.io/yaml"

	"github.com/grafana/grafana-app-sdk/codegen"
	"github.com/grafana/grafana-app-sdk/codegen/jennies"
	"github.com/grafana/grafana-app-sdk/codegen/templates"
	"github.com/grafana/grafana-app-sdk/k8s/apiserver"
)

// CRDGenerator returns a Generator which will create a CRD file
func CRDGenerator(outputEncoder jennies.CRDOutputEncoder, outputExtension string) *codejen.JennyList[codegen.AppManifest] {
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(jennies.CRDGenerator(outputEncoder, outputExtension))
	return g
}

// ResourceGenerator returns a collection of jennies which generate backend resource code from kinds.
// The `versioned` parameter governs whether to generate all versions where codegen.backend == true,
// or just generate code for the current version.
// If `groupKinds` is true, kinds within the same group will exist in the same package.
// When combined with `versioned`, each version package will contain all kinds in the group
// which have a schema for that version.
func ResourceGenerator(projectRepo, generatedAPIPath string, groupKinds bool) *codejen.JennyList[codegen.AppManifest] {
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(
		&jennies.GoTypes{
			Depth:                1,
			AddKubernetesCodegen: true,
			GroupByKind:          !groupKinds,
			AnyAsInterface:       true,                 // This is for compatibility with kube openAPI generator, which has issues with map[string]any
			ExcludeFields:        []string{"metadata"}, // We don't want an object generated for the metadata, as we use the k8s metadata objects
			OpenAPINamer: func(info jennies.OpenAPINamerInfo) string {
				path := filepath.Join(projectRepo, generatedAPIPath, jennies.GetGeneratedGoTypePath(!groupKinds, info.ShortGroup, info.Version, strings.ToLower(info.Kind)), info.TypeName)
				return apiserver.ToOpenAPIName(path)
			},
		},
		&jennies.ResourceObjectGenerator{
			SubresourceTypesArePrefixed: groupKinds,
			GroupByKind:                 !groupKinds,
			OpenAPINamer: func(info jennies.OpenAPINamerInfo) string {
				path := filepath.Join(projectRepo, generatedAPIPath, jennies.GetGeneratedGoTypePath(!groupKinds, info.ShortGroup, info.Version, strings.ToLower(info.Kind)), info.TypeName)
				return apiserver.ToOpenAPIName(path)
			},
		},
		&jennies.SchemaGenerator{
			GroupByKind: !groupKinds,
		},
		&jennies.CodecGenerator{
			GroupByKind: !groupKinds,
		},
		&jennies.Constants{
			GroupByKind: !groupKinds,
		},
	)
	return g
}

// BackendPluginGenerator returns a Generator which will produce boilerplate backend plugin code
func BackendPluginGenerator(projectRepo, generatedAPIPath string, groupKinds bool) *codejen.JennyList[codegen.AppManifest] {
	pluginSecurePkgFiles, _ := templates.GetBackendPluginSecurePackageFiles()

	g := codejen.JennyListWithNamer(namerFuncManifest)
	g.Append(
		jennies.RouterHandlerCodeGenerator(projectRepo, generatedAPIPath, !groupKinds),
		jennies.StaticManyToOneGenerator[codegen.AppManifest](codejen.File{
			RelativePath: "plugin/secure/data.go",
			Data:         pluginSecurePkgFiles["data.go"],
		}),
		jennies.StaticManyToOneGenerator[codegen.AppManifest](codejen.File{
			RelativePath: "plugin/secure/middleware.go",
			Data:         pluginSecurePkgFiles["middleware.go"],
		}),
		jennies.StaticManyToOneGenerator[codegen.AppManifest](codejen.File{
			RelativePath: "plugin/secure/retriever.go",
			Data:         pluginSecurePkgFiles["retriever.go"],
		}),
		jennies.RouterCodeGenerator(projectRepo),
		jennies.BackendPluginMainGenerator(projectRepo, generatedAPIPath, !groupKinds),
	)
	return g
}

// TypeScriptResourceGenerator returns a Generator which generates TypeScript resource code.
func TypeScriptResourceGenerator() *codejen.JennyList[codegen.AppManifest] {
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(&jennies.TypeScriptTypes{
		Depth: 1,
	}, &jennies.TypeScriptResourceTypes{})
	return g
}

// OperatorGenerator returns a Generator which will build out watcher boilerplate for each resource,
// and a main func to run an operator for the watchers.
func OperatorGenerator(projectRepo, codegenPath string, groupKinds bool) *codejen.JennyList[codegen.AppManifest] {
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(
		&jennies.OperatorKubeConfigJenny{},
		jennies.OperatorMainJenny(projectRepo, codegenPath, !groupKinds),
		&jennies.OperatorConfigJenny{},
	)
	return g
}

func AppGenerator(projectRepo, codegenPath string, manifestGoFilePath string, groupKinds bool) *codejen.JennyList[codegen.AppManifest] {
	parts := strings.Split(projectRepo, "/")
	if len(parts) == 0 {
		parts = []string{""}
	}
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(
		jennies.WatcherJenny(projectRepo, codegenPath, !groupKinds),
		&jennies.AppGenerator{
			GroupByKind:         !groupKinds,
			ProjectRepo:         projectRepo,
			ProjectName:         parts[len(parts)-1],
			CodegenPath:         codegenPath,
			ManifestPackagePath: manifestGoFilePath,
		},
	)
	return g
}

func PostResourceGenerationGenerator(projectRepo, goGenPath string, groupKinds bool) *codejen.JennyList[codegen.AppManifest] {
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(&jennies.OpenAPI{
		GoModName:   projectRepo,
		GoGenPath:   goGenPath,
		GroupByKind: !groupKinds,
	})
	return g
}

func ManifestGenerator(extension string, includeSchemas bool, version string) *codejen.JennyList[codegen.AppManifest] {
	generator := &jennies.ManifestGenerator{
		Encoder:         jsonEncoder,
		FileExtension:   extension,
		IncludeSchemas:  includeSchemas,
		ManifestVersion: version,
	}

	switch extension {
	case "json":
		generator.Encoder = jsonEncoder
	case "yaml":
		generator.Encoder = yaml.Marshal
	default:
		// defaults to json
	}

	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(generator)
	return g
}

// ManifestGoGeneratorConfig configures the ManifestGoGenerator JennyList.
type ManifestGoGeneratorConfig struct {
	Package            string
	IncludeSchemas     bool
	ProjectRepo        string
	GoGenPath          string
	ManifestGoFilePath string
	GroupKinds         bool
	// SkipImportsProcess skips the imports.Process calls in the generated code.
	// This is useful in tests where import resolution is unnecessary and slow.
	SkipImportsProcess bool
}

func ManifestGoGenerator(cfg ManifestGoGeneratorConfig) *codejen.JennyList[codegen.AppManifest] {
	g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
	g.Append(&jennies.ManifestGoGenerator{
		Package:            cfg.Package,
		IncludeSchemas:     cfg.IncludeSchemas,
		ProjectRepo:        cfg.ProjectRepo,
		CodegenPath:        cfg.GoGenPath,
		GroupByKind:        !cfg.GroupKinds,
		DestinationPath:    cfg.ManifestGoFilePath,
		SkipImportsProcess: cfg.SkipImportsProcess,
	},
		&jennies.CustomRouteGoTypesJenny{
			AddKubernetesCodegen: true,
			GroupByKind:          !cfg.GroupKinds,
			AnyAsInterface:       true, // This is for compatibility with kube openAPI generator, which has issues with map[string]any
			SkipImportsProcess:   cfg.SkipImportsProcess,
			OpenAPINamer: func(info jennies.OpenAPINamerInfo) string {
				path := filepath.Join(cfg.ProjectRepo, cfg.GoGenPath, jennies.GetGeneratedGoTypePath(!cfg.GroupKinds, info.ShortGroup, info.Version, strings.ToLower(info.Kind)), info.TypeName)
				return apiserver.ToOpenAPIName(path)
			},
		},
		&jennies.ClientJenny{
			GroupByKind:        !cfg.GroupKinds,
			SkipImportsProcess: cfg.SkipImportsProcess,
		})
	return g
}

func namerFuncManifest(m codegen.AppManifest) string {
	if m == nil {
		return "nil"
	}
	return m.Name()
}

var jsonEncoder = func(v any) ([]byte, error) {
	return json.MarshalIndent(v, "", "    ")
}
