package commands

import (
	"errors"
	"fmt"
	"os"
	"slices"

	"github.com/fatih/color"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"

	"github.com/golangci/golangci-lint/pkg/logutils"
)

func Execute(info BuildInfo) error {
	return newRootCommand(info).Execute()
}

type rootOptions struct {
	PrintVersion bool // Flag only.

	Verbose bool   // Flag only.
	Color   string // Flag only.
}

type rootCommand struct {
	cmd  *cobra.Command
	opts rootOptions

	log logutils.Log
}

func newRootCommand(info BuildInfo) *rootCommand {
	c := &rootCommand{}

	rootCmd := &cobra.Command{
		Use:   "golangci-lint",
		Short: "golangci-lint is a smart linters runner.",
		Long:  `Smart, fast linters runner.`,
		Args:  cobra.NoArgs,
		RunE: func(cmd *cobra.Command, _ []string) error {
			if c.opts.PrintVersion {
				_ = printVersion(logutils.StdOut, info)
				return nil
			}

			return cmd.Help()
		},
	}

	fs := rootCmd.Flags()
	fs.BoolVar(&c.opts.PrintVersion, "version", false, color.GreenString("Print version"))

	setupRootPersistentFlags(rootCmd.PersistentFlags(), &c.opts)

	log := logutils.NewStderrLog(logutils.DebugKeyEmpty)

	// Each command uses a dedicated configuration structure to avoid side effects of bindings.
	rootCmd.AddCommand(
		newLintersCommand(log).cmd,
		newRunCommand(log, info).cmd,
		newCacheCommand().cmd,
		newConfigCommand(log, info).cmd,
		newVersionCommand(info).cmd,
		newCustomCommand(log).cmd,
	)

	rootCmd.SetHelpCommand(newHelpCommand(log).cmd)

	c.log = log
	c.cmd = rootCmd

	return c
}

func (c *rootCommand) Execute() error {
	err := setupLogger(c.log)
	if err != nil {
		return err
	}

	return c.cmd.Execute()
}

func setupRootPersistentFlags(fs *pflag.FlagSet, opts *rootOptions) {
	fs.BoolP("help", "h", false, color.GreenString("Help for a command"))
	fs.BoolVarP(&opts.Verbose, "verbose", "v", false, color.GreenString("Verbose output"))
	fs.StringVar(&opts.Color, "color", "auto", color.GreenString("Use color when printing; can be 'always', 'auto', or 'never'"))
}

func setupLogger(logger logutils.Log) error {
	opts, err := forceRootParsePersistentFlags()
	if err != nil && !errors.Is(err, pflag.ErrHelp) {
		return err
	}

	if opts == nil {
		return nil
	}

	logutils.SetupVerboseLog(logger, opts.Verbose)

	switch opts.Color {
	case "always":
		color.NoColor = false
	case "never":
		color.NoColor = true
	case "auto":
		// nothing
	default:
		logger.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", opts.Color)
	}

	return nil
}

func forceRootParsePersistentFlags() (*rootOptions, error) {
	// We use another pflag.FlagSet here to not set `changed` flag on cmd.Flags() options.
	// Otherwise, string slice options will be duplicated.
	fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)

	// Ignore unknown flags because we will parse the command flags later.
	fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}

	opts := &rootOptions{}

	// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
	// `changed` variable inside string slice vars will be shared.
	// Use another config variable here,
	// to not affect main parsing by this parsing of only config option.
	setupRootPersistentFlags(fs, opts)

	fs.Usage = func() {} // otherwise, help text will be printed twice

	if err := fs.Parse(safeArgs(fs, os.Args)); err != nil {
		if errors.Is(err, pflag.ErrHelp) {
			return nil, err
		}

		return nil, fmt.Errorf("can't parse args: %w", err)
	}

	return opts, nil
}

// Shorthands are a problem because pflag, with UnknownFlags, will try to parse all the letters as options.
// A shorthand can aggregate several letters (ex `ps -aux`)
// The function replaces non-supported shorthands by a dumb flag.
func safeArgs(fs *pflag.FlagSet, args []string) []string {
	var shorthands []string
	fs.VisitAll(func(flag *pflag.Flag) {
		shorthands = append(shorthands, flag.Shorthand)
	})

	var cleanArgs []string
	for _, arg := range args {
		if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !slices.Contains(shorthands, string(arg[1])) {
			cleanArgs = append(cleanArgs, "--potato")
			continue
		}

		cleanArgs = append(cleanArgs, arg)
	}

	return cleanArgs
}
