package cliitem

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"os"
	"slices"
	"strings"

	"github.com/agext/levenshtein"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"

	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/core/args"
	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/core/reload"
	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/core/require"
	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
	"github.com/crowdsecurity/crowdsec/pkg/hubops"
)

// suggestNearestMessage returns a message with the most similar item name, if one is found
func suggestNearestMessage(hub *cwhub.Hub, itemType string, itemName string) string {
	const maxDistance = 7

	score := 100
	nearest := ""

	for _, item := range hub.GetItemsByType(itemType, false) {
		d := levenshtein.Distance(itemName, item.Name, nil)
		if d < score {
			score = d
			nearest = item.Name
		}
	}

	msg := fmt.Sprintf("can't find '%s' in %s", itemName, itemType)

	if score < maxDistance {
		msg += fmt.Sprintf(", did you mean '%s'?", nearest)
	}

	return msg
}

func (cli *cliItem) install(ctx context.Context, args []string, interactive bool, dryRun bool, downloadOnly bool, force bool, ignoreError bool) error {
	cfg := cli.cfg()

	hub, err := require.Hub(cfg, log.StandardLogger())
	if err != nil {
		return err
	}

	plan := hubops.NewActionPlan(hub)

	contentProvider, err := require.HubDownloader(ctx, cfg)
	if err != nil {
		return err
	}

	for _, name := range args {
		item := hub.GetItem(cli.name, name)
		if item == nil {
			msg := suggestNearestMessage(hub, cli.name, name)
			if !ignoreError {
				return errors.New(msg)
			}

			log.Error(msg)

			continue
		}

		if err = plan.AddCommand(hubops.NewDownloadCommand(item, contentProvider, force)); err != nil {
			return err
		}

		if !downloadOnly {
			if err = plan.AddCommand(hubops.NewEnableCommand(item, force)); err != nil {
				return err
			}
		}
	}

	showPlan := (log.StandardLogger().Level >= log.InfoLevel)
	verbosePlan := (cfg.Cscli.Output == "raw")

	err = plan.Execute(ctx, interactive, dryRun, showPlan, verbosePlan)
	switch {
	case errors.Is(err, hubops.ErrUserCanceled) && err != nil:
		// not a real error, and we'll want to print the reload message anyway
		fmt.Fprintln(os.Stdout, err.Error())
	case ignoreError:
		log.Error(err)
	case err != nil:
		return err
	}

	if msg := reload.UserMessage(); msg != "" && plan.ReloadNeeded {
		fmt.Fprintln(os.Stdout, "\n"+msg)
	}

	return nil
}

func compAllItems(itemType string, args []string, toComplete string, cfg csconfig.Getter) ([]string, cobra.ShellCompDirective) {
	hub, err := require.Hub(cfg(), nil)
	if err != nil {
		return nil, cobra.ShellCompDirectiveDefault
	}

	comp := make([]string, 0)

	for _, item := range hub.GetItemsByType(itemType, false) {
		if !slices.Contains(args, item.Name) && strings.Contains(item.Name, toComplete) {
			comp = append(comp, item.Name)
		}
	}

	cobra.CompDebugln(fmt.Sprintf("%s: %+v", itemType, comp), true)

	return comp, cobra.ShellCompDirectiveNoFileComp
}

func (cli *cliItem) newInstallCmd() *cobra.Command {
	var (
		interactive  bool
		dryRun       bool
		downloadOnly bool
		force        bool
		ignoreError  bool
	)

	cmd := &cobra.Command{
		Use:               cmp.Or(cli.installHelp.use, "install [item]..."),
		Short:             cmp.Or(cli.installHelp.short, "Install given "+cli.oneOrMore),
		Long:              cmp.Or(cli.installHelp.long, fmt.Sprintf("Fetch and install one or more %s from the hub", cli.name)),
		Example:           cli.installHelp.example,
		Args:              args.MinimumNArgs(1),
		DisableAutoGenTag: true,
		ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			return compAllItems(cli.name, args, toComplete, cli.cfg)
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			return cli.install(cmd.Context(), args, interactive, dryRun, downloadOnly, force, ignoreError)
		},
	}

	flags := cmd.Flags()
	flags.BoolVarP(&interactive, "interactive", "i", false, "Ask for confirmation before proceeding")
	flags.BoolVar(&dryRun, "dry-run", false, "Don't install or remove anything; print the execution plan")
	flags.BoolVarP(&downloadOnly, "download-only", "d", false, "Only download packages, don't enable")
	flags.BoolVar(&force, "force", false, "Force install: overwrite tainted and outdated files")
	flags.BoolVar(&ignoreError, "ignore", false, "Ignore errors when installing multiple "+cli.name)
	cmd.MarkFlagsMutuallyExclusive("interactive", "dry-run")

	return cmd
}
