// Command protoc-gen-gitaly-protolist is designed to be used as a protobuf compiler to generate a
// list of protobuf files via a publicly accessible variable.
//
// This plugin can be accessed by invoking the protoc compiler with the following arguments:
//
//	protoc --plugin=protoc-gen-gitaly-protolist --gitaly_protolist_out=.
//
// The plugin accepts a protobuf message in STDIN that describes the parsed protobuf files. A
// response is sent back on STDOUT that contains any errors.
package main

import (
	"bytes"
	"fmt"
	"go/format"
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/pluginpb"
)

const (
	gitalyProtoDirArg = "proto_dir"
	gitalypbDirArg    = "gitalypb_dir"
)

func main() {
	data, err := io.ReadAll(os.Stdin)
	if err != nil {
		log.Fatalf("reading input: %s", err)
	}

	req := &pluginpb.CodeGeneratorRequest{}

	if err := proto.Unmarshal(data, req); err != nil {
		log.Fatalf("parsing input proto: %s", err)
	}

	if err := generateProtolistGo(req); err != nil {
		log.Fatal(err)
	}
}

func parseArgs(argString string) (gitalyProtoDir string, gitalypbDir string) {
	for _, arg := range strings.Split(argString, ",") {
		argKeyValue := strings.Split(arg, "=")
		if len(argKeyValue) != 2 {
			continue
		}
		switch argKeyValue[0] {
		case gitalyProtoDirArg:
			gitalyProtoDir = argKeyValue[1]
		case gitalypbDirArg:
			gitalypbDir = argKeyValue[1]
		}
	}

	return gitalyProtoDir, gitalypbDir
}

func generateProtolistGo(req *pluginpb.CodeGeneratorRequest) error {
	var err error
	gitalyProtoDir, gitalypbDir := parseArgs(req.GetParameter())

	if gitalyProtoDir == "" {
		return fmt.Errorf("%s not provided", gitalyProtoDirArg)
	}
	if gitalypbDir == "" {
		return fmt.Errorf("%s not provided", gitalypbDirArg)
	}

	var protoNames []string

	if gitalyProtoDir, err = filepath.Abs(gitalyProtoDir); err != nil {
		return fmt.Errorf("failed to get absolute path for %s: %w", gitalyProtoDir, err)
	}

	files, err := os.ReadDir(gitalyProtoDir)
	if err != nil {
		return fmt.Errorf("failed to read %s: %w", gitalyProtoDir, err)
	}

	for _, fi := range files {
		if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".proto") {
			protoNames = append(protoNames, fmt.Sprintf(`%q`, fi.Name()))
		}
	}

	f, err := os.Create(filepath.Join(gitalypbDir, "protolist.go"))
	if err != nil {
		return fmt.Errorf("could not create protolist.go: %w", err)
	}
	defer f.Close()

	if err = renderProtoList(f, protoNames); err != nil {
		return fmt.Errorf("could not render go code: %w", err)
	}

	return nil
}

// renderProtoList generate a go file with a list of gitaly protos
func renderProtoList(dest io.WriteCloser, protoNames []string) error {
	joinFunc := template.FuncMap{"join": strings.Join}
	protoList := `package gitalypb
	// Code generated by protoc-gen-gitaly. DO NOT EDIT

	// GitalyProtos is a list of gitaly protobuf files
	var GitalyProtos = []string{
		{{join . ",\n"}},
	}
	`
	protoListTempl, err := template.New("protoList").Funcs(joinFunc).Parse(protoList)
	if err != nil {
		return fmt.Errorf("could not create go code template: %w", err)
	}

	var rawGo bytes.Buffer

	if err := protoListTempl.Execute(&rawGo, protoNames); err != nil {
		return fmt.Errorf("could not execute go code template: %w", err)
	}

	formattedGo, err := format.Source(rawGo.Bytes())
	if err != nil {
		return fmt.Errorf("could not format go code: %w", err)
	}

	if _, err = io.Copy(dest, bytes.NewBuffer(formattedGo)); err != nil {
		return fmt.Errorf("failed to write protolist.go file: %w", err)
	}

	return nil
}
