package cmd

import (
	"fmt"
	"time"

	"github.com/spf13/cobra"

	"github.com/betterleaks/betterleaks/logging"
	"github.com/betterleaks/betterleaks/report"
	"github.com/betterleaks/betterleaks/sources"
	"github.com/betterleaks/betterleaks/sources/scm"
)

// multipleErrors wraps multiple scan errors into a single error that supports
// errors.Unwrap so callers can inspect individual errors.
type multipleErrors struct {
	msg  string
	errs []error
}

func (e *multipleErrors) Error() string   { return e.msg }
func (e *multipleErrors) Unwrap() []error { return e.errs }

func init() {
	rootCmd.AddCommand(gitCmd)
	gitCmd.Flags().String("platform", "", "the target platform used to generate links (github, gitlab)")
	gitCmd.Flags().Bool("staged", false, "scan staged commits (good for pre-commit)")
	gitCmd.Flags().Bool("pre-commit", false, "scan using git diff")
	gitCmd.Flags().String("log-opts", "", "git log options")
	gitCmd.Flags().Int("git-workers", 0, "number of parallel git log workers (0 = single process)")
}

var gitCmd = &cobra.Command{
	Use:   "git [flags] [repo]",
	Short: "scan git repositories for secrets",
	Args:  cobra.MaximumNArgs(1),
	Run:   runGit,
}

func runGit(cmd *cobra.Command, args []string) {
	// start timer
	start := time.Now()

	// grab source
	source := "."
	if len(args) == 1 {
		source = args[0]
		if source == "" {
			source = "."
		}
	}

	// setup config (aka, the thing that defines rules)
	initConfig(source)
	initDiagnostics()

	cfg := Config(cmd)

	// create detector
	detector := Detector(cmd, cfg, source)

	// parse flags
	exitCode := mustGetIntFlag(cmd, "exit-code")
	logOpts := mustGetStringFlag(cmd, "log-opts")
	staged := mustGetBoolFlag(cmd, "staged")
	preCommit := mustGetBoolFlag(cmd, "pre-commit")
	gitWorkers := mustGetIntFlag(cmd, "git-workers")
	noColor := mustGetBoolFlag(cmd, "no-color")
	redact := mustGetUIntFlag(cmd, "redact")
	verbose := mustGetBoolFlag(cmd, "verbose")

	var (
		findings    []report.Finding
		err         error
		src         sources.Source
		scmPlatform scm.Platform
	)

	if preCommit || staged {
		gitCmd, cmdErr := sources.NewGitDiffCmdContext(cmd.Context(), source, staged)
		if cmdErr != nil {
			logging.Fatal().Err(cmdErr).Msg("could not create Git diff cmd")
		}
		// Remote info + links are irrelevant for staged changes.
		src = &sources.Git{
			Cmd:             gitCmd,
			ShouldSkip:      detector.SkipFunc(),
			Platform:        scm.NoPlatform,
			Sema:            detector.Sema,
			MaxArchiveDepth: detector.MaxArchiveDepth,
		}
	} else {
		if scmPlatform, err = scm.PlatformFromString(mustGetStringFlag(cmd, "platform")); err != nil {
			logging.Fatal().Err(err).Send()
		}
		resolvedPlatform, remoteURL := sources.ResolveRemote(cmd.Context(), scmPlatform, source)

		if gitWorkers > 0 {
			src = &sources.ParallelGit{
				RepoPath:        source,
				ShouldSkip:      detector.SkipFunc(),
				Platform:        resolvedPlatform,
				RemoteURL:       remoteURL,
				Sema:            detector.Sema,
				MaxArchiveDepth: detector.MaxArchiveDepth,
				LogOpts:         logOpts,
				Workers:         gitWorkers,
			}
		} else {
			gitCmd, cmdErr := sources.NewGitLogCmdContext(cmd.Context(), source, logOpts)
			if cmdErr != nil {
				logging.Fatal().Err(cmdErr).Msg("could not create Git log cmd")
			}
			src = &sources.Git{
				Cmd:             gitCmd,
				ShouldSkip:      detector.SkipFunc(),
				Platform:        resolvedPlatform,
				RemoteURL:       remoteURL,
				Sema:            detector.Sema,
				MaxArchiveDepth: detector.MaxArchiveDepth,
			}
		}
	}

	detector.SkipFindingAppend = true
	var scanErrs []error
	for result := range detector.Run(cmd.Context(), src) {
		if result.Err != nil {
			scanErrs = append(scanErrs, result.Err)
			// don't exit on error, just log it
			logging.Error().Err(result.Err).Msg("failed to scan Git repository")
			continue
		}

		findings = append(findings, result.Finding)
		if verbose {
			if detector.LegacyPrint {
				result.Finding.PrintLegacy(noColor, redact)
			} else {
				result.Finding.Print(noColor, redact)
			}
		}
	}

	if n := len(scanErrs); n > 0 {
		err = &multipleErrors{
			msg:  fmt.Sprintf("%d error(s) encountered during scan", n),
			errs: scanErrs,
		}
	}

	findingSummaryAndExit(detector, findings, exitCode, start, err)
}
