package entity

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

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/import/cloud_storage"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/utils"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
)

type CloudStorage struct {
	S3        *S3CloudStorage           `json:"s3,omitempty"`
	GCS       *GCSCloudStorage          `json:"gcs,omitempty"`
	AzureBlob *AzureBlobCloudStorage    `json:"azure_blob,omitempty"`
	OSS       *OSSCloudStorage          `json:"oss,omitempty"`
	Uri       string                    `json:"uri,omitempty"`
	Type      constant.CloudStorageType `json:"type,omitempty"`
}

type S3CloudStorage struct {
	RoleArn   *S3CloudStorageRoleArn   `json:"role_arn,omitempty"`
	AccessKey *S3CloudStorageAccessKey `json:"access_key,omitempty"`
}

type S3CloudStorageRoleArn struct {
	RoleArn    string `json:"role_arn,omitempty"`
	ExternalId string `json:"external_id,omitempty"`
}

type S3CloudStorageAccessKey struct {
	AccessKeyId     string `json:"access_key_id,omitempty"`
	SecretAccessKey string `json:"secret_access_key,omitempty"`
}

type GCSCloudStorage struct {
	ServiceAccountKey string `json:"service_account_key,omitempty"`
}

type AzureBlobCloudStorage struct {
	SasToken string `json:"sas_token,omitempty"`
}

type OSSCloudStorage struct {
	RoleArn   *OSSCloudStorageRoleArn   `json:"role_arn,omitempty"`
	AccessKey *OSSCloudStorageAccessKey `json:"access_key,omitempty"`
}

type OSSCloudStorageRoleArn struct {
	RoleArn    string `json:"role_arn,omitempty"`
	ExternalId string `json:"external_id,omitempty"`
}

type OSSCloudStorageAccessKey struct {
	AccessKeyId     string `json:"access_key_id,omitempty"`
	AccessKeySecret string `json:"access_key_secret,omitempty"`
}

func (c *CloudStorage) Validate(cloudProvider string) bizerr.BizError {
	if c == nil {
		return nil
	}
	// check uri format
	if c.Type != constant.CloudStorageTypeTiDBCloud &&
		!strings.HasSuffix(c.Uri, "/") {
		return bizerr.New(bizerr.ErrCodeParams, "uri must end with '/'")
	}
	switch c.Type {
	case constant.CloudStorageTypeS3:
		if !strings.HasPrefix(c.Uri, "s3://") {
			return bizerr.New(bizerr.ErrCodeParams, "s3 uri must start with 's3://'")
		}
	case constant.CloudStorageTypeOSS:
		if !strings.HasPrefix(c.Uri, "oss://") {
			return bizerr.New(bizerr.ErrCodeParams, "oss uri must start with 'oss://'")
		}
	case constant.CloudStorageTypeGCS:
		if !strings.HasPrefix(c.Uri, "gcs://") && !strings.HasPrefix(c.Uri, "gs://") {
			return bizerr.New(bizerr.ErrCodeParams, "gcs uri must start with 'gcs://' or 'gs://'")
		}
	case constant.CloudStorageTypeAzureBlob:
		_, _, err := utils.ParseAzureURI(c.Uri)
		if err != nil {
			return err
		}
	}
	// ali cluster does not support s3 role arn
	if c.Type == constant.CloudStorageTypeS3 {
		if c.S3.RoleArn != nil && cloudProvider != config.ProviderAWS {
			return bizerr.New(bizerr.ErrCodeParams, "s3 role arn is only supported for aws cluster")
		}
	}
	// aws cluster does not support oss role arn
	if c.Type == constant.CloudStorageTypeOSS {
		if c.OSS.RoleArn != nil && cloudProvider != config.ProviderAliCloud {
			return bizerr.New(bizerr.ErrCodeParams, "oss role arn is only supported for alicloud cluster")
		}
	}
	return nil
}

func (c *CloudStorage) ToRegionalCloudStorage() *regionalserverlessv1.CloudStorage {
	if c == nil {
		return nil
	}
	cs := &regionalserverlessv1.CloudStorage{}
	switch c.Type {
	case constant.CloudStorageTypeS3:
		cs.Uri = c.Uri
		cs.CloudStorageType = regionalserverlessv1.CloudStorageType_S3
		if c.S3.AccessKey != nil {
			cs.CloudStorage = &regionalserverlessv1.CloudStorage_S3{
				S3: &regionalserverlessv1.S3CloudStorage{
					AuthType: regionalserverlessv1.S3CloudStorage_ACCESS_KEY,
					Auth: &regionalserverlessv1.S3CloudStorage_AccessKey_{
						AccessKey: &regionalserverlessv1.S3CloudStorage_AccessKey{
							AccessKeyId:     c.S3.AccessKey.AccessKeyId,
							SecretAccessKey: c.S3.AccessKey.SecretAccessKey,
						},
					},
				},
			}
		} else if c.S3.RoleArn != nil {
			cs.CloudStorage = &regionalserverlessv1.CloudStorage_S3{
				S3: &regionalserverlessv1.S3CloudStorage{
					AuthType: regionalserverlessv1.S3CloudStorage_ROLE_ARN,
					Auth: &regionalserverlessv1.S3CloudStorage_RoleArn_{
						RoleArn: &regionalserverlessv1.S3CloudStorage_RoleArn{
							RoleArn:    c.S3.RoleArn.RoleArn,
							ExternalId: c.S3.RoleArn.ExternalId,
						},
					},
				},
			}
		}

	case constant.CloudStorageTypeGCS:
		cs.Uri = c.Uri
		cs.CloudStorageType = regionalserverlessv1.CloudStorageType_GCS
		cs.CloudStorage = &regionalserverlessv1.CloudStorage_Gcs{
			Gcs: &regionalserverlessv1.GCSCloudStorage{
				AuthType: regionalserverlessv1.GCSCloudStorage_SERVICE_ACCOUNT_KEY,
				Auth: &regionalserverlessv1.GCSCloudStorage_ServiceAccountKey{
					ServiceAccountKey: c.GCS.ServiceAccountKey,
				},
			},
		}

	case constant.CloudStorageTypeAzureBlob:
		cs.CloudStorageType = regionalserverlessv1.CloudStorageType_AZURE_BLOB
		cs.Uri = c.Uri
		cs.CloudStorage = &regionalserverlessv1.CloudStorage_AzureBlob{
			AzureBlob: &regionalserverlessv1.AzureBlobCloudStorage{
				AuthType: regionalserverlessv1.AzureBlobCloudStorage_SAS_TOKEN,
				Auth: &regionalserverlessv1.AzureBlobCloudStorage_SasToken{
					SasToken: c.AzureBlob.SasToken,
				},
			},
		}

	case constant.CloudStorageTypeOSS:
		cs.CloudStorageType = regionalserverlessv1.CloudStorageType_OSS
		cs.Uri = c.Uri
		if c.OSS.AccessKey != nil {
			cs.CloudStorage = &regionalserverlessv1.CloudStorage_Oss{
				Oss: &regionalserverlessv1.OSSCloudStorage{
					AuthType: regionalserverlessv1.OSSCloudStorage_ACCESS_KEY,
					Auth: &regionalserverlessv1.OSSCloudStorage_AccessKey_{
						AccessKey: &regionalserverlessv1.OSSCloudStorage_AccessKey{
							AccessKeyId:     c.OSS.AccessKey.AccessKeyId,
							AccessKeySecret: c.OSS.AccessKey.AccessKeySecret,
						},
					},
				},
			}
		} else if c.OSS.RoleArn != nil {
			cs.CloudStorage = &regionalserverlessv1.CloudStorage_Oss{
				Oss: &regionalserverlessv1.OSSCloudStorage{
					AuthType: regionalserverlessv1.OSSCloudStorage_ROLE_ARN,
					Auth: &regionalserverlessv1.OSSCloudStorage_RoleArn_{
						RoleArn: &regionalserverlessv1.OSSCloudStorage_RoleArn{
							RoleArn:    c.OSS.RoleArn.RoleArn,
							ExternalId: c.OSS.RoleArn.ExternalId,
						},
					},
				},
			}
		}
	case constant.CloudStorageTypeTiDBCloud:
		cs.CloudStorageType = regionalserverlessv1.CloudStorageType_INTERNAL
	default:
		return nil
	}
	return cs
}

// ToUri builds the cloud storage uri with credentials in query parameters
// The OSS cloud storage will use s3 uri format, so it does not support role arn currently.
func (c *CloudStorage) ToUri(
	ctx context.Context,
	clusterRegion, clusterCloudProvider string,
) (string, bizerr.BizError) {
	escapeUri, err := url.Parse(escapeBucketUri(c.Uri))
	if err != nil {
		return "", bizerr.New(bizerr.ErrCodeParams, "invalid uri: %s", err.Error())
	}
	switch c.Type {
	case constant.CloudStorageTypeS3:
		q := escapeUri.Query()
		if c.S3.RoleArn != nil {
			q.Set("role-arn", c.S3.RoleArn.RoleArn)
			q.Set("external-id", c.S3.RoleArn.ExternalId)
		}
		if c.S3.AccessKey != nil {
			q.Set("access-key", c.S3.AccessKey.AccessKeyId)
			q.Set("secret-access-key", c.S3.AccessKey.SecretAccessKey)
		}
		escapeUri.RawQuery = q.Encode()
		return escapeUri.String(), nil
	case constant.CloudStorageTypeGCS:
		q := escapeUri.Query()
		if c.GCS.ServiceAccountKey != "" {
			q.Set("credentials", c.GCS.ServiceAccountKey)
		}
		escapeUri.RawQuery = q.Encode()
		return escapeUri.String(), nil
	case constant.CloudStorageTypeAzureBlob:
		accountName, escapeUri, err := utils.ParseAzureURI(escapeUri.String())
		if err != nil {
			return "", err
		}
		q := escapeUri.Query()
		q.Set("account-name", accountName)
		if c.AzureBlob.SasToken != "" {
			q.Set("sas-token", c.AzureBlob.SasToken)
		}
		escapeUri.RawQuery = q.Encode()
		return escapeUri.String(), nil
	case constant.CloudStorageTypeOSS:
		if c.OSS.AccessKey == nil {
			return "", bizerr.New(bizerr.ErrCodeParams, "oss role arn is not supported turns to uri yet")
		}
		options := []cloud_storage.OSSOption{cloud_storage.WithOSSAccessKey(
			c.OSS.AccessKey.AccessKeyId, c.OSS.AccessKey.AccessKeySecret)}
		region, err := cloud_storage.GetOssBucketRegion(ctx, c.Uri, options...)
		if err != nil {
			return "", err
		}
		q := escapeUri.Query()
		if c.OSS.AccessKey != nil {
			q.Set("access-key", c.OSS.AccessKey.AccessKeyId)
			q.Set("secret-access-key", c.OSS.AccessKey.AccessKeySecret)
		}
		endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", region)
		if clusterCloudProvider == config.ProviderAliCloud && clusterRegion == region {
			endpoint = fmt.Sprintf("https://oss-%s-internal.aliyuncs.com", region)
		}
		q.Set("endpoint", endpoint)
		q.Set("provider", "alibaba")
		escapeUri.RawQuery = q.Encode()
		return escapeUri.String(), nil
	default:
		return "", nil
	}
}

func escapeBucketUri(uri string) string {
	var schema, bucketUri string
	if idx := strings.Index(uri, "://"); idx >= 0 {
		schema = uri[:idx+3]
		bucketUri = uri[idx+3:]
	}
	// oss use s3 schema
	if strings.EqualFold(schema, "oss://") {
		schema = "s3://"
	}

	// escape each path segment
	pathSegments := strings.Split(bucketUri, "/")

	var builder strings.Builder
	builder.WriteString(schema)
	for i, segment := range pathSegments {
		if i == 0 {
			// bucket
			builder.WriteString(url.PathEscape(segment))
		} else {
			// folder
			builder.WriteByte('/')
			builder.WriteString(url.PathEscape(segment))
		}
	}
	return builder.String()
}
