// Copyright 2023 Volvo Car Corporation
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"bufio"
	"bytes"
	"context"
	"flag"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/dave/jennifer/jen"
	"github.com/veggiemonk/strcase"
)

func main() {
	var out string

	var typemeta, apir bool

	flag.StringVar(
		&out,
		"out",
		"-",
		"output file to write the results to. Defaults to stdout.",
	)

	flag.BoolVar(
		&apir,
		"api-resources",
		false,
		"generate APIResources fields ",
	)
	flag.BoolVar(
		&typemeta,
		"typemeta",
		true,
		"generate TypeMeta variables",
	)

	flag.Parse()

	var buf bytes.Buffer
	if err := getResources(&buf); err != nil {
		fmt.Println(">>> getting resources failed. Is kubectl connected to a cluster?")
		panic(err)
	}
	rows, err := scan(&buf)
	if err != nil {
		panic(err)
	}

	var f *jen.File
	if typemeta {
		f = genTypeMeta(rows)
	}
	if apir {
		f = genAPIResources(rows)
	}

	if out == "-" {
		fmt.Printf("%#v", f)
		return
	}
	if !filepath.IsAbs(out) {
		cwd, err := os.Getwd()
		if err != nil {
			panic(err)
		}
		out = filepath.Join(cwd, out)
	}

	fmt.Printf("writing to %s\n", out)
	if err := f.Save(out); err != nil {
		panic(err)
	}
}

func getResources(w io.Writer) error {
	kubeconfigPath := os.Getenv("KUBECONFIG")
	if kubeconfigPath == "" {
		kubeconfigPath = filepath.Join(os.Getenv("HOME"), ".kube", "config")
	}

	ctx := context.Background()
	var buf bytes.Buffer
	err := kubectl(
		ctx,
		w,
		&buf,
		"--kubeconfig",
		kubeconfigPath,
		"api-resources",
		"--no-headers=true",
	)
	if err != nil {
		if strings.Contains(buf.String(), "connect: connection refused") {
			return fmt.Errorf(
				"kubectl api-resources: %w :"+
					"need to be connected to a kubernetes cluster to generate the api-resources",
				err,
			)
		}
		return fmt.Errorf(
			"kubectl api-resources: %w :\n output: %s",
			err,
			buf.String(),
		)
	}
	return nil
}

func scan(r io.Reader) ([]*APIResource, error) {
	scanner := bufio.NewScanner(r)
	var res []*APIResource

	for scanner.Scan() {
		txt := scanner.Text()
		row := strings.Fields(txt)
		switch l := len(row); l {
		case 4:
			// no short name
			l := &APIResource{
				Name:       row[0],
				ShortName:  replaceShortKind(row[0]),
				APIVersion: row[1],
				Namespaced: row[2] == "true",
				Kind:       row[3],
			}
			res = append(res, l)
		case 5:
			// has short name
			sns := strings.Split(row[1], ",")
			ar := &APIResource{
				Name:       row[0],
				ShortName:  replaceShortKind(sns[0]),
				APIVersion: row[2],
				Namespaced: row[3] == "true",
				Kind:       row[4],
			}
			if ar.Kind != "Event" { // skip events as it creates duplicates
				res = append(res, ar)
			}
		default:
			return nil, fmt.Errorf("invalid row %d: %s", len(res), txt)
		}
	}
	return res, nil
}

type APIResource struct {
	Name       string
	ShortName  string
	APIVersion string
	Namespaced bool
	Kind       string
}

func replaceShortKind(k string) string {
	switch k {
	case "roles":
		return "role"
	case "rolebindings":
		return "rb"
	case "clusterroles":
		return "cr"
	case "clusterrolebindings":
		return "crb"
	default:
		return k
	}
}

func genAPIResources(ar []*APIResource) *jen.File {
	const (
		structName       = "APIResource"
		pluralStructName = structName + "s"
		metaPkgPath      = "k8s.io/apimachinery/pkg/apis/meta/v1"
	)
	file := jen.NewFile("meta")

	file.ImportAlias(metaPkgPath, "metav1")

	file.PackageComment("Code generated by apisources. DO NOT EDIT.")
	file.Type().Id(structName).StructFunc(
		func(g *jen.Group) {
			g.Id("Name").String()
			g.Id("ShortName").String()
			g.Id("APIVersion").String()
			g.Id("Namespaced").Bool()
			g.Id("Kind").String()
		},
	)
	file.Type().Id(pluralStructName).Index().Id(structName)
	file.Func().Params(jen.Id("k").Id(structName+"s")).Id("ByKind").
		Params(jen.Id("kind").String()).
		Params(
			jen.Id(structName),
			jen.Id("bool"),
		).BlockFunc(
		func(g *jen.Group) {
			g.For(
				jen.List(
					jen.Id("_"),
					jen.Id("v"),
				).Op(":=").Range().Id("k"),
			).BlockFunc(
				func(g *jen.Group) {
					g.If(jen.Id("v").Dot("Kind").Op("==").Id("kind")).BlockFunc(
						func(g *jen.Group) {
							g.Return(jen.Id("v"), jen.True())
						},
					)
				},
			)
			g.Return(jen.Id(structName).Block(), jen.False())
		},
	)

	file.Line()
	// func TypeMeta(kind string) metav1.TypeMeta {
	file.Func().Id("TypeMeta").Params(jen.Id("kind").String()).Params(
		jen.Qual(metaPkgPath, "TypeMeta"),
	).BlockFunc(
		func(g *jen.Group) {
			g.Id("o").Op(",").Id("ok").Op(":=").
				Id("KAPI").Dot("ByKind").Call(jen.Id("kind"))
			g.If(jen.Op("!").Id("ok")).BlockFunc(
				func(g *jen.Group) {
					g.Panic(
						jen.Qual("fmt", "Sprintf").Call(
							jen.Lit("kind %q not implemented"), jen.Id("kind"),
						),
					)
				},
			)
			g.Return(
				jen.Qual(metaPkgPath, "TypeMeta").Values(
					jen.Dict{
						jen.Id("Kind"):       jen.Id("o").Dot("Kind"),
						jen.Id("APIVersion"): jen.Id("o").Dot("APIVersion"),
					},
				),
			)
		},
	)

	// var KAPI = APIResources{
	file.Var().Id("KAPI").Id(pluralStructName).Op("=").Id(pluralStructName).
		ValuesFunc(
			func(g *jen.Group) {
				for _, v := range ar {
					g.Values(
						jen.Dict{
							jen.Id("Name"):       jen.Lit(v.Name),
							jen.Id("ShortName"):  jen.Lit(v.ShortName),
							jen.Id("APIVersion"): jen.Lit(v.APIVersion),
							jen.Id("Namespaced"): jen.Lit(v.Namespaced),
							jen.Id("Kind"):       jen.Lit(v.Kind),
						},
					)
				}
			},
		)

	return file
}

func genTypeMeta(ar []*APIResource) *jen.File {
	const metaPkgPath = "k8s.io/apimachinery/pkg/apis/meta/v1"

	file := jen.NewFile("api")

	file.ImportAlias(metaPkgPath, "metav1")

	file.PackageComment("Code generated by apisources. DO NOT EDIT.")
	// var TypeXXX = metav1.TypeMeta{
	for _, r := range ar {
		ver := r.APIVersion[strings.LastIndex(r.APIVersion, "/")+1:]
		name := "Type" + r.Kind + strcase.Pascal(ver)
		file.Var().Id(name).Op("=").Qual(metaPkgPath, "TypeMeta").
			Values(
				jen.Dict{
					jen.Id("Kind"):       jen.Lit(r.Kind),
					jen.Id("APIVersion"): jen.Lit(r.APIVersion),
				},
			).Line()
	}
	return file
}

func kubectl(
	ctx context.Context,
	stdout io.Writer,
	stderr io.Writer,
	args ...string,
) error {
	cmd := exec.CommandContext(ctx, "kubectl", args...)
	cmd.Env = os.Environ() // inherit environment in case we need to use kubectl from a container

	cmd.Stdout = stdout
	cmd.Stderr = stderr

	if err := cmd.Start(); err != nil {
		return err
	}

	// waits for the command to exit and waits for any copying
	// to stdin or copying from stdout or stderr to complete
	return cmd.Wait()
}
