package update

import (
	"fmt"
	"os"
	"strings"
	"time"

	"gitlab.com/gitlab-org/cli/internal/config"
	"gitlab.com/gitlab-org/cli/pkg/utils"

	"gitlab.com/gitlab-org/cli/commands/cmdutils"

	"github.com/MakeNowJust/heredoc/v2"
	"github.com/hashicorp/go-version"
	"github.com/spf13/cobra"
	gitlab "gitlab.com/gitlab-org/api/client-go"
)

const (
	defaultProjectURL = "https://gitlab.com/gitlab-org/cli"
	commandUse        = "check-update"
)

var commandAliases = []string{"update"}

func NewCheckUpdateCmd(f *cmdutils.Factory, version string) *cobra.Command {
	cmd := &cobra.Command{
		Use:   commandUse,
		Short: "Check for latest glab releases.",
		Long: heredoc.Doc(`Checks for new versions every 24 hours after any 'glab' command is run. Does not recheck if the most recent recheck is less than 24 hours old.

		To override the recheck behavior and force an update check, set the GLAB_CHECK_UPDATE environment variable to 'true'.
		`),
		Aliases: commandAliases,
		RunE: func(cmd *cobra.Command, args []string) error {
			return CheckUpdate(f, version, false, "")
		},
	}

	return cmd
}

func CheckUpdate(f *cmdutils.Factory, version string, silentSuccess bool, previousCommand string) error {
	if shouldSkipUpdate(previousCommand) {
		return nil
	}

	moreThan24hAgo, err := checkLastUpdate(f)
	if err != nil {
		return err
	}
	// if the last update check was less than 24h ago we skip the version check
	if !moreThan24hAgo {
		return nil
	}

	// We set the project to the `glab` project to check for `glab` updates
	err = f.RepoOverride(defaultProjectURL)
	if err != nil {
		return err
	}
	repo, err := f.BaseRepo()
	if err != nil {
		return err
	}
	apiClient, err := f.HttpClient()
	if err != nil {
		return err
	}

	// Since the `gitlab.com/gitlab-org/cli` is public, we remove the token
	// for this single request. When users have a `GITLAB_TOKEN` set with a
	// token for GitLab Self-Managed or GitLab Dedicated, we shouldn't use it
	// to authenticate to gitlab.com.
	releases, _, err := apiClient.Releases.ListReleases(
		repo.FullName(), &gitlab.ListReleasesOptions{ListOptions: gitlab.ListOptions{Page: 1, PerPage: 1}}, gitlab.WithToken(gitlab.PrivateToken, ""))
	if err != nil {
		return fmt.Errorf("failed checking for glab updates: %s", err.Error())
	}
	if len(releases) < 1 {
		return fmt.Errorf("no release found for glab.")
	}
	latestRelease := releases[0]
	releaseURL := fmt.Sprintf("%s/-/releases/%s", defaultProjectURL, latestRelease.TagName)

	c := f.IO.Color()
	if isOlderVersion(latestRelease.Name, version) {
		fmt.Fprintf(f.IO.StdErr, "%s %s -> %s\n%s\n",
			c.Yellow("A new version of glab has been released:"),
			c.Red(version), c.Green(latestRelease.TagName),
			releaseURL)
	} else {
		if silentSuccess {
			return nil
		}
		fmt.Fprintf(f.IO.StdErr, "%v",
			c.Green("You are already using the latest version of glab!\n"))
	}
	return nil
}

// Don't CheckUpdate if previous command is CheckUpdate
// or it’s Completion, so it doesn’t take a noticably long time
// to start new shells and we don’t encourage users setting
// `check_update` to false in the config.
func shouldSkipUpdate(previousCommand string) bool {
	isCheckUpdate := previousCommand == commandUse || utils.PresentInStringSlice(commandAliases, previousCommand)
	isCompletion := previousCommand == "completion"

	return isCheckUpdate || isCompletion
}

func isOlderVersion(latestVersion, appVersion string) bool {
	latestVersion = strings.TrimSpace(latestVersion)
	appVersion = strings.TrimSpace(appVersion)

	vv, ve := version.NewVersion(latestVersion)
	vw, we := version.NewVersion(appVersion)

	return ve == nil && we == nil && vv.GreaterThan(vw)
}

// returns true if we should check for updates
//
// returns false if we should skip the update check
//
// We only want to check for updates once every 24 hours
func checkLastUpdate(f *cmdutils.Factory) (bool, error) {
	const updateCheckInterval = 24 * time.Hour
	cfg, err := f.Config()
	if err != nil {
		return false, err
	}

	// We don't care when the command was run if the environment variable is forcing an update
	if isEnvForcingUpdate() {
		if err := updateLastCheckTimestamp(cfg); err != nil {
			return false, err
		}
		return true, nil
	}

	last_update, err := cfg.Get("", "last_update_check_timestamp")
	if err != nil {
		return false, err
	}

	// this might be the first time running the command, so last_update might be empty
	// we want to save the current time and check for an update
	if last_update == "" {
		if err := updateLastCheckTimestamp(cfg); err != nil {
			return false, err
		}
		return true, nil
	}

	last_update_time, err := time.Parse(time.RFC3339, last_update)
	if err != nil {
		return false, err
	}

	// if the last check was more than 24h ago we check for an update
	moreThan24hAgo := time.Since(last_update_time) > updateCheckInterval
	if moreThan24hAgo {
		if err := updateLastCheckTimestamp(cfg); err != nil {
			return false, err
		}
	}
	return moreThan24hAgo, nil
}

// isEnvForcingUpdate - returns true if the environment variable `GLAB_CHECK_UPDATE` is set to true
func isEnvForcingUpdate() bool {
	if envVal, ok := os.LookupEnv("GLAB_CHECK_UPDATE"); ok {
		switch strings.ToUpper(envVal) {
		case "TRUE", "YES", "Y", "1":
			return true
		case "FALSE", "NO", "N", "0":
			return false
		}
	}
	// if the value is not set or is not a valid value
	return false
}

// updateLastCheckTimestamp - saves the current time as last_update_check_timestamp to config.yml
func updateLastCheckTimestamp(cfg config.Config) error {
	if err := cfg.Set("", "last_update_check_timestamp", time.Now().Format(time.RFC3339)); err != nil {
		return err
	}

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

	return nil
}
