package clisetup

import (
	"bytes"
	"fmt"
	"os"
	"strings"

	"github.com/AlecAivazis/survey/v2"
	"github.com/fatih/color"
	"github.com/hexops/gotextdiff"
	"github.com/hexops/gotextdiff/myers"
	"github.com/hexops/gotextdiff/span"
	"github.com/spf13/cobra"

	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clisetup/setup"
	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/core/args"
)

type acquisitionFlags struct {
	acquisDir string
}

func (f *acquisitionFlags) bind(cmd *cobra.Command) {
	flags := cmd.Flags()
	flags.StringVar(&f.acquisDir, "acquis-dir", "", "Directory for the acquisition configuration")
}

func (cli *cliSetup) newInstallAcquisitionCmd() *cobra.Command {
	var dryRun bool

	f := acquisitionFlags{}

	cmd := &cobra.Command{
		Use:   "install-acquisition [setup_file]",
		Short: "Generate acquisition configuration from a setup file",
		Long: `Generate acquisition configuration from a setup file.

This command reads a setup.yaml specification (typically generated by 'cscli setup detect')
and creates one acquisition file for each listed service.
By default the files are placed in the acquisition directory,
which you can override with --acquis-dir.`,
		Example: `# detect running services, create a setup file
cscli setup detect > setup.yaml

# write configuration files in acquis.d
cscli setup install-acquisition setup.yaml

# write files to a specific directory
cscli setup install-acquisition --acquis-dir /path/to/acquis.d

# dry-run to preview what would be created
cscli setup install-acquisition setup.yaml --dry-run
`,
		Args:              args.ExactArgs(1),
		DisableAutoGenTag: true,
		RunE: func(_ *cobra.Command, args []string) error {
			inputReader, err := maybeStdinFile(args[0])
			if err != nil {
				return err
			}

			stup, err := setup.ParseSetupYAML(inputReader, true, cli.cfg().Cscli.Color != "no")
			if err != nil {
				return err
			}

			return cli.acquisition(stup.CollectAcquisitionSpecs(), f.acquisDir, false, dryRun)
		},
	}

	f.bind(cmd)

	flags := cmd.Flags()
	flags.BoolVar(&dryRun, "dry-run", false, "simulate the installation without making any changes")

	return cmd
}

func colorizeDiff(diff string) string {
	var b strings.Builder

	for line := range strings.SplitSeq(diff, "\n") {
		switch {
		case strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++"):
			b.WriteString(color.GreenString(line) + "\n")
		case strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---"):
			b.WriteString(color.RedString(line) + "\n")
		case strings.HasPrefix(line, "@@"):
			b.WriteString(color.New(color.Bold, color.FgCyan).Sprint(line) + "\n")
		default:
			b.WriteString(line + "\n")
		}
	}

	return b.String()
}

func shouldOverwrite(path string, newContent []byte) (bool, error) {
	oldContent, err := os.ReadFile(path)
	if err != nil {
		// File doesn't exist or unreadable, assume overwrite
		return true, nil //nolint:nilerr
	}

	if err := VerifyChecksum(bytes.NewReader(oldContent)); err == nil {
		// Valid checksum, safe to overwrite silently
		return true, nil
	}

	// Invalid or missing checksum, show diff and ask
	edits := myers.ComputeEdits(span.URIFromPath(path), string(oldContent), string(newContent))
	diff := gotextdiff.ToUnified(path, path, string(oldContent), edits)

	fmt.Fprintln(os.Stdout, color.YellowString("This file was modified after being generated by 'cscli setup'. The following changes will be made:\n"))

	fmt.Fprint(os.Stdout, colorizeDiff(fmt.Sprintf("%s", diff)))

	var overwrite bool

	prompt := &survey.Confirm{
		Message: "Do you want to overwrite with the new version?",
		Default: false,
	}

	if err := survey.AskOne(prompt, &overwrite); err != nil {
		return false, fmt.Errorf("prompt failed: %w", err)
	}

	fmt.Fprintln(os.Stdout)

	return overwrite, nil
}

// processAcquisitionSpec handles the creation of a single acquisition file.
//
// It includes an header with the appropriate checksum.
// In dry-run, prints the content to stdout instead of writing to disk.
// In interactive mode, it prompts the user before overwriting an existing file unless it's pristine.
func (*cliSetup) processAcquisitionSpec(spec setup.AcquisitionSpec, toDir string, interactive, dryRun bool) error {
	if spec.Datasource == nil {
		return nil
	}

	path, err := spec.Path(toDir)
	if err != nil {
		return err
	}

	content, err := spec.ToYAML()
	if err != nil {
		return err
	}

	if dryRun {
		_, _ = fmt.Fprintln(os.Stdout, "(dry run) "+path+"\n"+color.BlueString(string(content)))
		return nil
	}

	contentWithHeader := spec.AddHeader(content)

	if interactive {
		ok, err := shouldOverwrite(path, contentWithHeader)
		if err != nil {
			return err
		}

		if !ok {
			fmt.Fprintln(os.Stdout, "skipped "+path)
			return nil
		}
	}

	fmt.Fprintln(os.Stdout, "creating "+path)

	writer, err := spec.Open(toDir)
	if err != nil {
		return err
	}
	defer writer.Close()

	_, err = writer.Write(contentWithHeader)
	if err != nil {
		return fmt.Errorf("writing acquisition to %q: %w", path, err)
	}

	return nil
}

func (cli *cliSetup) acquisition(acquisitionSpecs []setup.AcquisitionSpec, toDir string, interactive bool, dryRun bool) error {
	cfg := cli.cfg()

	if toDir == "" {
		toDir = cfg.Crowdsec.AcquisitionDirPath
	}

	if toDir == "" {
		return fmt.Errorf("no acquisition directory specified, please use --acquis-dir or set crowdsec_services.acquisition_dir in %q", cfg.FilePath)
	}

	for idx, spec := range acquisitionSpecs {
		if err := spec.Validate(); err != nil {
			return fmt.Errorf("invalid acquisition spec (%d): %w", idx, err)
		}

		if err := cli.processAcquisitionSpec(spec, toDir, interactive, dryRun); err != nil {
			return err
		}
	}

	return nil
}
