package cmd

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
	"github.com/ory/viper"
	"github.com/spf13/cobra"

	pacgit "github.com/openshift-pipelines/pipelines-as-code/pkg/git"

	"knative.dev/func/pkg/config"
	fn "knative.dev/func/pkg/functions"
	"knative.dev/func/pkg/git"
	"knative.dev/func/pkg/pipelines"
)

func NewConfigGitSetCmd(newClient ClientFactory) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "set",
		Short: "Set Git settings in the function configuration",
		Long: `Set Git settings in the function configuration

	Interactive prompt to set Git settings in the function project in the current
	directory or from the directory specified with --path.
	`,
		SuggestFor: []string{"add", "ad", "update", "create", "insert", "append"},
		PreRunE:    bindEnv("path", "builder", "builder-image", "image", "registry", "git-provider", "git-url", "git-branch", "git-dir", "gh-access-token", "config-local", "config-cluster", "config-remote"),
		RunE: func(cmd *cobra.Command, args []string) (err error) {
			return runConfigGitSetCmd(cmd, newClient)
		},
	}

	// Global Config
	cfg, err := config.NewDefault()
	if err != nil {
		fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
	}

	// Function Context
	f, _ := fn.NewFunction(effectivePath())
	if f.Initialized() {
		cfg = cfg.Apply(f)
	}

	// Flags
	//
	// Globally-Configurable Flags:
	//   Options whose value may be defined globally may also exist on the
	//  contextually relevant function; but sets are flattened via cfg.Apply(f)
	cmd.Flags().StringP("builder", "b", cfg.Builder,
		fmt.Sprintf("Builder to use when creating the function's container. Currently supported builders are %s.", KnownBuilders()))
	cmd.Flags().StringP("registry", "r", cfg.Registry,
		"Container registry + registry namespace. (ex 'ghcr.io/myuser').  The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY)")
	cmd.Flags().StringP("namespace", "n", cfg.Namespace,
		"Deploy into a specific namespace. Will use function's current namespace by default if already deployed, and the currently active namespace if it can be determined. ($FUNC_NAMESPACE)")

	// Function-Context Flags:
	// Options whose value is avaolable on the function with context only
	// (persisted but not globally configurable)
	builderImage := f.Build.BuilderImages[f.Build.Builder]
	cmd.Flags().StringP("builder-image", "", builderImage,
		"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
	cmd.Flags().StringP("image", "i", f.Image, "Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. ($FUNC_IMAGE)")

	// Git related Flags:
	cmd.Flags().String("git-provider", "",
		fmt.Sprintf("The type of the Git platform provider to setup webhook. This value is usually automatically generated from input URL, use this parameter to override this setting. Currently supported providers are %s.", git.SupportedProvidersList.PrettyString()))
	cmd.Flags().StringP("git-url", "g", "",
		"Repository url containing the function to build ($FUNC_GIT_URL)")
	cmd.Flags().StringP("git-branch", "t", "",
		"Git revision (branch) to be used when deploying via the Git repository ($FUNC_GIT_BRANCH)")
	cmd.Flags().StringP("git-dir", "d", "",
		"Directory in the Git repository containing the function (default is the root) ($FUNC_GIT_DIR)")

	// GitHub related Flags:
	cmd.Flags().String("gh-access-token", "",
		"GitHub Personal Access Token. For public repositories the scope is 'public_repo', for private is 'repo'. If you want to configure the webhook automatically, 'admin:repo_hook' is needed as well. Get more details: https://pipelines-as-code.pages.dev/docs/install/github_webhook/.")
	cmd.Flags().String("gh-webhook-secret", "",
		"GitHub Webhook Secret used for payload validation. If not specified, it will be generated automatically.")

	// Resources generated related Flags:
	cmd.Flags().Bool("config-local", false, "Configure local resources (pipeline templates).")
	cmd.Flags().Bool("config-cluster", false, "Configure cluster resources (credentials and config on the cluster).")
	cmd.Flags().Bool("config-remote", false, "Configure remote resources (webhook on the Git provider side).")

	addPathFlag(cmd)
	addVerboseFlag(cmd, cfg.Verbose)

	return cmd
}

type configGitSetConfig struct {
	buildConfig // further embeds config.Global

	GitProvider   string
	GitURL        string
	GitRevision   string
	GitContextDir string

	ConfigureRemoteResourcesSet bool // whether ConfigureRemoteResources value has been set

	metadata pipelines.PacMetadata
}

// newConfigGitSetConfig creates a buildConfig populated from command flags and
// environment variables; in that precedence.
func newConfigGitSetConfig(_ *cobra.Command) (c configGitSetConfig) {
	// decide what resources we should configure:
	// - by default all resources
	// - if any parameter is explicitly specified then get value from parameters
	configLocal := true
	configCluster := true
	configRemote := true

	configRemoteSet := false

	if viper.HasChanged("config-local") || viper.HasChanged("config-cluster") || viper.HasChanged("config-remote") {
		configLocal = viper.GetBool("config-local")
		configCluster = viper.GetBool("config-cluster")
		configRemote = viper.GetBool("config-remote")

		configRemoteSet = true
	}

	c = configGitSetConfig{
		buildConfig: newBuildConfig(),

		GitURL:        viper.GetString("git-url"),
		GitRevision:   viper.GetString("git-branch"),
		GitContextDir: viper.GetString("git-dir"),

		ConfigureRemoteResourcesSet: configRemoteSet,

		metadata: pipelines.PacMetadata{
			GitProvider:         viper.GetString("git-provider"),
			PersonalAccessToken: viper.GetString("gh-access-token"),
			WebhookSecret:       viper.GetString("gh-webhook-secret"),

			ConfigureLocalResources:   configLocal,
			ConfigureClusterResources: configCluster,
			ConfigureRemoteResources:  configRemote,
		},
	}

	return c
}

func (c configGitSetConfig) Prompt(f fn.Function) (configGitSetConfig, error) {
	var err error
	if c.buildConfig, err = c.buildConfig.Prompt(); err != nil {
		return c, err
	}

	// try to read git url from the local .git settings
	gitInfo := pacgit.GetGitInfo(c.Path)

	// prompt if git URL hasn't been set previously
	if c.GitURL == "" {
		url := f.Build.Git.URL
		if gitInfo.URL != "" {
			url = gitInfo.URL
		}
		if err := survey.AskOne(&survey.Input{
			Message: "The URL to Git repository with the function source code:",
			Default: url,
		}, &url, survey.WithValidator(survey.Required)); err != nil {
			return c, err
		}
		c.GitURL = url
	}

	// prompt if git revision hasn't been set previously
	if c.GitRevision == "" {
		revision := f.Build.Git.Revision
		if gitInfo.Branch != "" {
			revision = gitInfo.Branch
		}
		if err := survey.AskOne(&survey.Input{
			Message: "The Git branch or tag we are targeting:",
			Help:    "ie: main, refs/tags/*",
			Default: revision,
		}, &revision); err != nil {
			return c, err
		}
		c.GitRevision = revision
	}

	// prompt if contextDir hasn't been set previously
	if c.GitContextDir == "" {
		contextDir := f.Build.Git.ContextDir
		if err := survey.AskOne(&survey.Input{
			Message: "A subpath within the repository:",
			Help:    "A subpath within the repository where the source code of a function is located.",
			Default: contextDir,
		}, &contextDir); err != nil {
			return c, err
		}
		c.GitContextDir = contextDir
	}

	// prompt if webhook trigger setting hasn't been set previously
	if !c.ConfigureRemoteResourcesSet {
		trigger := true
		if err := survey.AskOne(&survey.Confirm{
			Message: "Do you want to configure webhook trigger?",
			Help:    "Webhook trigger also running pipeline on a git event, ie: commit, push",
			Default: trigger,
		}, &trigger, survey.WithValidator(survey.Required)); err != nil {
			return c, err
		}
		c.metadata.ConfigureRemoteResources = trigger
		c.ConfigureRemoteResourcesSet = true
	}

	if c.metadata.ConfigureRemoteResources {
		// Configure Git provider
		if c.metadata.GitProvider == "" {
			provider, err := git.GitProviderName(c.GitURL)
			if err != nil {
				msg := "Please select the type of the Git platform provider to setup webhook:"
				if err = survey.AskOne(&survey.Select{
					Message: msg,
					Options: git.SupportedProvidersList,
					Default: 0,
				}, &provider); err != nil {
					return c, err
				}
			}
			c.metadata.GitProvider = provider
		}

		// prompt if PersonalAccessToken hasn't been set previously
		if c.metadata.PersonalAccessToken == "" {
			var personalAccessToken string
			if err := survey.AskOne(&survey.Password{
				Message: "Please enter the GitHub Personal Access Token:",
				Help:    "For public repositories the scope is 'public_repo', for private is 'repo'. If you want to configure the webhook automatically 'admin:repo_hook' is needed as well. Get more details: https://pipelines-as-code.pages.dev/docs/install/github_webhook/.",
			}, &personalAccessToken, survey.WithValidator(survey.Required)); err != nil {
				return c, err
			}
			c.metadata.PersonalAccessToken = personalAccessToken
		}
	}

	return c, nil
}

func (c configGitSetConfig) Validate(cmd *cobra.Command) (err error) {
	// Bubble validation
	if err = c.buildConfig.Validate(); err != nil {
		return
	}

	return
}

// Configure the given function.  Updates a function struct with all
// configurable values.  Note that the config already includes function's
// current values, as they were passed through via flag defaults.
func (c configGitSetConfig) Configure(f fn.Function) (fn.Function, error) {
	var err error

	// Bubble configure request
	//
	// The member values on the config object now take absolute precidence
	// because they include 1) static config 2) user's global config
	// 3) Environment variables and 4) flag values (which were set with their
	// default being 1-3).
	f = c.buildConfig.Configure(f) // also configures .buildConfig.Global

	// Configure basic members
	f.Build.Git.URL = c.GitURL
	f.Build.Git.ContextDir = c.GitContextDir
	f.Build.Git.Revision = c.GitRevision // TODO: should match; perhaps "refSpec"

	// Save the function which has now been updated with flags/config
	if err = f.Write(); err != nil { // TODO: remove when client API uses 'f'
		return f, err
	}

	return f, nil
}

func runConfigGitSetCmd(cmd *cobra.Command, newClient ClientFactory) (err error) {
	var (
		cfg configGitSetConfig
		f   fn.Function
	)
	if err = config.CreatePaths(); err != nil { // for possible auth.json usage
		return
	}
	cfg = newConfigGitSetConfig(cmd)
	if f, err = fn.NewFunction(cfg.Path); err != nil {
		return
	}
	if cfg, err = cfg.Prompt(f); err != nil {
		return
	}
	if err = cfg.Validate(cmd); err != nil {
		return
	}
	if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
		return
	}

	client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, fn.WithRegistry(cfg.Registry))
	defer done()

	return client.ConfigurePAC(cmd.Context(), f, cfg.metadata)
}
