package auditlog

import (
	"context"
	"fmt"
	"strconv"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/sdk/auditlog"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (s serverImpl) GetAuditLogConfig(
	ctx context.Context,
	request *openapidedicatedv1beta1.GetAuditLogConfigRequest,
) (*openapidedicatedv1beta1.AuditLogConfig, error) {
	ctx, span := tracer.Start(ctx, "GetAuditLogConfig")
	defer span.End()

	// Extract request meta.
	_, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	clusterId, err := strconv.ParseUint(request.GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	cluster, err := s.clusterSvc.GetCluster(ctx, clusterId, entity.ClusterViewClusterViewLocal, false, false)
	if err != nil {
		return nil, err
	}

	resp, err := s.dedicatedCli.GetAuditLogConfig(ctx, &globaldedicatedv1.GetAuditLogConfigRequest{
		ClusterId: clusterId,
	})
	if err != nil {
		return nil, err
	}

	openApiConfig, err := configGlobalToOpenApi(resp, cluster.CloudProvider, resp.GetEnabled())
	if err != nil {
		return nil, err
	}

	// Do extra write permission check.
	if resp.GetBucketManager() == openapidedicatedv1beta1.AuditLogConfig_CUSTOMER.String() && resp.GetEnabled() {
		_, err := s.dedicatedCli.UpdateAuditLogConfig(
			ctx,
			&globaldedicatedv1.UpdateAuditLogConfigRequest{AuditLogConfig: resp, ValidateOnly: true},
		)
		if err != nil {
			berr, ok := bizerr.AsCode(err, bizerr.ErrCodePermission)
			if !ok {
				return nil, bizerr.WrapError(err, "failed to check bucket write permission")
			}
			openApiConfig.BucketWriteCheck = &openapidedicatedv1beta1.AuditLogConfig_BucketWriteCheck{
				Writable:    false,
				ErrorReason: berr.ErrMsg(),
			}
		}
	}

	return openApiConfig, nil
}

func (s serverImpl) UpdateAuditLogConfig(
	ctx context.Context,
	request *openapidedicatedv1beta1.UpdateAuditLogConfigRequest,
) (*openapidedicatedv1beta1.AuditLogConfig, error) {
	ctx, span := tracer.Start(ctx, "UpdateAuditLogConfig")
	defer span.End()

	var (
		cluster *entity.Cluster
		err     error
		reqMeta *metadata.RequestMeta
	)

	defer func() {
		if !request.GetValidateOnly() {
			s.auditLogSender.SubmitAuditLog(ctx, auditlog.EventUpdateAuditLogConfig, reqMeta, func() auditlog.Details {
				details := eventbus.NewAuditLogDetails(eventbus.TierTypeDedicated)
				details = eventbus.AppendDbAuditLogDetails(
					details,
					cluster.ClusterId,
					cluster.DisplayName,
					request.GetAuditLogConfig(),
				)
				return details
			}, err)
		}
	}()

	// Extract request meta.
	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	// Get the global cluster.
	clusterId, err := strconv.ParseUint(request.GetAuditLogConfig().GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}
	cluster, err = s.clusterSvc.GetCluster(ctx, clusterId, entity.ClusterViewClusterViewLocal, false, false)
	if err != nil {
		return nil, err
	}

	// Convert the openapi config to global config.
	var regionCode string
	if request.GetAuditLogConfig().GetBucketRegionId() != "" {
		_, regionCode, err = common.ParseOpenApiRegionNameOrId(request.GetAuditLogConfig().GetBucketRegionId())
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to parse region name")
		}
	}
	globalConfig := &globaldedicatedv1.AuditLogConfig{
		ClusterId:     clusterId,
		BucketUrl:     request.GetAuditLogConfig().GetBucketUri(),
		Region:        regionCode,
		RoleArn:       request.GetAuditLogConfig().GetAwsRoleArn(),
		SasToken:      request.GetAuditLogConfig().GetAzureSasToken(),
		Unredacted:    request.GetAuditLogConfig().Unredacted,
		Format:        request.GetAuditLogConfig().GetFormat().String(),
		MaxSizeMib:    request.GetAuditLogConfig().GetRotationPolicy().GetMaxSizeMib(),
		BucketManager: request.GetAuditLogConfig().GetBucketManager().String(),
		Enabled:       request.GetAuditLogConfig().Enabled,
	}
	if request.GetAuditLogConfig().GetRotationPolicy() != nil {
		globalConfig.MaxAgeSecond = request.GetAuditLogConfig().GetRotationPolicy().MaxAgeSecond
	}

	// Do update finally.
	resp, err := s.dedicatedCli.UpdateAuditLogConfig(ctx, &globaldedicatedv1.UpdateAuditLogConfigRequest{
		AuditLogConfig: globalConfig,
		ValidateOnly:   request.GetValidateOnly(),
	})
	if err != nil {
		return nil, err
	}

	// Convert the global config to openapi config.
	openApiConfig, err := configGlobalToOpenApi(resp, cluster.CloudProvider, resp.GetEnabled())
	if err != nil {
		return nil, err
	}

	return openApiConfig, nil
}

func (s serverImpl) ShowObjectStorageAccessIamPrincipal(
	ctx context.Context,
	request *openapidedicatedv1beta1.ShowObjectStorageAccessIamPrincipalRequest,
) (*openapidedicatedv1beta1.ShowObjectStorageAccessIamPrincipalResponse, error) {
	ctx, span := tracer.Start(ctx, "CreateAuditLogConfig")
	defer span.End()

	// Extract request meta.
	_, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	clusterId, err := strconv.ParseUint(request.GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	resp, err := s.dedicatedCli.GetAccount(ctx, &globaldedicatedv1.GetAccountRequest{
		ClusterId: clusterId,
	})
	if err != nil {
		return nil, err
	}

	return &openapidedicatedv1beta1.ShowObjectStorageAccessIamPrincipalResponse{
		IamPrincipal: resp.GetAccount(),
	}, nil
}

func configGlobalToOpenApi(
	c *globaldedicatedv1.AuditLogConfig,
	provider openapicommonv1beta1.Region_CloudProvider,
	enabled bool,
) (*openapidedicatedv1beta1.AuditLogConfig, bizerr.BizError) {
	_, regionId, _, err := common.BuildOpenApiRegionName(provider.String(), c.GetRegion())
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to build region name")
	}
	managedBy, err := enum.FromString[openapidedicatedv1beta1.AuditLogConfig_BucketManager](c.GetBucketManager())
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse bucket manager")
	}
	format, err := enum.FromString[openapidedicatedv1beta1.AuditLogConfig_Format](c.GetFormat())
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to parse format")
	}
	output := &openapidedicatedv1beta1.AuditLogConfig{
		Name:           fmt.Sprintf("clusters/%d/auditLogConfig", c.GetClusterId()),
		ClusterId:      strconv.FormatUint(c.GetClusterId(), 10),
		Enabled:        c.GetEnabled(),
		BucketUri:      c.GetBucketUrl(),
		BucketRegionId: regionId,
		AwsRoleArn:     c.GetRoleArn(),
		AzureSasToken:  c.GetSasToken(),
		BucketManager:  openapidedicatedv1beta1.AuditLogConfig_BucketManager(managedBy),
		Unredacted:     c.GetUnredacted(),
		Format:         openapidedicatedv1beta1.AuditLogConfig_Format(format),
		RotationPolicy: &openapidedicatedv1beta1.AuditLogConfig_RotationPolicy{
			MaxSizeMib:   c.GetMaxSizeMib(),
			MaxAgeSecond: lo.ToPtr(c.GetMaxAgeSecond()),
		},
		Legacy: lo.EmptyableToPtr(c.GetLegacy()),
	}
	if enabled {
		output.BucketWriteCheck = &openapidedicatedv1beta1.AuditLogConfig_BucketWriteCheck{
			Writable: true, // Default to true, will be set to false if write permission check fails.
		}
	}
	return output, nil
}
