// Command genevents reads Go event constants from events.go using go/ast
// and generates the corresponding TypeScript constants file.
//
// Usage:
//
//	go run ./cmd/genevents -source events.go -output ../../frontend/src/events.ts
package main

import (
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
)

func main() {
	source := flag.String("source", "events.go", "path to Go events source file")
	output := flag.String("output", "", "path to TypeScript output file (stdout if empty)")

	flag.Parse()

	consts, err := parseEvents(*source)
	if err != nil {
		fmt.Fprintf(os.Stderr, "genevents: %v\n", err)
		os.Exit(1)
	}

	ts := generateTypeScript(consts)

	if *output == "" || *output == "/dev/stdout" {
		fmt.Print(ts)

		return
	}

	if err := writeAtomic(*output, ts); err != nil {
		fmt.Fprintf(os.Stderr, "genevents: write %s: %v\n", *output, err)
		os.Exit(1)
	}
}

// constGroup holds a block of related constants with its doc comment.
type constGroup struct {
	Comment string // doc comment text (empty if none)
	Consts  []constEntry
}

// constEntry holds one constant name and its string value.
type constEntry struct {
	Name  string
	Value string
}

// parseEvents parses the Go source file and extracts typed string constant
// groups in declaration order.
func parseEvents(path string) ([]constGroup, error) {
	fset := token.NewFileSet()

	f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}

	var groups []constGroup

	for _, decl := range f.Decls {
		gd, ok := decl.(*ast.GenDecl)
		if !ok || gd.Tok != token.CONST {
			continue
		}

		var g constGroup

		// Extract doc comment from the const block.
		if gd.Doc != nil {
			g.Comment = cleanComment(gd.Doc.Text())
		}

		for _, spec := range gd.Specs {
			vs, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}

			for i, name := range vs.Names {
				if i >= len(vs.Values) {
					continue
				}

				bl, ok := vs.Values[i].(*ast.BasicLit)

				if !ok || bl.Kind != token.STRING {
					continue
				}
				// Strip quotes from the string literal value.
				val := strings.Trim(bl.Value, `"`)
				g.Consts = append(g.Consts, constEntry{Name: name.Name, Value: val})
			}
		}

		if len(g.Consts) > 0 {
			groups = append(groups, g)
		}
	}

	return groups, nil
}

// cleanComment trims whitespace and strips trailing periods from Go doc
// comment text (Go convention uses periods; TypeScript comments typically
// do not).
func cleanComment(s string) string {
	s = strings.TrimSpace(s)
	s = strings.TrimSuffix(s, ".")

	return s
}

// commentLines renders a doc comment as indented TypeScript line
// comments, one per source line.
func commentLines(comment string) []string {
	if comment == "" {
		return nil
	}

	var out []string

	for _, line := range strings.Split(comment, "\n") {
		if line == "" {
			out = append(out, "    //")

			continue
		}

		out = append(out, "    // "+line)
	}

	return out
}

// generateTypeScript produces the full TypeScript source from the parsed
// constant groups.
func generateTypeScript(groups []constGroup) string {
	var b strings.Builder

	b.WriteString("// Code generated by genevents from backend/events/events.go. DO NOT EDIT.\n")
	b.WriteString("\n")
	b.WriteString("export const Events = {\n")

	for i, g := range groups {
		// Every line, not just the first: a doc comment that runs to a
		// second paragraph used to emit its remainder as bare prose
		// inside the object literal, so `make generate` — a pre-commit
		// hook — produced TypeScript that does not parse.
		for _, line := range commentLines(g.Comment) {
			b.WriteString(line + "\n")
		}

		for _, c := range g.Consts {
			fmt.Fprintf(&b, "    %s: %q,\n", c.Name, c.Value)
		}
		// Blank line between groups, but not after the last one.
		if i < len(groups)-1 {
			b.WriteString("\n")
		}
	}

	b.WriteString("} as const;\n")
	b.WriteString("\n")
	b.WriteString("export type EventName = (typeof Events)[keyof typeof Events];\n")

	return b.String()
}

// writeAtomic writes data to a temporary file in the same directory as path,
// then renames it into place for atomic replacement.
func writeAtomic(path, data string) error {
	dir := filepath.Dir(path)

	tmp, err := os.CreateTemp(dir, ".genevents-*.tmp")
	if err != nil {
		return err
	}

	tmpName := tmp.Name()

	if _, err := tmp.WriteString(data); err != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)

		return err
	}

	if err := tmp.Close(); err != nil {
		_ = os.Remove(tmpName)

		return err
	}

	return os.Rename(tmpName, path)
}
