package azure

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

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
	"k8s.io/utils/ptr"
)

//go:generate mockgen -destination ./pkg/cloud/azure/client_mock.go -package azure -source ./pkg/cloud/azure/client.go
type Client interface {
	ManagedClusters(subscriptionID string) (ManagedClustersAPI, error)
	LoadBalancers(subscriptionID string) (LoadBalancersAPI, error)
	PrivateLinkServices(subscriptionID string) (PrivateLinkServicesAPI, error)
	PrivateEndpoints(subscriptionID string) (PrivateEndpointsAPI, error)
	StorageAccounts(subscriptionID string) (StorageAccountsAPI, error)
	BlobContainers(accountName string) (BlobContainersAPI, error)
}

type ManagedClustersAPI interface {
	ListClusterAdminCredentials(ctx context.Context, resourceGroupName, clusterName string, options *armcontainerservice.ManagedClustersClientListClusterAdminCredentialsOptions) (armcontainerservice.ManagedClustersClientListClusterAdminCredentialsResponse, error)
	ListClusterUserCredentials(ctx context.Context, resourceGroupName, clusterName string, options *armcontainerservice.ManagedClustersClientListClusterUserCredentialsOptions) (armcontainerservice.ManagedClustersClientListClusterUserCredentialsResponse, error)
}

type LoadBalancersAPI interface {
	Get(ctx context.Context, resourceGroupName, loadBalancerName string) (armnetwork.LoadBalancersClientGetResponse, error)
}

type PrivateLinkServicesAPI interface {
	ListPrivateEndpointConnections(ctx context.Context, resourceGroupName, clusterName string) ([]*armnetwork.PrivateEndpointConnection, error)
	UpdatePrivateEndpointConnection(ctx context.Context, resourceGroupName, clusterName, connName string, params armnetwork.PrivateEndpointConnection) error
	DeletePrivateEndpointConnection(ctx context.Context, resourceGroupName, clusterName, connName string) error
}

type PrivateEndpointsAPI interface {
	Get(ctx context.Context, resourceGroupName, privateEndpointName string) (armnetwork.PrivateEndpointsClientGetResponse, error)
}

type StorageAccountsAPI interface {
	StorageAccountHasContainers(ctx context.Context, resourceGroupName, accountName string) (bool, error)
	StorageAccountContainerExists(ctx context.Context, resourceGroupName, accountName, containerName string) (bool, error)
}

type BlobContainersAPI interface {
	ContainerHasBlobs(ctx context.Context, accountName, containerName string) (bool, error)
}

type ClientSet struct {
	cred azcore.TokenCredential

	mcClients  sync.Map
	lbClients  sync.Map
	plsClients sync.Map
	peClients  sync.Map
	saClients  sync.Map
	bcClients  sync.Map
}

var _ Client = &ClientSet{}

func NewClientSet(cred azcore.TokenCredential) *ClientSet {
	return &ClientSet{
		cred:       cred,
		mcClients:  sync.Map{},
		lbClients:  sync.Map{},
		plsClients: sync.Map{},
		peClients:  sync.Map{},
		saClients:  sync.Map{},
		bcClients:  sync.Map{},
	}
}

func NewDefaultClientSet() (*ClientSet, error) {
	cred, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		return nil, err
	}
	return NewClientSet(cred), nil
}

func (c *ClientSet) managedClustersClient(subscriptionID string) (*armcontainerservice.ManagedClustersClient, error) {
	if subscriptionID == "" {
		return nil, fmt.Errorf("subscription id is empty")
	}
	if cached, ok := c.mcClients.Load(subscriptionID); ok {
		return cached.(*armcontainerservice.ManagedClustersClient), nil
	}
	factory, err := armcontainerservice.NewClientFactory(subscriptionID, c.cred, nil)
	if err != nil {
		return nil, err
	}
	client := factory.NewManagedClustersClient()
	c.mcClients.Store(subscriptionID, client)
	return client, nil
}

func (c *ClientSet) privateLinkServicesClient(subscriptionID string) (*armnetwork.PrivateLinkServicesClient, error) {
	if subscriptionID == "" {
		return nil, fmt.Errorf("subscription id is empty")
	}
	if cached, ok := c.plsClients.Load(subscriptionID); ok {
		return cached.(*armnetwork.PrivateLinkServicesClient), nil
	}
	factory, err := armnetwork.NewClientFactory(subscriptionID, c.cred, nil)
	if err != nil {
		return nil, err
	}
	client := factory.NewPrivateLinkServicesClient()
	c.plsClients.Store(subscriptionID, client)
	return client, nil
}

func (c *ClientSet) loadBalancersClient(subscriptionID string) (*armnetwork.LoadBalancersClient, error) {
	if subscriptionID == "" {
		return nil, fmt.Errorf("subscription id is empty")
	}
	if cached, ok := c.lbClients.Load(subscriptionID); ok {
		return cached.(*armnetwork.LoadBalancersClient), nil
	}
	factory, err := armnetwork.NewClientFactory(subscriptionID, c.cred, nil)
	if err != nil {
		return nil, err
	}
	client := factory.NewLoadBalancersClient()
	c.lbClients.Store(subscriptionID, client)
	return client, nil
}

func (c *ClientSet) ManagedClusters(subscriptionID string) (ManagedClustersAPI, error) {
	client, err := c.managedClustersClient(subscriptionID)
	if err != nil {
		return nil, err
	}
	return &managedClustersClient{client: client}, nil
}

func (c *ClientSet) LoadBalancers(subscriptionID string) (LoadBalancersAPI, error) {
	client, err := c.loadBalancersClient(subscriptionID)
	if err != nil {
		return nil, err
	}
	return &loadBalancersClient{client: client}, nil
}

func (c *ClientSet) PrivateLinkServices(subscriptionID string) (PrivateLinkServicesAPI, error) {
	client, err := c.privateLinkServicesClient(subscriptionID)
	if err != nil {
		return nil, err
	}
	return &privateLinkServicesClient{client: client}, nil
}

func (c *ClientSet) privateEndpointsClient(subscriptionID string) (*armnetwork.PrivateEndpointsClient, error) {
	if subscriptionID == "" {
		return nil, fmt.Errorf("subscription id is empty")
	}
	if cached, ok := c.peClients.Load(subscriptionID); ok {
		return cached.(*armnetwork.PrivateEndpointsClient), nil
	}
	factory, err := armnetwork.NewClientFactory(subscriptionID, c.cred, nil)
	if err != nil {
		return nil, err
	}
	client := factory.NewPrivateEndpointsClient()
	c.peClients.Store(subscriptionID, client)
	return client, nil
}

func (c *ClientSet) PrivateEndpoints(subscriptionID string) (PrivateEndpointsAPI, error) {
	client, err := c.privateEndpointsClient(subscriptionID)
	if err != nil {
		return nil, err
	}
	return &privateEndpointsClient{client: client}, nil
}

type managedClustersClient struct {
	client *armcontainerservice.ManagedClustersClient
}

func (c *managedClustersClient) ListClusterAdminCredentials(ctx context.Context, resourceGroupName, clusterName string, options *armcontainerservice.ManagedClustersClientListClusterAdminCredentialsOptions) (armcontainerservice.ManagedClustersClientListClusterAdminCredentialsResponse, error) {
	return c.client.ListClusterAdminCredentials(ctx, resourceGroupName, clusterName, options)
}

func (c *managedClustersClient) ListClusterUserCredentials(ctx context.Context, resourceGroupName, clusterName string, options *armcontainerservice.ManagedClustersClientListClusterUserCredentialsOptions) (armcontainerservice.ManagedClustersClientListClusterUserCredentialsResponse, error) {
	return c.client.ListClusterUserCredentials(ctx, resourceGroupName, clusterName, options)
}

type privateLinkServicesClient struct {
	client *armnetwork.PrivateLinkServicesClient
}

type loadBalancersClient struct {
	client *armnetwork.LoadBalancersClient
}

func (c *privateLinkServicesClient) ListPrivateEndpointConnections(ctx context.Context, resourceGroupName, clusterName string) ([]*armnetwork.PrivateEndpointConnection, error) {
	pager := c.client.NewListPrivateEndpointConnectionsPager(resourceGroupName, clusterName, nil)
	var results []*armnetwork.PrivateEndpointConnection
	for pager.More() {
		resp, err := pager.NextPage(ctx)
		if err != nil {
			return nil, err
		}
		results = append(results, resp.Value...)
	}
	return results, nil
}

func (c *privateLinkServicesClient) UpdatePrivateEndpointConnection(ctx context.Context, resourceGroupName, clusterName, connName string, params armnetwork.PrivateEndpointConnection) error {
	_, err := c.client.UpdatePrivateEndpointConnection(ctx, resourceGroupName, clusterName, connName, params, nil)
	return err
}

func (c *privateLinkServicesClient) DeletePrivateEndpointConnection(ctx context.Context, resourceGroupName, clusterName, connName string) error {
	poller, err := c.client.BeginDeletePrivateEndpointConnection(ctx, resourceGroupName, clusterName, connName, nil)
	if err != nil {
		if isNotFound(err) {
			return nil
		}
		return err
	}
	_, err = poller.PollUntilDone(ctx, nil)
	if isNotFound(err) {
		return nil
	}
	return err
}

func (c *loadBalancersClient) Get(ctx context.Context, resourceGroupName, loadBalancerName string) (armnetwork.LoadBalancersClientGetResponse, error) {
	return c.client.Get(ctx, resourceGroupName, loadBalancerName, nil)
}

type privateEndpointsClient struct {
	client *armnetwork.PrivateEndpointsClient
}

func (c *privateEndpointsClient) Get(ctx context.Context, resourceGroupName, privateEndpointName string) (armnetwork.PrivateEndpointsClientGetResponse, error) {
	return c.client.Get(ctx, resourceGroupName, privateEndpointName, nil)
}

type storageAccountsClient struct {
	client *armstorage.BlobContainersClient
}

func (c *ClientSet) storageAccountsClient(subscriptionID string) (*armstorage.BlobContainersClient, error) {
	if subscriptionID == "" {
		return nil, fmt.Errorf("subscription id is empty")
	}
	if cached, ok := c.saClients.Load(subscriptionID); ok {
		return cached.(*armstorage.BlobContainersClient), nil
	}
	factory, err := armstorage.NewClientFactory(subscriptionID, c.cred, nil)
	if err != nil {
		return nil, err
	}
	client := factory.NewBlobContainersClient()
	c.saClients.Store(subscriptionID, client)
	return client, nil
}

func (c *ClientSet) StorageAccounts(subscriptionID string) (StorageAccountsAPI, error) {
	client, err := c.storageAccountsClient(subscriptionID)
	if err != nil {
		return nil, err
	}
	return &storageAccountsClient{client: client}, nil
}

func (c *storageAccountsClient) StorageAccountHasContainers(ctx context.Context, resourceGroupName, accountName string) (bool, error) {
	if resourceGroupName == "" || accountName == "" {
		return false, fmt.Errorf("resource group name or account name is empty")
	}
	pager := c.client.NewListPager(resourceGroupName, accountName, &armstorage.BlobContainersClientListOptions{
		Maxpagesize: ptr.To("1"),
	})
	resp, err := pager.NextPage(ctx)
	if err != nil {
		if isNotFound(err) {
			return false, nil
		}
		return false, err
	}
	return len(resp.Value) > 0, nil
}

func (c *storageAccountsClient) StorageAccountContainerExists(ctx context.Context, resourceGroupName, accountName, containerName string) (bool, error) {
	if resourceGroupName == "" || accountName == "" || containerName == "" {
		return false, fmt.Errorf("resource group name, account name, or container name is empty")
	}
	_, err := c.client.Get(ctx, resourceGroupName, accountName, containerName, nil)
	if err != nil {
		if isNotFound(err) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

type blobContainersClient struct {
	client *azblob.Client
}

func (c *ClientSet) blobContainersClient(accountName string) (*blobContainersClient, error) {
	if accountName == "" {
		return nil, fmt.Errorf("account name is empty")
	}
	if cached, ok := c.bcClients.Load(accountName); ok {
		return cached.(*blobContainersClient), nil
	}
	serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", accountName)
	accountClient, err := azblob.NewClient(serviceURL, c.cred, nil)
	if err != nil {
		return nil, err
	}
	client := &blobContainersClient{
		client: accountClient,
	}
	c.bcClients.Store(accountName, client)
	return client, nil
}

func (c *ClientSet) BlobContainers(accountName string) (BlobContainersAPI, error) {
	client, err := c.blobContainersClient(accountName)
	if err != nil {
		return nil, err
	}
	return client, nil
}

func (c *blobContainersClient) ContainerHasBlobs(ctx context.Context, accountName, containerName string) (bool, error) {
	if accountName == "" || containerName == "" {
		return false, fmt.Errorf("account name or container name is empty")
	}
	if c.client == nil {
		return false, fmt.Errorf("blob service client is not initialized")
	}
	maxResults := int32(1)
	pager := c.client.NewListBlobsFlatPager(containerName, &azblob.ListBlobsFlatOptions{
		MaxResults: &maxResults,
	})
	resp, err := pager.NextPage(ctx)
	if err != nil {
		if isNotFound(err) {
			return false, nil
		}
		return false, err
	}
	if resp.Segment == nil {
		return false, nil
	}
	return len(resp.Segment.BlobItems) > 0, nil
}

func isNotFound(err error) bool {
	var respErr *azcore.ResponseError
	if errors.As(err, &respErr) {
		return respErr.StatusCode == http.StatusNotFound
	}
	return false
}
