package metric_agent

import (
	"context"
	"fmt"
	"strings"

	awsutil "github.com/aws/aws-sdk-go-v2/aws"
	ec2api "github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/pingcap/errors"
	"gopkg.in/yaml.v2"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	v1 "k8s.io/api/scheduling/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/o11y"
	"github.com/tidbcloud/infra-provider/pkg/o11y/prom_config"
	"github.com/tidbcloud/infra-provider/pkg/providers/aws"
	"github.com/tidbcloud/infra-provider/pkg/vpc_endpoint"
)

const (
	agentStorageClassName = "o11y-metric-agent"

	prometheusConfigMapName = "db-monitor"
	promConfigmapKey1       = "prometheus.yml"
	promConfigmapKey2       = "prometheus-config"

	subnetFilterVpcIDKey       = "vpc-id"
	subnetFilterStateKey       = "state"
	subnetFilterStateAvailable = "available"
	subnetFilterAZKey          = "availability-zone"

	// FIXME(@xuyifan02): hard code shard env, for compatibility.
	shardEnv = "0"
	shardKey = "$(SHARD)"
)

// EnsureAgentStorageClass checks "auditlog" storage class exists. If not, it fallbacks to "default".
func EnsureAgentStorageClass(ctx context.Context, shootClient client.Client, providerType string) (err error) {
	switch providerType {
	case consts.ProviderTypeGCP:
		params := map[string]string{
			"type": "pd-ssd",
		}
		if err := cloudv1.EnsureGCPStorageClass(ctx, shootClient, agentStorageClassName, params); err != nil {
			return errors.Annotate(err, "failed to ensure gcp storage class")
		}
	case consts.ProviderTypeAWS:
		params := map[string]string{
			"type": "gp3",
		}
		if err := cloudv1.EnsureAWSStorageClass(ctx, shootClient, agentStorageClassName, params); err != nil {
			return errors.Annotate(err, "failed to ensure storage class exists")
		}
	default:
		return errors.New("unsupported provider type")
	}
	return nil
}

func EnsureAgentPriorityClass(ctx context.Context, shootClient client.Client) error {
	priorityClass := &v1.PriorityClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: ClusterCriticalPriorityClassName,
		},
		// Maximum allowed value of a user defined priority is 1000000000
		Value:         1000000000 - 2,
		GlobalDefault: false,
		Description:   "Priority class for o11y metric agent",
	}
	_, err := controllerutil.CreateOrUpdate(ctx, shootClient, priorityClass, func() error { return nil })
	if err != nil {
		return errors.Annotate(err, "fail to create or update priority class")
	}
	return nil
}

// CreateInterfaceEndpointForMetricStorage creates interface endpoints for the shoot vpc, on private subnets.
func CreateInterfaceEndpointForMetricStorage(
	ctx context.Context,
	cli client.Client,
	ec2Client aws.Ec2Client,
	vpcID, k8sIdentifier, shootName, serviceName, region, dnsName, providerConfig string,
	labels map[string]string,
	securityGroups []*string,
) (endpointName string, err error) {
	// Get endpoint service az info. Only pick one az in which the endpoint is created.
	// Refer to https://aws.amazon.com/premiumsupport/knowledge-center/interface-endpoint-availability-zone/
	endpointServiceIn := &ec2api.DescribeVpcEndpointServicesInput{
		ServiceNames: []string{serviceName},
	}
	endpointServiceOut, err := ec2Client.DescribeVpcEndpointServices(ctx, endpointServiceIn, aws.WithEC2Region(region))
	if err != nil {
		return "", errors.Annotate(err, "failed to describe endpoint service")
	}
	// If there is no common az between endpoint service and current dataplane, return an error.
	if len(endpointServiceOut.ServiceDetails) != 1 || len(endpointServiceOut.ServiceDetails[0].AvailabilityZones) < 1 {
		return "", errors.Errorf("no common az between endpoint service and current dataplane")
	}
	pickedAZ := endpointServiceOut.ServiceDetails[0].AvailabilityZones[0]

	// Get the private subnets under the vpc first.
	subnetIn := &ec2api.DescribeSubnetsInput{
		Filters: []ec2types.Filter{
			{
				Name:   awsutil.String(subnetFilterStateKey),
				Values: []string{subnetFilterStateAvailable},
			},
			{
				Name:   awsutil.String(subnetFilterVpcIDKey),
				Values: []string{vpcID},
			},
			{
				Name:   awsutil.String(subnetFilterAZKey),
				Values: []string{pickedAZ},
			},
		},
	}
	subnetOut, err := ec2Client.DescribeSubnets(ctx, subnetIn, aws.WithEC2Region(region))
	if err != nil {
		return "", errors.Annotate(err, "failed to get subnets")
	}
	subnetIDs := make([]*string, 0, len(subnetOut.Subnets))
	// Only keep subnets whose name is like "shoot--staging--fee904e3-private-utility-z2".
	for _, subnet := range subnetOut.Subnets {
		if vpc_endpoint.AwsNameTagHasPrefix(subnet.Tags, fmt.Sprintf("%s-private", k8sIdentifier)) {
			subnetIDs = append(subnetIDs, subnet.SubnetId)
		}
	}

	interfaceEndpointName := InterfaceEndpointName(dnsName, shootName)

	awsTags := make([]*v1alpha1.Tag, 0, len(labels))
	for k, v := range cloudv1.TagsFromLabels(labels) {
		awsTags = append(awsTags, &v1alpha1.Tag{
			Key:   awsutil.String(k),
			Value: awsutil.String(v),
		})
	}
	awsTags = append(awsTags, &v1alpha1.Tag{
		Key:   awsutil.String(vpc_endpoint.TagNameKey),
		Value: &interfaceEndpointName,
	})

	endpoint := &v1alpha1.VPCEndpoint{}
	endpoint.Name = interfaceEndpointName
	endpoint.Spec.ForProvider.Region = region
	// endpoint.Spec.ForProvider.ClientToken = &interfaceEndpointName
	endpoint.Spec.ForProvider.ServiceName = &serviceName
	endpoint.Spec.ForProvider.VPCEndpointType = awsutil.String(string(ec2types.VpcEndpointTypeInterface))
	endpoint.Spec.ForProvider.VPCID = &vpcID
	endpoint.Spec.ForProvider.SubnetIDs = subnetIDs
	// Enable private dns name.
	endpoint.Spec.ForProvider.PrivateDNSEnabled = awsutil.Bool(true)
	endpoint.Spec.ForProvider.SecurityGroupIDs = securityGroups
	endpoint.Spec.ForProvider.TagSpecifications = []*v1alpha1.TagSpecification{
		{
			ResourceType: awsutil.String(string(ec2types.ResourceTypeVpcEndpoint)),
			Tags:         awsTags,
		},
	}
	endpoint.Spec.ProviderConfigReference = &xpv1.Reference{Name: providerConfig}
	_, err = controllerutil.CreateOrUpdate(ctx, cli, endpoint, func() error { /* won't update anything */ return nil })
	if err != nil {
		return "", errors.Annotate(err, "failed to create vpc endpoint")
	}

	return interfaceEndpointName, nil
}

// FindExistedVPCEndpoint will return the vpc interface endpoint that matches the given serviceName and vpcID.
func FindExistedVPCEndpoint(ctx context.Context, infraCli client.Client, serviceName, vpcID string) (*v1alpha1.VPCEndpoint, error) {
	endpointList := &v1alpha1.VPCEndpointList{}
	// maybe indexed and list by field selector.
	if err := infraCli.List(ctx, endpointList); err != nil {
		return nil, errors.Annotate(err, "failed to list vpc endpoints")
	}
	for i, endpoint := range endpointList.Items {
		if isVPCEndpointConnected(&endpoint, serviceName, vpcID) {
			return &endpointList.Items[i], nil
		}
	}
	return nil, nil
}

func isVPCEndpointConnected(endpoint *v1alpha1.VPCEndpoint, serviceName, vpcID string) bool {
	return endpoint.Spec.ForProvider.ServiceName != nil &&
		*endpoint.Spec.ForProvider.ServiceName == serviceName &&
		endpoint.Spec.ForProvider.VPCID != nil &&
		*endpoint.Spec.ForProvider.VPCID == vpcID &&
		endpoint.Spec.ForProvider.VPCEndpointType != nil &&
		*endpoint.Spec.ForProvider.VPCEndpointType == string(ec2types.VpcEndpointTypeInterface)
}

func GetPromConfig(ctx context.Context, shootClient client.Client, namespace, k8sIdentifier string) (promConfig *prom_config.PromConfig, err error) {
	promConfigMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      prometheusConfigMapName,
			Namespace: namespace,
		},
	}
	if err := shootClient.Get(ctx, client.ObjectKeyFromObject(promConfigMap), promConfigMap); err != nil {
		return nil, errors.Annotate(err, "failed to get prom configmap")
	}

	promConfigRaw, ok := promConfigMap.Data[promConfigmapKey1]
	if !ok {
		if promConfigRaw, ok = promConfigMap.Data[promConfigmapKey2]; !ok {
			return nil, fmt.Errorf("%s or %s is not found in configmap %s/%s", promConfigmapKey1, promConfigmapKey2, namespace, prometheusConfigMapName)
		}
	}

	return UnmarshalAndEnrichPromConfig(promConfigRaw, k8sIdentifier, namespace)
}

func UnmarshalAndEnrichPromConfig(raw string, k8sIdentifier, namespace string) (*prom_config.PromConfig, error) {
	promConfig := &prom_config.PromConfig{}
	raw = strings.ReplaceAll(raw, shardKey, shardEnv)
	if err := yaml.Unmarshal([]byte(raw), promConfig); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal prom config")
	}
	// prometheus_replica label is helpful under old prometheus federation architecture.
	// In new o11y architecture, this label is useless and cost unnecessary resources.
	delete(promConfig.Global.ExternalLabels, "prometheus_replica")
	delete(promConfig.Global.ExternalLabels, "ignoreAlerts")
	clusterID := strings.TrimPrefix(namespace, "tidb")
	promConfig.Global.ExternalLabels["tidb_cluster_id"] = clusterID
	for _, target := range promConfig.ScrapeConfigs {
		// Set no sample limit for agent. Sample limit is only for protecting db-monitor-0 instance.
		target.SampleLimit = 0
	}
	sc, err := extraScrapeConf(namespace)
	if err != nil {
		return nil, err
	}
	promConfig.ScrapeConfigs = append(promConfig.ScrapeConfigs, sc...)
	return promConfig, nil
}

func AddMetricInterfaceEndpointNameToAnn(c *metav1.ObjectMeta, endpointName, o11yID, DNSName, o11yClusterStatus string, o11yVersion int) {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}
	c.Annotations[consts.MetricInterfaceEndpointNameAnnotationKey] = endpointName
	// should use MetricsO11yIDAnnotationKey to avoid being overwritten by log agent.
	// leave O11yIDAnnotationKey to be backward compatible.
	c.Annotations[consts.MetricsO11yIDAnnotationKey] = o11yID
	c.Annotations[consts.O11yIDAnnotationKey] = o11yID
	c.Annotations[consts.O11yServiceDNSNameAnnotationKey] = DNSName
	c.Annotations[consts.O11yClusterStatusAnnotationKey] = o11yClusterStatus
	vpc_endpoint.AddVersionToAnnotations(c.Annotations, o11yVersion)
}

func AddMetricInterfaceEndpointNameToAnnV2(c *metav1.ObjectMeta, endpointName, o11yID, DNSName string, o11yVersion int) {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}
	c.Annotations[consts.MetricInterfaceEndpointNameAnnotationKey] = endpointName
	c.Annotations[consts.MetricsO11yIDAnnotationKey] = o11yID
	c.Annotations[consts.O11yServiceDNSNameAnnotationKey] = DNSName
	vpc_endpoint.AddVersionToAnnotations(c.Annotations, o11yVersion)
}

func AddEndpointNameToNetworkAnn(n *cloudv1.Network, networkOrClusterName, endpointName string) {
	if n.Annotations == nil {
		n.Annotations = make(map[string]string)
	}
	n.Annotations[consts.O11yClusterEndpointAnnotationKey(networkOrClusterName)] = endpointName
}

// tenantID fallbacks to "unknown", projectID fallbacks to "network.name"
// clusterID fallbacks to "cluster.Name"
func GetIDsFromClusterCRD(c *cloudv1.Cluster, networkName string) (tenantID, projectID, clusterID string) {
	if v, ok := c.Labels[consts.ClusterLabelKey]; ok && v != "" {
		clusterID = v
	} else {
		clusterID = c.Name
	}
	if v, ok := c.Labels[consts.ProjectLabelKey]; ok && v != "" {
		projectID = v
	} else {
		projectID = networkName
	}
	if v, ok := c.Labels[consts.TenantLabelKey]; ok && v != "" {
		tenantID = v
	} else {
		tenantID = o11y.DefaultTenantLabelValue
	}
	return tenantID, projectID, clusterID
}

func DomainNamespace(domainName string) string {
	i := strings.Index(domainName, ".")
	if i > 0 {
		domainName = domainName[:i]
	}
	return domainName
}

// k8sIdentifier is semantically equal to the vpc/network identifier.
func InterfaceEndpointName(domainName, k8sIdentifier string) string {
	domainName = DomainNamespace(domainName)
	return domainName + "-" + k8sIdentifier + "-m"
}

func UpdateControlAnnotation(ctx context.Context, infraApiCli client.Client, o11yCluster *o11y.ObservabilityCluster, c *cloudv1.Cluster, n *cloudv1.Network, shootName string, o11yVersion int) error {
	endpointName := InterfaceEndpointName(*o11yCluster.MonitorInfo.DnsName, shootName)
	// Update o11y status to Cluster
	AddMetricInterfaceEndpointNameToAnn(&c.ObjectMeta, endpointName, o11yCluster.ID, *o11yCluster.MonitorInfo.DnsName, string(o11yCluster.Status), o11yVersion)
	if err := infraApiCli.Update(ctx, c); err != nil {
		return errors.Annotate(err, "failed to update cluster crd")
	}
	// Update o11y status to Network
	AddEndpointNameToNetworkAnn(n, c.Name, endpointName)
	if err := infraApiCli.Update(ctx, n); err != nil {
		return errors.Annotate(err, "failed to update network crd")
	}
	return nil
}

// UpdateControlAnnotationV2 just like UpdateControlAnnotation, but it uses the given endpoint name and do not update CR.
func UpdateControlAnnotationV2(o11yCluster *o11y.ObservabilityCluster, c *cloudv1.Cluster, epName string, o11yVersion int) {
	// Update o11y status to Cluster
	AddMetricInterfaceEndpointNameToAnn(&c.ObjectMeta, epName, o11yCluster.ID, *o11yCluster.MonitorInfo.DnsName, string(o11yCluster.Status), o11yVersion)
}

func EnsureAgentClusterRoleBinding(ctx context.Context, shootCli client.Client, releaseFullName, namespace string) error {
	clusterRoleBinding := &rbacv1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: releaseFullName + "-" + namespace,
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      "ServiceAccount",
				Name:      releaseFullName,
				Namespace: namespace,
			},
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: "rbac.authorization.k8s.io",
			Kind:     "ClusterRole",
			Name:     releaseFullName + "-clusterrole",
		},
	}
	if _, err := controllerutil.CreateOrUpdate(ctx, shootCli, clusterRoleBinding, func() error { /* won't update anything */ return nil }); err != nil {
		return errors.Annotate(err, "failed to create or update vmagent clusterrolebinding")
	}
	return nil
}
