package commands

import (
	"errors"

	"github.com/MakeNowJust/heredoc/v2"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"

	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	aliasCmd "gitlab.com/gitlab-org/cli/internal/commands/alias"
	apiCmd "gitlab.com/gitlab-org/cli/internal/commands/api"
	attestationCmd "gitlab.com/gitlab-org/cli/internal/commands/attestation"
	authCmd "gitlab.com/gitlab-org/cli/internal/commands/auth"
	changelogCmd "gitlab.com/gitlab-org/cli/internal/commands/changelog"
	pipelineCmd "gitlab.com/gitlab-org/cli/internal/commands/ci"
	clusterCmd "gitlab.com/gitlab-org/cli/internal/commands/cluster"
	completionCmd "gitlab.com/gitlab-org/cli/internal/commands/completion"
	configCmd "gitlab.com/gitlab-org/cli/internal/commands/config"
	containerRegistryCmd "gitlab.com/gitlab-org/cli/internal/commands/container_registry"
	deployKeyCmd "gitlab.com/gitlab-org/cli/internal/commands/deploy-key"
	duoCmd "gitlab.com/gitlab-org/cli/internal/commands/duo"
	gpgCmd "gitlab.com/gitlab-org/cli/internal/commands/gpg-key"
	"gitlab.com/gitlab-org/cli/internal/commands/help"
	incidentCmd "gitlab.com/gitlab-org/cli/internal/commands/incident"
	issueCmd "gitlab.com/gitlab-org/cli/internal/commands/issue"
	iterationCmd "gitlab.com/gitlab-org/cli/internal/commands/iteration"
	jobCmd "gitlab.com/gitlab-org/cli/internal/commands/job"
	labelCmd "gitlab.com/gitlab-org/cli/internal/commands/label"
	mcpCmd "gitlab.com/gitlab-org/cli/internal/commands/mcp"
	milestoneCmd "gitlab.com/gitlab-org/cli/internal/commands/milestone"
	mrCmd "gitlab.com/gitlab-org/cli/internal/commands/mr"
	opentofuCmd "gitlab.com/gitlab-org/cli/internal/commands/opentofu"
	orbitCmd "gitlab.com/gitlab-org/cli/internal/commands/orbit"
	packagesCmd "gitlab.com/gitlab-org/cli/internal/commands/packages"
	projectCmd "gitlab.com/gitlab-org/cli/internal/commands/project"
	releaseCmd "gitlab.com/gitlab-org/cli/internal/commands/release"
	runnerCmd "gitlab.com/gitlab-org/cli/internal/commands/runner"
	runnerControllerCmd "gitlab.com/gitlab-org/cli/internal/commands/runnercontroller"
	scheduleCmd "gitlab.com/gitlab-org/cli/internal/commands/schedule"
	searchCmd "gitlab.com/gitlab-org/cli/internal/commands/search"
	securefileCmd "gitlab.com/gitlab-org/cli/internal/commands/securefile"
	securityCmd "gitlab.com/gitlab-org/cli/internal/commands/security"
	skillsCmd "gitlab.com/gitlab-org/cli/internal/commands/skills"
	snippetCmd "gitlab.com/gitlab-org/cli/internal/commands/snippet"
	sshCmd "gitlab.com/gitlab-org/cli/internal/commands/ssh-key"
	stackCmd "gitlab.com/gitlab-org/cli/internal/commands/stack"
	todoCmd "gitlab.com/gitlab-org/cli/internal/commands/todo"
	tokenCmd "gitlab.com/gitlab-org/cli/internal/commands/token"
	updateCmd "gitlab.com/gitlab-org/cli/internal/commands/update"
	userCmd "gitlab.com/gitlab-org/cli/internal/commands/user"
	variableCmd "gitlab.com/gitlab-org/cli/internal/commands/variable"
	versionCmd "gitlab.com/gitlab-org/cli/internal/commands/version"
	whatsnewCmd "gitlab.com/gitlab-org/cli/internal/commands/whatsnew"
	workitemsCmd "gitlab.com/gitlab-org/cli/internal/commands/workitems"
)

// NewCmdRoot is the main root/parent command
func NewCmdRoot(f cmdutils.Factory) *cobra.Command {
	c := f.IO().Color()
	rootCmd := &cobra.Command{
		Use:           "glab <command> <subcommand> [flags]",
		Short:         "A GitLab CLI tool.",
		Long:          `GLab is an open source GitLab CLI tool that brings GitLab to your command line.`,
		SilenceErrors: true,
		SilenceUsage:  true,
		Annotations: map[string]string{
			"help:environment": heredoc.Docf(`
			BROWSER: The web browser to use for opening links.
			Can be set in the config with 'glab config set browser mybrowser'.

			GITLAB_CLIENT_ID: Provide custom 'client_id' generated by GitLab OAuth 2.0 application.
			Defaults to the 'client-id' for GitLab.com.

			GITLAB_HOST or GL_HOST: If GitLab Self-Managed or GitLab Dedicated, specify the URL of the GitLab server.
			(Example: %[1]shttps://gitlab.example.com%[1]s) Defaults to %[1]shttps://gitlab.com%[1]s.

			GITLAB_TOKEN: An authentication token for API requests. Set this variable to
			avoid prompts to authenticate. Overrides any previously-stored credentials.
			Can be set in the config with 'glab config set token xxxxxx'.

			GLAB_CHECK_UPDATE: Set to true to force an update check. By default the cli tool
			checks for updates once a day.

			GLAB_CONFIG_DIR: Set to a directory path to override the global configuration location.

			GLAB_DEBUG_HTTP: Set to true to output HTTP transport information (request / response).

			GLAB_SEND_TELEMETRY: Set to false to disable telemetry being sent to your GitLab instance.
			Can be set in the config with 'glab config set telemetry false'.
			See https://docs.gitlab.com/administration/settings/usage_statistics/ for more information

			NO_COLOR: Set to any value to avoid printing ANSI escape sequences for color output.

			REMOTE_ALIAS or GIT_REMOTE_URL_VAR: A 'git remote' variable or alias that contains
			the GitLab URL. Can be set in the config with 'glab config set remote_alias origin'.

			VISUAL, EDITOR (in order of precedence): The editor tool to use for authoring text.
			Can be set in the config with 'glab config set editor vim'.

			GLAB_DEBUG: Set to true to output more logging information, including underlying Git commands,
			expanded aliases and DNS error details.

			GLAB_ENABLE_CI_AUTOLOGIN: Set to %[1]strue%[1]s to enable auto-login in GitLab CI.
			CI auto-login detects if glab is running in a GitLab CI job by checking the predefined
			CI/CD variable %[1]sGITLAB_CI%[1]s.
			If detected, it uses predefined CI/CD variables such as %[1]sCI_SERVER_FQDN%[1]s and
			%[1]sCI_JOB_TOKEN%[1]s to sign in, and ignores host variables like %[1]sGITLAB_HOST%[1]s.
			If %[1]sGITLAB_TOKEN%[1]s, %[1]sGITLAB_ACCESS_TOKEN%[1]s, or %[1]sOAUTH_TOKEN%[1]s
			are set, they take precedence over %[1]sCI_JOB_TOKEN%[1]s.
			Only glab commands that support %[1]sCI_JOB_TOKEN%[1]s work with CI auto-login.
			For a list of supported commands, see https://docs.gitlab.com/ci/jobs/ci_job_token/#job-token-access.

			GLAB_FORCE_HYPERLINKS: Set to true to force hyperlinks in output, even when not outputting to a TTY.

			GLAB_GLAMOUR_STYLE: The environment variable to set your desired Markdown renderer style.
			Available options: dark, light, notty. To set a custom style, read
			https://github.com/charmbracelet/glamour#styles

			GLAB_NO_PROMPT: Set to true (or 1) to disable prompts, or false (or 0) to enable them.

			GLAB_SHOW_WHATS_NEW: Set to false to suppress the one-time post-upgrade banner
			that points at 'glab whatsnew' when a new version is detected.
			Can be set in the config with 'glab config set show_whats_new false'.
		`, "`"),
			"help:feedback": heredoc.Docf(`
			Encountered a bug or want to suggest a feature?
			Open an issue using '%s'
		`, c.Bold(c.Yellow("glab issue create -R gitlab-org/cli"))),
		},
	}

	// We deliberately do NOT call rootCmd.SetOut / rootCmd.SetErr here.
	// Cobra's c.Print() (used for deprecation warnings, "unknown help topic",
	// usage-on-error, etc.) routes through OutOrStderr, which returns Out when
	// it is set and falls back to os.Stderr otherwise. Wiring Out to StdOut
	// pollutes the stdout data channel with diagnostics — see
	// https://gitlab.com/gitlab-org/cli/-/issues/8371 and
	// https://github.com/spf13/cobra/issues/1708. Instead, the help and usage
	// funcs receive the IOStreams explicitly and route their own output.

	rootCmd.PersistentFlags().BoolP("help", "h", false, "Show help for this command.")
	rootCmd.SetHelpFunc(func(command *cobra.Command, args []string) {
		help.RootHelpFunc(f.IO(), command, args)
	})
	rootCmd.SetUsageFunc(func(command *cobra.Command) error {
		return help.RootUsageFunc(f.IO().StdErr, command)
	})
	rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
		if errors.Is(err, pflag.ErrHelp) {
			return err
		}
		return &cmdutils.FlagError{Err: err}
	})

	buildInfo := f.BuildInfo()
	formattedVersion := versionCmd.Scheme(buildInfo.Version, buildInfo.Commit)
	rootCmd.SetVersionTemplate(formattedVersion)
	rootCmd.Version = formattedVersion

	// Child commands
	rootCmd.AddCommand(aliasCmd.NewCmdAlias(f))
	rootCmd.AddCommand(configCmd.NewCmdConfig(f))
	rootCmd.AddCommand(completionCmd.NewCmdCompletion(f.IO()))
	rootCmd.AddCommand(versionCmd.NewCmdVersion(f))
	rootCmd.AddCommand(updateCmd.NewCheckUpdateCmd(f))
	rootCmd.AddCommand(whatsnewCmd.NewCmd(f))
	rootCmd.AddCommand(authCmd.NewCmdAuth(f))

	rootCmd.AddCommand(apiCmd.NewCmdApi(f, nil))
	rootCmd.AddCommand(changelogCmd.NewCmdChangelog(f))
	rootCmd.AddCommand(clusterCmd.NewCmdCluster(f))
	rootCmd.AddCommand(containerRegistryCmd.NewCmd(f))
	rootCmd.AddCommand(deployKeyCmd.NewCmdDeployKey(f))
	rootCmd.AddCommand(duoCmd.NewCmd(f))
	rootCmd.AddCommand(gpgCmd.NewCmdGPGKey(f))
	rootCmd.AddCommand(incidentCmd.NewCmdIncident(f))
	rootCmd.AddCommand(issueCmd.NewCmdIssue(f))
	rootCmd.AddCommand(iterationCmd.NewCmdIteration(f))
	rootCmd.AddCommand(jobCmd.NewCmdJob(f))
	rootCmd.AddCommand(labelCmd.NewCmdLabel(f))
	rootCmd.AddCommand(mcpCmd.NewCmdMCP(f))
	rootCmd.AddCommand(milestoneCmd.NewCmdMilestone(f))
	rootCmd.AddCommand(mrCmd.NewCmdMR(f))
	rootCmd.AddCommand(opentofuCmd.NewCmd(f))
	rootCmd.AddCommand(orbitCmd.NewCmd(f))
	rootCmd.AddCommand(packagesCmd.NewCmd(f))
	rootCmd.AddCommand(attestationCmd.NewCmdAttestation(f))
	rootCmd.AddCommand(pipelineCmd.NewCmdCI(f))
	rootCmd.AddCommand(projectCmd.NewCmdRepo(f))
	rootCmd.AddCommand(releaseCmd.NewCmdRelease(f))
	rootCmd.AddCommand(runnerCmd.NewCmdRunner(f))
	rootCmd.AddCommand(scheduleCmd.NewCmdSchedule(f))
	rootCmd.AddCommand(searchCmd.NewCmd(f))
	rootCmd.AddCommand(securefileCmd.NewCmdSecurefile(f))
	rootCmd.AddCommand(securityCmd.NewCmd(f))
	rootCmd.AddCommand(snippetCmd.NewCmdSnippet(f))
	rootCmd.AddCommand(sshCmd.NewCmdSSHKey(f))
	rootCmd.AddCommand(stackCmd.NewCmdStack(f))
	rootCmd.AddCommand(todoCmd.NewCmd(f))
	rootCmd.AddCommand(tokenCmd.NewTokenCmd(f))
	rootCmd.AddCommand(userCmd.NewCmdUser(f))
	rootCmd.AddCommand(variableCmd.NewVariableCmd(f))
	rootCmd.AddCommand(runnerControllerCmd.NewCmd(f))
	rootCmd.AddCommand(skillsCmd.NewCmdSkills(f))
	rootCmd.AddCommand(workitemsCmd.NewCmdWorkItems(f))
	// TODO: This can probably be removed by GitLab 18.3
	// See: https://gitlab.com/gitlab-org/cli/-/issues/7885
	// Add global repo override flag but keep it hidden
	cmdutils.AddGlobalRepoOverride(rootCmd, f)

	rootCmd.Flags().BoolP("version", "v", false, "Show glab version information.")
	return rootCmd
}
