package infra

import (
	"context"
	"encoding/json"
	"strconv"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

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

var logger = logf.Log.WithName("infra-metrics")

var (
	InfraResourcesNamespaceInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "namespace_info"),
		"Information and status of infra namespace.",
		[]string{"name", "status"},
		nil)

	InfraResourcesClusterInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "cluster_info"),
		"Information and status of infra cluster.",
		[]string{"name", "namespace", "network_name", "metrics_base_url", "proxy_endpoint", "version", "status"},
		nil)

	InfraResourcesClusterCreated = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "cluster_created"),
		"Creation timestamp and status of infra cluster.",
		[]string{"name", "namespace", "status"},
		nil)

	InfraResourcesClusterPrivateLinkServiceCreated = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "cluster_privatelink_service_created"),
		"Information and status of privatelink service in clusters",
		[]string{"name", "namespace", "status"},
		nil)

	InfraResourcesNetworkInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "network_info"),
		"Information and status of infra network.",
		[]string{"name", "namespace", "shoot_name", "bucket_name", "project_id", "region", "provider", "cidr", "account_id", "email", "external_id", "status"},
		nil)

	InfraResourcesImportInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "import_info"),
		"Information and status of import job.",
		[]string{"name", "namespace", "cluster_name", "cloud_provider", "project_id", "tenant_id", "cluster_id", "status", "import_config"},
		nil)

	InfraResourceBackupInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "backup_info"),
		"Information and status of backup job.",
		[]string{"name", "namespace", "cluster_name", "cloud_provider", "project_id", "tenant_id", "cluster_id", "bucket_name", "status"},
		nil)

	InfraResourceRestoreInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "restore_info"),
		"Information and status of restore job.",
		[]string{"name", "namespace", "cluster_name", "cloud_provider", "project_id", "tenant_id", "cluster_id", "backup", "status"},
		nil)

	InfraResourcesNetworkPeerInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "networkpeer_info"),
		"Information and status of networkpeer.",
		[]string{"name", "namespace", "source", "target_account_id", "target_vpc_id", "peer_cidr", "peer_region", "target_project_id", "peer_network", "target_network", "peering_id", "hosted_zone_id", "vpc_cidr", "status"},
		nil)

	InfraResourcesTiCDCInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "ticdc_info"),
		"Information and status of TiCDC.",
		[]string{"name", "namespace", "network_name", "cluster_name", "replicas", "instance_type", "status"},
		nil)

	InfraResourcesClusterConfigInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "clusterconfig_info"),
		"Information and status of ClusterConfig.",
		[]string{"name", "cluster_id", "tenant_id", "cluster_version", "cluster_config"},
		nil)

	InfraFirstPassWhenCreatingCluster = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "first_pass_stage_when_creating_cluster_duration_seconds"),
		"First pass stage when creating cluster.",
		[]string{"resource_namespace", "name", "createtime", "tenant_id", "project_id", "cluster_id", "stage"},
		nil)

	InfraFirstPassWhenCreatingNetwork = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "api_resources", "first_pass_stage_when_creating_network_duration_seconds"),
		"First pass stage when creating network.",
		[]string{"resource_namespace", "name", "createtime", "tenant_id", "project_id", "stage"},
		nil)
)

type MetricsCollector struct {
	infraCli client.Client
}

func NewMetricsCollector(infraCli client.Client) *MetricsCollector {
	return &MetricsCollector{
		infraCli: infraCli,
	}
}

func (collector *MetricsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- InfraResourcesNamespaceInfo
	ch <- InfraResourcesNetworkInfo
	ch <- InfraResourcesClusterInfo
	ch <- InfraResourcesClusterCreated
	ch <- InfraResourcesImportInfo
	ch <- InfraResourceBackupInfo
	ch <- InfraResourceRestoreInfo
	ch <- InfraResourcesNetworkPeerInfo
	ch <- InfraResourcesTiCDCInfo
	ch <- InfraResourcesClusterConfigInfo
	ch <- InfraFirstPassWhenCreatingCluster
	ch <- InfraFirstPassWhenCreatingNetwork
}

func (collector *MetricsCollector) Collect(ch chan<- prometheus.Metric) {
	cli := collector.infraCli
	ctx := context.Background()

	if err := collectNamespaceMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect namespace metrics failed")
		return
	}

	if err := collectNetworkMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect network metrics failed")
		return
	}

	if err := collectClusterMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect cluster metrics failed")
		return
	}

	if err := collectImportMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect import metrics failed")
		return
	}

	if err := collectBackupMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect backup metrics failed")
		return
	}

	if err := collectRestoreMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect restore metrics failed")
		return
	}

	if err := collectNetworkPeerMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect networkpeer metrics failed")
		return
	}

	if err := collectTiCDCMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect TiCDC metrics failed")
		return
	}

	if err := collectClusterConfigMetrics(ctx, cli, ch); err != nil {
		logger.Error(err, "collect clusterconfig metrics failed")
		return
	}
}

func collectNamespaceMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	namespaces := corev1.NamespaceList{}
	if err := cli.List(ctx, &namespaces); err != nil {
		logger.Error(err, "list namespaces failed")
		return err
	}
	for _, namespace := range namespaces.Items {
		ch <- prometheus.MustNewConstMetric(
			InfraResourcesNamespaceInfo,
			prometheus.GaugeValue,
			1.0,
			namespace.Name,
			string(namespace.Status.Phase),
		)
	}
	return nil
}

func collectNetworkMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	networks := cloudv1.NetworkList{}
	if err := cli.List(ctx, &networks); err != nil {
		logger.Error(err, "list networks failed")
		return err
	}

	err := collentInfraFirstPassWhenCreatingNetwork(ctx, ch, networks.Items)
	if err != nil {
		logger.Error(err, "collect creating cluster info")
	}

	for _, network := range networks.Items {
		var shootName, bucketName string
		status, err := cloudv1.GardenerNetworkStatusFromNetwork(&network)
		if err != nil {
			logger.Error(err, "get network status from network")
		} else {
			shootName, bucketName = status.ShootName, status.BucketName
		}

		ch <- prometheus.MustNewConstMetric(
			InfraResourcesNetworkInfo,
			prometheus.GaugeValue,
			1.0,
			network.Name,
			network.Namespace,
			shootName,
			bucketName,
			network.GetProjectId(),
			network.Spec.Region,
			network.Spec.Provider,
			network.Spec.CIDR,
			network.Status.IAM.AccountID,
			network.Status.IAM.ServiceAccountEmail,
			network.Status.IAM.ExternalId,
			network.Status.Phase,
		)
	}
	return nil
}

func collectClusterMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	clusters := cloudv1.ClusterList{}
	if err := cli.List(ctx, &clusters); err != nil {
		logger.Error(err, "list clusters failed")
		return err
	}

	err := collentInfraFirstPassWhenCreatingCluster(ctx, ch, clusters.Items)
	if err != nil {
		logger.Error(err, "collect creating cluster info")
	}

	for _, cluster := range clusters.Items {
		var (
			version, baseURL, endpoint string
		)
		if cluster.Status.DB != nil {
			version = cluster.Status.DB.Version
		}
		if cluster.Status.Metrics != nil {
			baseURL = cluster.Status.Metrics.BaseURL
		}
		if cluster.Status.Proxy != nil {
			endpoint = cluster.Status.Proxy.Endpoint
		}
		ch <- prometheus.MustNewConstMetric(
			InfraResourcesClusterInfo,
			prometheus.GaugeValue,
			1.0,
			cluster.Name,
			cluster.Namespace,
			cluster.Spec.NetworkName,
			baseURL,
			endpoint,
			version,
			string(cluster.Status.Phase),
		)

		ch <- prometheus.MustNewConstMetric(
			InfraResourcesClusterCreated,
			prometheus.GaugeValue,
			float64(cluster.CreationTimestamp.Unix()),
			cluster.Name,
			cluster.Namespace,
			string(cluster.Status.Phase),
		)

		privatelinkService := cluster.Status.SqlEndpoints.PrivateLinkService
		if privatelinkService != nil {
			ch <- prometheus.MustNewConstMetric(
				InfraResourcesClusterPrivateLinkServiceCreated,
				prometheus.GaugeValue,
				1.0,
				cluster.Name,
				cluster.Namespace,
				string(privatelinkService.Phase),
			)
		}
	}
	return nil
}

func collectImportMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	imports := cloudv1.ImportList{}
	if err := cli.List(ctx, &imports); err != nil {
		logger.Error(err, "list import jobs failed")
		return err
	}

	for _, importJob := range imports.Items {
		importSpec := importJob.Spec
		// we must remove sensitive informations in import job
		importSpec.Source.UserName = ""
		importSpec.Source.Password = ""

		importConfig, err := json.Marshal(importSpec)
		if err != nil {
			logger.Error(err, "marshal import config failed")
			return err
		}

		ch <- prometheus.MustNewConstMetric(
			InfraResourcesImportInfo,
			prometheus.GaugeValue,
			1.0,
			importJob.Name,
			importJob.Namespace,
			importJob.Spec.ClusterName,
			importJob.Labels[cloudv1.ProviderTypeLabel],
			importJob.Labels[consts.ProjectTagKey],
			importJob.Labels[consts.TenantTagKey],
			importJob.Labels[consts.ClusterTagKey],
			string(importJob.Status.Phase),
			string(importConfig),
		)
	}
	return nil
}

func collectBackupMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	backups := cloudv1.BackupList{}
	if err := cli.List(ctx, &backups); err != nil {
		logger.Error(err, "list backup jobs failed")
		return err
	}

	for _, backup := range backups.Items {
		ch <- prometheus.MustNewConstMetric(
			InfraResourceBackupInfo,
			prometheus.GaugeValue,
			1.0,
			backup.Name,
			backup.Namespace,
			backup.Spec.ClusterName,
			backup.Labels[cloudv1.ProviderTypeLabel],
			backup.Labels[consts.ProjectTagKey],
			backup.Labels[consts.TenantTagKey],
			backup.Labels[consts.ClusterTagKey],
			backup.Status.BucketName,
			string(backup.Status.Phase),
		)
	}
	return nil
}

func collectRestoreMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	restores := cloudv1.RestoreList{}
	if err := cli.List(ctx, &restores); err != nil {
		logger.Error(err, "list restore jobs failed")
		return err
	}

	for _, restore := range restores.Items {
		ch <- prometheus.MustNewConstMetric(
			InfraResourceRestoreInfo,
			prometheus.GaugeValue,
			1.0,
			restore.Name,
			restore.Namespace,
			restore.Spec.ClusterName,
			restore.Labels[cloudv1.ProviderTypeLabel],
			restore.Labels[consts.ProjectTagKey],
			restore.Labels[consts.TenantTagKey],
			restore.Labels[consts.ClusterTagKey],
			restore.Spec.From.Backup,
			string(restore.Status.Phase),
		)
	}
	return nil
}

func collectNetworkPeerMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	networkPeers := cloudv1.NetworkPeerList{}
	if err := cli.List(ctx, &networkPeers); err != nil {
		logger.Error(err, "list networkpeers failed")
		return err
	}

	for _, np := range networkPeers.Items {
		var accountID, vpcID, peerCIDR, peerRegion, projectID, peerNetwork, targetNetwork string

		if np.Spec.Target.AWS != nil {
			accountID = np.Spec.Target.AWS.AccountID
			vpcID = np.Spec.Target.AWS.VpcID
			peerCIDR = np.Spec.Target.AWS.PeerCIDR
			peerRegion = np.Spec.Target.AWS.PeerRegion
		}
		if np.Spec.Target.GCP != nil {
			projectID = np.Spec.Target.GCP.ProjectID
			peerNetwork = np.Spec.Target.GCP.PeerNetwork
		}
		if np.Spec.Target.TargetNetwork != nil {
			targetNetwork = np.Spec.Target.TargetNetwork.NetworkName
		}

		ch <- prometheus.MustNewConstMetric(
			InfraResourcesNetworkPeerInfo,
			prometheus.GaugeValue,
			1.0,
			np.Name,
			np.Namespace,
			np.Spec.Source,
			accountID,
			vpcID,
			peerCIDR,
			peerRegion,
			projectID,
			peerNetwork,
			targetNetwork,
			np.Status.PeeringID,
			np.Status.HostedZoneID,
			np.Status.VpcCidr,
			np.Status.Phase,
		)
	}
	return nil
}

func collectTiCDCMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	ticdcs := cloudv1.TiCDCList{}
	if err := cli.List(ctx, &ticdcs); err != nil {
		logger.Error(err, "list TiCDC failed")
		return err
	}

	for _, ticdc := range ticdcs.Items {
		ch <- prometheus.MustNewConstMetric(
			InfraResourcesTiCDCInfo,
			prometheus.GaugeValue,
			1.0,
			ticdc.Name,
			ticdc.Namespace,
			ticdc.Spec.NetworkName,
			ticdc.Spec.ClusterName,
			strconv.Itoa(ticdc.Spec.Replicas),
			ticdc.Spec.InstanceType,
			string(ticdc.Status.Phase),
		)
	}
	return nil
}

func collentInfraFirstPassWhenCreatingNetwork(ctx context.Context, ch chan<- prometheus.Metric, networks []cloudv1.Network) error {
	for _, network := range networks {
		for k, v := range network.Annotations {
			prefix := "first-pass-when-creating.cloud.pingcap.com/"
			if !strings.HasPrefix(k, prefix) {
				continue
			}

			name := strings.TrimPrefix(k, prefix)
			du, err := time.ParseDuration(v)
			if err != nil {
				log.FromContext(ctx).Error(err, "parse duration failed", "duration", v)
				continue
			}

			ch <- prometheus.MustNewConstMetric(
				InfraFirstPassWhenCreatingNetwork,
				prometheus.GaugeValue,
				du.Seconds(),
				network.Namespace,
				network.Name,
				network.CreationTimestamp.Time.Format(time.RFC3339),
				network.Labels[consts.TenantTagKey],
				network.Labels[consts.ProjectTagKey],
				name,
			)

		}
	}
	return nil
}

func collentInfraFirstPassWhenCreatingCluster(ctx context.Context, ch chan<- prometheus.Metric, clusters []cloudv1.Cluster) error {
	for _, cluster := range clusters {
		for k, v := range cluster.Annotations {
			prefix := "first-pass-when-creating.cloud.pingcap.com/"
			if !strings.HasPrefix(k, prefix) {
				continue
			}

			name := strings.TrimPrefix(k, prefix)
			du, err := time.ParseDuration(v)
			if err != nil {
				log.FromContext(ctx).Error(err, "parse duration failed", "duration", v)
				continue
			}

			ch <- prometheus.MustNewConstMetric(
				InfraFirstPassWhenCreatingCluster,
				prometheus.GaugeValue,
				du.Seconds(),
				cluster.Namespace,
				cluster.Name,
				cluster.CreationTimestamp.Time.Format(time.RFC3339),
				cluster.Labels[consts.TenantTagKey],
				cluster.Labels[consts.ProjectTagKey],
				cluster.Labels[consts.ClusterTagKey],
				name,
			)

		}
	}
	return nil
}

func collectClusterConfigMetrics(ctx context.Context, cli client.Client, ch chan<- prometheus.Metric) error {
	clusterConfigs := cloudv1.ClusterConfigList{}
	if err := cli.List(ctx, &clusterConfigs); err != nil {
		logger.Error(err, "list cluster config failed")
		return err
	}

	for _, clusterConfig := range clusterConfigs.Items {
		clusterConfigjson, err := json.Marshal(clusterConfig.Spec)
		if err != nil {
			logger.Error(err, "marshal cluster config failed")
			return err
		}
		ch <- prometheus.MustNewConstMetric(
			InfraResourcesClusterConfigInfo,
			prometheus.GaugeValue,
			1.0,
			clusterConfig.Name,
			clusterConfig.Labels[cloudv1.ClusterLabel],
			clusterConfig.Labels[cloudv1.TenantLabel],
			clusterConfig.Labels[cloudv1.VersionLabel],
			string(clusterConfigjson),
		)
	}
	return nil
}
