package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/config"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	"github.com/liggitt/tabwriter"

	"k8s.io/apimachinery/pkg/util/duration"

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

var (
	scheme = runtime.NewScheme()

	providerLabels = map[string]string{}
	dryRun         bool
	providerType   string
)

func init() {
	log.SetLogger(zap.New())
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(cloudv1.SchemeBuilder.AddToScheme(scheme))
}

type Network struct {
	cloudv1.Network
	Clusters     []string
	BackupCount  int
	PeeringCount int
	NetworkType  NetworkType
}

type NetworkType string

var (
	networkCanbeDelete        NetworkType = "Delete"
	networkCanbeMigrate       NetworkType = "MigrateNetwork"
	networkNeedMigrateCluster NetworkType = "MigrateCluster"
)

func main() {
	log := log.Log.WithName("network-cleanup")
	flag.BoolVar(&dryRun, "dry-run", true, "Clean up network in dry-run mode.")
	flag.StringVar(&providerType, "provider-type", "gcp", "Network provider type name.")
	flag.Parse()

	err := validateProviderType(providerType)
	if err != nil {
		log.Error(err, "invalid provider tpye")
		os.Exit(1)
	}
	// Setup a Manager
	log.Info("setting up manager")
	mgr, err := ctrl.NewManager(config.GetConfigOrDie(), ctrl.Options{
		Scheme: scheme,
	})
	if err != nil {
		log.Error(err, "unable to set up manager")
		os.Exit(1)
	}
	// Direct access apiserver
	reader := mgr.GetAPIReader()
	ctx := context.TODO()
	nls := new(cloudv1.NetworkList)

	providerLabels[cloudv1.ProviderTypeLabel] = providerType
	// List networks contains privder-type=aws
	err = reader.List(ctx, nls, &client.ListOptions{LabelSelector: labels.SelectorFromSet(providerLabels)})
	if err != nil {
		log.Error(err, "failed list network")
		os.Exit(1)
	}
	log.Info("list networks", "network counts", len(nls.Items))

	var results []Network
	for _, n := range nls.Items {
		var item Network
		item.Network = n

		res, err := listClustersInNetwork(ctx, reader, n)
		if err != nil {
			log.Error(err, "failed list clusters in network")
			os.Exit(1)
		}
		item.Clusters = res

		backupRes, err := listBackupInNetwork(ctx, reader, n)
		if err != nil {
			log.Error(err, "failed list backups in network")
			os.Exit(1)
		}
		item.BackupCount = len(backupRes)

		peerRes, err := listPeeringInNetwork(ctx, reader, n)
		if err != nil {
			log.Error(err, "failed list peering in network")
			os.Exit(1)
		}

		item.PeeringCount = len(peerRes)
		if len(item.Clusters) > 0 {
			item.NetworkType = networkNeedMigrateCluster
		} else if item.BackupCount > 0 || item.PeeringCount > 0 {
			item.NetworkType = networkCanbeMigrate
		} else {
			item.NetworkType = networkCanbeDelete
		}
		results = append(results, item)
	}
	fmt.Println("Networks can be deleted:")
	printNetwork(ctx, results, networkCanbeDelete)
	fmt.Println("Networks can be migrate:")
	printNetwork(ctx, results, networkCanbeMigrate)
	fmt.Println("Networks need migrate cluster:")
	printNetwork(ctx, results, networkNeedMigrateCluster)
}

func printNetwork(ctx context.Context, nt []Network, t NetworkType) {
	w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
	fmt.Fprintln(w, "Namespace\tNetwork\tTenant\tCidr\tStatus\tClusters\tBackupCount\tPeeringCounts\tHowTo\tAge")
	defer w.Flush()
	for _, n := range nt {
		if n.NetworkType == t {
			fmt.Fprintln(w, n.Namespace+"\t"+
				n.Name+"\t"+
				n.Labels["tags.tidbcloud.com/tenant"]+"\t"+
				n.Spec.CIDR+"\t"+
				n.Status.Phase+"\t"+
				strings.Join(n.Clusters, ",")+"\t"+
				strconv.Itoa(n.BackupCount)+"\t"+
				strconv.Itoa(n.PeeringCount)+"\t"+
				string(n.NetworkType)+"\t"+
				duration.HumanDuration(time.Since(n.CreationTimestamp.Time)))
		}
	}
}

func validateProviderType(pt string) error {
	if pt != cloudv1.ProviderAWS && pt != cloudv1.ProviderGCP {
		return fmt.Errorf("provider type must aws or gcp")
	}
	return nil
}

func listClustersInNetwork(ctx context.Context, cli client.Reader, n cloudv1.Network) (res []string, err error) {
	clusters := new(cloudv1.ClusterList)
	err = cli.List(ctx, clusters, &client.ListOptions{
		LabelSelector: labels.SelectorFromSet(providerLabels),
		Namespace:     n.Namespace,
	})
	if err != nil {
		return
	}

	for _, cluster := range clusters.Items {
		if cluster.Spec.NetworkName == n.Name {
			res = append(res, cluster.Name)
		}
	}
	return
}

func listBackupInNetwork(ctx context.Context, cli client.Reader, n cloudv1.Network) (res []string, err error) {
	backups := new(cloudv1.BackupList)
	err = cli.List(ctx, backups, &client.ListOptions{
		Namespace: n.Namespace,
	})
	if err != nil {
		return
	}

	status, err := cloudv1.GardenerNetworkStatusFromNetwork(&n)
	if err != nil {
		return
	}

	for _, backup := range backups.Items {
		if backup.Status.BucketName == status.BucketName {
			res = append(res, backup.Name)
		}
	}
	return
}

func listPeeringInNetwork(ctx context.Context, cli client.Reader, n cloudv1.Network) (res []string, err error) {
	peers := new(cloudv1.NetworkPeerList)
	err = cli.List(ctx, peers, &client.ListOptions{
		Namespace: n.Namespace,
	})
	if err != nil {
		return
	}

	for _, peer := range peers.Items {
		// TargetNetwork used for TiCDC cross-region.
		if peer.Spec.Source == n.Name || (peer.Spec.Target.TargetNetwork != nil && peer.Spec.Target.TargetNetwork.NetworkName == n.Name) {
			res = append(res, peer.Name)
		}
	}
	return
}
