package docs

import (
	"bytes"
	_ "embed"
	"fmt"
	"io"
	"os"
	"reflect"
	"regexp"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"text/template"
	"unicode/utf8"

	"github.com/cpuguy83/go-md2man/v2/md2man"
	"github.com/urfave/cli/v3"
)

var (
	//go:embed markdown.md.gotmpl
	MarkdownDocTemplate string

	//go:embed markdown_tabular.md.gotmpl
	MarkdownTabularDocTemplate string

	isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on"
)

func tracef(format string, a ...any) {
	if !isTracingOn {
		return
	}

	if !strings.HasSuffix(format, "\n") {
		format = format + "\n"
	}

	pc, file, line, _ := runtime.Caller(1)
	cf := runtime.FuncForPC(pc)

	fmt.Fprintf(
		os.Stderr,
		strings.Join([]string{
			"## URFAVE CLI TRACE ",
			file,
			":",
			fmt.Sprintf("%v", line),
			" ",
			fmt.Sprintf("(%s)", cf.Name()),
			" ",
			format,
		}, ""),
		a...,
	)
}

// ToTabularMarkdown creates a tabular markdown documentation for
// the `*cli.Command`. The function errors if either parsing or
// writing of the string fails.
func ToTabularMarkdown(cmd *cli.Command, appPath string) (string, error) {
	if appPath == "" {
		appPath = "app"
	}

	const name = "cli"

	t, err := template.New(name).Funcs(template.FuncMap{
		"join": strings.Join,
	}).Parse(MarkdownTabularDocTemplate)
	if err != nil {
		return "", err
	}

	var (
		w  bytes.Buffer
		tt tabularTemplate
	)

	if err = t.ExecuteTemplate(&w, name, cliTabularAppTemplate{
		AppPath:     appPath,
		Name:        cmd.Name,
		Description: tt.PrepareMultilineString(cmd.Description),
		Usage:       tt.PrepareMultilineString(cmd.Usage),
		UsageText:   strings.FieldsFunc(cmd.UsageText, func(r rune) bool { return r == '\n' }),
		ArgsUsage:   tt.PrepareMultilineString(cmd.ArgsUsage),
		GlobalFlags: tt.PrepareFlags(cmd.VisibleFlags()),
		Commands:    tt.PrepareCommands(cmd.VisibleCommands(), appPath, "", 0),
	}); err != nil {
		return "", err
	}

	return tt.Prettify(w.String()), nil
}

// ToTabularToFileBetweenTags creates a tabular markdown documentation for the `*App` and updates the file between
// the tags in the file. The function errors if either parsing or writing of the string fails.
func ToTabularToFileBetweenTags(cmd *cli.Command, appPath, filePath string, startEndTags ...string) error {
	var start, end = "<!--GENERATED:CLI_DOCS-->", "<!--/GENERATED:CLI_DOCS-->" // default tags

	if len(startEndTags) == 2 {
		start, end = startEndTags[0], startEndTags[1]
	}

	// read original file content
	content, err := os.ReadFile(filePath)
	if err != nil {
		return err
	}

	// generate markdown
	md, err := ToTabularMarkdown(cmd, appPath)
	if err != nil {
		return err
	}

	// prepare regexp to replace content between start and end tags
	re, err := regexp.Compile("(?s)" + regexp.QuoteMeta(start) + "(.*?)" + regexp.QuoteMeta(end))
	if err != nil {
		return err
	}

	const comment = "<!-- Documentation inside this block generated by github.com/urfave/cli-docs/v3; DO NOT EDIT -->"

	// replace content between start and end tags
	updated := re.ReplaceAll(content, []byte(strings.Join([]string{start, comment, md, end}, "\n")))

	// write updated content to file
	if err = os.WriteFile(filePath, updated, 0664); err != nil {
		return err
	}

	return nil
}

// ToMarkdown creates a markdown string for the `*cli.Command`
// The function errors if either parsing or writing of the string fails.
func ToMarkdown(cmd *cli.Command) (string, error) {
	var w bytes.Buffer
	if err := writeDocTemplate(cmd, &w, 0); err != nil {
		return "", err
	}
	return w.String(), nil
}

// ToMan creates a man page string with section number for the
// `*cli.Command` The function errors if either parsing or writing
// of the string fails.
func ToManWithSection(cmd *cli.Command, sectionNumber int) (string, error) {
	var w bytes.Buffer
	if err := writeDocTemplate(cmd, &w, sectionNumber); err != nil {
		return "", err
	}
	man := md2man.Render(w.Bytes())
	return string(man), nil
}

// ToMan creates a man page string for the `*cli.Command`
// The function errors if either parsing or writing of the string fails.
func ToMan(cmd *cli.Command) (string, error) {
	man, err := ToManWithSection(cmd, 8)
	return man, err
}

type cliCommandTemplate struct {
	Command      *cli.Command
	SectionNum   int
	Commands     []string
	GlobalArgs   []string
	SynopsisArgs []string
}

func writeDocTemplate(cmd *cli.Command, w io.Writer, sectionNum int) error {
	tracef("using MarkdownDocTemplate starting %[1]q", string([]byte(MarkdownDocTemplate)[0:8]))

	const name = "cli"
	t, err := template.New(name).Parse(MarkdownDocTemplate)
	if err != nil {
		return err
	}

	return t.ExecuteTemplate(w, name, &cliCommandTemplate{
		Command:      cmd,
		SectionNum:   sectionNum,
		Commands:     prepareCommands(cmd.Commands, 0),
		GlobalArgs:   prepareArgsWithValues(cmd.VisibleFlags()),
		SynopsisArgs: prepareArgsSynopsis(cmd.VisibleFlags()),
	})
}

func prepareCommands(commands []*cli.Command, level int) []string {
	var coms []string
	for _, command := range commands {
		if command.Hidden {
			continue
		}

		usageText := prepareUsageText(command)

		usage := prepareUsage(command, usageText)

		prepared := fmt.Sprintf("%s %s\n\n%s%s",
			strings.Repeat("#", level+2),
			strings.Join(command.Names(), ", "),
			usage,
			usageText,
		)

		flags := prepareArgsWithValues(command.VisibleFlags())
		if len(flags) > 0 {
			prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
		}

		coms = append(coms, prepared)

		// recursively iterate subcommands
		if len(command.Commands) > 0 {
			coms = append(
				coms,
				prepareCommands(command.Commands, level+1)...,
			)
		}
	}

	return coms
}

func prepareArgsWithValues(flags []cli.Flag) []string {
	return prepareFlags(flags, ", ", "**", "**", `""`, true)
}

func prepareArgsSynopsis(flags []cli.Flag) []string {
	return prepareFlags(flags, "|", "[", "]", "[value]", false)
}

func prepareFlags(
	flags []cli.Flag,
	sep, opener, closer, value string,
	addDetails bool,
) []string {
	args := []string{}
	for _, f := range flags {
		flag, ok := f.(cli.DocGenerationFlag)
		if !ok {
			continue
		}
		modifiedArg := opener

		for _, s := range f.Names() {
			trimmed := strings.TrimSpace(s)
			if len(modifiedArg) > len(opener) {
				modifiedArg += sep
			}
			if len(trimmed) > 1 {
				modifiedArg += fmt.Sprintf("--%s", trimmed)
			} else {
				modifiedArg += fmt.Sprintf("-%s", trimmed)
			}
		}
		modifiedArg += closer
		if flag.TakesValue() {
			modifiedArg += fmt.Sprintf("=%s", value)
		}

		if addDetails {
			modifiedArg += flagDetails(flag)
		}

		args = append(args, modifiedArg+"\n")

	}
	sort.Strings(args)
	return args
}

// flagDetails returns a string containing the flags metadata
func flagDetails(flag cli.DocGenerationFlag) string {
	description := flag.GetUsage()
	value := getFlagDefaultValue(flag)
	if value != "" {
		description += " (default: " + value + ")"
	}
	return ": " + description
}

func prepareUsageText(command *cli.Command) string {
	if command.UsageText == "" {
		return ""
	}

	// Remove leading and trailing newlines
	preparedUsageText := strings.Trim(command.UsageText, "\n")

	var usageText string
	if strings.Contains(preparedUsageText, "\n") {
		// Format multi-line string as a code block using the 4 space schema to allow for embedded markdown such
		// that it will not break the continuous code block.
		for _, ln := range strings.Split(preparedUsageText, "\n") {
			usageText += fmt.Sprintf("    %s\n", ln)
		}
	} else {
		// Style a single line as a note
		usageText = fmt.Sprintf(">%s\n", preparedUsageText)
	}

	return usageText
}

func prepareUsage(command *cli.Command, usageText string) string {
	if command.Usage == "" {
		return ""
	}

	usage := command.Usage + "\n"
	// Add a newline to the Usage IFF there is a UsageText
	if usageText != "" {
		usage += "\n"
	}

	return usage
}

type (
	cliTabularAppTemplate struct {
		AppPath     string
		Name        string
		Usage       string
		ArgsUsage   string
		UsageText   []string
		Description string
		GlobalFlags []cliTabularFlagTemplate
		Commands    []cliTabularCommandTemplate
	}

	cliTabularCommandTemplate struct {
		AppPath     string
		Name        string
		Aliases     []string
		Usage       string
		ArgsUsage   string
		UsageText   []string
		Description string
		Category    string
		Flags       []cliTabularFlagTemplate
		SubCommands []cliTabularCommandTemplate
		Level       uint
	}

	cliTabularFlagTemplate struct {
		Name       string
		Aliases    []string
		Usage      string
		TakesValue bool
		Default    string
		EnvVars    []string
		Type       string
	}
)

// tabularTemplate is a struct for the tabular template preparation.
type tabularTemplate struct{}

// PrepareCommands converts CLI commands into a structs for the rendering.
func (tt tabularTemplate) PrepareCommands(commands []*cli.Command, appPath, parentCommandName string, level uint) []cliTabularCommandTemplate {
	var result = make([]cliTabularCommandTemplate, 0, len(commands))

	for _, cmd := range commands {
		var command = cliTabularCommandTemplate{
			AppPath:     appPath,
			Name:        strings.TrimSpace(strings.Join([]string{parentCommandName, cmd.Name}, " ")),
			Aliases:     cmd.Aliases,
			Usage:       tt.PrepareMultilineString(cmd.Usage),
			UsageText:   strings.FieldsFunc(cmd.UsageText, func(r rune) bool { return r == '\n' }),
			ArgsUsage:   tt.PrepareMultilineString(cmd.ArgsUsage),
			Description: tt.PrepareMultilineString(cmd.Description),
			Category:    cmd.Category,
			Flags:       tt.PrepareFlags(cmd.VisibleFlags()),
			SubCommands: tt.PrepareCommands( // note: recursive call
				cmd.Commands,
				appPath,
				strings.Join([]string{parentCommandName, cmd.Name}, " "),
				level+1,
			),
			Level: level,
		}

		result = append(result, command)
	}

	return result
}

// PrepareFlags converts CLI flags into a structs for the rendering.
func (tt tabularTemplate) PrepareFlags(flags []cli.Flag) []cliTabularFlagTemplate {
	var result = make([]cliTabularFlagTemplate, 0, len(flags))

	for _, appFlag := range flags {
		flag, ok := appFlag.(cli.DocGenerationFlag)
		if !ok {
			continue
		}

		var f = cliTabularFlagTemplate{
			Usage:      tt.PrepareMultilineString(flag.GetUsage()),
			EnvVars:    flag.GetEnvVars(),
			TakesValue: flag.TakesValue(),
			Default:    getFlagDefaultValue(flag),
			Type:       flag.TypeName(),
		}

		if boolFlag, isBool := appFlag.(*cli.BoolFlag); isBool {
			f.Default = strconv.FormatBool(boolFlag.Value)
		}

		for i, name := range appFlag.Names() {
			name = strings.TrimSpace(name)

			if i == 0 {
				f.Name = "--" + name

				continue
			}

			if len(name) > 1 {
				name = "--" + name
			} else {
				name = "-" + name
			}

			f.Aliases = append(f.Aliases, name)
		}

		result = append(result, f)
	}

	return result
}

// PrepareMultilineString prepares a string (removes line breaks).
func (tabularTemplate) PrepareMultilineString(s string) string {
	return strings.TrimRight(
		strings.TrimSpace(
			strings.ReplaceAll(s, "\n", " "),
		),
		".\r\n\t",
	)
}

func (tabularTemplate) Prettify(s string) string {
	var max = func(x, y int) int {
		if x > y {
			return x
		}
		return y
	}

	var b strings.Builder

	// search for tables
	for _, rawTable := range regexp.MustCompile(`(?m)^(\|[^\n]+\|\r?\n)((?:\|:?-+:?)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$`).FindAllString(s, -1) {
		var lines = strings.FieldsFunc(rawTable, func(r rune) bool { return r == '\n' })

		if len(lines) < 3 { // header, separator, body
			continue
		}

		// parse table into the matrix
		var matrix = make([][]string, 0, len(lines))
		for _, line := range lines {
			items := strings.FieldsFunc(strings.Trim(line, "| "), func(r rune) bool { return r == '|' })

			for i := range items {
				items[i] = strings.TrimSpace(items[i]) // trim spaces in cells
			}

			matrix = append(matrix, items)
		}

		// determine centered columns
		var centered = make([]bool, 0, len(matrix[1]))
		for _, cell := range matrix[1] {
			centered = append(centered, strings.HasPrefix(cell, ":") && strings.HasSuffix(cell, ":"))
		}

		// calculate max lengths
		var lengths = make([]int, len(matrix[0]))
		for n, row := range matrix {
			for i, cell := range row {
				if n == 1 {
					continue // skip separator
				}

				if l := utf8.RuneCountInString(cell); l > lengths[i] {
					lengths[i] = l
				}
			}
		}

		// format cells
		for i, row := range matrix {
			for j, cell := range row {
				if i == 1 { // is separator
					if centered[j] {
						b.Reset()
						b.WriteRune(':')
						b.WriteString(strings.Repeat("-", max(0, lengths[j])))
						b.WriteRune(':')

						row[j] = b.String()
					} else {
						row[j] = strings.Repeat("-", max(0, lengths[j]+2))
					}

					continue
				}

				var (
					cellWidth         = utf8.RuneCountInString(cell)
					padLeft, padRight = 1, max(1, lengths[j]-cellWidth+1) // align to the left
				)

				if centered[j] { // is centered
					padLeft = max(1, (lengths[j]-cellWidth)/2)
					padRight = max(1, lengths[j]-cellWidth-(padLeft-1))
				}

				b.Reset()
				b.WriteString(strings.Repeat(" ", padLeft))

				if padLeft+cellWidth+padRight <= lengths[j]+1 {
					b.WriteRune(' ') // add an extra space if the cell is not full
				}

				b.WriteString(cell)
				b.WriteString(strings.Repeat(" ", padRight))

				row[j] = b.String()
			}
		}

		b.Reset()

		for _, row := range matrix { // build new table
			b.WriteRune('|')
			b.WriteString(strings.Join(row, "|"))
			b.WriteRune('|')
			b.WriteRune('\n')
		}

		s = strings.Replace(s, rawTable, b.String(), 1)
	}

	s = regexp.MustCompile(`\n{2,}`).ReplaceAllString(s, "\n\n") // normalize newlines
	s = strings.Trim(s, " \n")                                   // trim spaces and newlines

	return s + "\n" // add an extra newline
}

// getFlagDefaultValue returns the default value of a flag. Previously, the [cli.DocGenerationFlag] interface included
// a GetValue string method, but it was removed in https://github.com/urfave/cli/pull/1988.
// This function serves as a workaround, attempting to retrieve the value using the removed method; if that fails, it
// tries to obtain it via reflection (the [cli.FlagBase] still has a Value field).
func getFlagDefaultValue(f cli.DocGenerationFlag) string {
	if !f.TakesValue() {
		return ""
	}

	if v, ok := f.(interface{ GetValue() string }); ok {
		return v.GetValue()
	}

	var ref = reflect.ValueOf(f)
	if ref.Kind() != reflect.Ptr {
		return ""
	} else {
		ref = ref.Elem()
	}

	if ref.Kind() != reflect.Struct {
		return ""
	}

	if val := ref.FieldByName("Value"); val.IsValid() && val.Type().Kind() != reflect.Bool {
		return fmt.Sprintf("%v", val.Interface())
	}

	return ""
}
