package gcp

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

	"cloud.google.com/go/storage"
	"github.com/pingcap/errors"
	"google.golang.org/api/compute/v1"
	"google.golang.org/api/dns/v1"
	"google.golang.org/api/googleapi"
)

const (
	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"

	// Reserve 192.168.11.0/24 for pcs ip addresses.
	// Refer to https://cloud.google.com/vpc/docs/configure-private-service-connect-apis#ip-address-requirements
	googleApiPscAddress   = "192.168.11.1"
	googleApiPscNamespace = "googleapipsc"
)

type Client interface {
	CreateGoogleApiPscEndpoint(ctx context.Context, project, region, network, name string, tags map[string]string) error
	DeleteGoogleApiPscEndpoint(ctx context.Context, project, name string) error
	CreatePublishedServicePscEndpoint(ctx context.Context, project, region, network, subnet, target, name, namespace string, tags map[string]string) error
	CreatePublishedServicePscEndpointWithoutDNS(ctx context.Context, project, region, network, subnet, target, name string, tags map[string]string) error
	DeletePublishedServicePscEndpoint(ctx context.Context, project, region, name string) error
	GetPscEndpointIP(ctx context.Context, project, region, name string) (string, error)
}

type ClientSet struct {
	compute *compute.Service
	storage *storage.Client
	dns     *dns.Service
}

var _ Client = &ClientSet{}

func NewClientSet() (Client, error) {
	ctx := context.Background()
	computeSvc, err := compute.NewService(ctx)
	if err != nil {
		return nil, err
	}
	s, err := storage.NewClient(ctx)
	if err != nil {
		return nil, err
	}
	dns, err := dns.NewService(ctx)
	if err != nil {
		return nil, err
	}

	return &ClientSet{
		compute: computeSvc,
		storage: s,
		dns:     dns,
	}, nil
}

func (c *ClientSet) CreateGoogleApiPscEndpoint(ctx context.Context, project, region, network, name string, tags map[string]string) error {
	networkUri := fmt.Sprintf(globalNetworkURLTemplate, project, network)
	add := &compute.Address{
		Name:        name,
		Network:     networkUri,
		Address:     googleApiPscAddress,
		Purpose:     "PRIVATE_SERVICE_CONNECT",
		AddressType: "INTERNAL",
		Description: fmt.Sprintf("Tags: %v", tags),
	}
	// Cross-plane provider gcp lacks forwarding rule crd, so we create psc via api directly.
	if _, err := c.compute.GlobalAddresses.Insert(project, add).Context(ctx).Do(); err != nil && !isAlreadyExists(err) {
		return errors.Annotate(err, "failed to create google api ip address")
	}

	addressUri := fmt.Sprintf(globalAddressURLTemplate, project, name)
	fRule := &compute.ForwardingRule{
		Name:        name,
		Network:     networkUri,
		IPAddress:   addressUri,
		Target:      "all-apis",
		Description: fmt.Sprintf("Tags: %v", tags),
		ServiceDirectoryRegistrations: []*compute.ForwardingRuleServiceDirectoryRegistration{
			{
				ServiceDirectoryRegion: region,
				Namespace:              googleApiPscNamespace,
			},
		},
	}
	if _, err := c.compute.GlobalForwardingRules.Insert(project, fRule).Context(ctx).Do(); err != nil && !isAlreadyExists(err) {
		return errors.Annotate(err, "failed to create google api forwarding rule")
	}

	// Redirect default *.googleapis.com dns to the newly created psc ip address.
	if _, err := c.createDNSZone(project, network, name, "googleapis.com."); err != nil {
		return errors.Annotate(err, "failed to create dns zone for googleapis.com")
	}
	if err := c.changeGoogleApiPscDnsRecord(project, name, false); err != nil {
		return errors.Annotate(err, "failed to add googleapis.com and *.googleapis.com dns record")
	}

	return nil
}

func (c *ClientSet) DeleteGoogleApiPscEndpoint(ctx context.Context, project, name string) error {
	// Delete global forwarding rule
	if _, err := c.compute.GlobalForwardingRules.Delete(project, name).Context(ctx).Do(); err != nil && !isNotFound(err) {
		return errors.Annotate(err, "failed to delete global forwarding rule")
	}
	// Delete global address
	if _, err := c.compute.GlobalAddresses.Delete(project, name).Context(ctx).Do(); err != nil && !isNotFound(err) {
		return errors.Annotate(err, "failed to delete global address")
	}
	// Delete dns record
	if err := c.changeGoogleApiPscDnsRecord(project, name, true); err != nil {
		return errors.Annotate(err, "failed to delete googleapis.com and *.googleapis.com dns record")
	}
	// Delete dns zone
	if err := c.dns.ManagedZones.Delete(project, name).Context(ctx).Do(); err != nil && !isNotFound(err) {
		return errors.Annotate(err, "failed to delete dns zone")
	}
	return nil
}

func (c *ClientSet) getDNSZone(project, zoneName string) (string, error) {
	resp, err := c.dns.ManagedZones.Get(project, zoneName).Do()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%d", resp.Id), nil
}

func (c *ClientSet) createDNSZone(project, network, zoneName, dnsName string) (string, error) {
	// check if zone exist
	zoneID, err := c.getDNSZone(project, zoneName)
	if err == nil {
		return zoneID, nil
	} else if !strings.Contains(err.Error(), "does not exist") {
		// if it is an unknown error,return error
		return "", errors.Annotatef(err, "get hosted zone failed for name: %s", zoneName)
	}

	// if zone does not exist, create it
	zone := &dns.ManagedZone{
		Name:        zoneName,
		DnsName:     dnsName,
		Description: "private host zone for the gke cluster",
		Visibility:  "private",
		PrivateVisibilityConfig: &dns.ManagedZonePrivateVisibilityConfig{
			Kind: "dns#managedZonePrivateVisibilityConfig",
			Networks: []*dns.ManagedZonePrivateVisibilityConfigNetwork{
				{
					Kind:       "dns#managedZonePrivateVisibilityConfigNetwork",
					NetworkUrl: fmt.Sprintf(globalNetworkURLTemplate, project, network),
				},
			},
		},
	}

	resp, err := c.dns.ManagedZones.Create(project, zone).Do()
	if err != nil {
		return "", errors.Annotatef(err, "create hosted zone failed for vpc: %s", zoneName)
	}

	if resp.Name == "" {
		return "", fmt.Errorf("the return private zone id is nil, vpc: %s", zoneName)
	}

	return fmt.Sprintf("%d", resp.Id), nil
}

func (c *ClientSet) changeGoogleApiPscDnsRecord(project, zoneName string, isDelete bool) error {
	recordA := &dns.ResourceRecordSet{
		Type:    "A",
		Name:    "googleapis.com.",
		Ttl:     7200,
		Rrdatas: []string{googleApiPscAddress},
	}
	recordCname := &dns.ResourceRecordSet{
		Type:    "CNAME",
		Name:    "*.googleapis.com.",
		Ttl:     7200,
		Rrdatas: []string{"googleapis.com."},
	}
	if isDelete {
		change := &dns.Change{
			Deletions: []*dns.ResourceRecordSet{recordA, recordCname},
		}
		if _, err := c.dns.Changes.Create(project, zoneName, change).Do(); err != nil && !isAlreadyExists(err) && !isNotFound(err) {
			return errors.Annotatef(err, "failed to delete dns record for google api psc access, zone %q", zoneName)
		}
	} else {
		change := &dns.Change{
			Additions: []*dns.ResourceRecordSet{recordA, recordCname},
		}
		if _, err := c.dns.Changes.Create(project, zoneName, change).Do(); err != nil && !isAlreadyExists(err) && !isNotFound(err) {
			return errors.Annotatef(err, "failed to create dns record for google api psc access, zone %q", zoneName)
		}
	}

	return nil
}

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

	add := &compute.Address{
		Name:        name,
		AddressType: "INTERNAL",
		Subnetwork:  subnetUri,
		Description: fmt.Sprintf("Tags: %v", tags),
	}
	if _, err := c.compute.Addresses.Insert(project, region, add).Context(ctx).Do(); err != nil && !isAlreadyExists(err) {
		return errors.Annotate(err, "failed to create address")
	}

	addressUri := fmt.Sprintf(regionsAddressURLTemplate, project, region, name)

	fRule := &compute.ForwardingRule{
		Network:     networkUri,
		IPAddress:   addressUri,
		Name:        name,
		Target:      target,
		Description: fmt.Sprintf("Tags: %v", tags),
		ServiceDirectoryRegistrations: []*compute.ForwardingRuleServiceDirectoryRegistration{
			{
				Namespace: namespace,
			},
		},
	}
	if _, err := c.compute.ForwardingRules.Insert(project, region, fRule).Context(ctx).Do(); err != nil && !isAlreadyExists(err) {
		return errors.Annotate(err, "failed to create forwarding rule")
	}

	return nil
}

// CreatePublishedServicePscEndpointWithoutDNS creates a Private Service Connect endpoint without Service Directory registration.
// This is used when private DNS is disabled and the endpoint IP will be used directly.
func (c *ClientSet) CreatePublishedServicePscEndpointWithoutDNS(ctx context.Context, project, region, network, subnet, target, name string, tags map[string]string) error {
	networkUri := fmt.Sprintf(globalNetworkURLTemplate, project, network)
	subnetUri := fmt.Sprintf(regionsSubnetURLTemplate, project, region, subnet)

	add := &compute.Address{
		Name:        name,
		AddressType: "INTERNAL",
		Subnetwork:  subnetUri,
		Description: fmt.Sprintf("Tags: %v", tags),
	}
	if _, err := c.compute.Addresses.Insert(project, region, add).Context(ctx).Do(); err != nil && !isAlreadyExists(err) {
		return errors.Annotate(err, "failed to create address")
	}

	addressUri := fmt.Sprintf(regionsAddressURLTemplate, project, region, name)

	fRule := &compute.ForwardingRule{
		Network:     networkUri,
		IPAddress:   addressUri,
		Name:        name,
		Target:      target,
		Description: fmt.Sprintf("Tags: %v", tags),
	}
	if _, err := c.compute.ForwardingRules.Insert(project, region, fRule).Context(ctx).Do(); err != nil && !isAlreadyExists(err) {
		return errors.Annotate(err, "failed to create forwarding rule")
	}

	return nil
}

func (c *ClientSet) DeletePublishedServicePscEndpoint(ctx context.Context, project, region, name string) error {
	// Delete forwarding rule
	if _, err := c.compute.ForwardingRules.Delete(project, region, name).Context(ctx).Do(); err != nil && !isNotFound(err) {
		return errors.Annotate(err, "failed to delete forwarding rule")
	}
	// Delete address
	if _, err := c.compute.Addresses.Delete(project, region, name).Context(ctx).Do(); err != nil && !isNotFound(err) {
		return errors.Annotate(err, "failed to delete address")
	}

	return nil
}

// GetPscEndpointIP retrieves the IP address of a PSC endpoint.
func (c *ClientSet) GetPscEndpointIP(ctx context.Context, project, region, name string) (string, error) {
	address, err := c.compute.Addresses.Get(project, region, name).Context(ctx).Do()
	if err != nil {
		return "", errors.Annotate(err, "failed to get address")
	}
	if address.Address == "" {
		return "", errors.New("address IP is empty")
	}
	return address.Address, nil
}

func isNotFound(err error) bool {
	if err == nil {
		return false
	}
	var ae *googleapi.Error
	if goerrors.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 goerrors.As(err, &ae) {
		return ae.Code == http.StatusConflict
	}
	return false
}
