package jennies

import (
	"bytes"
	"fmt"
	"go/format"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/grafana/codejen"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/gengo/v2"
	"k8s.io/gengo/v2/generator"
	"k8s.io/kube-openapi/cmd/openapi-gen/args"
	"k8s.io/kube-openapi/pkg/generators"

	"github.com/grafana/grafana-app-sdk/codegen"
)

// OpenAPI jenny uses kube-openapi to build OpenAPI spec.
// Because k8s.io/gengo doesn't allow you to use in-memory files (as the Overlay in the config is not accessible),
// this jenny must be run _after_ the go type has been written to disk.
type OpenAPI struct {
	GoModName string
	GoGenPath string

	// GroupByKind determines whether kinds are grouped by GroupVersionKind or just GroupVersion.
	// If GroupByKind is true, generated paths are <kind>/<version>/<file>, instead of the default <version>/<file>.
	// When GroupByKind is false, only one generated OpenAPI file will exist for the entire GroupVersion.
	GroupByKind bool
}

func (*OpenAPI) JennyName() string {
	return "OpenAPI"
}

func (o *OpenAPI) Generate(appManifest codegen.AppManifest) (codejen.Files, error) {
	fs := codejen.NewFS()

	// Group kinds by package name
	if o.GroupByKind {
		for v, k := range codegen.VersionedKinds(appManifest) {
			if !k.Codegen.Go.Enabled {
				continue
			}

			relativePkg := filepath.Join(o.GoGenPath, GetGeneratedGoTypePath(o.GroupByKind, appManifest.Properties().Group, v.Name(), k.MachineName))
			err := gengo.Execute(generators.NameSystems(),
				generators.DefaultNameSystem(),
				o.getTargetsFunc(relativePkg, fs),
				gengo.StdBuildTag,
				[]string{filepath.Join(o.GoModName, filepath.ToSlash(relativePkg))},
			)
			if err != nil {
				return nil, err
			}
		}
	} else {
		gvs := make(map[schema.GroupVersion]struct{})
		for _, v := range appManifest.Versions() {
			codegenEnabled := false
			for _, k := range v.Kinds() {
				if k.Codegen.Go.Enabled {
					codegenEnabled = true
					break
				}
			}
			if !codegenEnabled {
				continue
			}
			gvs[schema.GroupVersion{Group: appManifest.Properties().Group, Version: v.Name()}] = struct{}{}
		}
		for gv := range gvs {
			relativePkg := filepath.Join(o.GoGenPath, ToPackageName(strings.ToLower(gv.Group)), ToPackageName(gv.Version))
			err := gengo.Execute(generators.NameSystems(),
				generators.DefaultNameSystem(),
				o.getTargetsFunc(relativePkg, fs),
				gengo.StdBuildTag,
				[]string{filepath.Join(o.GoModName, filepath.ToSlash(relativePkg))},
			)
			if err != nil {
				return nil, err
			}
		}
	}

	return fs.AsFiles(), nil
}

func (o *OpenAPI) getTargetsFunc(relativePkgPath string, fs *codejen.FS) func(context *generator.Context) []generator.Target {
	return func(context *generator.Context) []generator.Target {
		context.FileTypes[generator.GoFileType] = &GoFile{
			FS:                 fs,
			Source:             o,
			OutputRelativePath: relativePkgPath,
			FixMetaV1Refs:      true,
		}
		arguments := args.New()
		// Use full import path so openapi-gen matches the context package and does not add
		// a self-import or package-qualified type names (kube-openapi/gengo compare by path).
		arguments.OutputPkg = filepath.Join(o.GoModName, filepath.ToSlash(relativePkgPath))
		arguments.OutputDir = relativePkgPath
		arguments.OutputFile = "zz_openapi_gen.go"
		return generators.GetOpenAPITargets(context, arguments, nil)
	}
}

type GoFile struct {
	FS                 *codejen.FS
	Source             codejen.NamedJenny
	OutputRelativePath string
	FixMetaV1Refs      bool
}

var metav1regexp = regexp.MustCompile(`"k8s\.io/apimachinery/pkg/apis/meta/v1\.([A-Za-z0-9]+)"`)

//nolint:revive
func (g *GoFile) AssembleFile(f *generator.File, _ string) error {
	buf := &bytes.Buffer{}

	// Writing go file copied from k8s.io/gengo/generator.assembleGolangFile
	// (https://github.com/kubernetes/gengo/blob/master/generator/execute.go#L128)
	buf.Write(f.Header)
	fmt.Fprintf(buf, "package %v\n\n", f.PackageName)
	if len(f.Imports) > 0 {
		fmt.Fprint(buf, "import (\n")
		for i := range f.Imports {
			if strings.Contains(i, "\"") {
				// they included quotes, or are using the
				// `name "path/to/pkg"` format.
				fmt.Fprintf(buf, "\t%s\n", i)
			} else {
				fmt.Fprintf(buf, "\t%q\n", i)
			}
		}
		if g.FixMetaV1Refs {
			fmt.Fprintf(buf, "\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n")
		}
		fmt.Fprint(buf, ")\n\n")
	}

	if f.Vars.Len() > 0 {
		fmt.Fprint(buf, "var (\n")
		buf.Write(f.Vars.Bytes())
		fmt.Fprint(buf, ")\n\n")
	}

	if f.Consts.Len() > 0 {
		fmt.Fprint(buf, "const (\n")
		buf.Write(f.Consts.Bytes())
		fmt.Fprint(buf, ")\n\n")
	}

	bodyBytes := f.Body.Bytes()
	if g.FixMetaV1Refs {
		bodyBytes = metav1regexp.ReplaceAll(bodyBytes, []byte("metav1.$1{}.OpenAPIModelName()"))
	}
	buf.Write(bodyBytes)
	formatted, err := format.Source(buf.Bytes())
	if err != nil {
		return err
	}

	// Use the relative path we passed in so output is under GoGenPath (e.g. codegen/.../v1),
	// not the full package path the generator may set in f.PackagePath.
	relativePath := filepath.Join(g.OutputRelativePath, f.Name)
	return g.FS.Add(codejen.File{
		RelativePath: relativePath,
		Data:         formatted,
		From:         []codejen.NamedJenny{g.Source},
	})
}
