package cli

import (
	"bytes"
	"fmt"
	"io"
	"strings"
	"text/template"
)

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

type fishCommandCompletionTemplate struct {
	Command     *Command
	Completions []string
	AllCommands []string
}

func (cmd *Command) writeFishCompletionTemplate(w io.Writer) error {
	const name = "cli"
	t, err := template.New(name).Parse(FishCompletionTemplate)
	if err != nil {
		return err
	}

	// Add global flags
	completions := prepareFishFlags(cmd.Name, cmd)

	if cmd.ShellComplete != nil {
		var completion strings.Builder
		fmt.Fprintf(&completion,
			"complete -c %s -n '%s' -xa '(%s %s 2>/dev/null)'",
			cmd.Name,
			fishFlagHelper(cmd.Name, cmd),
			cmd.Name,
			completionFlag,
		)
		completions = append(completions, completion.String())
	}

	// Add commands and their flags
	completions = append(
		completions,
		prepareFishCommands(cmd.Name, cmd)...,
	)

	toplevelCommandNames := []string{}
	for _, child := range cmd.Commands {
		toplevelCommandNames = append(toplevelCommandNames, child.Names()...)
	}

	return t.ExecuteTemplate(w, name, &fishCommandCompletionTemplate{
		Command:     cmd,
		Completions: completions,
		AllCommands: toplevelCommandNames,
	})
}

func prepareFishCommands(binary string, parent *Command) []string {
	commands := parent.Commands
	completions := []string{}
	for _, command := range commands {
		if !command.Hidden {
			var completion strings.Builder
			fmt.Fprintf(&completion,
				"complete -x -c %s -n '%s' -a '%s'",
				binary,
				fishSubcommandHelper(binary, parent, commands),
				command.Name,
			)

			if command.Usage != "" {
				fmt.Fprintf(&completion,
					" -d '%s'",
					escapeSingleQuotes(command.Usage))
			}
			completions = append(completions, completion.String())
		}

		if command.ShellComplete != nil {
			var completion strings.Builder
			var path []string
			lineage := command.Lineage()
			for i := len(lineage) - 2; i >= 0; i-- {
				path = append(path, lineage[i].Name)
			}

			fmt.Fprintf(&completion,
				"complete -c %s -n '%s' -xa '(%s %s %s 2>/dev/null)'",
				binary,
				fishFlagHelper(binary, command),
				binary,
				strings.Join(path, " "),
				completionFlag,
			)
			completions = append(completions, completion.String())
		}

		completions = append(
			completions,
			prepareFishFlags(binary, command)...,
		)

		// recursively iterate subcommands
		completions = append(
			completions,
			prepareFishCommands(binary, command)...,
		)
	}

	return completions
}

func prepareFishFlags(binary string, owner *Command) []string {
	flags := owner.VisibleFlags()
	completions := []string{}
	for _, f := range flags {
		completion := &strings.Builder{}
		fmt.Fprintf(completion,
			"complete -c %s -n '%s'",
			binary,
			fishFlagHelper(binary, owner),
		)

		fishAddFileFlag(f, completion)

		for idx, opt := range f.Names() {
			if idx == 0 {
				fmt.Fprintf(completion,
					" -l %s", strings.TrimSpace(opt),
				)
			} else {
				fmt.Fprintf(completion,
					" -s %s", strings.TrimSpace(opt),
				)
			}
		}

		if flag, ok := f.(DocGenerationFlag); ok {
			if flag.TakesValue() {
				completion.WriteString(" -r")
			}

			if flag.GetUsage() != "" {
				fmt.Fprintf(completion,
					" -d '%s'",
					escapeSingleQuotes(flag.GetUsage()))
			}
		}

		completions = append(completions, completion.String())
	}

	return completions
}

func fishAddFileFlag(flag Flag, completion *strings.Builder) {
	switch f := flag.(type) {
	case *StringFlag:
		if f.TakesFile {
			return
		}
	case *StringSliceFlag:
		if f.TakesFile {
			return
		}
	}
	completion.WriteString(" -f")
}

func fishSubcommandHelper(binary string, command *Command, siblings []*Command) string {
	fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary)
	if len(command.Lineage()) > 1 {
		var siblingNames []string
		for _, sibling := range siblings {
			siblingNames = append(siblingNames, sibling.Names()...)
		}
		ancestry := commandAncestry(command)
		fishHelper = fmt.Sprintf(
			"%s; and not __fish_seen_subcommand_from %s",
			ancestry,
			strings.Join(siblingNames, " "),
		)
	}
	return fishHelper
}

func fishFlagHelper(binary string, command *Command) string {
	fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary)
	if len(command.Lineage()) > 1 {
		fishHelper = commandAncestry(command)
	}
	return fishHelper
}

func commandAncestry(command *Command) string {
	var ancestry []string
	ancestors := command.Lineage()
	for i := len(ancestors) - 2; i >= 0; i-- {
		ancestry = append(
			ancestry,
			fmt.Sprintf(
				"__fish_seen_subcommand_from %s",
				strings.Join(ancestors[i].Names(), " "),
			),
		)
	}
	return strings.Join(ancestry, "; and ")
}

// escapeSingleQuotes escapes a string for use inside a fish single-quoted
// string, such as a `-d '...'` description. Within single quotes fish only
// recognizes the escape sequences \\ and \', so the backslash must be escaped
// as well as the single quote. Escaping the quote without escaping the
// backslash corrupts any description that contains a backslash, and a trailing
// backslash even leaves the single-quoted string unterminated.
// See https://fishshell.com/docs/current/language.html
func escapeSingleQuotes(input string) string {
	return fishSingleQuoteReplacer.Replace(input)
}

var fishSingleQuoteReplacer = strings.NewReplacer(`\`, `\\`, `'`, `\'`)
