package login

import (
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"strings"

	"gitlab.com/gitlab-org/cli/commands/auth/authutils"

	"gitlab.com/gitlab-org/cli/pkg/iostreams"

	"github.com/AlecAivazis/survey/v2"
	"github.com/MakeNowJust/heredoc"
	"github.com/spf13/cobra"
	"gitlab.com/gitlab-org/cli/api"
	"gitlab.com/gitlab-org/cli/commands/cmdutils"
	"gitlab.com/gitlab-org/cli/internal/config"
	"gitlab.com/gitlab-org/cli/pkg/glinstance"
)

type LoginOptions struct {
	IO     *iostreams.IOStreams
	Config func() (config.Config, error)

	Interactive bool

	Hostname string
	Token    string
}

var opts *LoginOptions

func NewCmdLogin(f *cmdutils.Factory) *cobra.Command {
	opts = &LoginOptions{
		IO:     f.IO,
		Config: f.Config,
	}

	var tokenStdin bool

	cmd := &cobra.Command{
		Use:   "login",
		Args:  cobra.ExactArgs(0),
		Short: "Authenticate with a GitLab instance",
		Long: heredoc.Docf(`
			Authenticate with a GitLab instance.
			You can pass in a token on standard input by using %[1]s--stdin%[1]s.
			The minimum required scopes for the token are: %[1]sapi%[1]s, %[1]swrite_repository%[1]s.
		`, "`"),
		Example: heredoc.Docf(`
			# start interactive setup
			$ glab auth login
			# authenticate against %[1]sgitlab.com%[1]s by reading the token from a file
			$ glab auth login --stdin < myaccesstoken.txt
			# authenticate with a self-hosted GitLab instance
			$ glab auth login --hostname salsa.debian.org
		`, "`"),
		RunE: func(cmd *cobra.Command, args []string) error {
			if !opts.IO.PromptEnabled() && !tokenStdin && opts.Token == "" {
				return &cmdutils.FlagError{Err: errors.New("--stdin or --token required when not running interactively")}
			}

			if opts.Token != "" && tokenStdin {
				return &cmdutils.FlagError{Err: errors.New("specify one of --token or --stdin. You cannot use both flags at the same time")}
			}

			if tokenStdin {
				defer opts.IO.In.Close()
				token, err := io.ReadAll(opts.IO.In)
				if err != nil {
					return fmt.Errorf("failed to read token from STDIN: %w", err)
				}
				opts.Token = strings.TrimSpace(string(token))
			}

			if opts.IO.PromptEnabled() && opts.Token == "" && opts.IO.IsaTTY {
				opts.Interactive = true
			}

			if cmd.Flags().Changed("hostname") {
				if err := hostnameValidator(opts.Hostname); err != nil {
					return &cmdutils.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
				}
			}

			if !opts.Interactive {
				if opts.Hostname == "" {
					opts.Hostname = glinstance.Default()
				}
			}

			return loginRun()
		},
	}

	cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitLab instance to authenticate with")
	cmd.Flags().StringVarP(&opts.Token, "token", "t", "", "Your GitLab access token")
	cmd.Flags().BoolVar(&tokenStdin, "stdin", false, "Read token from standard input")

	return cmd
}

func loginRun() error {
	c := opts.IO.Color()
	cfg, err := opts.Config()
	if err != nil {
		return err
	}

	if opts.Token != "" {
		if opts.Hostname == "" {
			return errors.New("empty hostname would leak oauth_token")
		}

		err := cfg.Set(opts.Hostname, "token", opts.Token)
		if err != nil {
			return err
		}

		return cfg.Write()
	}

	hostname := opts.Hostname
	apiHostname := opts.Hostname
	defaultHostname := glinstance.OverridableDefault()
	isSelfHosted := false

	if hostname == "" {
		var hostType int
		err := survey.AskOne(&survey.Select{
			Message: "What GitLab instance do you want to log into?",
			Options: []string{
				defaultHostname,
				"GitLab Self-hosted Instance",
			},
		}, &hostType)
		if err != nil {
			return fmt.Errorf("could not prompt: %w", err)
		}

		isSelfHosted = hostType == 1

		hostname = defaultHostname
		apiHostname = hostname
		if isSelfHosted {
			err := survey.AskOne(&survey.Input{
				Message: "GitLab hostname:",
			}, &hostname, survey.WithValidator(hostnameValidator))
			if err != nil {
				return fmt.Errorf("could not prompt: %w", err)
			}
			err = survey.AskOne(&survey.Input{
				Message: "API hostname:",
				Help:    "For instances with different hostname for the API endpoint",
				Default: hostname,
			}, &apiHostname, survey.WithValidator(hostnameValidator))
			if err != nil {
				return fmt.Errorf("could not prompt: %w", err)
			}
		}
	}

	fmt.Fprintf(opts.IO.StdErr, "- Logging into %s\n", hostname)

	if token := config.GetFromEnv("token"); token != "" {
		fmt.Fprintf(opts.IO.StdErr, "%s you have GITLAB_TOKEN or OAUTH_TOKEN environment variable set. Unset if you don't want to use it for glab\n", c.Yellow("!WARNING:"))
	}
	existingToken, _, _ := cfg.GetWithSource(hostname, "token", false)

	if existingToken != "" && opts.Interactive {
		apiClient, err := cmdutils.LabClientFunc(hostname, cfg, false)
		if err != nil {
			return err
		}

		user, err := api.CurrentUser(apiClient)
		if err == nil {
			username := user.Username
			var keepGoing bool
			err = survey.AskOne(&survey.Confirm{
				Message: fmt.Sprintf(
					"You're already logged into %s as %s. Do you want to re-authenticate?",
					hostname,
					username),
				Default: false,
			}, &keepGoing)
			if err != nil {
				return fmt.Errorf("could not prompt: %w", err)
			}

			if !keepGoing {
				return nil
			}
		}
	}

	fmt.Fprintln(opts.IO.StdErr)
	fmt.Fprintln(opts.IO.StdErr, heredoc.Doc(getAccessTokenTip(hostname)))
	var token string
	err = survey.AskOne(&survey.Password{
		Message: "Paste your authentication token:",
	}, &token, survey.WithValidator(survey.Required))
	if err != nil {
		return fmt.Errorf("could not prompt: %w", err)
	}

	if hostname == "" {
		return errors.New("empty hostname would leak token")
	}

	err = cfg.Set(hostname, "token", token)
	if err != nil {
		return err
	}
	err = cfg.Set(hostname, "api_host", apiHostname)
	if err != nil {
		return err
	}

	gitProtocol := "https"
	apiProtocol := "https"

	glabExecutable := "glab"
	if exe, err := os.Executable(); err == nil {
		glabExecutable = exe
	}
	credentialFlow := &authutils.GitCredentialFlow{Executable: glabExecutable}

	if opts.Interactive {
		err = survey.AskOne(&survey.Select{
			Message: "Choose default git protocol",
			Options: []string{
				"SSH",
				"HTTPS",
				"HTTP",
			},
			Default: "HTTPS",
		}, &gitProtocol)
		if err != nil {
			return fmt.Errorf("could not prompt: %w", err)
		}

		gitProtocol = strings.ToLower(gitProtocol)
		if opts.Interactive && gitProtocol != "ssh" {
			if err := credentialFlow.Prompt(hostname, gitProtocol); err != nil {
				return err
			}
		}

		if isSelfHosted {
			err = survey.AskOne(&survey.Select{
				Message: "Choose host API protocol",
				Options: []string{
					"HTTPS",
					"HTTP",
				},
				Default: "HTTPS",
			}, &apiProtocol)
			if err != nil {
				return fmt.Errorf("could not prompt: %w", err)
			}

			apiProtocol = strings.ToLower(apiProtocol)
		}

		fmt.Fprintf(opts.IO.StdErr, "- glab config set -h %s git_protocol %s\n", hostname, gitProtocol)
		err = cfg.Set(hostname, "git_protocol", gitProtocol)
		if err != nil {
			return err
		}

		fmt.Fprintf(opts.IO.StdErr, "%s Configured git protocol\n", c.GreenCheck())

		fmt.Fprintf(opts.IO.StdErr, "- glab config set -h %s api_protocol %s\n", hostname, apiProtocol)
		err = cfg.Set(hostname, "api_protocol", apiProtocol)
		if err != nil {
			return err
		}

		fmt.Fprintf(opts.IO.StdErr, "%s Configured API protocol\n", c.GreenCheck())
	}
	apiClient, err := cmdutils.LabClientFunc(hostname, cfg, false)
	if err != nil {
		return err
	}

	user, err := api.CurrentUser(apiClient)
	if err != nil {
		return fmt.Errorf("error using api: %w", err)
	}
	username := user.Username

	err = cfg.Set(hostname, "user", username)
	if err != nil {
		return err
	}

	err = cfg.Write()
	if err != nil {
		return err
	}

	if credentialFlow.ShouldSetup() {
		err := credentialFlow.Setup(hostname, gitProtocol, username, token)
		if err != nil {
			return err
		}
	}

	fmt.Fprintf(opts.IO.StdErr, "%s Logged in as %s\n", c.GreenCheck(), c.Bold(username))

	return nil
}

func hostnameValidator(v interface{}) error {
	val := fmt.Sprint(v)
	if len(strings.TrimSpace(val)) < 1 {
		return errors.New("a value is required")
	}
	re := regexp.MustCompile(`^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])(:[0-9]+)?(/[a-z0-9]*)*$`)
	if !re.MatchString(val) {
		return fmt.Errorf("invalid hostname %q", val)
	}
	return nil
}

func getAccessTokenTip(hostname string) string {
	glHostname := hostname
	if glHostname == "" {
		glHostname = glinstance.OverridableDefault()
	}
	return fmt.Sprintf(`
	Tip: you can generate a Personal Access Token here https://%s/-/profile/personal_access_tokens
	The minimum required scopes are 'api' and 'write_repository'.`, glHostname)
}
