//go:build ignore

// This program runs on a given package and generates one function
// for each client API method.
// Can be invoked by running go generate.
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"go/format"
	"log"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"text/template"
	"unicode"
)

func main() {
	cwd, err := os.Getwd()
	if err != nil {
		log.Fatalf("cannot get working dir: %s", err)
	}

	// We're looking for source code in <package-name>/client.go files.
	filePath, err := filepath.Abs(path.Join(cwd, "client.go"))
	if err != nil {
		log.Fatal(fmt.Errorf("get absolute file path: %w", err))
	}

	file, err := os.Open(filePath)
	if err != nil {
		log.Fatal(fmt.Errorf("open %s: %w", filePath, err))
	}
	defer file.Close()

	var b bytes.Buffer
	// Start writing with the header.
	err = headTempl.Execute(&b, headVars{
		Command:     "gen",
		PackageName: path.Base(cwd),
	})
	if err != nil {
		log.Fatal(fmt.Errorf("%s: write header: %w", filePath, err))
	}

	// Now write all Client methods as functions.
	sc := bufio.NewScanner(file)
	var comments strings.Builder
	for sc.Scan() {
		line := strings.Trim(sc.Text(), " ")

		// Gather all comments, they might be a method's
		// godoc.
		if strings.HasPrefix(line, "//") {
			comments.WriteString("\n" + line)
		}
		if line == "" {
			comments.Reset()
		}

		// Not a *Client method, skip
		if !strings.HasPrefix(line, lineStartsWith) {
			continue
		}

		// We've reached a line containing a method definition. Let's
		// write the method godoc comments.
		_, err := b.WriteString(comments.String())
		if err != nil {
			log.Fatal(fmt.Errorf("cannot write comments: %w", err))
		}
		comments.Reset()

		// Gather all exported *Client functions
		vars := getFuncVars(line)
		isExported := false
		for i, r := range vars.FuncName {
			if i > 0 {
				break
			}
			isExported = unicode.IsUpper(r)
		}
		if !isExported {
			continue
		}
		err = funcTempl.Execute(&b, vars)
		if err != nil {
			log.Fatal(fmt.Errorf("write func from %s: %w", line, err))
		}
	}
	if err := sc.Err(); err != nil {
		log.Fatal(fmt.Errorf("read file %s: %w", filePath, err))
	}

	// Write the last parts of the file.
	err = footTempl.Execute(&b, nil)
	if err != nil {
		log.Fatal(fmt.Errorf("%s: write footer: %w", filePath, err))
	}

	// Format the source code.
	formatted, err := format.Source(b.Bytes())
	if err != nil {
		log.Fatal(fmt.Errorf("formatting %s: %w", filePath, err))
	}

	// We'll write to <package-name>/api.go files. Open a file for writing.
	writePath := strings.ReplaceAll(filePath, "client.go", "api.go")
	f, err := os.Create(writePath)
	if err != nil {
		log.Fatal(fmt.Errorf("create file %s for writing: %w", writePath, err))
	}
	// Write the file
	_, err = f.Write(formatted)
	if err != nil {
		log.Fatal(fmt.Errorf("commit func: %w", err))
	}
}

// We care only about *Client methods. Method declarations begin
// with lineStartsWith.
const lineStartsWith = "func (c *Client) "

var nameRE = regexp.MustCompile("^\\w+\\(")
var argsRE = regexp.MustCompile("^.+\\)\\s[\\(e]")
var returnRE = regexp.MustCompile("^.+\\s{")

// Parse a method definition line and get the name, arguments and
// return types.
// The line has the format
// func (c *Client) MethodName(ctx context.Context, params *Params) (*clerk.Resource, error) {
// Quick and dirty method but it works :).
func getFuncVars(line string) funcVars {
	line = strings.TrimPrefix(line, "func (c *Client) ")

	name := nameRE.FindString(line)
	line = strings.TrimPrefix(line, name)

	args := argsRE.FindString(line)
	args = strings.TrimSuffix(args, ") e")
	line = strings.TrimPrefix(line, args)

	returnV := returnRE.FindString(line)
	returnV = strings.TrimSuffix(returnV, " {")

	name = strings.Trim(name, "(")
	args = strings.Trim(args, ") (")
	returnV = strings.Trim(returnV, ")")
	allArgs := strings.Split(args, ",")
	params := make([]string, len(allArgs))
	for i, arg := range allArgs {
		params[i] = strings.Split(strings.TrimPrefix(arg, " "), " ")[0]
	}

	return funcVars{
		FuncName:   name,
		FuncArgs:   args,
		FuncReturn: returnV,
		FuncParams: strings.Join(params, ","),
	}
}

type headVars struct {
	Command     string
	PackageName string
}

var headTempl = template.Must(template.New("").Parse(`
// Code generated by "{{.Command}}"; DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package {{.PackageName}}

import (
	"context"

	"github.com/clerk/clerk-sdk-go/v2"
)
`))

type funcVars struct {
	FuncName   string
	FuncArgs   string
	FuncReturn string
	FuncParams string
}

var funcTempl = template.Must(template.New("").Parse(`
func {{.FuncName}}({{.FuncArgs}}) ({{.FuncReturn}}) {
	return getClient().{{.FuncName}}({{.FuncParams}})
}
`))

var footTempl = template.Must(template.New("").Parse(`
func getClient() *Client {
	return &Client{
		Backend: clerk.GetBackend(),
	}
}
`))
