package server

import (
	"context"
	"fmt"
	"net/url"
	"os"
	"regexp"
	"strings"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
	"github.com/pingcap/errors"
)

var _ LogHandler = &AZBlobHandler{}

// AZBlobHandler handle pushing log to Azure Blob Storage.
type AZBlobHandler struct {
	Config
	// containerURL format: https://<account>.blob.core.windows.net/<container>?<sas token>
	containerURL string
	account      string
	container    string
	client       *container.Client
}

//nolint:gocritic //config will be reloaded
func NewAZBlobHandler(config Config) (*AZBlobHandler, error) {
	h := &AZBlobHandler{
		Config: config,
	}
	var err error
	h.account, h.container, err = parseAccountContainer(h.BucketURL)
	if err != nil {
		return nil, errors.Annotate(err, "can't get account and container from bucket url")
	}
	sasToken := ""
	if h.Azure != nil {
		sasToken = h.Azure.SASToken
	}
	h.containerURL = buildAZBlobContainerURL(h.account, h.container, sasToken)

	if strings.TrimSpace(sasToken) != "" {
		client, clientErr := container.NewClientWithNoCredential(h.containerURL, nil)
		if clientErr != nil {
			return nil, errors.Annotate(clientErr, "failed to create SAS-backed container client")
		}
		h.client = client
		return h, nil
	}

	credOpts := &azidentity.ManagedIdentityCredentialOptions{}
	clientID := strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID"))
	if clientID == "" {
		return nil, errors.New("AZURE_CLIENT_ID is required when using Azure managed identity auth")
	}
	credOpts.ID = azidentity.ClientID(clientID)

	cred, credErr := azidentity.NewManagedIdentityCredential(credOpts)
	if credErr != nil {
		return nil, errors.Annotate(credErr, "failed to create managed identity credential")
	}

	client, clientErr := container.NewClient(h.containerURL, cred, nil)
	if clientErr != nil {
		return nil, errors.Annotate(clientErr, "failed to create managed-identity-backed container client")
	}
	h.client = client

	return h, nil
}

func buildAZBlobContainerURL(account, containerName, sasToken string) string {
	containerURL := fmt.Sprintf("https://%s.blob.core.windows.net/%s", account, containerName)
	if strings.TrimSpace(sasToken) != "" {
		containerURL = fmt.Sprintf("%s?%s", containerURL, sasToken)
	}
	return containerURL
}

// Parse storage account name and container name from bucketURL
// bucketURL format: https://{account}.blob.core.windows.net/{container}/path/to/file
func parseAccountContainer(bucketURL string) (account, containerName string, err error) {
	u, err := url.Parse(bucketURL)
	if err != nil {
		return "", "", errors.Annotatef(err, "invalid url %q", bucketURL)
	}
	// account name can only contain lowercase letters and numbers
	re := regexp.MustCompile(`^([a-z0-9]+)\.blob\.core\.windows\.net`)
	match := re.FindStringSubmatch(u.Host)
	if len(match) <= 1 {
		return "", "", fmt.Errorf("invalid bucket url for azure blob storage, no storage account specified")
	}
	account = match[1]

	s := strings.SplitN(u.Path, "/", 3)
	if len(s) < 2 || s[1] == "" {
		return "", "", fmt.Errorf("invalid bucket url for azure blob storage, no container specified")
	}
	containerName = s[1]
	return account, containerName, nil
}

// Push implements LogHandler interface.
func (h *AZBlobHandler) Push(ctx context.Context, fname, targetKey string) error {
	file, err := os.Open(fname)
	if err != nil {
		return errors.Annotatef(err, "failed to open file %q", fname)
	}
	defer file.Close()

	// targetKey starts with container name, blob name should exclude it
	blobName := strings.TrimLeft(targetKey, h.container+"/")
	_, err = h.client.NewBlockBlobClient(blobName).UploadFile(ctx, file, nil)
	if err != nil {
		return errors.Annotate(err, "failed to upload file to blob storage")
	}
	return nil
}

// Close implements LogHandler interface.
func (h *AZBlobHandler) Close() {
}
