// Command gen_action_catalog_manifest generates the ActionSpec group builder
// manifest used by the catalog-first runtime.
package main

import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const (
	defaultSourceDir  = "internal/tools"
	defaultOutputPath = "internal/tools/action_specs_manifest_gen.go"
)

func main() {
	sourceDir := flag.String("source", defaultSourceDir, "directory containing action spec group builder source files")
	outputPath := flag.String("output", defaultOutputPath, "generated manifest path")
	check := flag.Bool("check", false, "verify generated manifest is up to date without writing")
	flag.Parse()

	root, err := repositoryRoot(".")
	if err != nil {
		fatalf("find repository root: %v", err)
	}
	builders, err := discoverActionSpecGroupBuilders(filepath.Join(root, *sourceDir))
	if err != nil {
		fatalf("discover action spec group builders: %v", err)
	}
	content, err := generateManifest(builders)
	if err != nil {
		fatalf("generate manifest: %v", err)
	}
	targetPath := filepath.Join(root, *outputPath)
	if *check {
		if checkErr := checkManifest(targetPath, content); checkErr != nil {
			fatalf("check manifest: %v", checkErr)
		}
		return
	}
	if writeErr := os.WriteFile(targetPath, content, 0o600); writeErr != nil {
		fatalf("write %s: %v", targetPath, writeErr)
	}
}

func discoverActionSpecGroupBuilders(sourceDir string) ([]string, error) {
	fileSet := token.NewFileSet()
	builders := make(map[string]string)
	err := filepath.WalkDir(sourceDir, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			if path != sourceDir {
				return filepath.SkipDir
			}
			return nil
		}
		name := entry.Name()
		if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") || strings.HasSuffix(name, "_gen.go") {
			return nil
		}
		file, parseErr := parser.ParseFile(fileSet, path, nil, 0)
		if parseErr != nil {
			return fmt.Errorf("parse %s: %w", path, parseErr)
		}
		for _, declaration := range file.Decls {
			function, ok := declaration.(*ast.FuncDecl)
			if !ok || function.Recv != nil || !isActionSpecGroupBuilderName(function.Name.Name) {
				continue
			}
			if previousPath, exists := builders[function.Name.Name]; exists {
				return fmt.Errorf("duplicate action spec group builder %s in %s and %s", function.Name.Name, previousPath, path)
			}
			builders[function.Name.Name] = path
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	if len(builders) == 0 {
		return nil, errors.New("no action spec group builders found")
	}
	names := make([]string, 0, len(builders))
	for name := range builders {
		names = append(names, name)
	}
	sort.Strings(names)
	return names, nil
}

func isActionSpecGroupBuilderName(name string) bool {
	return strings.HasPrefix(name, "build") && strings.HasSuffix(name, "ActionSpecs") && len(name) > len("buildActionSpecs")
}

func generateManifest(builders []string) ([]byte, error) {
	var buffer bytes.Buffer
	buffer.WriteString("// Code generated by cmd/gen_action_catalog_manifest; DO NOT EDIT.\n\n")
	buffer.WriteString("package tools\n\n")
	buffer.WriteString("func actionSpecGroupBuilders() []actionSpecGroupBuilder {\n")
	buffer.WriteString("\treturn []actionSpecGroupBuilder{\n")
	for _, builder := range builders {
		fmt.Fprintf(&buffer, "\t\t%s,\n", builder)
	}
	buffer.WriteString("\t}\n")
	buffer.WriteString("}\n")
	formatted, err := format.Source(buffer.Bytes())
	if err != nil {
		return nil, fmt.Errorf("format generated manifest: %w", err)
	}
	return formatted, nil
}

func checkManifest(path string, want []byte) error {
	got, err := os.ReadFile(path) // #nosec G304 -- path is resolved from the local repository root and points to the generated manifest.
	if err != nil {
		return fmt.Errorf("read %s: %w", path, err)
	}
	if !bytes.Equal(got, want) {
		return fmt.Errorf("%s is stale; run go run ./cmd/gen_action_catalog_manifest/", path)
	}
	return nil
}

func repositoryRoot(start string) (string, error) {
	current, err := filepath.Abs(start)
	if err != nil {
		return "", err
	}
	for {
		if _, statErr := os.Stat(filepath.Join(current, "go.mod")); statErr == nil {
			return current, nil
		}
		parent := filepath.Dir(current)
		if parent == current {
			return "", fmt.Errorf("go.mod not found from %s", start)
		}
		current = parent
	}
}

func fatalf(message string, args ...any) {
	fmt.Fprintf(os.Stderr, message+"\n", args...)
	os.Exit(1)
}
