package tidbgroup

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"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/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (s serverImpl) GetPublicEndpointSetting(
	ctx context.Context,
	req *openapidedicatedv1beta1.GetPublicEndpointSettingRequest,
) (*openapidedicatedv1beta1.PublicEndpointSetting, error) {
	ctx, span := tracer.Start(ctx, "GetPublicEndpointSetting")
	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")
	}

	// Validate request.
	tidbGroupId, err := strconv.ParseUint(req.GetTidbNodeGroupId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid tidb node group id")
	}

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

	tidbGroups, err := s.dedicatedCli.ListTidbNodeGroups(ctx, &globaldedicatedv1.ListTidbNodeGroupsRequest{
		ClusterIds:  []uint64{clusterId},
		Ids:         []uint64{tidbGroupId},
		ShowDetails: true,
	})
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to query tidb node group")
	}

	if len(tidbGroups.GetTidbNodeGroups()) == 0 {
		return nil, bizerror.NewNotFound(nil, "tidb node group not found")
	}

	return cvtGlobalToPublicEndpointSetting(
		tidbGroups.GetTidbNodeGroups()[0].GetEndpoint().GetPublicEndpoint(),
		req.GetTidbNodeGroupId(),
	)
}

func (s serverImpl) UpdatePublicEndpointSetting(
	ctx context.Context,
	req *openapidedicatedv1beta1.UpdatePublicEndpointSettingRequest,
) (*openapidedicatedv1beta1.PublicEndpointSetting, error) {
	ctx, span := tracer.Start(ctx, "UpdateTidbNodeGroup")
	defer span.End()

	var (
		err     error
		reqMeta *metadata.RequestMeta
	)

	// Validate request.
	groupId, err := strconv.ParseUint(req.GetPublicEndpointSetting().GetTidbNodeGroupId(), 10, 64)
	if err != nil {
		err = bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid tidb node group id")
		return nil, err
	}

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

	listTidbNodeGroupsResponse, err := s.dedicatedCli.ListTidbNodeGroups(ctx, &globaldedicatedv1.ListTidbNodeGroupsRequest{
		ClusterIds:  []uint64{clusterId},
		Ids:         []uint64{groupId},
		ShowDetails: true,
	})
	if err != nil {
		err = bizerror.NewInternal(err, "failed to query tidb node group")
		return nil, err
	}
	if len(listTidbNodeGroupsResponse.GetTidbNodeGroups()) == 0 {
		err = bizerror.NewNotFound(nil, "tidb node group not found")
		return nil, err
	}

	nodeGroup := listTidbNodeGroupsResponse.GetTidbNodeGroups()[0]

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeDedicated)
		details[auditlog.KeyDisplayName] = nodeGroup.GetDisplayName()
		details[auditlog.KeyTidbNodeGroupId] = req.GetPublicEndpointSetting().GetTidbNodeGroupId()
		details[auditlog.KeyIPAllowlist] = req.GetPublicEndpointSetting().GetIpAccessList()

		publicEndpointEnabled := nodeGroup.GetEndpoint().GetPublicEndpoint().GetEnablePublicLb()
		if req.GetPublicEndpointSetting() != nil && req.GetPublicEndpointSetting().Enabled != nil {
			publicEndpointEnabled = req.GetPublicEndpointSetting().GetEnabled()
			details[auditlog.KeyAction] = lo.Ternary(publicEndpointEnabled, "enable", "disable")
		}
		details[auditlog.KeyPublicEndpoint] = lo.Ternary(publicEndpointEnabled, "enabled", "disabled")

		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLog(ctx, auditlog.EventUpdateTidbNodeGroup, 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")
	}

	publicEndpointSetting := &globaldedicatedv1.TidbNodeGroup_Endpoint_PublicEndpoint{}
	if req.GetPublicEndpointSetting() != nil {
		publicEndpointSetting.EnablePublicLb = req.GetPublicEndpointSetting().Enabled
	}

	// check ClusterAction_CLUSTER_ACTION_SET_TRAFFIC_FILTER mutex
	isMutex, status, err := s.customStatusRepo.IsMutexClusterAction(
		ctx,
		clusterId,
		tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_SET_TRAFFIC_FILTER,
	)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			err = fmt.Errorf("failed to check mutex cluster action: %w", err)
			return nil, bizerror.NewInternal(err, err.Error())
		}
	}

	if isMutex {
		tilog.Error(
			ctx,
			"mutex custom status is exist",
			zap.Uint64("cluster_id", clusterId),
			zap.Any("custom status", status),
		)
		return nil, bizerror.NewForbiddenForClusterCustomStatus(
			constant.ErrClusterCustomStatusForbidden,
			status.StatusType.String(),
		)
	}

	if len(req.GetPublicEndpointSetting().GetIpAccessList()) > 0 {
		publicEndpointSetting.IpAllowList = make(
			[]*globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress,
			0,
			len(req.GetPublicEndpointSetting().GetIpAccessList()),
		)
		for _, ipAccessList := range req.GetPublicEndpointSetting().GetIpAccessList() {
			publicEndpointSetting.IpAllowList = append(
				publicEndpointSetting.IpAllowList,
				&globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress{
					CidrBlock:   ipAccessList.GetCidrNotation(),
					Description: ipAccessList.GetDescription(),
				},
			)
		}
	}

	// update tidb node group
	var tidbGroup *globaldedicatedv1.TidbNodeGroup
	tidbGroup, err = s.dedicatedCli.UpdateTidbNodeGroup(ctx, &globaldedicatedv1.UpdateTidbNodeGroupRequest{
		TidbNodeGroup: &globaldedicatedv1.TidbNodeGroup{
			Id:        groupId,
			ClusterId: clusterId,
			Endpoint:  &globaldedicatedv1.TidbNodeGroup_Endpoint{PublicEndpoint: publicEndpointSetting},
		},
		ValidateOnly: req.GetValidateOnly(),
	})

	if err != nil {
		return nil, err
	}

	return cvtGlobalToPublicEndpointSetting(
		tidbGroup.GetEndpoint().GetPublicEndpoint(),
		req.GetPublicEndpointSetting().GetTidbNodeGroupId(),
	)
}

func cvtGlobalToPublicEndpointSetting(
	endpoint *globaldedicatedv1.TidbNodeGroup_Endpoint_PublicEndpoint,
	tidbGroupId string,
) (*openapidedicatedv1beta1.PublicEndpointSetting, error) {
	if endpoint == nil {
		return nil, bizerror.NewInternal(fmt.Errorf("endpoint is nil"), "endpoint is nil")
	}

	ipAccessList := make(
		[]*openapidedicatedv1beta1.PublicEndpointSetting_IpAccessList,
		0,
		len(endpoint.GetIpAllowList()),
	)

	for _, ipList := range endpoint.GetIpAllowList() {
		ipAccessList = append(ipAccessList, &openapidedicatedv1beta1.PublicEndpointSetting_IpAccessList{
			CidrNotation: ipList.GetCidrBlock(),
			Description:  ipList.GetDescription(),
		})
	}

	return &openapidedicatedv1beta1.PublicEndpointSetting{
		Name:            fmt.Sprintf("tidbNodeGroups/%s/publicEndpointSetting", tidbGroupId),
		TidbNodeGroupId: tidbGroupId,
		Enabled:         endpoint.EnablePublicLb,
		IpAccessList:    ipAccessList,
	}, nil
}
