package user

import (
	"context"
	"errors"
	"strconv"

	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"gorm.io/gorm"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/constant"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	clustersvc "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/service/serverless/cluster"
	usersvc "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/service/serverless/user"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type User interface {
	ChangeRootPassword(
		ctx context.Context,
		req *openapiserverlessv1beta1.ChangeRootPasswordRequest,
	) (*openapiserverlessv1beta1.ChangeRootPasswordResponse, error)
	RotateRootPassword(
		ctx context.Context,
		req *openapiserverlessv1beta1.RotateRootPasswordRequest,
	) (*openapiserverlessv1beta1.RotateRootPasswordResponse, error)
	RedeemRootPassword(
		ctx context.Context,
		req *openapiserverlessv1beta1.RedeemRootPasswordRequest,
	) (*openapiserverlessv1beta1.RedeemRootPasswordResponse, error)
}

func NewUser(
	clusterSvc clustersvc.Service,
	userSvc usersvc.Service,
	auditLogSender eventbus.ConsoleAuditLogSender,
) User {
	return &userImpl{
		clusterSvc:     clusterSvc,
		userSvc:        userSvc,
		auditLogSender: auditLogSender,
	}
}

type userImpl struct {
	clusterSvc     clustersvc.Service
	userSvc        usersvc.Service
	auditLogSender eventbus.ConsoleAuditLogSender
}

func (u userImpl) ChangeRootPassword(
	ctx context.Context,
	req *openapiserverlessv1beta1.ChangeRootPasswordRequest,
) (*openapiserverlessv1beta1.ChangeRootPasswordResponse, error) {
	var (
		err     error
		reqMeta *metadata.RequestMeta
	)

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
		return details
	}
	defer func() {
		u.auditLogSender.SubmitAuditLog(ctx, auditlog.EventSetRootPassword, reqMeta, detailsFunc, 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 cluster record from domain service
	cluster := &entity.Cluster{}
	cluster.ClusterId, err = strconv.ParseUint(req.ClusterId, 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}
	cluster, err = u.clusterSvc.GetCluster(ctx, cluster.ClusterId, entity.ClusterViewClusterViewBasic)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "cluster not found")
		}
		return nil, bizerror.NewInternal(err, "failed to get cluster record")
	}

	// validate password
	if req.Password == "" {
		err = errors.New("new password is empty")
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "password are not allowed to be empty")
	}

	var serverlessCluster *globalserverlessv1.ServerlessServiceCluster
	serverlessCluster, err = cluster.GetServerlessCluster()
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to get serverless cluster")
	}

	// Then, change root password by requesting serverless global service.
	username := ""
	if cluster.EngineType == openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE {
		if serverlessCluster.GetCluster().GetUserPrefix() == "" {
			username = "root"
		} else {
			username = serverlessCluster.GetCluster().GetUserPrefix() + ".root"
		}
	}

	var customStatus *entity.ClusterCustomStatus
	customStatus, err = u.userSvc.ChangeSqlUserPassword(ctx,
		strconv.FormatUint(cluster.ClusterId, 10),
		username,
		req.Password,
	)
	if err != nil {
		if errors.Is(err, constant.ErrClusterCustomStatusForbidden) {
			return nil, bizerror.NewForbiddenForClusterCustomStatus(err, customStatus.StatusType.String())
		}
		return nil, err
	}

	return &openapiserverlessv1beta1.ChangeRootPasswordResponse{}, nil
}

func (u userImpl) RotateRootPassword(
	ctx context.Context,
	req *openapiserverlessv1beta1.RotateRootPasswordRequest,
) (*openapiserverlessv1beta1.RotateRootPasswordResponse, error) {
	var reqMeta *metadata.RequestMeta
	detailsFunc := func() auditlog.Details {
		return eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
	}
	var err error
	defer func() {
		u.auditLogSender.SubmitAuditLog(ctx, auditlog.EventSetRootPassword, reqMeta, detailsFunc, err)
	}()

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

	if req.GetClusterId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster id is required")
	}

	resp, customStatus, err := u.userSvc.RotateRootPassword(ctx, req.GetClusterId())
	if err != nil {
		if errors.Is(err, constant.ErrClusterCustomStatusForbidden) {
			statusType := ""
			if customStatus != nil {
				statusType = customStatus.StatusType.String()
			}
			return nil, bizerror.NewForbiddenForClusterCustomStatus(err, statusType)
		}
		return nil, err
	}

	return &openapiserverlessv1beta1.RotateRootPasswordResponse{Rotating: resp.GetRotating()}, nil
}

func (u userImpl) RedeemRootPassword(
	ctx context.Context,
	req *openapiserverlessv1beta1.RedeemRootPasswordRequest,
) (*openapiserverlessv1beta1.RedeemRootPasswordResponse, error) {
	if req.GetClusterId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster id is required")
	}

	resp, err := u.userSvc.RedeemRootPassword(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	return &openapiserverlessv1beta1.RedeemRootPasswordResponse{
		Rotating: resp.GetRotating(),
		Password: resp.GetPassword(),
	}, nil
}
