/*
 *
 * Copyright 2020 The KubeSphere 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 openpitrix

import (
	"fmt"
	"github.com/golang/protobuf/ptypes/wrappers"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/api/core/v1"
	"k8s.io/api/extensions/v1beta1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/client-go/informers"
	"k8s.io/klog"
	"kubesphere.io/kubesphere/pkg/constants"
	"kubesphere.io/kubesphere/pkg/models"
	"kubesphere.io/kubesphere/pkg/server/params"
	"kubesphere.io/kubesphere/pkg/simple/client/openpitrix"
	"openpitrix.io/openpitrix/pkg/pb"
	"strings"
)

type ApplicationInterface interface {
	ListApplications(conditions *params.Conditions, limit, offset int, orderBy string, reverse bool) (*models.PageableResponse, error)
	DescribeApplication(namespace, clusterId string) (*Application, error)
	CreateApplication(namespace string, request CreateClusterRequest) error
	ModifyApplication(request ModifyClusterAttributesRequest) error
	DeleteApplication(id string) error
}

type applicationOperator struct {
	informers informers.SharedInformerFactory
	opClient  openpitrix.Client
}

func newApplicationOperator(informers informers.SharedInformerFactory, opClient openpitrix.Client) ApplicationInterface {
	return &applicationOperator{informers: informers, opClient: opClient}
}

type Application struct {
	Name      string            `json:"name" description:"application name"`
	Cluster   *Cluster          `json:"cluster,omitempty" description:"application cluster info"`
	Version   *AppVersion       `json:"version,omitempty" description:"application template version info"`
	App       *App              `json:"app,omitempty" description:"application template info"`
	WorkLoads *workLoads        `json:"workloads,omitempty" description:"application workloads"`
	Services  []v1.Service      `json:"services,omitempty" description:"application services"`
	Ingresses []v1beta1.Ingress `json:"ingresses,omitempty" description:"application ingresses"`
}

type workLoads struct {
	Deployments  []appsv1.Deployment  `json:"deployments,omitempty" description:"deployment list"`
	Statefulsets []appsv1.StatefulSet `json:"statefulsets,omitempty" description:"statefulset list"`
	Daemonsets   []appsv1.DaemonSet   `json:"daemonsets,omitempty" description:"daemonset list"`
}

func (c *applicationOperator) ListApplications(conditions *params.Conditions, limit, offset int, orderBy string, reverse bool) (*models.PageableResponse, error) {
	describeClustersRequest := &pb.DescribeClustersRequest{
		Limit:  uint32(limit),
		Offset: uint32(offset)}
	if keyword := conditions.Match[Keyword]; keyword != "" {
		describeClustersRequest.SearchWord = &wrappers.StringValue{Value: keyword}
	}
	if runtimeId := conditions.Match[RuntimeId]; runtimeId != "" {
		describeClustersRequest.RuntimeId = []string{runtimeId}
	}
	if appId := conditions.Match[AppId]; appId != "" {
		describeClustersRequest.AppId = []string{appId}
	}
	if versionId := conditions.Match[VersionId]; versionId != "" {
		describeClustersRequest.VersionId = []string{versionId}
	}
	if status := conditions.Match[Status]; status != "" {
		describeClustersRequest.Status = strings.Split(status, "|")
	}
	if orderBy != "" {
		describeClustersRequest.SortKey = &wrappers.StringValue{Value: orderBy}
	}
	describeClustersRequest.Reverse = &wrappers.BoolValue{Value: reverse}
	resp, err := c.opClient.DescribeClusters(openpitrix.SystemContext(), describeClustersRequest)
	if err != nil {
		klog.Errorln(err)
		return nil, err
	}
	result := models.PageableResponse{TotalCount: int(resp.TotalCount)}
	result.Items = make([]interface{}, 0)
	for _, cluster := range resp.ClusterSet {
		app, err := c.describeApplication(cluster)
		if err != nil {
			klog.Errorln(err)
			return nil, err
		}
		result.Items = append(result.Items, app)
	}

	return &result, nil
}

func (c *applicationOperator) describeApplication(cluster *pb.Cluster) (*Application, error) {
	var app Application
	app.Name = cluster.Name.Value
	app.Cluster = convertCluster(cluster)
	versionInfo, err := c.opClient.DescribeAppVersions(openpitrix.SystemContext(), &pb.DescribeAppVersionsRequest{VersionId: []string{cluster.GetVersionId().GetValue()}})
	if err != nil {
		klog.Errorln(err)
		return nil, err
	}
	if len(versionInfo.AppVersionSet) > 0 {
		app.Version = convertAppVersion(versionInfo.AppVersionSet[0])
	}
	appInfo, err := c.opClient.DescribeApps(openpitrix.SystemContext(), &pb.DescribeAppsRequest{AppId: []string{cluster.GetAppId().GetValue()}, Limit: 1})
	if err != nil {
		klog.Errorln(err)
		return nil, err
	}
	if len(appInfo.AppSet) > 0 {
		app.App = convertApp(appInfo.GetAppSet()[0])
	}
	return &app, nil
}

func (c *applicationOperator) DescribeApplication(namespace string, clusterId string) (*Application, error) {

	clusters, err := c.opClient.DescribeClusters(openpitrix.SystemContext(), &pb.DescribeClustersRequest{ClusterId: []string{clusterId}, Limit: 1})

	if err != nil {
		klog.Errorln(err)
		return nil, err
	}

	var cluster *pb.Cluster
	if len(clusters.ClusterSet) > 0 {
		cluster = clusters.GetClusterSet()[0]
	} else {
		err := status.New(codes.NotFound, "resource not found").Err()
		klog.Errorln(err)
		return nil, err
	}
	app, err := c.describeApplication(cluster)
	if err != nil {
		klog.Errorln(err)
		return nil, err
	}

	workloads, err := c.getWorkLoads(namespace, cluster.ClusterRoleSet)

	if err != nil {
		klog.Errorln(err)
		return nil, err
	}
	app.WorkLoads = workloads
	workloadLabels := c.getLabels(namespace, app.WorkLoads)
	app.Services = c.getSvcs(namespace, workloadLabels)
	app.Ingresses = c.getIng(namespace, app.Services)
	return app, nil
}

func (c *applicationOperator) getWorkLoads(namespace string, clusterRoles []*pb.ClusterRole) (*workLoads, error) {

	var works workLoads
	for _, clusterRole := range clusterRoles {
		workLoadName := clusterRole.Role.Value
		if len(workLoadName) > 0 {
			if strings.HasSuffix(workLoadName, openpitrix.DeploySuffix) {
				name := strings.Split(workLoadName, openpitrix.DeploySuffix)[0]

				item, err := c.informers.Apps().V1().Deployments().Lister().Deployments(namespace).Get(name)

				if err != nil {
					// app not ready
					if errors.IsNotFound(err) {
						continue
					}
					klog.Errorln(err)
					return nil, err
				}

				works.Deployments = append(works.Deployments, *item)
				continue
			}

			if strings.HasSuffix(workLoadName, openpitrix.DaemonSuffix) {
				name := strings.Split(workLoadName, openpitrix.DaemonSuffix)[0]
				item, err := c.informers.Apps().V1().DaemonSets().Lister().DaemonSets(namespace).Get(name)
				if err != nil {
					// app not ready
					if errors.IsNotFound(err) {
						continue
					}
					klog.Errorln(err)
					return nil, err
				}
				works.Daemonsets = append(works.Daemonsets, *item)
				continue
			}

			if strings.HasSuffix(workLoadName, openpitrix.StateSuffix) {
				name := strings.Split(workLoadName, openpitrix.StateSuffix)[0]
				item, err := c.informers.Apps().V1().StatefulSets().Lister().StatefulSets(namespace).Get(name)
				if err != nil {
					// app not ready
					if errors.IsNotFound(err) {
						continue
					}
					klog.Errorln(err)
					return nil, err
				}
				works.Statefulsets = append(works.Statefulsets, *item)
				continue
			}
		}
	}
	return &works, nil
}

func (c *applicationOperator) getLabels(namespace string, workloads *workLoads) *[]map[string]string {

	var workloadLabels []map[string]string
	if workloads == nil {
		return nil
	}

	for _, workload := range workloads.Deployments {
		deploy, err := c.informers.Apps().V1().Deployments().Lister().Deployments(namespace).Get(workload.Name)
		if errors.IsNotFound(err) {
			continue
		}
		workloadLabels = append(workloadLabels, deploy.Labels)
	}

	for _, workload := range workloads.Daemonsets {
		daemonset, err := c.informers.Apps().V1().DaemonSets().Lister().DaemonSets(namespace).Get(workload.Name)
		if errors.IsNotFound(err) {
			continue
		}
		workloadLabels = append(workloadLabels, daemonset.Labels)
	}

	for _, workload := range workloads.Statefulsets {
		statefulset, err := c.informers.Apps().V1().StatefulSets().Lister().StatefulSets(namespace).Get(workload.Name)
		if errors.IsNotFound(err) {
			continue
		}
		workloadLabels = append(workloadLabels, statefulset.Labels)
	}

	return &workloadLabels
}

func (c *applicationOperator) isExist(svcs []v1.Service, svc *v1.Service) bool {
	for _, item := range svcs {
		if item.Name == svc.Name && item.Namespace == svc.Namespace {
			return true
		}
	}
	return false
}

func (c *applicationOperator) getSvcs(namespace string, workLoadLabels *[]map[string]string) []v1.Service {
	if len(*workLoadLabels) == 0 {
		return nil
	}
	var services []v1.Service
	for _, label := range *workLoadLabels {
		labelSelector := labels.Set(label).AsSelector()
		svcs, err := c.informers.Core().V1().Services().Lister().Services(namespace).List(labelSelector)
		if err != nil {
			klog.Errorf("get app's svc failed, reason: %v", err)
		}
		for _, item := range svcs {
			if !c.isExist(services, item) {
				services = append(services, *item)
			}
		}
	}

	return services
}

func (c *applicationOperator) getIng(namespace string, services []v1.Service) []v1beta1.Ingress {
	if services == nil {
		return nil
	}

	var ings []v1beta1.Ingress
	for _, svc := range services {
		ingresses, err := c.informers.Extensions().V1beta1().Ingresses().Lister().Ingresses(namespace).List(labels.Everything())
		if err != nil {
			klog.Error(err)
			return ings
		}

		for _, ingress := range ingresses {
			if ingress.Spec.Backend.ServiceName != svc.Name {
				continue
			}

			exist := false
			var tmpRules []v1beta1.IngressRule
			for _, rule := range ingress.Spec.Rules {
				for _, p := range rule.HTTP.Paths {
					if p.Backend.ServiceName == svc.Name {
						exist = true
						tmpRules = append(tmpRules, rule)
					}
				}
			}

			if exist {
				ing := v1beta1.Ingress{}
				ing.Name = ingress.Name
				ing.Spec.Rules = tmpRules
				ings = append(ings, ing)
			}
		}
	}

	return ings
}

func (c *applicationOperator) CreateApplication(namespace string, request CreateClusterRequest) error {
	ns, err := c.informers.Core().V1().Namespaces().Lister().Get(namespace)
	if err != nil {
		klog.Error(err)
		return err
	}

	if runtimeId := ns.Annotations[constants.OpenPitrixRuntimeAnnotationKey]; runtimeId != "" {
		request.RuntimeId = runtimeId
	} else {
		return fmt.Errorf("runtime not init: namespace %s", namespace)
	}

	_, err = c.opClient.CreateCluster(openpitrix.ContextWithUsername(request.Username), &pb.CreateClusterRequest{
		AppId:     &wrappers.StringValue{Value: request.AppId},
		VersionId: &wrappers.StringValue{Value: request.VersionId},
		RuntimeId: &wrappers.StringValue{Value: request.RuntimeId},
		Conf:      &wrappers.StringValue{Value: request.Conf},
	})

	if err != nil {
		klog.Errorln(err)
		return err
	}

	return nil
}

func (c *applicationOperator) ModifyApplication(request ModifyClusterAttributesRequest) error {

	modifyClusterAttributesRequest := &pb.ModifyClusterAttributesRequest{ClusterId: &wrappers.StringValue{Value: request.ClusterID}}
	if request.Name != nil {
		modifyClusterAttributesRequest.Name = &wrappers.StringValue{Value: *request.Name}
	}
	if request.Description != nil {
		modifyClusterAttributesRequest.Description = &wrappers.StringValue{Value: *request.Description}
	}

	_, err := c.opClient.ModifyClusterAttributes(openpitrix.SystemContext(), modifyClusterAttributesRequest)

	if err != nil {
		klog.Errorln(err)
		return err
	}

	return nil
}

func (c *applicationOperator) DeleteApplication(clusterId string) error {
	_, err := c.opClient.DeleteClusters(openpitrix.SystemContext(), &pb.DeleteClustersRequest{ClusterId: []string{clusterId}, Force: &wrappers.BoolValue{Value: true}})

	if err != nil {
		klog.Errorln(err)
		return err
	}

	return nil
}
