package cli

import (
	"context"
	"errors"
	"os"

	"github.com/MakeNowJust/heredoc/v2"
	"github.com/spf13/cobra"
	"golang.org/x/sys/cpu"

	"gitlab.com/gitlab-org/cli/internal/binarymgr"
	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	"gitlab.com/gitlab-org/cli/internal/config"
	"gitlab.com/gitlab-org/cli/internal/iostreams"
)

// duoMaxCompatibleMajorVersion caps Duo CLI auto-updates to this major
// version. Bump after validating compatibility against the new major.
const duoMaxCompatibleMajorVersion = 9

// Spec returns the binarymgr.Spec describing the Duo CLI binary. It is
// exported so tests and other consumers can introspect it.
func Spec() binarymgr.Spec {
	return binarymgr.Spec{
		DisplayName:        "GitLab Duo CLI",
		ProjectID:          "46519181",
		PackageName:        "duo-cli",
		ConfigPrefix:       "duo_cli",
		EnvVarPrefix:       "GLAB_DUO_CLI",
		MaxCompatibleMajor: duoMaxCompatibleMajorVersion,
		SupportedOS:        []string{"darwin", "linux", "windows"},
		NormalizeArch:      duoNormalizeArch,
		AssetName:          duoAssetName,
		InstalledName:      duoInstalledName,
		// Duo ships raw binaries — no extraction needed.
		Extract: nil,
	}
}

func duoNormalizeArch(goos, goarch string) (string, error) {
	return duoNormalizeArchFor(goos, goarch, detectLinuxX64ArchVariant)
}

// duoNormalizeArchFor is duoNormalizeArch's testable core. The Linux x64
// variant detector is injected so tests don't depend on the host CPU.
func duoNormalizeArchFor(goos, goarch string, linuxX64Variant func() string) (string, error) {
	switch goarch {
	case "amd64":
		switch goos {
		case "windows":
			return "x64-baseline", nil
		case "linux":
			return linuxX64Variant(), nil
		}
		return "x64", nil
	case "arm64", "aarch64":
		return "arm64", nil
	}
	return "", binarymgr.ErrUnsupportedPlatform
}

// detectLinuxX64ArchVariant mirrors the upstream Duo CLI installer's
// detect_linux_x64_variant: "x64-modern" when the host CPU advertises AVX2,
// otherwise "x64" (baseline).
//
// AVX2 is a sufficient proxy for the full feature set Bun's modern target
// requires (AVX2 + BMI2 + FMA): every CPU that advertises AVX2 also has
// BMI2 and FMA (Haswell+ / Excavator+), so checking AVX2 alone matches the
// upstream installer's bash detect_linux_x64_variant exactly.
func detectLinuxX64ArchVariant() string {
	// Linux uses "x64" instead of "x64-baseline" to preserve
	// compatibility with previous versions of glab as x64 should work on most systems
	// and it matches the duo cli install script:
	// https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/247ec22ab64e4160ff5776c4254598623f537738/packages/cli/scripts/install_duo_cli.sh
	if cpu.X86.HasAVX2 {
		return "x64-modern"
	}
	return "x64"
}

func duoAssetName(goos, arch string) string {
	name := "duo-" + goos + "-" + arch
	if goos == "windows" {
		name += ".exe"
	}
	return name
}

func duoInstalledName(goos string) string {
	if goos == "windows" {
		return "duo.exe"
	}
	return "duo"
}

// NewCmd creates the `glab duo cli` command.
func NewCmd(f cmdutils.Factory) *cobra.Command {
	spec := Spec()
	cmd := &cobra.Command{
		Use:   "cli [command]",
		Short: "Run the GitLab Duo CLI.",
		Long: heredoc.Docf(`Run the GitLab Duo CLI.

Use the GitLab Duo CLI to bring the GitLab Duo Agent Platform to your terminal.
Ask GitLab Duo questions about your codebase and use it to autonomously perform actions
on your behalf.

When you use the GitLab Duo CLI in the GitLab CLI, %[1]sglab%[1]s handles
authentication for you automatically.
You only need to authenticate once.

Prerequisites:

- Use GitLab 19.2 or later.
- Run %[1]sglab auth login%[1]s to authenticate.
- Meet the [prerequisites for GitLab Duo Agent Platform](https://docs.gitlab.com/user/duo_agent_platform/#prerequisites).
- For GitLab Self-Managed and GitLab Dedicated on 19.2 or later, turn on [GitLab Duo CLI access](https://docs.gitlab.com/user/gitlab_duo_cli/#turn-gitlab-duo-cli-access-on-or-off). It is on by default.

Note: If you are on GitLab 18.11 to 19.1, you can use the GitLab Duo CLI by turning on [beta and experimental features](https://docs.gitlab.com/user/duo_agent_platform/turn_on_off.md#turn-on-beta-and-experimental-features).

Configuration options:

- %[1]sduo_cli_auto_run%[1]s: Skip the run confirmation prompt.
- %[1]sduo_cli_auto_download%[1]s: Skip the download confirmation prompt.

All other arguments and flags are passed through to the GitLab Duo CLI binary.

For more information, see the [GitLab Duo CLI documentation](https://docs.gitlab.com/user/gitlab_duo_cli/).
`, "`"),
		Annotations: map[string]string{
			"help:environment": heredoc.Docf(`
				- %[1]sGLAB_DUO_CLI_BINARY_PATH%[1]s: Use a local binary instead of the managed one.
				  Skips download, version checks, and updates. Can also be set via the
				  %[1]sduo_cli_binary_path%[1]s configuration key.
				`, "`"),
		},
		Example: heredoc.Doc(`
			# Run the GitLab Duo CLI
			glab duo cli

			# Pass any command or flag through to the GitLab Duo CLI binary (for example: version, run, help)
			glab duo cli <command>

			# Show this help
			glab duo cli --help

			# Show GitLab Duo CLI help
			glab duo cli help

			# Run without prompts (for use in scripts and non-interactive environments)
			glab duo cli --yes

			# Install the GitLab Duo CLI binary
			glab duo cli --install

			# Install the GitLab Duo CLI binary without prompts
			glab duo cli --install --yes

			# Check for and install updates
			glab duo cli --update`),
		DisableFlagParsing: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			runner := newRunner(f.IO(), f.Config(), spec)

			// DisableFlagParsing is on, so split glab-owned flags from
			// pass-through args manually. --help/-h shows glab's help only
			// when nothing has been collected for pass-through yet.
			var remaining []string
			for _, arg := range args {
				switch arg {
				case "--update":
					runner.Update = true
				case "--install":
					runner.Install = true
				case "--yes", "-y":
					runner.Yes = true
				case "--help", "-h":
					if len(remaining) == 0 {
						return cmd.Help()
					}
					remaining = append(remaining, arg)
				default:
					remaining = append(remaining, arg)
				}
			}
			runner.Args = remaining

			if runner.Install && runner.Update {
				return errors.New("the --install and --update flags are mutually exclusive")
			}

			warnIfSnapConfined(f.IO(), os.Getenv, runner.Install, runner.Update)

			if runner.Install {
				return runner.HandleInstall(cmd.Context())
			}
			return runner.Run(cmd.Context())
		},
	}

	// Registered for documentation only — DisableFlagParsing means Cobra
	// never parses these; the RunE switch above handles them manually.
	fl := cmd.Flags()
	fl.BoolP("yes", "y", false, "Skip confirmation prompts.")
	fl.Bool("install", false, "Install the GitLab Duo CLI binary without running it.")
	fl.Bool("update", false, "Check for and install updates to the binary.")

	return cmd
}

// warnIfSnapConfined prints a clear warning when glab is running inside a
// snap. The Duo CLI authenticates by spawning `glab auth credential-helper`,
// but snap's AppArmor profile blocks the downloaded Duo binary from exec'ing
// back into glab, so the credential lookup fails with a confusing
// "no credentials found" error even after a successful `glab auth login`.
//
// The warning only applies to the actual `glab duo cli` run path: --install
// and --update don't depend on the credential-helper callback and stay
// silent. getenv is injected for testability.
func warnIfSnapConfined(io *iostreams.IOStreams, getenv func(string) string, install, update bool) {
	if install || update {
		return
	}
	if getenv("SNAP") == "" {
		return
	}
	msg := heredoc.Docf(`
		%[1]s glab is running under snap confinement.

		The GitLab Duo CLI authenticates by spawning %[2]sglab auth credential-helper%[2]s, but
		snap's sandbox blocks that callback. Authentication is likely to fail with
		"no credentials found" even after a successful %[2]sglab auth login%[2]s.

		To use %[2]sglab duo cli%[2]s, install glab from a non-snap source:
		  - Homebrew:     %[2]sbrew install glab%[2]s
		  - mise:         %[2]smise use -g glab@latest%[2]s
		  - Native binary: https://gitlab.com/gitlab-org/cli/-/releases

		Or set the GITLAB_TOKEN environment variable (%[2]sGITLAB_TOKEN=glpat-xyz glab duo cli%[2]s).
		For GitLab Self-Managed, also set GITLAB_URL (%[2]sGITLAB_URL=https://gitlab.example.com GITLAB_TOKEN=glpat-xyz glab duo cli%[2]s).

	`, io.Color().DotWarnIcon(), "`")
	io.LogError(msg)
}

// newRunner builds a binarymgr.Runner for the Duo CLI. The Executor is
// platform-specific: syscall.Exec on Unix (in execute_unix.go), subprocess
// on Windows (in execute_windows.go). Each declares executeDuoCLI(io, ...).
func newRunner(io *iostreams.IOStreams, cfg config.Config, spec binarymgr.Spec) *binarymgr.Runner {
	return &binarymgr.Runner{
		IO:      io,
		Cfg:     cfg,
		Spec:    spec,
		Manager: binarymgr.NewManager(io, spec),
		Executor: func(ctx context.Context, binaryPath string, args []string) error {
			return executeDuoCLI(ctx, io, binaryPath, args)
		},
		UpdateCommand: "duo cli",
	}
}
