/*
Copyright 2021 The KubeVela 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 common

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/format"
	"cuelang.org/go/encoding/openapi"
	"github.com/AlecAivazis/survey/v2"
	"github.com/hashicorp/hcl/v2/hclparse"
	clustergatewayapi "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
	"github.com/oam-dev/terraform-config-inspect/tfconfig"
	terraformv1beta1 "github.com/oam-dev/terraform-controller/api/v1beta1"
	kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
	errors2 "github.com/pkg/errors"
	certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
	istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
	v1 "k8s.io/api/core/v1"
	crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	k8sruntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/util/flowcontrol"
	apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
	ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
	ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1"
	ocmworkv1 "open-cluster-management.io/api/work/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/config"
	"sigs.k8s.io/yaml"

	oamcore "github.com/oam-dev/kubevela/apis/core.oam.dev"
	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
	oamstandard "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
	"github.com/oam-dev/kubevela/apis/types"
	velacue "github.com/oam-dev/kubevela/pkg/cue"
	"github.com/oam-dev/kubevela/pkg/cue/model"
	"github.com/oam-dev/kubevela/pkg/oam"
)

var (
	// Scheme defines the default KubeVela schema
	Scheme = k8sruntime.NewScheme()
)

const (
	// AddonObservabilityApplication is the application name for Addon Observability
	AddonObservabilityApplication = "addon-observability"
	// AddonObservabilityGrafanaSvc is grafana service name for Addon Observability
	AddonObservabilityGrafanaSvc = "grafana"
)

// CreateCustomNamespace display the create namespace message
const CreateCustomNamespace = "create new namespace"

func init() {
	_ = clientgoscheme.AddToScheme(Scheme)
	_ = apiregistrationv1.AddToScheme(Scheme)
	_ = crdv1.AddToScheme(Scheme)
	_ = oamcore.AddToScheme(Scheme)
	_ = oamstandard.AddToScheme(Scheme)
	_ = istioclientv1beta1.AddToScheme(Scheme)
	_ = certmanager.AddToScheme(Scheme)
	_ = kruise.AddToScheme(Scheme)
	_ = terraformv1beta1.AddToScheme(Scheme)
	_ = ocmclusterv1alpha1.Install(Scheme)
	_ = ocmclusterv1.Install(Scheme)
	_ = ocmworkv1.Install(Scheme)
	_ = clustergatewayapi.AddToScheme(Scheme)
	// +kubebuilder:scaffold:scheme
}

// InitBaseRestConfig will return reset config for create controller runtime client
func InitBaseRestConfig() (Args, error) {
	restConf, err := config.GetConfig()
	if err != nil && os.Getenv("IGNORE_KUBE_CONFIG") != "true" {
		fmt.Println("get kubeConfig err", err)
		os.Exit(1)
	} else if err != nil {
		return Args{}, err
	}
	restConf.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(100, 200)
	return Args{
		Config: restConf,
		Schema: Scheme,
	}, nil
}

// globalClient will be a client for whole command lifecycle
var globalClient client.Client

// SetGlobalClient will set a client for one cli command
func SetGlobalClient(clt client.Client) error {
	globalClient = clt
	return nil
}

// GetClient will K8s client in args
func GetClient() (client.Client, error) {
	if globalClient != nil {
		return globalClient, nil
	}
	return nil, errors.New("client not set, call SetGlobalClient first")
}

// HTTPGet will send GET http request with context
func HTTPGet(ctx context.Context, url string) ([]byte, error) {
	// Change NewRequest to NewRequestWithContext and pass context it
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	//nolint:errcheck
	defer resp.Body.Close()
	return io.ReadAll(resp.Body)
}

// GetCUEParameterValue converts definitions to cue format
func GetCUEParameterValue(cueStr string) (cue.Value, error) {
	r := cue.Runtime{}
	template, err := r.Compile("", cueStr+velacue.BaseTemplate)
	if err != nil {
		return cue.Value{}, err
	}
	tempStruct, err := template.Value().Struct()
	if err != nil {
		return cue.Value{}, err
	}
	// find the parameter definition
	var paraDef cue.FieldInfo
	var found bool
	for i := 0; i < tempStruct.Len(); i++ {
		paraDef = tempStruct.Field(i)
		if paraDef.Name == model.ParameterFieldName {
			found = true
			break
		}
	}
	if !found {
		return cue.Value{}, errors.New("parameter not exist")
	}
	arguments := paraDef.Value

	return arguments, nil
}

// GenOpenAPI generates OpenAPI json schema from cue.Instance
func GenOpenAPI(inst *cue.Instance) ([]byte, error) {
	if inst.Err != nil {
		return nil, inst.Err
	}
	paramOnlyIns, err := RefineParameterInstance(inst)
	if err != nil {
		return nil, err
	}
	defaultConfig := &openapi.Config{}
	b, err := openapi.Gen(paramOnlyIns, defaultConfig)
	if err != nil {
		return nil, err
	}
	var out = &bytes.Buffer{}
	_ = json.Indent(out, b, "", "   ")
	return out.Bytes(), nil
}

// extractParameterDefinitionNodeFromInstance extracts the `#parameter` ast.Node from root instance, if failed fall back to `parameter` by LookUpDef
func extractParameterDefinitionNodeFromInstance(inst *cue.Instance) ast.Node {
	opts := []cue.Option{cue.All(), cue.DisallowCycles(true), cue.ResolveReferences(true), cue.Docs(true)}
	node := inst.Value().Syntax(opts...)
	if fileNode, ok := node.(*ast.File); ok {
		for _, decl := range fileNode.Decls {
			if field, ok := decl.(*ast.Field); ok {
				if label, ok := field.Label.(*ast.Ident); ok && label.Name == "#"+model.ParameterFieldName {
					return decl.(*ast.Field).Value
				}
			}
		}
	}
	paramVal := inst.LookupDef(model.ParameterFieldName)
	return paramVal.Syntax(opts...)
}

// RefineParameterInstance refines cue instance to merely include `parameter` identifier
func RefineParameterInstance(inst *cue.Instance) (*cue.Instance, error) {
	r := cue.Runtime{}
	paramVal := inst.LookupDef(model.ParameterFieldName)
	var paramOnlyStr string
	switch k := paramVal.IncompleteKind(); k {
	case cue.StructKind, cue.ListKind:
		paramSyntax, _ := format.Node(extractParameterDefinitionNodeFromInstance(inst))
		paramOnlyStr = fmt.Sprintf("#%s: %s\n", model.ParameterFieldName, string(paramSyntax))
	case cue.IntKind, cue.StringKind, cue.FloatKind, cue.BoolKind:
		paramOnlyStr = fmt.Sprintf("#%s: %v", model.ParameterFieldName, paramVal)
	case cue.BottomKind:
		paramOnlyStr = fmt.Sprintf("#%s: {}", model.ParameterFieldName)
	default:
		return nil, fmt.Errorf("unsupport parameter kind: %s", k.String())
	}
	paramOnlyIns, err := r.Compile("-", paramOnlyStr)
	if err != nil {
		return nil, err
	}
	return paramOnlyIns, nil
}

// RealtimePrintCommandOutput prints command output in real time
// If logFile is "", it will prints the stdout, or it will write to local file
func RealtimePrintCommandOutput(cmd *exec.Cmd, logFile string) error {
	var writer io.Writer
	if logFile == "" {
		writer = io.MultiWriter(os.Stdout)
	} else {
		if _, err := os.Stat(filepath.Dir(logFile)); err != nil {
			return err
		}
		f, err := os.Create(filepath.Clean(logFile))
		if err != nil {
			return err
		}
		writer = io.MultiWriter(f)
	}
	cmd.Stdout = writer
	cmd.Stderr = writer
	if err := cmd.Run(); err != nil {
		return err
	}
	return nil
}

// ClusterObject2Map convert ClusterObjectReference to a readable map
func ClusterObject2Map(refs []common.ClusterObjectReference) map[string]string {
	clusterResourceRefTmpl := "Cluster: %s | Namespace: %s | Component: %s | Kind: %s"
	objs := make(map[string]string, len(refs))
	for _, r := range refs {
		if r.Cluster == "" {
			r.Cluster = "local"
		}
		objs[r.Cluster+"/"+r.Namespace+"/"+r.Name+"/"+r.Kind] = fmt.Sprintf(clusterResourceRefTmpl, r.Cluster, r.Namespace, r.Name, r.Kind)
	}
	return objs
}

// ResourceLocation indicates the resource location
type ResourceLocation struct {
	Cluster   string
	Namespace string
}

type clusterObjectReferenceFilter func(common.ClusterObjectReference) bool

func clusterObjectReferenceTypeFilterGenerator(allowedKinds ...string) clusterObjectReferenceFilter {
	allowedKindMap := map[string]bool{}
	for _, allowedKind := range allowedKinds {
		allowedKindMap[allowedKind] = true
	}
	return func(item common.ClusterObjectReference) bool {
		_, exists := allowedKindMap[item.Kind]
		return exists
	}
}

var isWorkloadClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment", "StatefulSet", "CloneSet", "Job", "Configuration")
var isPortForwardEndpointClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment",
	"StatefulSet", "CloneSet", "Job", "Service", "HelmRelease")

func filterResource(inputs []common.ClusterObjectReference, filters ...clusterObjectReferenceFilter) (outputs []common.ClusterObjectReference) {
	for _, item := range inputs {
		flag := true
		for _, filter := range filters {
			if !filter(item) {
				flag = false
				break
			}
		}
		if flag {
			outputs = append(outputs, item)
		}
	}
	return
}

func askToChooseOneResource(app *v1beta1.Application, filters ...clusterObjectReferenceFilter) (*common.ClusterObjectReference, error) {
	resources := app.Status.AppliedResources
	if len(resources) == 0 {
		return nil, fmt.Errorf("no resources in the application deployed yet")
	}
	resources = filterResource(resources, filters...)
	if app.Name == AddonObservabilityApplication {
		resources = filterClusterObjectRefFromAddonObservability(resources)
	}
	// filter locations
	if len(resources) == 0 {
		return nil, fmt.Errorf("no supported resources detected in deployed resources")
	}
	if len(resources) == 1 {
		return &resources[0], nil
	}
	opMap := ClusterObject2Map(resources)
	var ops []string
	for _, r := range opMap {
		ops = append(ops, r)
	}
	prompt := &survey.Select{
		Message: fmt.Sprintf("You have %d deployed resources in your app. Please choose one:", len(ops)),
		Options: ops,
	}
	var selectedRsc string
	err := survey.AskOne(prompt, &selectedRsc)
	if err != nil {
		return nil, fmt.Errorf("choosing resource err %w", err)
	}
	for k, resource := range ops {
		if selectedRsc == resource {
			return &resources[k], nil
		}
	}
	return nil, fmt.Errorf("choosing resource err %w", err)
}

// AskToChooseOneNamespace ask for choose one namespace as env
func AskToChooseOneNamespace(c client.Client, envMeta *types.EnvMeta) error {
	var nsList v1.NamespaceList
	if err := c.List(context.TODO(), &nsList); err != nil {
		return err
	}
	var ops = []string{CreateCustomNamespace}
	for _, r := range nsList.Items {
		ops = append(ops, r.Name)
	}
	prompt := &survey.Select{
		Message: "Would you like to choose an existing namespaces as your env?",
		Options: ops,
	}
	err := survey.AskOne(prompt, &envMeta.Namespace)
	if err != nil {
		return fmt.Errorf("choosing namespace err %w", err)
	}
	if envMeta.Namespace == CreateCustomNamespace {
		err = survey.AskOne(&survey.Input{
			Message: "Please name the new namespace:",
		}, &envMeta.Namespace)
		if err != nil {
			return err
		}
		return nil
	}
	for _, ns := range nsList.Items {
		if ns.Name == envMeta.Namespace && envMeta.Name == "" {
			envMeta.Name = ns.Labels[oam.LabelNamespaceOfEnvName]
			return nil
		}
	}
	return nil
}

func filterClusterObjectRefFromAddonObservability(resources []common.ClusterObjectReference) []common.ClusterObjectReference {
	var observabilityResources []common.ClusterObjectReference
	for _, res := range resources {
		if res.Namespace == types.DefaultKubeVelaNS && res.Name == AddonObservabilityGrafanaSvc {
			res.Kind = "Service"
			res.APIVersion = "v1"
			observabilityResources = append(observabilityResources, res)
		}
	}
	resources = observabilityResources
	return resources
}

// AskToChooseOneEnvResource will ask users to select one applied resource of the application if more than one
// resource is a map for component to applied resources
// return the selected ClusterObjectReference
func AskToChooseOneEnvResource(app *v1beta1.Application) (*common.ClusterObjectReference, error) {
	return askToChooseOneResource(app, isWorkloadClusterObjectReferenceFilter)
}

// AskToChooseOnePortForwardEndpoint will ask user to select one applied resource as port forward endpoint
func AskToChooseOnePortForwardEndpoint(app *v1beta1.Application) (*common.ClusterObjectReference, error) {
	return askToChooseOneResource(app, isPortForwardEndpointClusterObjectReferenceFilter)
}

func askToChooseOneInApplication(category string, options []string) (decision string, err error) {
	if len(options) == 0 {
		return "", fmt.Errorf("no %s exists in the application", category)
	}
	if len(options) == 1 {
		return options[0], nil
	}
	prompt := &survey.Select{
		Message: fmt.Sprintf("You have multiple %ss in your app. Please choose one %s: ", category, category),
		Options: options,
	}
	if err = survey.AskOne(prompt, &decision); err != nil {
		return "", errors2.Wrapf(err, "choosing %s failed", category)
	}
	return
}

// AskToChooseOneService will ask users to select one service of the application if more than one
func AskToChooseOneService(svcNames []string) (string, error) {
	return askToChooseOneInApplication("service", svcNames)
}

// AskToChooseOnePods will ask users to select one pods of the resource if more than one
func AskToChooseOnePods(podNames []string) (string, error) {
	return askToChooseOneInApplication("pod", podNames)
}

// ReadYamlToObject will read a yaml K8s object to runtime.Object
func ReadYamlToObject(path string, object k8sruntime.Object) error {
	data, err := os.ReadFile(filepath.Clean(path))
	if err != nil {
		return err
	}
	return yaml.Unmarshal(data, object)
}

// ParseTerraformVariables get variables from Terraform Configuration
func ParseTerraformVariables(configuration string) (map[string]*tfconfig.Variable, error) {
	p := hclparse.NewParser()
	hclFile, diagnostic := p.ParseHCL([]byte(configuration), "")
	if diagnostic != nil {
		return nil, errors.New(diagnostic.Error())
	}
	mod := tfconfig.Module{Variables: map[string]*tfconfig.Variable{}}
	diagnostic = tfconfig.LoadModuleFromFile(hclFile, &mod)
	if diagnostic != nil {
		return nil, errors.New(diagnostic.Error())
	}
	return mod.Variables, nil
}

// GenerateUnstructuredObj generate UnstructuredObj
func GenerateUnstructuredObj(name, ns string, gvk schema.GroupVersionKind) *unstructured.Unstructured {
	u := &unstructured.Unstructured{}
	u.SetGroupVersionKind(gvk)
	u.SetName(name)
	u.SetNamespace(ns)
	return u
}

// SetSpecObjIntoUnstructuredObj set UnstructuredObj spec field
func SetSpecObjIntoUnstructuredObj(spec interface{}, u *unstructured.Unstructured) error {
	bts, err := json.Marshal(spec)
	if err != nil {
		return err
	}
	data := make(map[string]interface{})
	if err := json.Unmarshal(bts, &data); err != nil {
		return err
	}
	_ = unstructured.SetNestedMap(u.Object, data, "spec")
	return nil
}

// NewK8sClient init a local k8s client which add oamcore scheme
func NewK8sClient() (client.Client, error) {
	conf, err := config.GetConfig()
	if err != nil {
		return nil, err
	}
	scheme := k8sruntime.NewScheme()
	if err := clientgoscheme.AddToScheme(scheme); err != nil {
		return nil, err
	}
	if err := oamcore.AddToScheme(scheme); err != nil {
		return nil, err
	}

	k8sClient, err := client.New(conf, client.Options{Scheme: scheme})
	if err != nil {
		return nil, err
	}
	return k8sClient, nil
}
