package auditlog_svc

import (
	"context"
	"strconv"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/console"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/domain/service"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	"github.com/tidbcloud/serverless-service/internal/errorcode"
	aws2 "github.com/tidbcloud/serverless-service/internal/infra/aws"
	"github.com/tidbcloud/serverless-service/internal/utils"
	serverlesscs "github.com/tidbcloud/serverless-service/pkg/cloud_storage"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

var consoleTracer = otel.Tracer("auditlog.console")

type databaseAuditLogConsoleService struct {
	mgmtsvcFacade     facade.MgmtSvcFacade
	s3ClientProvider  *aws2.S3ClientProvider
	preSignerProvider *aws2.PresignerProvider
	auditLogSvc       service.AuditLogService
	console.UnimplementedDatabaseAuditLogServiceServer
}

func NewDatabaseAuditLogConsoleService(mgmtsvcFacade facade.MgmtSvcFacade, s3ClientProvider *aws2.S3ClientProvider,
	preSignerProvider *aws2.PresignerProvider, auditLogSvc service.AuditLogService,
) console.DatabaseAuditLogServiceServer {
	return &databaseAuditLogConsoleService{
		mgmtsvcFacade:     mgmtsvcFacade,
		s3ClientProvider:  s3ClientProvider,
		preSignerProvider: preSignerProvider,
		auditLogSvc:       auditLogSvc,
	}
}

func (a *databaseAuditLogConsoleService) ListAuditLogFiles(
	ctx context.Context,
	request *console.ListAuditLogFilesRequest,
) (_ *console.ListAuditLogFilesResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.ListAuditLogFiles")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to list audit log files")
	// check request
	switch {
	case request.PageSize < 0:
		return nil, bizerr.New(bizerr.ErrCodeParams, "page size must be positive")
	case request.PageSize == 0:
		request.PageSize = listAuditLogsDefaultPageSize
	case request.PageSize > listAuditLogsMaxPageSize:
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"page size must be less than or equal to %d",
			listAuditLogsMaxPageSize,
		)
	}
	if request.Date == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "date cannot be empty")
	}
	// list audit logs
	auditLogs, nextPageToken, err := a.auditLogSvc.ListAuditLogFiles(
		ctx,
		request.ClusterId,
		request.PageToken,
		request.Date,
		request.PageSize,
	)
	if err != nil {
		return nil, err
	}

	// convert to response
	auditLogFiles := make([]*console.AuditLogFile, 0, len(auditLogs))
	for _, file := range auditLogs {
		auditLogFiles = append(auditLogFiles, file.ConvertToConsole())
	}
	return &console.ListAuditLogFilesResponse{
		AuditLogFiles: auditLogFiles,
		NextPageToken: nextPageToken,
	}, nil
}

func (a *databaseAuditLogConsoleService) DownloadAuditLogFiles(
	ctx context.Context,
	request *console.DownloadAuditLogFilesRequest,
) (_ *console.DownloadAuditLogFilesResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.DownloadAuditLogFiles")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to download audit log files")
	// check request
	if len(request.AuditLogFileNames) > downloadAuditLogsMaxNumber {
		return nil, bizerr.New(errorcode.ErrCodeDownloadExceedMaxNumber, "")
	}
	// download audit logs
	auditLogs, err := a.auditLogSvc.DownloadAuditLogFiles(ctx, request.ClusterId, request.AuditLogFileNames)
	if err != nil {
		return nil, err
	}
	auditLogFiles := make([]*console.AuditLogFile, 0, len(auditLogs))
	for _, file := range auditLogs {
		auditLogFiles = append(auditLogFiles, file.ConvertToConsole())
	}
	return &console.DownloadAuditLogFilesResponse{
		AuditLogFiles: auditLogFiles,
	}, nil
}

func (a *databaseAuditLogConsoleService) GetAuditLogFilterRule(
	ctx context.Context,
	request *console.GetAuditLogFilterRuleRequest,
) (_ *console.AuditLogFilterRule, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.GetAuditLogFilterRule")
	logger := log.WithContextL(ctx).With(zap.String("filter_rule_id", request.FilterRuleId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("filterRuleId", request.FilterRuleId),
	)
	logger.Info("start to get audit log filter rule")
	if request.FilterRuleId == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "empty cluster id or filter rule id")
	}

	filterRule, err := a.auditLogSvc.GetAuditLogFilterRule(ctx, request.ClusterId, request.FilterRuleId)
	if err != nil {
		return nil, err
	}
	return vo.FilterRuleToConsole(filterRule), nil
}

func (a *databaseAuditLogConsoleService) ListAuditLogFilterRules(
	ctx context.Context,
	request *console.ListAuditLogFilterRulesRequest,
) (_ *console.ListAuditLogFilterRulesResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.ListAuditLogFilterRules")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to list audit log filter rules")

	pager := utils.NewPagerNumberPager()
	err = pager.Normalize(request)
	if err != nil {
		return nil, err
	}
	filterRules, total, err := a.auditLogSvc.ListAuditLogFilterRules(ctx, &vo.ListFilterRuleOpts{
		ClusterID: request.ClusterId,
		Limit:     int32(pager.PageSize()),
		Offset:    int64(pager.Offset()),
	})
	if err != nil {
		return nil, err
	}

	// build response
	consoleFilteRules := make([]*console.AuditLogFilterRule, 0, len(filterRules))
	for _, f := range filterRules {
		consoleFilteRules = append(consoleFilteRules, vo.FilterRuleToConsole(f))
	}
	nextPageToken := pager.NextPageToken(total)
	return &console.ListAuditLogFilterRulesResponse{
		FilterRules:   consoleFilteRules,
		NextPageToken: nextPageToken,
		TotalSize:     total,
	}, nil
}

func (a *databaseAuditLogConsoleService) CreateAuditLogFilterRule(
	ctx context.Context,
	request *console.CreateAuditLogFilterRuleRequest,
) (_ *console.AuditLogFilterRule, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.CreateAuditLogFilterRule")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to create audit log filter rule")

	filterRule, err := vo.ConsoleCreateToFilterRule(request.FilterRule)
	if err != nil {
		logger.Error("build filter rule failed", zap.Error(err))
		return nil, err
	}

	filterRule, err = a.auditLogSvc.CreateAuditLogFilterRule(ctx, request.ClusterId, filterRule)
	if err != nil {
		return nil, err
	}
	return vo.FilterRuleToConsole(filterRule), nil
}

func (a *databaseAuditLogConsoleService) DeleteAuditLogFilterRule(
	ctx context.Context,
	request *console.DeleteAuditLogFilterRuleRequest,
) (_ *console.AuditLogFilterRule, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.DeleteAuditLogFilterRule")
	logger := log.WithContextL(ctx).With(zap.String("filter_rule_id", request.FilterRuleId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("filterRuleId", request.FilterRuleId),
	)
	logger.Info("start to delete audit log filter rule")
	if request.FilterRuleId == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "empty filter rule id")
	}
	filterRule, err := a.auditLogSvc.DeleteAuditLogFilterRule(ctx, request.ClusterId, request.FilterRuleId)
	if err != nil {
		return nil, err
	}
	return vo.FilterRuleToConsole(filterRule), nil
}

func (a *databaseAuditLogConsoleService) UpdateAuditLogFilterRule(
	ctx context.Context,
	request *console.UpdateAuditLogFilterRuleRequest,
) (_ *console.AuditLogFilterRule, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.UpdateAuditLogFilterRule")
	logger := log.WithContextL(ctx).With(zap.String("filter_rule_id", request.FilterRuleId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("filterRuleId", request.FilterRuleId),
	)
	logger.Info("start to update audit log filter rule")

	filterRule, err := vo.ConsoleUpdateToFilterRule(request)
	if err != nil {
		logger.Error("build filter rule failed", zap.Error(err))
		return nil, err
	}

	filterRule, err = a.auditLogSvc.UpdateAuditLogFilterRule(ctx, request.ClusterId, filterRule)
	if err != nil {
		return nil, err
	}
	return vo.FilterRuleToConsole(filterRule), nil
}

func (a *databaseAuditLogConsoleService) GetAuditLogConfig(
	ctx context.Context,
	request *console.GetAuditLogConfigRequest,
) (_ *console.AuditLogConfig, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.GetAuditLogConfig")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to get audit log config")

	auditLogConfig, err := a.auditLogSvc.GetAuditLogConfig(ctx, request.ClusterId)
	if err != nil {
		logger.Error("failed to get audit log config", zap.Error(err))
		return nil, err
	}
	return vo.AuditLogConfigToConsole(auditLogConfig), nil
}

func (a *databaseAuditLogConsoleService) UpdateAuditLogConfig(
	ctx context.Context,
	request *console.UpdateAuditLogConfigRequest,
) (_ *console.AuditLogConfig, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.UpdateAuditLogConfig")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to update audit log config")

	auditLogConfig, err := vo.ConsoleUpdateToAuditLogConfig(request.ClusterId, request.AuditLogConfig)
	if err != nil {
		logger.Error("build audit log config failed", zap.Error(err))
		return nil, err
	}

	auditLogConfig, err = a.auditLogSvc.UpdateAuditLogConfig(ctx, auditLogConfig)
	if err != nil {
		logger.Error("failed to update audit log config", zap.Error(err))
		return nil, err
	}

	return vo.AuditLogConfigToConsole(auditLogConfig), nil
}

func (a *databaseAuditLogConsoleService) PreCheckAuditLogConfig(
	ctx context.Context,
	request *console.PreCheckAuditLogConfigRequest,
) (_ *console.PreCheckAuditLogConfigResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.PreCheckAuditLogConfig")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to pre check audit log config")

	auditLogConfig, err := vo.ConsoleUpdateToAuditLogConfig(request.ClusterId, &console.AuditLogConfig{
		CloudStorage: request.CloudStorage,
	})
	if err != nil {
		logger.Error("build audit log config failed", zap.Error(err))
		return nil, err
	}

	err = a.auditLogSvc.PreCheckAuditLogConfig(ctx, auditLogConfig)
	if err != nil {
		logger.Error("failed to pre check audit log config", zap.Error(err))
		return nil, err
	}

	return &console.PreCheckAuditLogConfigResponse{}, nil
}

func (a *databaseAuditLogConsoleService) GetCloudStorageProfile(
	ctx context.Context,
	request *console.GetCloudStorageProfileRequest,
) (_ *console.CloudStorageProfile, err error) {
	ctx, span := consoleTracer.Start(ctx, "databaseAuditLogConsoleService.GetCloudStorageProfile")
	logger := log.WithContextL(ctx).With(zap.String("cluster_id", request.ClusterId))
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareAuditLogErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	logger.Info("start to get cloud storage profile")

	clusterIdUint64, err := strconv.ParseUint(request.ClusterId, 10, 64)
	if err != nil {
		logger.Error("invalid cluster id", zap.Error(err))
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cluster id")
	}

	cluster, err := a.mgmtsvcFacade.GetCluster(ctx, clusterIdUint64, tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC)
	if err != nil {
		logger.Error("get cluster failed", zap.Error(err))
		return nil, err
	}

	resp := &console.CloudStorageProfile{
		ClusterId: request.ClusterId,
	}

	stage := config.Global().ServerConfig.Stage
	switch cluster.CloudProvider {
	case config.ProviderAliCloud:
	case config.ProviderAWS:
		profile := serverlesscs.GetAWSProfileV2(stage, request.ClusterId)
		if profile != nil {
			resp.S3Profile = &console.CloudStorageProfile_S3Profile{
				AwsAccountId:  profile.AccountId,
				AwsExternalId: profile.ExternalId,
			}
		}
	}
	return resp, nil
}
