package gcp

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/tidbcloud/observability/pkg/common/metrics"
	"github.com/tidbcloud/observability/pkg/common/utils"

	compute "cloud.google.com/go/compute/apiv1"
	"github.com/tidbcloud/observability/pkg/resourceworker/providers/gcp/psc"
	"google.golang.org/api/dns/v1"

	computepb "cloud.google.com/go/compute/apiv1/computepb"
	"google.golang.org/api/googleapi"
)

const (
	serviceAttachmentsClient      = "service-attachments-client"
	AddressClient                 = "address-client"
	ForwardingRulesClient         = "forwarding-rules-client"
	globalNetworkURLTemplate      = "https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s"
	globalAddressURLTemplate      = "https://www.googleapis.com/compute/v1/projects/%s/global/addresses/%s"
	regionsSubnetURLTemplate      = "https://www.googleapis.com/compute/v1/projects/%s/regions/%s/subnetworks/%s"
	regionsAddressURLTemplate     = "https://www.googleapis.com/compute/v1/projects/%s/regions/%s/addresses/%s"
	DNSManagedZoneTemplate        = "%s--%s" // <namespace>--<location>
	errGetServiceAttachmentFailed = "get service attachment failed"
	errSetServiceAttachmentFailed = "set service attachment failed"
	errCreateAddressFailed        = "create address failed"
	errCreateForwardingRuleFailed = "create forwarding rule failed"
	errDeleteAddressFailed        = "delete address failed"
	errDeleteForwardingRuleFailed = "delete forwarding rule failed"
)

type PSCClient struct {
	cli         psc.CloudPSCClient
	rulecli     psc.CloudPSCRuleClient
	addrsscli   psc.CloudPSCAddressClient
	dnsZonesCli psc.CloudDNSZonesClient
	project     string
	region      string
}

func NewPSC(project string, region string) (*PSCClient, error) {
	cli, err := compute.NewServiceAttachmentsRESTClient(context.TODO())
	if err != nil {
		return nil, err
	}
	rulecli, err := compute.NewForwardingRulesRESTClient(context.TODO())
	if err != nil {
		return nil, err
	}
	addrcli, err := compute.NewAddressesRESTClient(context.TODO())
	if err != nil {
		return nil, err
	}

	dnsCli, err := dns.NewService(context.TODO())
	if err != nil {
		return nil, err
	}

	return &PSCClient{
		cli:         &psc.PSCClientWrap{ServiceAttachmentsClient: cli},
		rulecli:     &psc.PSCRuleClientWrap{ForwardingRulesClient: rulecli},
		addrsscli:   &psc.PSCAddressClientWrap{AddressesClient: addrcli},
		dnsZonesCli: &psc.DNSZonesClientWrap{ManagedZonesService: dnsCli.ManagedZones},
		project:     project,
		region:      region,
	}, nil

}

func (ic *PSCClient) AssumeProject(ctx context.Context, project string) *PSCClient {
	// in gcp, we do not need to assume role,we only change project.
	// the service account should have the permission to access the project
	return &PSCClient{
		// we do not need to change cscli, because the cloud storage we only need in own project
		cli:         ic.cli,
		rulecli:     ic.rulecli,
		addrsscli:   ic.addrsscli,
		dnsZonesCli: ic.dnsZonesCli,
		project:     project,
		region:      ic.region,
	}
}

func (psc *PSCClient) GetPSCService(ctx context.Context, name string) (*computepb.ServiceAttachment, error) {
	req := computepb.GetServiceAttachmentRequest{
		Project:           psc.project,
		Region:            psc.region,
		ServiceAttachment: name,
	}
	sa, err := psc.cli.Get(ctx, &req)
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(psc.region, serviceAttachmentsClient, "Get", name, errGetServiceAttachmentFailed)
	}
	return sa, err

}

func (psc *PSCClient) SetPSCService(ctx context.Context, name string, setting *computepb.ServiceAttachment) error {
	req := computepb.PatchServiceAttachmentRequest{
		Project:                   psc.project,
		Region:                    psc.region,
		ServiceAttachment:         name,
		ServiceAttachmentResource: setting,
	}
	o, err := psc.cli.Patch(ctx, &req)
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(psc.region, serviceAttachmentsClient, "Patch", name, errSetServiceAttachmentFailed)
		return err
	}
	return o.Wait(ctx)
}

// CreatePublishedServicePscEndpoint creates a Private Service Connect endpoint for connecting self-published service in other VPC.
func (psc *PSCClient) CreatePublishedServicePscEndpoint(ctx context.Context, region, network, subnet, target, name, namespace string, tags map[string]string) error {
	networkUri := fmt.Sprintf(globalNetworkURLTemplate, psc.project, network)
	subnetUri := fmt.Sprintf(regionsSubnetURLTemplate, psc.project, region, subnet)

	add := &computepb.Address{
		Name:        utils.StringPtr(name),
		AddressType: utils.StringPtr("INTERNAL"),
		Subnetwork:  utils.StringPtr(subnetUri),
		Description: utils.StringPtr(fmt.Sprintf("Tags: %v", tags)),
	}
	addrreq := &computepb.InsertAddressRequest{
		Project:         psc.project,
		Region:          region,
		AddressResource: add,
	}
	o, err := psc.addrsscli.Insert(ctx, addrreq)
	if err != nil && !isAlreadyExists(err) {
		metrics.HandleGCPAPIRequestMetrics(psc.region, AddressClient, "Insert", name, errCreateAddressFailed)
		return err
	}
	if err == nil {
		err = o.Wait(ctx)
		if err != nil && !isAlreadyExists(err) {
			metrics.HandleGCPAPIRequestMetrics(psc.region, AddressClient, "Insert", name, errCreateAddressFailed)
			return err
		}
	}
	addressUri := fmt.Sprintf(regionsAddressURLTemplate, psc.project, region, name)
	fRule := &computepb.ForwardingRule{
		Network:     utils.StringPtr(networkUri),
		IPAddress:   utils.StringPtr(addressUri),
		Name:        utils.StringPtr(name),
		Target:      utils.StringPtr(target),
		Description: utils.StringPtr(fmt.Sprintf("Tags: %v", tags)),
		ServiceDirectoryRegistrations: []*computepb.ForwardingRuleServiceDirectoryRegistration{
			{
				Namespace: utils.StringPtr(namespace),
			},
		},
	}
	fRuleReq := &computepb.InsertForwardingRuleRequest{
		Project:                psc.project,
		Region:                 region,
		ForwardingRuleResource: fRule,
	}
	o, err = psc.rulecli.Insert(ctx, fRuleReq)
	if err != nil && !isAlreadyExists(err) {
		metrics.HandleGCPAPIRequestMetrics(psc.region, ForwardingRulesClient, "Insert", name, errCreateForwardingRuleFailed)
		return err
	}
	if err == nil {
		err = o.Wait(ctx)
		if err != nil && !isAlreadyExists(err) {
			metrics.HandleGCPAPIRequestMetrics(psc.region, ForwardingRulesClient, "Insert", name, errCreateForwardingRuleFailed)
			return err
		}
	}
	return nil
}

func (psc *PSCClient) DeletePublishedServicePscEndpoint(ctx context.Context, region, name string) error {
	// Delete forwarding rule
	rulereq := computepb.DeleteForwardingRuleRequest{
		Project:        psc.project,
		Region:         region,
		ForwardingRule: name,
	}
	o, err := psc.rulecli.Delete(ctx, &rulereq)
	if err != nil && !isNotFound(err) {
		metrics.HandleGCPAPIRequestMetrics(psc.region, ForwardingRulesClient, "Delete", name, errDeleteForwardingRuleFailed)
		return err
	}
	if err == nil {
		err = o.Wait(ctx)
		if err != nil && !isNotFound(err) {
			metrics.HandleGCPAPIRequestMetrics(psc.region, ForwardingRulesClient, "Delete", name, errDeleteForwardingRuleFailed)
			return err
		}
	}
	// Delete address
	addrreq := computepb.DeleteAddressRequest{
		Project: psc.project,
		Region:  region,
		Address: name,
	}
	o, err = psc.addrsscli.Delete(ctx, &addrreq)
	if err != nil && !isNotFound(err) {
		metrics.HandleGCPAPIRequestMetrics(psc.region, AddressClient, "Delete", name, errDeleteAddressFailed)
		return err
	}
	if err == nil {
		err = o.Wait(ctx)
		if err != nil && !isNotFound(err) {
			metrics.HandleGCPAPIRequestMetrics(psc.region, AddressClient, "Delete", name, errDeleteAddressFailed)
			return err
		}
	}
	return nil
}

func (psc *PSCClient) GetPublishedServicePscEndpoint(ctx context.Context, region, name string) (*computepb.ForwardingRule, error) {
	req := computepb.GetForwardingRuleRequest{
		Project:        psc.project,
		Region:         region,
		ForwardingRule: name,
	}
	rule, err := psc.rulecli.Get(ctx, &req)
	if isNotFound(err) {
		return nil, nil
	}
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(psc.region, ForwardingRulesClient, "Get", name, errGetServiceAttachmentFailed)
	}
	return rule, nil
}

func (psc *PSCClient) CheckNamespaceNetworkAssociation(ctx context.Context, region, network, namespace string) (bool, error) {
	zone, err := psc.dnsZonesCli.Get(psc.project, fmt.Sprintf(DNSManagedZoneTemplate, namespace, region)).Do()
	if err != nil {
		if isNotFound(err) {
			// zone not found
			return false, nil
		}
		return false, err
	}
	if zone.PrivateVisibilityConfig != nil {
		for _, vpc := range zone.PrivateVisibilityConfig.Networks {
			if strings.Contains(vpc.NetworkUrl, network) {
				return true, nil
			}
		}
	}
	return false, nil
}

func isNotFound(err error) bool {
	if err == nil {
		return false
	}
	var ae *googleapi.Error
	if errors.As(err, &ae) {
		return ae.Code == http.StatusNotFound
	}
	return false
}

func isAlreadyExists(err error) bool {
	if err == nil {
		return false
	}
	var ae *googleapi.Error
	if errors.As(err, &ae) {
		return ae.Code == http.StatusConflict
	}
	return false
}
