package cmd

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/linkerd/linkerd2/pkg/k8s"
	"github.com/linkerd/linkerd2/pkg/k8s/resource"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/selection"
	yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/yaml"
)

var (
	// DefaultDockerRegistry specifies the default location for Linkerd's images.
	DefaultDockerRegistry = "cr.l5d.io/linkerd"
)

const (
	JsonOutput = "json"
	YamlOutput = "yaml"
)

// GetDefaultNamespace fetches the default namespace
// used in the current KubeConfig context
func GetDefaultNamespace(kubeconfigPath, kubeContext string) string {
	rules := clientcmd.NewDefaultClientConfigLoadingRules()

	if kubeconfigPath != "" {
		rules.ExplicitPath = kubeconfigPath
	}

	overrides := &clientcmd.ConfigOverrides{CurrentContext: kubeContext}
	kubeCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
	ns, _, err := kubeCfg.Namespace()

	if err != nil {
		log.Warnf(`could not set namespace from kubectl context, using 'default' namespace: %s
		 ensure the KUBECONFIG path %s is valid`, err, kubeconfigPath)
		return corev1.NamespaceDefault
	}

	return ns
}

// Uninstall prints all cluster-scoped resources matching the given selector
// for the purposes of deleting them.
func Uninstall(ctx context.Context, k8sAPI *k8s.KubernetesAPI, selector string, format string) error {
	resources, err := resource.FetchKubernetesResources(ctx, k8sAPI,
		metav1.ListOptions{LabelSelector: selector},
	)
	if err != nil {
		return err
	}

	if len(resources) == 0 {
		return errors.New("No resources found to uninstall")
	}
	for _, r := range resources {
		if format == YamlOutput {
			if err := r.RenderResource(os.Stdout); err != nil {
				return fmt.Errorf("error rendering Kubernetes resource: %w", err)
			}
		} else if format == JsonOutput {
			if err := r.RenderResourceJSON(os.Stdout); err != nil {
				return fmt.Errorf("error rendering Kubernetes resource: %w", err)
			}
		} else {
			return fmt.Errorf("unsupported format %s", format)
		}
	}
	return nil
}

// Prune takes an install manifest and prints all resources on the cluster which
// match the given label selector but are not in the given manifest. Users are
// expected to pipe these resources to `kubectl delete` to clean up resources
// left on the cluster which are no longer part of the install manifest.
func Prune(ctx context.Context, k8sAPI *k8s.KubernetesAPI, expectedManifests string, selector string, format string) error {
	expectedResources := []resource.Kubernetes{}
	reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(strings.NewReader(expectedManifests), 4096))
	for {
		manifest, err := reader.Read()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return err
		}
		resource := resource.Kubernetes{}
		err = yaml.Unmarshal(manifest, &resource)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error parsing manifest: %s", manifest)
			os.Exit(1)
		}
		expectedResources = append(expectedResources, resource)
	}

	listOptions := metav1.ListOptions{
		LabelSelector: selector,
	}
	resources, err := resource.FetchPrunableResources(ctx, k8sAPI, metav1.NamespaceAll, listOptions)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error fetching resources: %s\n", err)
		os.Exit(1)
	}

	for _, resource := range resources {
		// If the resource is not in the expected resource list, render it for
		// pruning.
		if !resourceListContains(expectedResources, resource) {
			if format == YamlOutput {
				err = resource.RenderResource(os.Stdout)
			} else if format == JsonOutput {
				err = resource.RenderResourceJSON(os.Stdout)
			} else {
				return fmt.Errorf("unsupported format %s", format)
			}
			if err != nil {
				return fmt.Errorf("error rendering Kubernetes resource: %w\n", err)
			}
		}
	}
	return nil
}

func resourceListContains(list []resource.Kubernetes, a resource.Kubernetes) bool {
	for _, r := range list {
		if resourceEquals(a, r) {
			return true
		}
	}
	return false
}

func resourceEquals(a resource.Kubernetes, b resource.Kubernetes) bool {
	return a.GroupVersionKind().GroupKind() == b.GroupVersionKind().GroupKind() &&
		a.GetName() == b.GetName() &&
		a.GetNamespace() == b.GetNamespace()
}

// ConfigureNamespaceFlagCompletion sets up resource-aware completion for command
// flags that accept a namespace name
func ConfigureNamespaceFlagCompletion(
	cmd *cobra.Command,
	flagNames []string,
	kubeconfigPath string,
	impersonate string,
	impersonateGroup []string,
	kubeContext string,
) {
	for _, flagName := range flagNames {
		cmd.RegisterFlagCompletionFunc(flagName,
			func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
				k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
				if err != nil {
					return nil, cobra.ShellCompDirectiveError
				}

				cc := k8s.NewCommandCompletion(k8sAPI, "")
				results, err := cc.Complete([]string{k8s.Namespace}, toComplete)
				if err != nil {
					return nil, cobra.ShellCompDirectiveError
				}

				return results, cobra.ShellCompDirectiveDefault
			})
	}
}

// ConfigureOutputFlagCompletion sets up resource-aware completion for command
// flags that accept an output name.
func ConfigureOutputFlagCompletion(cmd *cobra.Command) {
	cmd.RegisterFlagCompletionFunc("output",
		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			return []string{"basic", "json", "short", "table"}, cobra.ShellCompDirectiveDefault
		})
}

// ConfigureKubeContextFlagCompletion sets up resource-aware completion for command
// flags based off of a kubeconfig
func ConfigureKubeContextFlagCompletion(cmd *cobra.Command, kubeconfigPath string) {
	cmd.RegisterFlagCompletionFunc("context",
		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			rules := clientcmd.NewDefaultClientConfigLoadingRules()
			rules.ExplicitPath = kubeconfigPath
			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
			config, err := loader.RawConfig()
			if err != nil {
				return nil, cobra.ShellCompDirectiveError
			}

			suggestions := []string{}
			uniqContexts := map[string]struct{}{}
			for ctxName := range config.Contexts {
				if strings.HasPrefix(ctxName, toComplete) {
					if _, ok := uniqContexts[ctxName]; !ok {
						suggestions = append(suggestions, ctxName)
						uniqContexts[ctxName] = struct{}{}
					}
				}
			}

			return suggestions, cobra.ShellCompDirectiveDefault
		})
}

// GetLabelSelector creates a label selector as a string based on a label key
// whose value may be in the set provided as an argument to the function. If the
// value set is empty then the selector will match resources where the label key
// exists regardless of value.
func GetLabelSelector(labelKey string, labelValues ...string) (string, error) {
	selectionOp := selection.In
	if len(labelValues) < 1 {
		selectionOp = selection.Exists
	}

	labelRequirement, err := labels.NewRequirement(labelKey, selectionOp, labelValues)
	if err != nil {
		return "", err
	}

	selector := labels.NewSelector().Add(*labelRequirement)
	return selector.String(), nil
}

// RegistryOverride replaces the registry-portion of the provided image with the provided registry.
func RegistryOverride(image, newRegistry string) string {
	if image == "" {
		return image
	}
	registry := newRegistry
	if registry != "" && !strings.HasSuffix(registry, "/") {
		registry += "/"
	}
	imageName := image
	if strings.Contains(image, "/") {
		imageName = image[strings.LastIndex(image, "/")+1:]
	}
	return registry + imageName
}

// Given a buffer containing one or more YAML documents separated by `---`,
// render each document to the writer in the specified format: json or yaml.
// Json documents are separated by a newline character.
func RenderYAMLAs(buf *bytes.Buffer, writer io.Writer, format string) error {
	if format == JsonOutput {
		reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(buf, 4096))
		for {
			manifest, err := reader.Read()
			if err != nil {
				if errors.Is(err, io.EOF) {
					break
				}
				return err
			}
			bytes, err := yaml.YAMLToJSON(manifest)
			if err != nil {
				return err
			}
			_, err = writer.Write(append(bytes, '\n'))
			if err != nil {
				return err
			}
		}
		return nil
	}
	if format == YamlOutput {
		_, err := writer.Write(buf.Bytes())
		return err
	}
	return fmt.Errorf("unsupported format %s", format)
}
