package gke

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
)

const (
	serviceAttachmentTemplate = "projects/%s/regions/%s/serviceAttachments/%s"
	endpointURIPrefix         = `https://www.googleapis.com/compute/v1/`
)

var (
	endpointRe = regexp.MustCompile(`^projects/(\S+)/regions/(\S+)/forwardingRules/(\S+)$`)
)

type ServiceAttachment struct {
	*task.BaseTask
	gcpCli gcp.Client
}

func NewServiceAttachment(ctx *task.Context, gcpCli gcp.Client) *ServiceAttachment {
	return &ServiceAttachment{BaseTask: &task.BaseTask{Context: ctx}, gcpCli: gcpCli}
}

func (t *ServiceAttachment) Name() string {
	return "ServiceAttachment"
}

// no need to create or update service attachment again for migreated resource.
func (t *ServiceAttachment) Create(ctx context.Context) deploy.Result {
	if t.PrivateLinkService.Labels[v1alpha1.IsLegacyKey] == "true" {
		return deploy.Done()
	}
	if len(t.PrivateLinkService.Status.LoadBalancers) == 0 || t.PrivateLinkService.Status.LoadBalancers[0].CloudResourceId == "" {
		t.Logger.Error(nil, "loadbalancer not ready")
		return deploy.Panic(fmt.Errorf("loadbalancer not ready"))
	}

	projects, err := t.getAcceptedProjectListFromSpec()
	if err != nil {
		t.Logger.Error(err, "get accepted project list from spec")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	// Create or update service attathment
	err = t.gcpCli.CreateOrUpdatePSCServiceAttachment(ctx,
		t.VPC.Spec.AccountRef.AccountID,
		t.PrivateLinkService.Spec.Region,
		t.serviceAttachmentName(),
		t.PrivateLinkService.Status.NATSubnet.Name,
		t.PrivateLinkService.Status.LoadBalancers[0].CloudResourceId,
		// t.Namer.GetPrivateServiceConnectDomain(),
		t.privateServiceConnectDomain(),
		projects,
	)
	if err != nil {
		t.Logger.Error(err, "create or update service attathment")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *ServiceAttachment) privateServiceConnectDomain() string {
	// prefix := t.PrivateLinkService.Name
	// if len(prefix) > 8 {
	// 	prefix = prefix[:8]
	// }
	return fmt.Sprintf("%s.%s", t.PrivateLinkService.Status.DNSIdentify, t.Config.PublicHostedZoneDomain)
}

func (t *ServiceAttachment) serviceAttachmentName() string {
	if name := t.PrivateLinkService.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNamePSCServiceAttachment]; name != "" {
		return name
	}
	return fmt.Sprintf("tidb%s", t.PrivateLinkService.Name)
}

func (t *ServiceAttachment) getAcceptedProjectListFromSpec() ([]string, error) {
	projects := make([]string, 0)
	for _, e := range t.PrivateLinkService.Spec.Connections {
		strs := endpointRe.FindStringSubmatch(e.Endpoint)
		if len(strs) != 4 {
			return nil, fmt.Errorf("parse endpoint in spec error, find %d submatch strings", len(strs))
		}
		projects = append(projects, strs[1])
	}

	return util.RemoveDuplicatesInSlice(projects), nil
}

func (t *ServiceAttachment) PostCreate(ctx context.Context) deploy.Result {
	exists, err := t.gcpCli.IsPSCServiceAttachmentExists(ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.serviceAttachmentName())
	if err != nil {
		t.Logger.Error(err, "check service attathment exists")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if !exists {
		t.Logger.Error(nil, "service attathment not exists")
		return deploy.Panic(fmt.Errorf("service attathment not exists"))
	}

	t.PrivateLinkService.Status.ServiceAttachment = v1alpha1.ServiceAttachmentStatus{
		Name:              t.serviceAttachmentName(),
		ServiceAttachment: fmt.Sprintf(serviceAttachmentTemplate, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.serviceAttachmentName()),
		ConnectDomain:     t.privateServiceConnectDomain(),
		Port:              t.getServiceAttachmentPort(),
	}
	if err := t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService); err != nil {
		t.Logger.Error(err, "update status")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	// Refresh status of connections with GCP
	if err := t.refreshStatusConnsWithGCP(ctx); err != nil {
		t.Logger.Error(err, "refresh status of connections with GCP")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *ServiceAttachment) getServiceAttachmentPort() int32 {
	if len(t.PrivateLinkService.Status.LoadBalancers) != 1 {
		return v1alpha1.DefaultPrivateSQLEndpointPort
	}
	if t.PrivateLinkService.Status.LoadBalancers[0].Port == 0 {
		return v1alpha1.DefaultPrivateSQLEndpointPort
	}
	return t.PrivateLinkService.Status.LoadBalancers[0].Port
}

// refreshStatusConnsWithGCP synchronize status of connections with GCP
// Note it only records the endpoints exist in cluster spec, not all
func (t *ServiceAttachment) refreshStatusConnsWithGCP(ctx context.Context) error {
	conns, err := t.gcpCli.GetPSCServiceAttachmentConnections(ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.serviceAttachmentName())
	if err != nil {
		return err
	}

	connsMap := make(map[string]string)
	for _, c := range conns {
		connsMap[c.Endpoint] = c.Status
	}

	statusConns := make([]v1alpha1.ConnectionStatus, 0)
	inActiveCount := 0
	for _, e := range t.PrivateLinkService.Spec.Connections {
		status, ok := connsMap[endpointURIPrefix+e.Endpoint]
		if ok {
			sc := t.convertGCPEndpointStauts(e.Endpoint, status)
			if sc.Phase != v1alpha1.ConnectionPhaseActive {
				inActiveCount++
			}
			statusConns = append(statusConns, sc)
		} else {
			statusConns = append(statusConns, v1alpha1.ConnectionStatus{
				Endpoint: e.Endpoint,
				Phase:    v1alpha1.ConnectionPhaseFailed,
				Message:  "The endpoint does not exist",
			})
			inActiveCount++
		}
	}

	// t.Cluster.Status.SqlEndpoints.PrivateLinkService.GCP.Connections = statusConns
	t.PrivateLinkService.Status.Connections = statusConns
	if err := t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService); err != nil {
		return err
	}

	if inActiveCount > 0 {
		return fmt.Errorf("%d connections are not active", inActiveCount)
	}

	return nil
}

func (t *ServiceAttachment) convertGCPEndpointStauts(e, s string) v1alpha1.ConnectionStatus {
	// Possible values:
	//   "ACCEPTED" - The connection has been accepted by the producer.
	//   "CLOSED" - The connection has been closed by the producer.
	//   "NEEDS_ATTENTION" - The connection has been accepted by the
	// producer, but the producer needs to take further action before the
	// forwarding rule can serve traffic.
	//   "PENDING" - The connection is pending acceptance by the producer.
	//   "REJECTED" - The consumer is still connected but not using the
	// connection.
	//   "STATUS_UNSPECIFIED"
	switch s {
	case "ACCEPTED", "REJECTED":
		return v1alpha1.ConnectionStatus{
			Endpoint: e,
			DNSName:  t.getEndpointHost(e),
			Phase:    v1alpha1.ConnectionPhaseActive,
		}
	case "PENDING":
		return v1alpha1.ConnectionStatus{
			Endpoint: e,
			DNSName:  t.getEndpointHost(e),
			Phase:    v1alpha1.ConnectionPhasePending,
		}
	case "NEEDS_ATTENTION":
		return v1alpha1.ConnectionStatus{
			Endpoint: e,
			DNSName:  t.getEndpointHost(e),
			Phase:    v1alpha1.ConnectionPhaseFailed,
			Message:  "The connection has been accepted, but needs to take further action, please contact Support",
		}
	// CLOSED, STATUS_UNSPECIFIED
	default:
		return v1alpha1.ConnectionStatus{
			Endpoint: e,
			DNSName:  t.getEndpointHost(e),
			Phase:    v1alpha1.ConnectionPhaseFailed,
			Message:  fmt.Sprintf("Unexpected status: %s, please contact Support", s),
		}
	}
}

func getEndpointName(endpoint string) string {
	strs := strings.Split(endpoint, "/")
	return strs[len(strs)-1]
}

func (t *ServiceAttachment) getEndpointHost(endpoint string) string {
	if endpoint == "" || getEndpointName(endpoint) == "" {
		return t.privateServiceConnectDomain()
	}
	return fmt.Sprintf("%s.%s", getEndpointName(endpoint), t.privateServiceConnectDomain())
}

func (t *ServiceAttachment) Delete(ctx context.Context) deploy.Result {
	err := t.gcpCli.DeletePSCServiceAttachment(
		ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.serviceAttachmentName())
	if err != nil {
		t.Logger.Error(err, "delete service attathment")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *ServiceAttachment) PostDelete(ctx context.Context) deploy.Result {
	exists, err := t.gcpCli.IsPSCServiceAttachmentExists(
		ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.serviceAttachmentName())
	if err != nil {
		t.Logger.Error(err, "check service attachment exists")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if exists {
		t.Logger.Error(nil, "service attathment not deleted")
		return deploy.Panic(fmt.Errorf("service attathment not deleted"))
	}
	return deploy.Done()
}
