// Package commands implements the ipfs command interface
//
// Using github.com/ipfs/kubo/commands to define the command line and HTTP
// APIs.  This is the interface available to folks using IPFS from outside of
// the Go language.
package commands

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"slices"
	"strings"

	cmds "github.com/ipfs/go-ipfs-cmds"
)

type commandEncoder struct {
	w io.Writer
}

func (e *commandEncoder) Encode(v interface{}) error {
	var (
		cmd *Command
		ok  bool
	)

	if cmd, ok = v.(*Command); !ok {
		return fmt.Errorf(`core/commands: unexpected type %T, expected *"core/commands".Command`, v)
	}

	for _, s := range cmdPathStrings(cmd, cmd.showOpts) {
		_, err := e.w.Write([]byte(s + "\n"))
		if err != nil {
			return err
		}
	}

	return nil
}

type Command struct {
	Name        string
	Subcommands []Command
	Options     []Option

	showOpts bool
}

type Option struct {
	Names []string
}

const (
	flagsOptionName = "flags"
)

// CommandsCmd takes in a root command,
// and returns a command that lists the subcommands in that root
func CommandsCmd(root *cmds.Command) *cmds.Command {
	return &cmds.Command{
		Helptext: cmds.HelpText{
			Tagline:          "List all available commands.",
			ShortDescription: `Lists all available commands (and subcommands) and exits.`,
		},
		Subcommands: map[string]*cmds.Command{
			"completion": CompletionCmd(root),
		},
		Options: []cmds.Option{
			cmds.BoolOption(flagsOptionName, "f", "Show command flags"),
		},
		Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
		Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
			rootCmd := cmd2outputCmd("ipfs", root)
			rootCmd.showOpts, _ = req.Options[flagsOptionName].(bool)
			return cmds.EmitOnce(res, &rootCmd)
		},
		Encoders: cmds.EncoderMap{
			cmds.Text: func(req *cmds.Request) func(io.Writer) cmds.Encoder {
				return func(w io.Writer) cmds.Encoder { return &commandEncoder{w} }
			},
		},
		Type: Command{},
	}
}

func cmd2outputCmd(name string, cmd *cmds.Command) Command {
	opts := make([]Option, len(cmd.Options))
	for i, opt := range cmd.Options {
		opts[i] = Option{opt.Names()}
	}

	output := Command{
		Name:        name,
		Subcommands: make([]Command, 0, len(cmd.Subcommands)),
		Options:     opts,
	}

	for name, sub := range cmd.Subcommands {
		output.Subcommands = append(output.Subcommands, cmd2outputCmd(name, sub))
	}

	return output
}

func cmdPathStrings(cmd *Command, showOptions bool) []string {
	var cmds []string

	var recurse func(prefix string, cmd *Command)
	recurse = func(prefix string, cmd *Command) {
		newPrefix := prefix + cmd.Name
		cmds = append(cmds, newPrefix)
		if prefix != "" && showOptions {
			for _, options := range cmd.Options {
				var cmdOpts []string
				for _, flag := range options.Names {
					if len(flag) == 1 {
						flag = "-" + flag
					} else {
						flag = "--" + flag
					}
					cmdOpts = append(cmdOpts, newPrefix+" "+flag)
				}
				cmds = append(cmds, strings.Join(cmdOpts, " / "))
			}
		}
		for _, sub := range cmd.Subcommands {
			recurse(newPrefix+" ", &sub)
		}
	}

	recurse("", cmd)
	slices.Sort(cmds)
	return cmds
}

func CompletionCmd(root *cmds.Command) *cmds.Command {
	return &cmds.Command{
		Helptext: cmds.HelpText{
			Tagline: "Generate shell completions.",
		},
		NoRemote: true,
		Subcommands: map[string]*cmds.Command{
			"bash": {
				Helptext: cmds.HelpText{
					Tagline:          "Generate bash shell completions.",
					ShortDescription: "Generates command completions for the bash shell.",
					LongDescription: `
Generates command completions for the bash shell.

The simplest way to see it working is write the completions
to a file and then source it:

  > ipfs commands completion bash > ipfs-completion.bash
  > source ./ipfs-completion.bash

To install the completions permanently, they can be moved to
/etc/bash_completion.d or sourced from your ~/.bashrc file.
`,
				},
				NoRemote: true,
				Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
					var buf bytes.Buffer
					if err := writeBashCompletions(root, &buf); err != nil {
						return err
					}
					res.SetLength(uint64(buf.Len()))
					return res.Emit(&buf)
				},
			},
			"zsh": {
				Helptext: cmds.HelpText{
					Tagline:          "Generate zsh shell completions.",
					ShortDescription: "Generates command completions for the zsh shell.",
					LongDescription: `
Generates command completions for the zsh shell.

The simplest way to see it working is write the completions
to a file and then source it:

  > ipfs commands completion zsh > ipfs-completion.zsh
  > source ./ipfs-completion.zsh

To install the completions permanently, they can be moved to
/etc/zsh/completions or sourced from your ~/.zshrc file.
`,
				},
				NoRemote: true,
				Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
					var buf bytes.Buffer
					if err := writeZshCompletions(root, &buf); err != nil {
						return err
					}
					res.SetLength(uint64(buf.Len()))
					return res.Emit(&buf)
				},
			},
			"fish": {
				Helptext: cmds.HelpText{
					Tagline:          "Generate fish shell completions.",
					ShortDescription: "Generates command completions for the fish shell.",
					LongDescription: `
Generates command completions for the fish shell.

The simplest way to see it working is write the completions
to a file and then source it:

  > ipfs commands completion fish > ipfs-completion.fish
  > source ./ipfs-completion.fish

To install the completions permanently, they can be moved to
/etc/fish/completions or ~/.config/fish/completions or sourced from your ~/.config/fish/config.fish file.
`,
				},
				NoRemote: true,
				Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
					var buf bytes.Buffer
					if err := writeFishCompletions(root, &buf); err != nil {
						return err
					}
					res.SetLength(uint64(buf.Len()))
					return res.Emit(&buf)
				},
			},
		},
	}
}

type nonFatalError string

// streamResult is a helper function to stream results that possibly
// contain non-fatal errors.  The helper function is allowed to panic
// on internal errors.
func streamResult(procVal func(interface{}, io.Writer) nonFatalError) func(cmds.Response, cmds.ResponseEmitter) error {
	return func(res cmds.Response, re cmds.ResponseEmitter) (rerr error) {
		defer func() {
			if r := recover(); r != nil {
				rerr = fmt.Errorf("internal error: %v", r)
			}
		}()

		var errors bool
		for {
			v, err := res.Next()
			if err != nil {
				if err == io.EOF {
					break
				}
				rerr = err
				return
			}

			errorMsg := procVal(v, os.Stdout)

			if errorMsg != "" {
				errors = true
				fmt.Fprintf(os.Stderr, "%s\n", errorMsg)
			}
		}

		if errors {
			rerr = fmt.Errorf("errors while displaying some entries")
		}
		return
	}
}
