package clisetup

import (
	"bytes"
	"cmp"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"slices"
	"strings"

	"github.com/AlecAivazis/survey/v2"
	"github.com/fatih/color"
	"github.com/sirupsen/logrus"

	"github.com/crowdsecurity/go-cs-lib/csyaml"

	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clisetup/setup"
	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
	"github.com/crowdsecurity/crowdsec/pkg/hubops"
)

// isGeneratedYAML returns true if all the docs are generated by "cscli setup", empty or contain only comments.
func isGeneratedOrEmpty(input io.Reader, logger logrus.FieldLogger) (bool, error) {
	docs, err := csyaml.SplitDocuments(input)
	if err != nil {
		return false, err
	}

	for _, doc := range docs {
		empty, err := csyaml.IsEmptyYAML(bytes.NewReader(doc))
		if err != nil {
			return false, err
		}

		if empty {
			continue
		}

		// just check the whole doc, but it could be more precise and check only the first comment

		if !strings.Contains(string(doc), `Configuration generated by "cscli setup"`) {
			return false, nil
		}

		// has the document been altered by the user?
		if err = VerifyChecksum(bytes.NewReader(doc)); err != nil {
			logger.WithError(err).Debug("checksum verification failed")
			return false, nil
		}
	}

	return true, nil
}

func filterGeneratedAcquis(acquisFiles []string, logger logrus.FieldLogger) ([]string, error) {
	ret := []string{}

	for _, fileName := range acquisFiles {
		file, err := os.Open(fileName)
		if err != nil {
			return nil, err
		}

		isGenerated, err := isGeneratedOrEmpty(file, logger)
		if err != nil {
			file.Close()
			return nil, fmt.Errorf("parsing %q: %w", fileName, err)
		}

		file.Close()

		if !isGenerated {
			ret = append(ret, fileName)
		}
	}

	return ret, nil
}

func leavingMessage(w io.Writer) {
	fmt.Fprintln(w, "Leaving crowdsec configuration.")
	fmt.Fprintln(w, "You can always run 'cscli setup' later.")
}

func (cli *cliSetup) wizard(ctx context.Context, detectConfig *setup.DetectConfig, opts setup.DetectOptions, acquisDir string, interactive bool, dryRun bool, logger logrus.FieldLogger) error {
	cfg := cli.cfg()

	if err := require.Agent(cfg); err != nil {
		return err
	}

	userAcquisFiles, err := filterGeneratedAcquis(cfg.Crowdsec.AcquisitionFiles, logger)
	if err != nil {
		return err
	}

	if dryRun {
		fmt.Fprintln(os.Stdout, color.YellowString("Dry run mode enabled. No changes will be made."))
		fmt.Fprintln(os.Stdout)
	}

	detect := true

	if !dryRun && len(userAcquisFiles) != 0 {
		fmt.Fprintln(os.Stdout, "The following acquisition files were not generated by crowdsec, or were altered:")

		for _, f := range userAcquisFiles {
			fmt.Fprintln(os.Stdout, " - "+f)
		}

		if !interactive {
			fmt.Fprintln(os.Stdout, "The automatic setup will not proceed to avoid any conflict with the above files. To add new services, run 'cscli setup' in a shell.")
			return nil
		}

		fmt.Fprintln(os.Stdout)
		fmt.Fprintln(os.Stdout, "After the setup, make sure that these files don't conflict with the new acquisition configuration. In particular, don't parse the same log files multiple times.")
		fmt.Fprintln(os.Stdout)

		// change to a safe default to avoid overwriting anything without user consent
		detect = false
	}

	if interactive {
		prompt := survey.Confirm{
			Message: "Detect and configure services?",
			Default: detect,
		}

		if err := survey.AskOne(&prompt, &detect); err != nil {
			return err
		}
	}

	fmt.Fprintln(os.Stdout)

	if !detect {
		leavingMessage(os.Stdout)
		return nil
	}

	units := setup.UnitMap{}

	if !opts.SkipSystemd {
		if units, err = setup.DetectSystemdUnits(ctx, exec.CommandContext); err != nil {
			return err
		}
	}

	procs, err := setup.DetectProcesses(ctx, logger)
	if err != nil {
		return err
	}

	stup, err := setup.BuildSetup(ctx, detectConfig, opts,
		setup.OSExprPath{},
		units, procs, logger)
	if err != nil {
		return err
	}

	svcDetected := stup.DetectedServices()

	switch {
	case len(svcDetected) == 0:
		fmt.Fprintln(os.Stdout, "No services detected.")
		return nil
	case interactive:
		svcSelected := []string{}

		prompt := &survey.MultiSelect{
			Message: "Please confirm the services to configure. Excluding them will skip the related scenarios and log acquisition.\n",
			Options: svcDetected,
			Default: svcDetected,
		}

		err := survey.AskOne(prompt, &svcSelected)
		if err != nil {
			return err
		}

		svcFiltered := []setup.ServicePlan{}

		for _, svc := range stup.Plans {
			if slices.Contains(svcSelected, svc.Name) {
				svcFiltered = append(svcFiltered, svc)
			}
		}

		stup.Plans = svcFiltered
	default:
		fmt.Fprintln(os.Stdout, "The following services will be configured:")

		for _, svc := range svcDetected {
			fmt.Fprintf(os.Stdout, "- %s\n", svc)
		}
	}

	fmt.Fprintln(os.Stdout)

	hubSpecs := stup.CollectHubSpecs()

	if len(hubSpecs) > 0 {
		_, err := cli.install(ctx, interactive, dryRun, hubSpecs, logger)
		switch {
		case errors.Is(err, hubops.ErrUserCanceled):
			// not a real error... no need to exit with 1
			// and nothing happened, so no reload message
			fmt.Fprintln(os.Stdout, err.Error())
			leavingMessage(os.Stdout)

			return nil
		case err != nil:
			return err
		}

		fmt.Fprintln(os.Stdout)
	}

	acquisitionSpecs := stup.CollectAcquisitionSpecs()

	if len(acquisitionSpecs) > 0 {
		installAcquis := true

		if interactive {
			prompt := survey.Confirm{
				Message: "Generate acquisition configuration?",
				Default: true,
			}

			if err := survey.AskOne(&prompt, &installAcquis); err != nil {
				return err
			}
		}

		fmt.Fprintln(os.Stdout)

		if installAcquis {
			acquisDir = cmp.Or(acquisDir, cli.cfg().Crowdsec.AcquisitionDirPath)
			if err := cli.acquisition(acquisitionSpecs, acquisDir, interactive, dryRun); err != nil {
				return err
			}
		} else {
			leavingMessage(os.Stdout)
		}
	}

	if msg := reload.UserMessage(); msg != "" && (len(acquisitionSpecs) > 0 || len(hubSpecs) > 0) {
		yellow := color.YellowString
		yellowBold := color.New(color.FgYellow, color.Bold).SprintFunc()

		fmt.Fprintln(os.Stdout, yellow(`
CrowdSec alone will not block any IP address. If you want to block them, you must use a remediation component.
You can find them on`), yellowBold("https://app.crowdsec.net/hub/remediation-components"))

		fmt.Fprintln(os.Stdout, "\n"+msg)
	}

	return nil
}

func maybeStdinFile(path string) (*os.File, error) {
	if path == "-" {
		return os.Stdin, nil
	}

	reader, err := os.Open(path)
	if err != nil {
		return nil, err
	}

	return reader, nil
}
