package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"sync"
	"time"

	"cloud.google.com/go/pubsub"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	ec2v1beta1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	cpeksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	iamv1beta1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
	awskmsv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/kms/v1alpha1"
	lambdav1alpha1 "github.com/crossplane-contrib/provider-aws/apis/lambda/v1alpha1"
	awsdnsv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/route53/v1alpha1"
	s3v1alpha3 "github.com/crossplane-contrib/provider-aws/apis/s3/v1alpha3"
	s3v1beta1 "github.com/crossplane-contrib/provider-aws/apis/s3/v1beta1"
	cpawsv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	containerv1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	awsvpcpeeringv1alpha1 "github.com/crossplane/provider-aws/apis/vpcpeering/v1alpha1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

var (
	infraKubeconfig    string
	gardenerKubeconfig string
	infraScheme        = runtime.NewScheme()
	gardenScheme       = runtime.NewScheme()
	gcpProjectID       string
	pubsubTopicID      string
)

func init() {
	flag.StringVar(&infraKubeconfig, "infra-kubeconfig", "/kube/infra/config", "path to infra kubeconfig")
	flag.StringVar(&gardenerKubeconfig, "gardener-kubeconfig", "/kube/gardener/config", "path to gardener kubeconfig")
	flag.StringVar(&gcpProjectID, "gcp-project-id", "dbaas-eng-bi", "gcp project id")
	flag.StringVar(&pubsubTopicID, "pubsub-topic-id", "dev-infra-inspection", "pubsub topic id")
	flag.Parse()
	// Load gardener API Scheme
	utilruntime.Must(clientgoscheme.AddToScheme(gardenScheme))
	// Load infra API Scheme
	utilruntime.Must(clientgoscheme.AddToScheme(infraScheme))
	utilruntime.Must(cloudv1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(s3v1beta1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(s3v1alpha3.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(cpeksv1beta1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(cpawsv1beta1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(lambdav1alpha1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(ec2v1alpha1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(ec2v1beta1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(awskmsv1alpha1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(manualv1alpha1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(awsvpcpeeringv1alpha1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(awsdnsv1alpha1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(iamv1beta1.SchemeBuilder.AddToScheme(infraScheme))
	utilruntime.Must(containerv1beta2.SchemeBuilder.AddToScheme(infraScheme)) // gke cluster is v1beta2
}

type infraNetwork struct {
	Namespace  string       `json:"namespace"`
	Name       string       `json:"name"`
	Generation int          `json:"generation"`
	TenantID   string       `json:"tenantId"`
	ProjectID  string       `json:"projectId"`
	Provider   string       `json:"provider"`
	Region     string       `json:"region"`
	Account    string       `json:"account"`
	Cluster    string       `json:"cluster"`
	Status     string       `json:"status"`
	CreatedAt  metav1.Time  `json:"createdAt"`
	DeletedAt  *metav1.Time `json:"deletedAt"`
}

type managedCluster struct {
	Name        string       `json:"name"`
	TenantID    string       `json:"tenantId"`
	ProjectID   string       `json:"projectId"`
	Region      string       `json:"region"`
	Status      string       `json:"status"`
	Ready       bool         `json:"ready"`
	CreatedAt   metav1.Time  `json:"createdAt"`
	DeletedAt   *metav1.Time `json:"deletedAt"`
	SucceededAt *metav1.Time `json:"succeededAt"`
	PendingAt   *metav1.Time `json:"pendingAt"`
}

type Rules struct {
	Name          string                              `json:"name"`
	InspectorFunc func(RuleContext) InspectTaskResult `json:"inspectorFunc"`
}

type RuleContext struct {
	Context   context.Context
	TopicCli  *pubsub.Topic
	InfraCli  client.Client
	GardenCli client.Client
}

type InspectTaskResult struct {
	ResultDetails interface{} `json:"resultDetails"`
	Error         error       `json:"error"`
}

type NotificationMsg struct {
	RuleName      string      `json:"ruleName"`
	ResultDetails interface{} `json:"resultDetails"`
	Time          time.Time   `json:"time"`
}

func main() {
	gardenCfg, err := clientcmd.BuildConfigFromFlags("", gardenerKubeconfig)
	if err != nil {
		panic(err)
	}
	gardenCli, err := client.New(gardenCfg, client.Options{Scheme: gardenScheme})
	if err != nil {
		panic(err)
	}

	infraCfg, err := clientcmd.BuildConfigFromFlags("", infraKubeconfig)
	if err != nil {
		panic(err)
	}
	infraCli, err := client.New(infraCfg, client.Options{Scheme: infraScheme})
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	pubSubClient, err := pubsub.NewClient(ctx, gcpProjectID)
	if err != nil {
		panic(err)
	}
	defer pubSubClient.Close()
	topic := pubSubClient.Topic(pubsubTopicID)
	defer topic.Stop()

	ruleContext := RuleContext{
		Context:   ctx,
		TopicCli:  topic,
		InfraCli:  infraCli,
		GardenCli: gardenCli,
	}

	rules := []Rules{
		{
			Name:          "abnormal networks",
			InspectorFunc: findAbnormalNetworks,
		},
		{
			Name:          "abnormal clusters",
			InspectorFunc: findAbnormalClusters,
		},
	}

	var wg sync.WaitGroup

	inspectionTime := time.Now()
	for _, rule := range rules {
		wg.Add(1)
		go func(ruleName string, ruleInspectorFunc func(RuleContext) InspectTaskResult) {
			defer wg.Done()
			result := ruleInspectorFunc(ruleContext)
			if result.Error != nil {
				fmt.Println(result.Error)
			}
			if result.ResultDetails != "" {
				if err := sendNotification(ruleContext, ruleName, result, inspectionTime); err != nil {
					fmt.Println(err)
				}
			}
		}(rule.Name, rule.InspectorFunc)
	}
	wg.Wait()

}

func sendNotification(ruleContext RuleContext, ruleName string, inspectTaskResult InspectTaskResult, inspectionTime time.Time) error {
	body, err := json.Marshal(NotificationMsg{
		RuleName:      ruleName,
		ResultDetails: inspectTaskResult.ResultDetails,
		Time:          inspectionTime,
	})
	if err != nil {
		return err
	}

	msg := ruleContext.TopicCli.Publish(ruleContext.Context, &pubsub.Message{
		Data: body,
	})

	id, err := msg.Get(ruleContext.Context)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Published a message with a message ID: %s\n", id)
	return nil
}

func fetchEKS(cli client.Client, listOpts client.ListOptions) []managedCluster {
	eksList := &cpeksv1beta1.ClusterList{}
	if err := cli.List(context.TODO(), eksList, &listOpts); err != nil {
		panic(err)
	}
	eksClusters := []managedCluster{}
	for _, eks := range eksList.Items {
		var ready bool
		for _, condition := range eks.Status.Conditions {
			if condition.Type == "Ready" {
				ready = condition.Status == "True"
				break
			}
		}
		var succeededAt, pendingAt *metav1.Time
		if eks.Annotations != nil {
			if pending, ok := eks.Annotations["crossplane.io/external-create-pending"]; ok {
				ts, err := time.Parse(time.RFC3339, pending)
				if err != nil {
					panic(err)
				}
				pendingAt = &metav1.Time{Time: ts}
			}
			if succeeded, ok := eks.Annotations["crossplane.io/external-create-succeeded"]; ok {
				ts, err := time.Parse(time.RFC3339, succeeded)
				if err != nil {
					panic(err)
				}
				succeededAt = &metav1.Time{Time: ts}
			}
		}
		eksClusters = append(eksClusters, managedCluster{
			Name:        eks.Name,
			TenantID:    eks.Spec.ForProvider.Tags["tenant"],
			ProjectID:   eks.Spec.ForProvider.Tags["project"],
			Region:      *eks.Spec.ForProvider.Region,
			Status:      string(eks.Status.AtProvider.Status),
			Ready:       ready,
			CreatedAt:   eks.CreationTimestamp,
			DeletedAt:   eks.DeletionTimestamp,
			SucceededAt: succeededAt,
			PendingAt:   pendingAt,
		})
	}
	return eksClusters
}

func fetchNetwork(cli client.Client, listOpts client.ListOptions) []infraNetwork {
	networkList := &cloudv1.NetworkList{}
	if err := cli.List(context.TODO(), networkList, &listOpts); err != nil {
		panic(err)
	}
	infraNetworks := []infraNetwork{}
	for _, network := range networkList.Items {
		var tenantID, projectID, cluster, account string
		// filter out devtier
		if network.Spec.Provider == "aws-free-tier" {
			continue
		}
		if network.Labels != nil {
			projectID = network.Labels["tags.tidbcloud.com/project"]
			tenantID = network.Labels["tags.tidbcloud.com/tenant"]
		}
		if network.Annotations != nil {
			if network.Spec.Provider == "eks" {
				cluster = network.Annotations["network.cloud.pingcap.com/eks-name"]
			}
		}
		if network.Spec.AccountRef != nil {
			account = network.Spec.AccountRef.Name
		}
		infraNetworks = append(infraNetworks, infraNetwork{
			Name:       network.Name,
			Namespace:  network.Namespace,
			Generation: int(network.Generation),
			ProjectID:  projectID,
			TenantID:   tenantID,
			CreatedAt:  network.CreationTimestamp,
			DeletedAt:  network.DeletionTimestamp,
			Status:     network.Status.Phase,
			Account:    account,
			Provider:   network.Spec.Provider,
			Region:     network.Spec.Region,
			Cluster:    cluster,
		})
	}
	return infraNetworks
}

func fetchGKE(cli client.Client, listOpts client.ListOptions) []managedCluster {
	gkeList := &containerv1beta2.ClusterList{}
	if err := cli.List(context.TODO(), gkeList, &listOpts); err != nil {
		panic(err)
	}
	gkeClusters := []managedCluster{}
	for _, gke := range gkeList.Items {
		var ready bool
		for _, condition := range gke.Status.Conditions {
			if condition.Type == "Ready" {
				ready = condition.Status == "True"
				break
			}
		}
		var succeededAt, pendingAt *metav1.Time
		if gke.Annotations != nil {
			if pending, ok := gke.Annotations["crossplane.io/external-create-pending"]; ok {
				ts, err := time.Parse(time.RFC3339, pending)
				if err != nil {
					panic(err)
				}
				pendingAt = &metav1.Time{Time: ts}
			}
			if succeeded, ok := gke.Annotations["crossplane.io/external-create-succeeded"]; ok {
				ts, err := time.Parse(time.RFC3339, succeeded)
				if err != nil {
					panic(err)
				}
				succeededAt = &metav1.Time{Time: ts}
			}
		}
		gkeClusters = append(gkeClusters, managedCluster{
			Name:        gke.Name,
			TenantID:    gke.Spec.ForProvider.ResourceLabels["tenant"],
			ProjectID:   gke.Spec.ForProvider.ResourceLabels["project"],
			Region:      gke.Spec.ForProvider.Location,
			Status:      gke.Status.AtProvider.Status,
			Ready:       ready,
			CreatedAt:   gke.CreationTimestamp,
			DeletedAt:   gke.DeletionTimestamp,
			SucceededAt: succeededAt,
			PendingAt:   pendingAt,
		})
	}
	return gkeClusters
}

func findAbnormalNetworks(r RuleContext) InspectTaskResult {
	listOpts := client.ListOptions{Namespace: ""}
	infraNetworks := fetchNetwork(r.InfraCli, listOpts)

	networks := []infraNetwork{}
	for _, network := range infraNetworks {
		if network.DeletedAt != nil || (network.Status != "Normal" && network.Status != "Hibernated") {
			networks = append(networks, network)
		}
	}

	return InspectTaskResult{
		ResultDetails: networks,
		Error:         nil,
	}
}

func findAbnormalClusters(r RuleContext) InspectTaskResult {
	listOpts := client.ListOptions{Namespace: ""}
	eksClusters := fetchEKS(r.InfraCli, listOpts)
	gkeClusters := fetchGKE(r.InfraCli, listOpts)

	managedClusters := []managedCluster{}
	managedClusters = append(managedClusters, eksClusters...)
	managedClusters = append(managedClusters, gkeClusters...)

	clusters := []managedCluster{}
	for _, cluster := range managedClusters {
		if cluster.DeletedAt != nil || !cluster.Ready ||
			(cluster.Status != "RUNNING" && cluster.Status != "ACTIVE") {
			clusters = append(clusters, cluster)
		}
	}
	return InspectTaskResult{
		ResultDetails: clusters,
		Error:         nil,
	}
}
