//go:build scripts

// This program generates /internal/api/handlers.go.
// It can be invoked by running go run -tags scripts scripts/handlers/gen_handlers.go

// Supported args:
// -print-only

package handlers

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"text/template"

	"allaboutapps.dev/aw/go-starter/scripts/internal/util"
)

// https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/

var (
	handlersPackage = "/internal/api/handlers"

	pathProjectRoot  = util.GetProjectRootDir()
	pathHandlersRoot = pathProjectRoot + handlersPackage
	pathModFile      = pathProjectRoot + "/go.mod"
	pathHandlersFile = pathHandlersRoot + "/handlers.go"

	// methodPrefixes defines all keywords that we search for in the handlers sub packages
	// <methodPrefixes>*<methodSuffix> must be the naming of the route (Capitalized)
	methodPrefixes = []string{
		// https://swagger.io/specification/v2/ fixed fields: GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH
		"Get", "Put", "Post", "Delete", "Options", "Head", "Patch",
	}
	methodSuffix = "Route"

	packageTemplate = template.Must(template.New("").Parse(`// Code generated by go run -tags scripts scripts/handlers/gen_handlers.go; DO NOT EDIT.
package handlers

import (
	"allaboutapps.dev/aw/go-starter/internal/api"
	{{- range .SubPkgs }}
	"{{ . }}"
	{{- end }}
	"github.com/labstack/echo/v4"
)

func AttachAllRoutes(s *api.Server) {
	// attach our routes
	s.Router.Routes = []*echo.Route{
		{{- range .Funcs }}
		{{ .PackageName }}.{{ .FunctionName }}(s),
		{{- end }}
	}
}
`))
)

type ResolvedFunction struct {
	FunctionName    string
	PackageName     string
	PackageNameFQDN string
}

type TempateData struct {
	SubPkgs []string
	Funcs   []ResolvedFunction
}

// get all functions in above handler packages
// that match <methodPrefixes>*<methodSuffix>
func GenHandlers(printOnly bool) {
	funcs := []ResolvedFunction{}

	baseModuleName, err := util.GetModuleName(pathModFile)

	if err != nil {
		log.Fatal(err)
	}

	set := token.NewFileSet()

	err = filepath.Walk(pathHandlersRoot, func(path string, f os.FileInfo, err error) error {
		if err != nil {
			fmt.Printf("Failed to access path %q: %v\n", path, err)
			os.Exit(1)
		}

		// ignore handler file to be generated
		// ignore directories
		// ignore non go files
		// ignore test go files
		if path == pathHandlersFile || f.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "test.go") {
			return nil
		}

		gofile, err := parser.ParseFile(set, path, nil, 0)

		if err != nil {
			fmt.Println("Failed to parse package:", err)
			os.Exit(1)
		}

		fileDir := filepath.Dir(path)
		packageNameFQDN := strings.Replace(fileDir, pathProjectRoot, baseModuleName, 1)

		for _, d := range gofile.Decls {

			if fn, isFn := d.(*ast.FuncDecl); isFn {

				fnName := fn.Name.String()

				for _, prefix := range methodPrefixes {
					if strings.HasPrefix(fnName, prefix) && strings.HasSuffix(fnName, methodSuffix) {
						funcs = append(funcs, ResolvedFunction{
							FunctionName:    fnName,
							PackageName:     gofile.Name.Name,
							PackageNameFQDN: packageNameFQDN,
						})
					}
				}
			}
		}

		// fmt.Println(packageNameFQDN, path)

		return nil
	})

	if err != nil {
		log.Fatal(err)
	}

	// stable sort the functions
	// first PackageName then FunctionName
	sort.Slice(funcs[:], func(i, j int) bool {
		return funcs[i].PackageNameFQDN+funcs[i].FunctionName < funcs[j].PackageNameFQDN+funcs[j].FunctionName
	})

	// only add subPkg if there was actually a route within it found
	subPkgs := []string{}
	for _, fun := range funcs {

		mustAppend := true

		for _, a := range subPkgs {
			if a == fun.PackageNameFQDN {
				mustAppend = false
			}
		}

		if mustAppend {
			subPkgs = append(subPkgs, fun.PackageNameFQDN)
		}
	}

	if printOnly {
		for _, function := range funcs {
			fmt.Println(function.PackageNameFQDN, function.FunctionName)
		}

		// bailout
		return
	}

	f, err := os.Create(pathHandlersFile)

	if err != nil {
		log.Fatal(err)
	}

	defer f.Close()

	if err = packageTemplate.Execute(f, TempateData{
		SubPkgs: subPkgs,
		Funcs:   funcs,
	}); err != nil {
		log.Fatal(err)
	}
}
