// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package content

import (
	"fmt"
	"strings"
	"time"

	"github.com/hashicorp/go-multierror"

	"istio.io/istio/istioctl/pkg/util/formatting"
	"istio.io/istio/pkg/config/analysis/analyzers"
	"istio.io/istio/pkg/config/analysis/diag"
	"istio.io/istio/pkg/config/analysis/local"
	"istio.io/istio/pkg/config/resource"
	"istio.io/istio/pkg/kube"
	"istio.io/istio/pkg/log"
	"istio.io/istio/pkg/util/istiomultierror"
	"istio.io/istio/tools/bug-report/pkg/common"
	"istio.io/istio/tools/bug-report/pkg/kubectlcmd"
)

const (
	coredumpDir = "/var/lib/istio"
)

// Params contains parameters for running a kubectl fetch command.
type Params struct {
	Runner             *kubectlcmd.Runner
	DryRun             bool
	Verbose            bool
	ClusterVersion     string
	Namespace          string
	IstioNamespace     string
	Pod                string
	Container          string
	KubeConfig         string
	KubeContext        string
	ProxyAdminPort     int
	IncludedNamespaces []string
}

func (p *Params) SetDryRun(dryRun bool) *Params {
	out := *p
	out.DryRun = dryRun
	return &out
}

func (p *Params) SetVerbose(verbose bool) *Params {
	out := *p
	out.Verbose = verbose
	return &out
}

func (p *Params) SetProxyAdminPort(proxyAdminPort int) *Params {
	out := *p
	out.ProxyAdminPort = proxyAdminPort
	return &out
}

func (p *Params) SetNamespace(namespace string) *Params {
	out := *p
	out.Namespace = namespace
	return &out
}

func (p *Params) SetIstioNamespace(namespace string) *Params {
	out := *p
	out.IstioNamespace = namespace
	return &out
}

func (p *Params) SetPod(pod string) *Params {
	out := *p
	out.Pod = pod
	return &out
}

func (p *Params) SetContainer(container string) *Params {
	out := *p
	out.Container = container
	return &out
}

// GetK8sResources returns all k8s cluster resources.
func GetK8sResources(p *Params) (map[string]string, error) {
	// Cluster-scoped resources are always fetched cluster-wide.
	clusterResources := "nodes,namespaces,customresourcedefinitions," +
		"mutatingwebhookconfigurations,validatingwebhookconfigurations"

	// Namespace-scoped resources can be filtered when specific namespaces are included.
	nsResources := "all,jobs,ingresses,endpointslices,configmaps,networkpolicies"

	nsFlag := "--all-namespaces"
	if len(p.IncludedNamespaces) > 0 && p.IncludedNamespaces[0] != "" {
		nsFlag = "-n " + strings.Join(p.IncludedNamespaces, ",")
	}

	// Fetch cluster-scoped resources
	clusterOut, err := p.Runner.RunCmd("get "+clusterResources+" -o yaml", "", p.KubeConfig, p.KubeContext, p.DryRun)
	if err != nil {
		return nil, err
	}

	// Fetch namespace-scoped resources (scoped when possible)
	nsOut, err := p.Runner.RunCmd("get "+nsFlag+" "+nsResources+" -o yaml", "", p.KubeConfig, p.KubeContext, p.DryRun)
	if err != nil {
		return nil, err
	}

	return map[string]string{
		"k8s-resources":         nsOut,
		"k8s-cluster-resources": clusterOut,
	}, nil
}

// GetSecrets returns all k8s secrets. If full is set, the secret contents are also returned.
func GetSecrets(p *Params) (map[string]string, error) {
	nsFlag := "--all-namespaces"
	if len(p.IncludedNamespaces) > 0 && p.IncludedNamespaces[0] != "" {
		nsFlag = "-n " + strings.Join(p.IncludedNamespaces, ",")
	}
	cmdStr := "get secrets " + nsFlag
	if p.Verbose {
		cmdStr += " -o yaml"
	}
	out, err := p.Runner.RunCmd(cmdStr, "", p.KubeConfig, p.KubeContext, p.DryRun)
	return map[string]string{
		"secrets": out,
	}, err
}

// GetCRs returns CR contents for all CRDs in the cluster.
func GetCRs(p *Params) (map[string]string, error) {
	crds, err := getCRDList(p)
	if err != nil {
		return nil, err
	}
	if len(crds) == 0 {
		return map[string]string{"crs": ""}, nil
	}
	nsFlag := "--all-namespaces"
	if len(p.IncludedNamespaces) > 0 && p.IncludedNamespaces[0] != "" {
		nsFlag = "-n " + strings.Join(p.IncludedNamespaces, ",")
	}
	out, err := p.Runner.RunCmd("get "+nsFlag+" "+strings.Join(crds, ",")+" -o yaml", "", p.KubeConfig, p.KubeContext, p.DryRun)
	return map[string]string{
		"crs": out,
	}, err
}

// GetClusterInfo returns the cluster info.
func GetClusterInfo(p *Params) (map[string]string, error) {
	out, err := p.Runner.RunCmd("config current-context", "", p.KubeConfig, p.KubeContext, p.DryRun)
	if err != nil {
		return nil, err
	}
	ret := make(map[string]string)
	// Add the endpoint to the context
	ret["cluster-context"] = out + p.Runner.Client.RESTConfig().Host + "\n"
	out, err = p.Runner.RunCmd("version", "", p.KubeConfig, p.KubeContext, p.DryRun)
	if err != nil {
		return nil, err
	}
	ret["kubectl-version"] = out
	return ret, nil
}

// GetClusterContext returns the cluster context.
func GetClusterContext(runner *kubectlcmd.Runner, kubeConfig string) (string, error) {
	return runner.RunCmd("config current-context", "", kubeConfig, "", false)
}

// GetNodeInfo returns node information.
func GetNodeInfo(p *Params) (map[string]string, error) {
	out, err := p.Runner.RunCmd("get nodes -o yaml", "", p.KubeConfig, p.KubeContext, p.DryRun)
	return map[string]string{
		"nodes": out,
	}, err
}

// GetPodInfo returns pod details for istioNamespace.
func GetPodInfo(p *Params) (map[string]string, error) {
	if p.IstioNamespace == "" {
		return nil, fmt.Errorf("getPodInfo requires the Istio namespace")
	}
	out, err := p.Runner.RunCmd("get pods -o yaml", p.IstioNamespace, p.KubeConfig, p.KubeContext, p.DryRun)
	return map[string]string{
		"pods": out,
	}, err
}

// GetEvents returns events for all namespaces, or scoped namespaces if specified.
func GetEvents(p *Params) (map[string]string, error) {
	nsFlag := "--all-namespaces"
	if len(p.IncludedNamespaces) > 0 && p.IncludedNamespaces[0] != "" {
		nsFlag = "-n " + strings.Join(p.IncludedNamespaces, ",")
	}
	out, err := p.Runner.RunCmd("get events "+nsFlag+" -o wide --sort-by=.metadata.creationTimestamp", "", p.KubeConfig, p.KubeContext, p.DryRun)
	return map[string]string{
		"events": out,
	}, err
}

// GetIstiodInfo returns internal Istiod debug info.
func GetIstiodInfo(p *Params) (map[string]string, error) {
	if p.Namespace == "" || p.Pod == "" {
		return nil, fmt.Errorf("getIstiodInfo requires namespace and pod")
	}
	errs := istiomultierror.New()
	ret := make(map[string]string)
	for _, url := range common.IstiodDebugURLs(p.ClusterVersion) {
		out, err := p.Runner.Exec(p.Namespace, p.Pod, common.DiscoveryContainerName, fmt.Sprintf(`pilot-discovery request GET %s`, url), p.DryRun)
		if err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		ret[url] = out
	}
	if errs.ErrorOrNil() != nil {
		return nil, errs
	}
	return ret, nil
}

// GetProxyInfo returns internal proxy debug info.
func GetProxyInfo(p *Params) (map[string]string, error) {
	errs := istiomultierror.New()
	if p.Namespace == "" || p.Pod == "" {
		return nil, fmt.Errorf("getProxyInfo requires namespace and pod")
	}
	ret := make(map[string]string)
	for _, url := range common.ProxyDebugURLs(p.ClusterVersion) {
		out, err := p.Runner.EnvoyGet(p.Namespace, p.Pod, url, p.DryRun, p.ProxyAdminPort)
		if err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		ret[url] = out
	}
	if errs.ErrorOrNil() != nil {
		return nil, errs
	}
	return ret, nil
}

func GetZtunnelInfo(p *Params) (map[string]string, error) {
	if p.Namespace == "" || p.Pod == "" {
		return nil, fmt.Errorf("getZtunnelInfo requires namespace and pod")
	}
	errs := istiomultierror.New()
	ret := make(map[string]string)
	for _, url := range common.ZtunnelDebugURLs(p.ClusterVersion) {
		out, err := p.Runner.EnvoyGet(p.Namespace, p.Pod, url, p.DryRun, p.ProxyAdminPort)
		if err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		ret[url] = out
	}
	if errs.ErrorOrNil() != nil {
		return nil, errs
	}
	return ret, nil
}

// GetNetstat returns netstat for the given container.
func GetNetstat(p *Params) (map[string]string, error) {
	if p.Namespace == "" || p.Pod == "" {
		return nil, fmt.Errorf("getNetstat requires namespace and pod")
	}

	out, err := p.Runner.Exec(p.Namespace, p.Pod, common.ProxyContainerName, "netstat -natpw", p.DryRun)
	if err != nil {
		return nil, err
	}
	return map[string]string{
		"netstat": out,
	}, err
}

// GetAnalyze returns the output of istioctl analyze.
func GetAnalyze(p *Params, timeout time.Duration) (map[string]string, error) {
	out := make(map[string]string)
	sa := local.NewSourceAnalyzer(analyzers.AllCombined(), resource.Namespace(p.Namespace), resource.Namespace(p.IstioNamespace), nil)

	k, err := kube.NewClient(kube.NewClientConfigForRestConfig(p.Runner.Client.RESTConfig()), "")
	if err != nil {
		return nil, err
	}
	k = kube.EnableCrdWatcher(k)
	sa.AddRunningKubeSource(k)

	cancel := make(chan struct{})
	result, err := sa.Analyze(cancel)
	if err != nil {
		return nil, err
	}

	if len(result.SkippedAnalyzers) > 0 {
		log.Infof("Skipped analyzers:")
		for _, a := range result.SkippedAnalyzers {
			log.Infof("\t: %s", a)
		}
	}
	if len(result.ExecutedAnalyzers) > 0 {
		log.Infof("Executed analyzers:")
		for _, a := range result.ExecutedAnalyzers {
			log.Infof("\t: %s", a)
		}
	}

	// Get messages for output
	outputMessages := result.Messages.SetDocRef("istioctl-analyze").FilterOutLowerThan(diag.Info)

	// Print all the messages to stdout in the specified format
	output, err := formatting.Print(outputMessages, formatting.LogFormat, false)
	if err != nil {
		return nil, err
	}
	if p.Namespace == common.NamespaceAll {
		out[common.StrNamespaceAll] = output
	} else {
		out[p.Namespace] = output
	}
	return out, nil
}

// GetCoredumps returns coredumps for the given namespace/pod/container.
func GetCoredumps(p *Params) (map[string]string, error) {
	if p.Namespace == "" || p.Pod == "" {
		return nil, fmt.Errorf("getCoredumps requires namespace and pod")
	}
	cds, err := getCoredumpList(p)
	if err != nil {
		return nil, err
	}

	ret := make(map[string]string)
	log.Infof("%s/%s/%s has %d coredumps", p.Namespace, p.Pod, p.Container, len(cds))
	for idx, cd := range cds {
		outStr, err := p.Runner.Cat(p.Namespace, p.Pod, p.Container, cd, p.DryRun)
		if err != nil {
			log.Warn(err)
			continue
		}
		ret[fmt.Sprint(idx)+".core"] = outStr
	}
	return ret, nil
}

func getCoredumpList(p *Params) ([]string, error) {
	out, err := p.Runner.Exec(p.Namespace, p.Pod, p.Container, fmt.Sprintf("find %s -name core.*", coredumpDir), p.DryRun)
	if err != nil {
		return nil, err
	}
	var cds []string
	for _, cd := range strings.Split(out, "\n") {
		if strings.TrimSpace(cd) != "" {
			cds = append(cds, cd)
		}
	}
	return cds, nil
}

func getCRDList(p *Params) ([]string, error) {
	crdStr, err := p.Runner.RunCmd("get customresourcedefinitions --no-headers", "", p.KubeConfig, p.KubeContext, p.DryRun)
	if err != nil {
		return nil, err
	}
	var out []string
	for _, crd := range strings.Split(crdStr, "\n") {
		if strings.TrimSpace(crd) == "" {
			continue
		}
		out = append(out, strings.Split(crd, " ")[0])
	}
	return out, nil
}
