package cmd

import (
	"fmt"
	"net/http"
	"os"

	"knative.dev/func/cmd/prompt"
	"knative.dev/func/pkg/builders/buildpacks"
	"knative.dev/func/pkg/config"
	"knative.dev/func/pkg/docker"
	"knative.dev/func/pkg/docker/creds"
	fn "knative.dev/func/pkg/functions"
	fnhttp "knative.dev/func/pkg/http"
	"knative.dev/func/pkg/k8s"
	"knative.dev/func/pkg/knative"
	"knative.dev/func/pkg/pipelines/tekton"
)

// ClientConfig settings for use with NewClient
// These are the minimum settings necessary to create the default client
// instance which has most subsystems initialized.
type ClientConfig struct {
	// Verbose logging.  By default, logging output is kept to the bare minimum.
	// Use this flag to configure verbose logging throughout.
	Verbose bool

	// Allow insecure server connections when using SSL
	InsecureSkipVerify bool
}

// ClientFactory defines a constructor which assists in the creation of a Client
// for use by commands.
// See the NewClient constructor which is the fully populated ClientFactory used
// by commands by default.
// See NewClientFactory which constructs a minimal ClientFactory for use
// during testing.
type ClientFactory func(ClientConfig, ...fn.Option) (*fn.Client, func())

// NewTestClient returns a client factory which will ignore options used,
// instead using those provided when creating the factory.  This allows
// for tests to create an entirely default client but with N mocks.
func NewTestClient(options ...fn.Option) ClientFactory {
	return func(_ ClientConfig, _ ...fn.Option) (*fn.Client, func()) {
		return fn.New(options...), func() {}
	}
}

// NewClient constructs an fn.Client with the majority of
// the concrete implementations set.  Provide additional Options to this constructor
// to override or augment as needed, or override the ClientFactory passed to
// commands entirely to mock for testing. Note the returned cleanup function.
// 'Namespace' is optional.  If not provided (see DefaultNamespace commentary),
// the currently configured is used.
// 'Verbose' indicates the system should write out a higher amount of logging.
func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) {
	var (
		t  = newTransport(cfg.InsecureSkipVerify)    // may provide a custom impl which proxies
		c  = newCredentialsProvider(config.Dir(), t) // for accessing registries
		d  = newKnativeDeployer(cfg.Verbose)
		pp = newTektonPipelinesProvider(c, cfg.Verbose)
		o  = []fn.Option{ // standard (shared) options for all commands
			fn.WithVerbose(cfg.Verbose),
			fn.WithTransport(t),
			fn.WithRepositoriesPath(config.RepositoriesPath()),
			fn.WithBuilder(buildpacks.NewBuilder(buildpacks.WithVerbose(cfg.Verbose))),
			fn.WithRemover(knative.NewRemover(cfg.Verbose)),
			fn.WithDescriber(knative.NewDescriber(cfg.Verbose)),
			fn.WithLister(knative.NewLister(cfg.Verbose)),
			fn.WithDeployer(d),
			fn.WithPipelinesProvider(pp),
			fn.WithPusher(docker.NewPusher(
				docker.WithCredentialsProvider(c),
				docker.WithTransport(t),
				docker.WithVerbose(cfg.Verbose))),
		}
	)

	// Client is constructed with standard options plus any additional options
	// which either augment or override the defaults.
	client := fn.New(append(o, options...)...)

	// A deferrable cleanup function which is used to perform any cleanup, such
	// as closing the transport
	cleanup := func() {
		if err := t.Close(); err != nil {
			fmt.Fprintf(os.Stderr, "error closing http transport. %v", err)
		}
	}

	return client, cleanup
}

// newTransport returns a transport with cluster-flavor-specific variations
// which take advantage of additional features offered by cluster variants.
func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser {
	return fnhttp.NewRoundTripper(fnhttp.WithInsecureSkipVerify(insecureSkipVerify), fnhttp.WithOpenShiftServiceCA())
}

// newCredentialsProvider returns a credentials provider which possibly
// has cluster-flavor specific additional credential loaders to take advantage
// of features or configuration nuances of cluster variants.
func newCredentialsProvider(configPath string, t http.RoundTripper) docker.CredentialsProvider {
	options := []creds.Opt{
		creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin, os.Stdout, os.Stderr)),
		creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()),
		creds.WithTransport(t),
		creds.WithAdditionalCredentialLoaders(k8s.GetOpenShiftDockerCredentialLoaders()...),
	}

	// Other cluster variants can be supported here
	return creds.NewCredentialsProvider(configPath, options...)
}

func newTektonPipelinesProvider(creds docker.CredentialsProvider, verbose bool) *tekton.PipelinesProvider {
	options := []tekton.Opt{
		tekton.WithCredentialsProvider(creds),
		tekton.WithVerbose(verbose),
		tekton.WithPipelineDecorator(deployDecorator{}),
	}

	return tekton.NewPipelinesProvider(options...)
}

func newKnativeDeployer(verbose bool) fn.Deployer {
	options := []knative.DeployerOpt{
		knative.WithDeployerVerbose(verbose),
		knative.WithDeployerDecorator(deployDecorator{}),
	}

	return knative.NewDeployer(options...)
}

type deployDecorator struct {
	oshDec k8s.OpenshiftMetadataDecorator
}

func (d deployDecorator) UpdateAnnotations(function fn.Function, annotations map[string]string) map[string]string {
	if k8s.IsOpenShift() {
		return d.oshDec.UpdateAnnotations(function, annotations)
	}
	return annotations
}

func (d deployDecorator) UpdateLabels(function fn.Function, labels map[string]string) map[string]string {
	if k8s.IsOpenShift() {
		return d.oshDec.UpdateLabels(function, labels)
	}
	return labels
}
