// Command cli2md generates documentation pages from CLI flags.
package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"github.com/alecthomas/kingpin/v2"
	"github.com/pkg/errors"

	"github.com/kopia/kopia/cli"
	_ "github.com/kopia/kopia/internal/logfile"
)

//nolint:gochecknoglobals
var baseDir = flag.String("base-dir", "content/docs/Reference/Command-Line", "Base directory")

const (
	advancedSection        = "Advanced"
	advancedCommandsWeight = 6

	commonSection        = "Common"
	commonCommandsWeight = 5

	dirMode = 0o750
)

// The group at the start is to avoid matching things like "in-place".
var escapeFlagsRe = regexp.MustCompile(`(^|\s)(--?\w+[-\w]*)`)

// escapeFlags escapes command-line flag references in help text by wrapping them in backticks.
func escapeFlags(text string) string {
	return escapeFlagsRe.ReplaceAllString(text, "$1`$2`")
}

//nolint:gochecknoglobals
var overrideDefault = map[string]string{
	"config-file": "repository.config",
	"log-dir":     "kopia",
}

func emitFlags(w io.Writer, flags []*kingpin.FlagModel) {
	if len(flags) == 0 {
		return
	}

	fmt.Fprintf(w, "| Flag | Short | Default | Help |\n") //nolint:errcheck
	fmt.Fprintf(w, "| ---- | ----- | --- | --- |\n")      //nolint:errcheck

	for _, f := range sortFlags(flags) {
		maybeAdvanced := ""

		if f.Hidden {
			maybeAdvanced = "[ADV] "
		}

		shortFlag := ""
		if f.Short != 0 {
			shortFlag = "`-" + string([]byte{byte(f.Short)}) + "`"
		}

		defaultValue := ""
		if len(f.Default) > 0 {
			defaultValue = f.Default[0]
		}

		if def, ok := overrideDefault[f.Name]; ok {
			defaultValue = def
		}

		if defaultValue != "" {
			defaultValue = "`" + defaultValue + "`"
		}

		if f.IsBoolFlag() {
			if defaultValue == "" {
				defaultValue = "`false`"
			}

			fmt.Fprintf(w, "| `--[no-]%v` | %v | %v | %v%v |\n", f.Name, shortFlag, defaultValue, maybeAdvanced, escapeFlags(f.Help)) //nolint:errcheck
		} else {
			fmt.Fprintf(w, "| `--%v` | %v | %v | %v%v |\n", f.Name, shortFlag, defaultValue, maybeAdvanced, escapeFlags(f.Help)) //nolint:errcheck
		}
	}

	fmt.Fprintf(w, "\n") //nolint:errcheck
}

func combineFlags(lists ...[]*kingpin.FlagModel) []*kingpin.FlagModel {
	var result []*kingpin.FlagModel

	for _, list := range lists {
		result = append(result, list...)
	}

	return result
}

func sortFlags(f []*kingpin.FlagModel) []*kingpin.FlagModel {
	sort.Slice(f, func(i, j int) bool {
		a, b := f[i], f[j]

		if l, r := a.Hidden, b.Hidden; l != r {
			return !l
		}

		return a.Name < b.Name
	})

	return f
}

func emitArgs(w io.Writer, args []*kingpin.ArgModel) {
	if len(args) == 0 {
		return
	}

	fmt.Fprintf(w, "| Argument | Help |\n") //nolint:errcheck
	fmt.Fprintf(w, "| -------- | ---  |\n") //nolint:errcheck

	args2 := append([]*kingpin.ArgModel(nil), args...)
	sort.Slice(args2, func(i, j int) bool {
		return args2[i].Name < args2[j].Name
	})

	for _, f := range args2 {
		fmt.Fprintf(w, "| `%v` | %v |\n", f.Name, escapeFlags(f.Help)) //nolint:errcheck
	}

	fmt.Fprintf(w, "\n") //nolint:errcheck
}

func generateAppFlags(app *kingpin.ApplicationModel) error {
	f, err := os.Create(filepath.Join(*baseDir, "flags.md"))
	if err != nil {
		return errors.Wrap(err, "unable to create common flags file")
	}
	defer f.Close() //nolint:errcheck

	title := "Flags"

	//nolint:errcheck
	fmt.Fprintf(f, `---
title: %q
linkTitle: %q
weight: 3
---
`, title, title)
	emitFlags(f, app.Flags)

	return nil
}

func generateCommands(app *kingpin.ApplicationModel, section string, weight int, advanced bool) error {
	dir := filepath.Join(*baseDir, section)

	if err := os.MkdirAll(dir, dirMode); err != nil {
		return errors.Wrapf(err, "error creating section directory for %v", section)
	}

	f, err := os.Create(filepath.Join(dir, "_index.md")) //nolint:gosec
	if err != nil {
		return errors.Wrap(err, "unable to create common flags file")
	}
	defer f.Close() //nolint:errcheck

	title := section + " Commands"

	//nolint:errcheck
	fmt.Fprintf(f, `---
title: %q
linkTitle: %q
weight: %v
hide_summary: true
no_list: true
---
`, title, title, weight)

	flat := flattenCommands(app.Commands)
	for _, c := range flat {
		generateSubcommands(f, dir, c.Help, c.Commands, advanced)
	}

	return nil
}

func flattenCommands(cmds []*kingpin.CmdModel) []*kingpin.CmdModel {
	var result []*kingpin.CmdModel

	commonRoot := &kingpin.CmdModel{
		Name:          "Common Commands",
		Help:          "Common Commands",
		CmdGroupModel: &kingpin.CmdGroupModel{},
	}
	result = append(result, commonRoot)

	for _, c := range cmds {
		if len(c.Commands) == 0 {
			commonRoot.Commands = append(commonRoot.Commands, c)
			continue
		}

		root := &kingpin.CmdModel{
			Name:          c.Name,
			FullCommand:   c.FullCommand,
			Help:          c.Help,
			Hidden:        c.Hidden,
			CmdGroupModel: &kingpin.CmdGroupModel{},
		}
		result = append(result, root)
		root.Commands = flattenChildren(c, nil, c.Hidden)
	}

	return result
}

func flattenChildren(cmd *kingpin.CmdModel, parentFlags []*kingpin.FlagModel, forceHidden bool) []*kingpin.CmdModel {
	var result []*kingpin.CmdModel

	cmdFlags := combineFlags(parentFlags, cmd.Flags)

	if len(cmd.Commands) == 0 {
		cmdClone := *cmd
		if forceHidden {
			cmdClone.Hidden = true
		}

		cmdClone.Flags = cmdFlags

		result = append(result, &cmdClone)
	} else {
		for _, c := range cmd.Commands {
			result = append(result, flattenChildren(c, cmdFlags, c.Hidden || forceHidden)...)
		}
	}

	return result
}

func generateSubcommands(w io.Writer, dir, sectionTitle string, cmds []*kingpin.CmdModel, advanced bool) {
	cmds = append([]*kingpin.CmdModel(nil), cmds...)

	first := true

	for _, c := range cmds {
		if c.Hidden != advanced {
			continue
		}

		if first {
			fmt.Fprintf(w, "\n### %v\n\n", strings.TrimSuffix(sectionTitle, ".")) //nolint:errcheck

			first = false
		}

		subcommandSlug := strings.ReplaceAll(c.FullCommand, " ", "-")
		helpSummary := strings.SplitN(c.Help, "\n", 2)[0] //nolint:mnd
		helpSummary = strings.TrimSuffix(helpSummary, ".")
		fmt.Fprintf(w, "* [`%v`](%v) - %v\n", c.FullCommand, subcommandSlug+"/", helpSummary) //nolint:errcheck
		generateSubcommandPage(filepath.Join(dir, subcommandSlug+".md"), c)
	}
}

func generateSubcommandPage(fname string, cmd *kingpin.CmdModel) {
	f, err := os.Create(fname) //nolint:gosec
	if err != nil {
		log.Fatalf("unable to create page: %v", err)
	}
	defer f.Close() //nolint:errcheck

	title := cmd.FullCommand

	//nolint:errcheck
	fmt.Fprintf(f, `---
title: %q
linkTitle: %q
weight: 10
toc_hide: true
hide_summary: true
---

`, title, title)

	var (
		argSummary  strings.Builder
		flagSummary strings.Builder
	)

	for _, a := range cmd.Args {
		if a.Required {
			argSummary.WriteString(" <")
			argSummary.WriteString(a.Name)
			argSummary.WriteRune('>')
		} else {
			argSummary.WriteString(" [")
			argSummary.WriteString(a.Name)
			argSummary.WriteRune(']')
		}
	}

	for _, fl := range cmd.Flags {
		if fl.Required {
			flagSummary.WriteString(" \\\n        --" + fl.Name + "=...")
		}
	}

	fmt.Fprintf(f, "```shell\n$ kopia %v%v%v\n```\n\n", cmd.FullCommand, flagSummary.String(), argSummary.String()) //nolint:errcheck
	fmt.Fprintf(f, "%v\n\n", escapeFlags(cmd.Help))                                                                 //nolint:errcheck

	emitFlags(f, cmd.Flags)
	emitArgs(f, cmd.Args)
}

func main() {
	flag.Parse()

	if _, err := os.Stat(*baseDir); err != nil {
		log.Fatalf("invalid base directory: %v", err)
	}

	_ = os.RemoveAll(filepath.Join(*baseDir, commonSection))
	_ = os.RemoveAll(filepath.Join(*baseDir, advancedSection))

	kingpinApp := kingpin.New("kopia", "Kopia - Fast And Secure Open-Source Backup").Author("http://kopia.github.io/")
	cli.NewApp().Attach(kingpinApp)

	app := kingpinApp.Model()

	if err := generateAppFlags(app); err != nil {
		log.Fatalf("unable to generate common flags: %v", err)
	}

	if err := generateCommands(app, commonSection, commonCommandsWeight, false); err != nil {
		log.Fatalf("unable to generate common commands: %v", err)
	}

	if err := generateCommands(app, advancedSection, advancedCommandsWeight, true); err != nil {
		log.Fatalf("unable to generate advanced commands: %v", err)
	}
}
