package server

import (
	"context"
	"fmt"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	pdDeploymentStatusReasonInferredFromSpec = "InferredFromSpec"
	pdDeploymentStatusReasonUnknownPDMode    = "UnknownPDMode"
	pdDeploymentStatusReasonSwitching        = "PDDeploymentModeSwitching"
	pdDeploymentStatusMessageSwitching       = "PD deployment mode switch is in progress"
)

func (s *serverImpl) mergePDDeploymentStatusBestEffort(ctx context.Context, sharedPool *entity.SharedPool) {
	status, err := s.getPDDeploymentStatus(ctx, sharedPool, true)
	if err != nil {
		s.setPDDeploymentStatusUnavailable(ctx, sharedPool, err)
		return
	}
	sharedPool.PdDeploymentStatus = status
}

func (s *serverImpl) mergeRunningPDDeploymentStatusBestEffort(ctx context.Context, sharedPool *entity.SharedPool) {
	status, err := s.getPDDeploymentStatus(ctx, sharedPool, false)
	if err != nil {
		s.setPDDeploymentStatusUnavailable(ctx, sharedPool, err)
		return
	}
	sharedPool.PdDeploymentStatus = buildRunningPDDeploymentStatus(status)
}

func (s *serverImpl) mergePDDeploymentStatusForReadBestEffort(ctx context.Context, sharedPool *entity.SharedPool) {
	runningDags, err := s.getRunningDagIns(ctx, pdDeploymentModeDAGPlans(), []string{sharedPool.GetId()}, nil, nil, nil)
	if err != nil {
		tilog.Warn(ctx, "failed to get running pd deployment mode dag",
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.Error(err))
		s.mergePDDeploymentStatusBestEffort(ctx, sharedPool)
		return
	}
	s.mergePDDeploymentStatusForReadWithRunningDAGBestEffort(ctx, sharedPool, len(runningDags) > 0)
}

func (s *serverImpl) mergePDDeploymentStatusForReadWithRunningDAGBestEffort(
	ctx context.Context,
	sharedPool *entity.SharedPool,
	hasRunningPDDeploymentModeDAG bool,
) {
	if hasRunningPDDeploymentModeDAG {
		s.mergeRunningPDDeploymentStatusBestEffort(ctx, sharedPool)
		return
	}
	s.mergePDDeploymentStatusBestEffort(ctx, sharedPool)
}

func (s *serverImpl) setPDDeploymentStatusUnavailable(ctx context.Context, sharedPool *entity.SharedPool, err error) {
	tilog.Warn(ctx, "failed to get pd deployment status",
		zap.String("sharedPoolId", sharedPool.GetId()),
		zap.Error(err))
	sharedPool.PdDeploymentStatus = &entity.PDDeploymentStatus{
		Phase:   apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_UNKNOWN,
		Reason:  "PDGroupUnavailable",
		Message: fmt.Sprintf("failed to read PDGroup: %s", err),
	}
}

func (s *serverImpl) getPDDeploymentStatus(
	ctx context.Context,
	sharedPool *entity.SharedPool,
	allowSpecInference bool,
) (*entity.PDDeploymentStatus, error) {
	pdGroup, err := s.getSharedPoolPDGroup(ctx, sharedPool)
	if err != nil {
		return nil, err
	}
	return buildPDDeploymentStatus(sharedPool, pdGroup, allowSpecInference), nil
}

func (s *serverImpl) getSharedPoolPDGroup(ctx context.Context, sharedPool *entity.SharedPool) (*v1alpha1.PDGroup, error) {
	if s.dataplaneManager == nil {
		return nil, fmt.Errorf("dataplane manager is nil")
	}
	remoteCli, err := s.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return nil, fmt.Errorf("failed to get remote client for shared pool %s: %w", sharedPool.GetId(), err)
	}

	pdGroupID := sharedPool.GetDefaultComponenId()
	if pdGroupID == "" {
		pdGroupID = utils.FnvHash(sharedPool.GetId())
	}

	pdGroup, err := tioperator.GetPDGroupCR(ctx, remoteCli, sharedPool.GetId(), pdGroupID)
	if err != nil {
		return nil, err
	}
	return pdGroup, nil
}

func buildPDDeploymentStatus(
	sharedPool *entity.SharedPool,
	pdGroup *v1alpha1.PDGroup,
	allowSpecInference bool,
) *entity.PDDeploymentStatus {
	desiredMode := entity.GetEffectivePDDeploymentMode(sharedPool.GetPdDeploymentMode())
	currentMode := tioperator.PDModeToAPI(pdGroup.Status.Mode)
	condition := tioperator.GetPDModeSwitchingCondition(pdGroup)

	status := &entity.PDDeploymentStatus{
		CurrentMode: currentMode,
		Phase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING,
	}
	if currentMode == apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_UNSPECIFIED {
		status.Phase = apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_UNKNOWN
		status.Reason = pdDeploymentStatusReasonUnknownPDMode
		status.Message = fmt.Sprintf("unknown PDGroup.status.mode %q", pdGroup.Status.Mode)
		return status
	}

	if allowSpecInference &&
		pdGroup.Status.Mode == v1alpha1.PDModeNormal &&
		pdGroup.Spec.Template.Spec.Mode == v1alpha1.PDModeMS &&
		!tioperator.IsPDModeSwitchingTrue(condition) {
		status.CurrentMode = apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE
		status.Reason = pdDeploymentStatusReasonInferredFromSpec
		status.Message = "PDGroup.status.mode is unavailable; current mode is inferred from PDGroup.spec.template.spec.mode"
	}

	if status.CurrentMode == desiredMode &&
		isPDDeploymentModeSpecInTarget(pdGroup, desiredMode) &&
		!tioperator.IsPDModeSwitchingTrue(condition) {
		status.Phase = apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE
		if condition != nil && status.GetReason() == "" {
			status.Reason = condition.Reason
			status.Message = condition.Message
		}
		return status
	}
	if tioperator.IsPDModeSwitchingTrue(condition) {
		status.Reason = condition.Reason
		status.Message = condition.Message
	} else {
		setPDDeploymentStatusSwitchingMessage(status)
	}
	return status
}

func buildRunningPDDeploymentStatus(status *entity.PDDeploymentStatus) *entity.PDDeploymentStatus {
	if status == nil {
		return nil
	}
	oldPhase := status.GetPhase()
	if oldPhase != apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_UNKNOWN {
		status.Phase = apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING
		if oldPhase == apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE ||
			status.GetReason() == "" ||
			status.GetReason() == v1alpha1.ReasonModeSwitchComplete {
			setPDDeploymentStatusSwitchingMessage(status)
		}
	}
	return status
}

func setPDDeploymentStatusSwitchingMessage(status *entity.PDDeploymentStatus) {
	status.Reason = pdDeploymentStatusReasonSwitching
	status.Message = pdDeploymentStatusMessageSwitching
}

func isPDDeploymentModeStableInTarget(pdGroup *v1alpha1.PDGroup, targetMode entity.PDDeploymentMode) bool {
	return isPDDeploymentModeSpecInTarget(pdGroup, targetMode) &&
		tioperator.PDModeToAPI(pdGroup.Status.Mode) == targetMode &&
		!tioperator.IsPDModeSwitchingTrue(tioperator.GetPDModeSwitchingCondition(pdGroup))
}

func isPDDeploymentModeSpecInTarget(pdGroup *v1alpha1.PDGroup, targetMode entity.PDDeploymentMode) bool {
	return tioperator.PDModeToAPI(pdGroup.Spec.Template.Spec.Mode) == targetMode
}
