// gen-restic-cmds parses restic's man pages and generates a Go source file
// containing every command and its accepted flags. Run via go generate:
//
//	//go:generate go run git.secluded.site/keld/cmd/gen-restic-cmds
package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"text/template"
)

// command mirrors the runtime type but lives here so the generator has no
// import dependency on the package it writes into.
type command struct {
	Name        string
	Description string
	Options     []option
}

type option struct {
	Name        string
	Alias       string
	Default     string
	Description string
	Repeatable  bool
	IsBool      bool
}

// Regexes for the small subset of troff macros cobra emits.
var (
	sectionStart   = regexp.MustCompile(`^\.SH (.+)$`)
	paragraphBreak = regexp.MustCompile(`^\.PP$`)
	escapeSequence = regexp.MustCompile(`\\f[A-Z]|\\&|\\`)
	strayBackticks = regexp.MustCompile("[`]{2,}")
	filePattern    = regexp.MustCompile(`^restic(-.*)?\.1$`)
)

func main() {
	manDir, err := os.MkdirTemp("", "keld-gen-*")
	if err != nil {
		fatalf("creating temp dir: %v", err)
	}
	defer func() { _ = os.RemoveAll(manDir) }()

	resticBin := os.Getenv("KELD_EXECUTABLE")
	if resticBin == "" {
		resticBin = "restic"
	}

	// Capture the version string for the header comment.
	versionOut, err := exec.Command(resticBin, "version").Output()
	if err != nil {
		fatalf("running %s version: %v", resticBin, err)
	}
	version := strings.TrimSpace(string(versionOut))

	genCmd := exec.Command(resticBin, "generate", "--man", manDir)
	genCmd.Stderr = os.Stderr
	if err := genCmd.Run(); err != nil {
		fatalf("running %s generate --man: %v", resticBin, err)
	}

	entries, err := os.ReadDir(manDir)
	if err != nil {
		fatalf("reading man dir: %v", err)
	}

	var commands []command
	for _, entry := range entries {
		if entry.IsDir() || !filePattern.MatchString(entry.Name()) {
			continue
		}

		cmd, err := parseManPage(filepath.Join(manDir, entry.Name()))
		if err != nil {
			fatalf("parsing %s: %v", entry.Name(), err)
		}
		if cmd != nil {
			commands = append(commands, *cmd)
		}
	}

	sort.Slice(commands, func(i, j int) bool {
		return commands[i].Name < commands[j].Name
	})

	outPath := "commands_gen.go"
	if err := writeGoFile(outPath, version, commands); err != nil {
		fatalf("writing %s: %v", outPath, err)
	}

	fmt.Fprintf(os.Stderr, "generated %s (%d commands from %s)\n", outPath, len(commands), version)
}

// parseManPage reads a single restic-*.1 man page and extracts the command
// name, description, and per-command options. Inherited (global) options are
// skipped for subcommand pages; they appear once via restic.1.
func parseManPage(path string) (*command, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer func() { _ = f.Close() }()

	base := filepath.Base(path)
	m := filePattern.FindStringSubmatch(base)
	if m == nil {
		return nil, fmt.Errorf("unexpected filename %s", base)
	}

	// restic.1 → "" → "global", restic-backup.1 → "-backup" → "backup"
	cmdName := strings.TrimPrefix(m[1], "-")
	if cmdName == "" {
		cmdName = "global"
	}

	cmd := &command{Name: cmdName}
	var (
		section string
		opt     *option
	)

	flush := func() {
		if opt != nil {
			opt.Description = strings.TrimSpace(opt.Description)
			cmd.Options = append(cmd.Options, *opt)
			opt = nil
		}
	}

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()

		// Handle troff macro lines.
		if strings.HasPrefix(line, ".") {
			if sm := sectionStart.FindStringSubmatch(line); sm != nil {
				flush()
				section = sm[1]
				continue
			}
			if paragraphBreak.MatchString(line) {
				flush()
			}
			continue
		}

		// Strip all font/escape sequences and backtick artifacts from content lines.
		line = escapeSequence.ReplaceAllString(line, "")
		line = strayBackticks.ReplaceAllString(line, "")

		switch section {
		case "NAME":
			// "restic-backup - Create a new backup" → description after " - "
			if idx := strings.Index(line, " - "); idx >= 0 {
				cmd.Description = line[idx+3:]
			}

		case "OPTIONS":
			parseOptionLine(line, &opt, cmd)

			// Skip inherited options on subcommand pages; they duplicate
			// what restic.1 provides as "global".
		}
	}

	flush()
	return cmd, scanner.Err()
}

// parseOptionLine handles a single content line within an OPTIONS section.
// It either starts a new option (when the line contains "--") or appends to
// the current option's description.
func parseOptionLine(line string, opt **option, cmd *command) {
	if *opt == nil {
		// Looking for a new option — must contain a long flag.
		if !strings.Contains(line, "--") {
			return
		}
		*opt = &option{}

		// Split on "=" to separate the flag spec from the default value.
		//   "-n, --dry-run[=false]"  →  ["-n, --dry-run[", "false]"]
		//   "--exclude=[]"           →  ["--exclude", "[]"]
		parts := strings.SplitN(line, "=", 2)
		flagSpec := parts[0]

		if len(parts) > 1 {
			def := strings.Trim(parts[1], "[]\"")
			(*opt).Default = def
			(*opt).Repeatable = strings.Contains(parts[1], "[]") && def == ""
			(*opt).IsBool = def == "false" || def == "true"
		}

		// Split the flag spec on "," to handle "-n, --dry-run" form.
		for _, param := range strings.Split(flagSpec, ",") {
			param = strings.TrimSpace(param)
			param = strings.Trim(param, "[]")
			if strings.HasPrefix(param, "--") {
				(*opt).Name = strings.TrimPrefix(param, "--")
			} else if strings.HasPrefix(param, "-") {
				(*opt).Alias = strings.TrimPrefix(param, "-")
			}
		}
	} else {
		// Continuation line — append to description.
		desc := strings.TrimSpace(line)
		if desc == "" {
			return
		}
		if (*opt).Description != "" {
			(*opt).Description += " "
		}
		(*opt).Description += desc
	}
}

func writeGoFile(path, version string, commands []command) error {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() { _ = f.Close() }()

	return outputTmpl.Execute(f, struct {
		Version  string
		Commands []command
	}{
		Version:  version,
		Commands: commands,
	})
}

var outputTmpl = template.Must(template.New("").Funcs(template.FuncMap{
	"quote": func(s string) string { return fmt.Sprintf("%q", s) },
}).Parse(`// Code generated by gen-restic-cmds; DO NOT EDIT.
// Source: {{.Version}}

package restic

// Commands maps restic command names to their parsed man-page definitions.
// The "global" entry contains flags common to all commands.
var Commands = map[string]Command{
{{- range .Commands}}
	{{quote .Name}}: {
		Name:        {{quote .Name}},
		Description: {{quote .Description}},
		Options: []Option{
{{- range .Options}}
			{Name: {{quote .Name}}, Alias: {{quote .Alias}}, Default: {{quote .Default}}, Description: {{quote .Description}}, Repeatable: {{printf "%v" .Repeatable}}, IsBool: {{printf "%v" .IsBool}}},
{{- end}}
		},
	},
{{- end}}
}
`))

func fatalf(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "gen-restic-cmds: "+format+"\n", args...)
	os.Exit(1)
}
