package server

import (
	"context"
	"fmt"
	"math"
	"time"

	v1alpha1br "github.com/pingcap/tidb-operator/api/v2/br/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/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/types"
)

type GetClassicRestoreInput struct {
	*apiv1.GetClassicRestoreRequest
}

type GetClassicRestoreOutput struct {
	*apiv1.ClassicRestore
}

func (s *serverImpl) GetClassicRestore(ctx context.Context, input *GetClassicRestoreInput) (*GetClassicRestoreOutput, error) {
	restore, err := s.classicRestoreRepo.Load(ctx, input.RestoreId, true)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			return nil, status.Errorf(codes.NotFound, "ClassicRestore not found")
		}
		tilog.Error(ctx, "failed to load classic restore record", zap.Error(err), zap.String("restoreId", input.RestoreId))
		return nil, fmt.Errorf("failed to load classic restore record: %w", err)
	}
	if restore.DeletedAt.Valid {
		return &GetClassicRestoreOutput{ClassicRestore: &apiv1.ClassicRestore{Status: apiv1.ClassicRestore_DELETED}}, nil
	}

	switch restore.Status {
	case schema.ClassicRestoreStatusRunning:
		return s.getRestoreInRunning(ctx, restore)
	case schema.ClassicRestoreStatusSucceeded:
		return &GetClassicRestoreOutput{ClassicRestore: &apiv1.ClassicRestore{Status: apiv1.ClassicRestore_SUCCEED}}, nil
	case schema.ClassicRestoreStatusFailed:
		return &GetClassicRestoreOutput{ClassicRestore: &apiv1.ClassicRestore{
			Status:  apiv1.ClassicRestore_FAILED,
			Failure: restoreFailureOutput(restore.FailureSource, restore.FailureCode, restore.FailureMessage),
		}}, nil
	case schema.ClassicRestoreStatusCancelled:
		return &GetClassicRestoreOutput{ClassicRestore: &apiv1.ClassicRestore{Status: apiv1.ClassicRestore_DELETING}}, nil
	}

	return &GetClassicRestoreOutput{ClassicRestore: &apiv1.ClassicRestore{Status: apiv1.ClassicRestore_CLASSIC_RESTORE_STATUS_UNSPECIFIED}}, nil
}

func (s *serverImpl) getRestoreInRunning(ctx context.Context, restore *entity.ClassicRestore) (*GetClassicRestoreOutput, error) {
	switch restore.GetLastAction() {
	case "", schema.ClassicRestoreActionScaleTiKV:
		return &GetClassicRestoreOutput{
			ClassicRestore: &apiv1.ClassicRestore{
				Status:  apiv1.ClassicRestore_PROVISIONING,
				Message: "provisioning resource for restore, current action is " + restore.GetLastAction(),
			},
		}, nil
	case schema.ClassicRestoreActionExecRestore:
		return s.getDataRestoringOutput(ctx, restore)
	case schema.ClassicRestoreActionRestartTiDB, schema.ClassicRestoreActionDeleteCloudAdmin:
		return &GetClassicRestoreOutput{
			ClassicRestore: &apiv1.ClassicRestore{
				Status:  apiv1.ClassicRestore_POST_RESTORE_ACTION,
				Message: "data restoring done, performing post restore action, current action is " + restore.GetLastAction(),
			},
		}, nil
	default:
		return &GetClassicRestoreOutput{
			ClassicRestore: &apiv1.ClassicRestore{
				Status:  apiv1.ClassicRestore_CLASSIC_RESTORE_STATUS_UNSPECIFIED,
				Message: "unknown action in running status, should not happen",
			},
		}, nil
	}
}

func (s *serverImpl) getDataRestoringOutput(ctx context.Context, restore *entity.ClassicRestore) (*GetClassicRestoreOutput, error) {
	// get restore cr to get the data restoring progress
	targetSharedPool, err := s.sharedPoolRepo.Load(ctx, restore.TargetSharedPoolID, false)
	if err != nil {
		return nil, fmt.Errorf("failed to load target shared pool: %w", err)
	}
	remoteCli, err := s.dataplaneManager.GetClient(targetSharedPool.GetDataplaneName())
	if err != nil {
		return nil, fmt.Errorf("failed to get remote client for target shared pool: %w", err)
	}

	restoreCR := &v1alpha1br.Restore{}
	restoreName := tioperator.ClassicRestoreCRName(restore.RestoreID)
	namespace := targetSharedPool.GetDataPlaneNamespace()
	err = remoteCli.Get(ctx, types.NamespacedName{
		Name:      restoreName,
		Namespace: namespace,
	}, restoreCR)
	if err != nil {
		tilog.Error(ctx, "failed to get restore", zap.String("name", restoreName), zap.String("namespace", namespace), zap.Error(err))
		return nil, fmt.Errorf("failed to get restore: %w", err)
	}

	restoreOutput := calRestoreProgress(restoreCR)

	switch restoreCR.Status.Phase {
	case v1alpha1br.RestoreFailed, v1alpha1br.RestoreInvalid:
		restoreOutput.Status = apiv1.ClassicRestore_FAILED
		restoreOutput.Message = "data restoring failed, current action is " + restore.GetLastAction()
		restoreOutput.Failure = restoreFailureOutput(entity.RestoreFailureSourceKernel, "KERNEL_TASK_FAILED", restoreOutput.Message)
	case v1alpha1br.RestoreScheduled, v1alpha1br.RestoreRunning, v1alpha1br.RestoreRetryFailed, v1alpha1br.RestoreComplete:
		restoreOutput.Status = apiv1.ClassicRestore_DATA_RESTORING
		restoreOutput.Message = "data restoring, current action is " + restore.GetLastAction()
	default:
		restoreOutput.Status = apiv1.ClassicRestore_CLASSIC_RESTORE_STATUS_UNSPECIFIED
		restoreOutput.Message = "unknown phase for restore, should not happen"
	}
	return &GetClassicRestoreOutput{ClassicRestore: restoreOutput}, nil
}

/*
A typical restore cr status is like this:
  status:
    commitTs: "462164376207228929"
    conditions:
    - lastTransitionTime: "2025-11-13T10:07:24Z"
      message: ""
      reason: Scheduled
      status: "True"
      type: Scheduled
    - lastTransitionTime: "2025-11-13T10:07:25Z"
      message: ""
      reason: Running
      status: "True"
      type: Running
    - lastTransitionTime: "2025-11-13T10:07:39Z"
      message: ""
      reason: Complete
      status: "True"
      type: Complete
    phase: Complete
    progresses:
    - lastTransitionTime: "2025-11-13T10:07:30Z"
      progress: 100
      step: Split&Scatter Regions
    - lastTransitionTime: "2025-11-13T10:07:36Z"
      progress: 100
      step: Download&Ingest SST
    - lastTransitionTime: "2025-11-13T10:07:36Z"
      progress: 100
      step: Restore Pipeline
    timeCompleted: "2025-11-13T10:07:39Z"
    timeStarted: "2025-11-13T10:07:25Z"
    timeTaken: 13s
*/

func calRestoreProgress(restoreCR *v1alpha1br.Restore) *apiv1.ClassicRestore {
	restoreOutput := &apiv1.ClassicRestore{}
	restoreOutput.DataRestoreDurationSeconds = calDataRestoreDurationSeconds(restoreCR)
	restoreOutput.DataRestoreStep, restoreOutput.DataRestoreStepProgressPercentage = calDataRestoreStep(restoreCR)
	restoreOutput.DataRestoreTotalProgressPercentage = calDataRestoreTotalProgressPercentage(restoreCR)
	return restoreOutput
}

func calDataRestoreDurationSeconds(restoreCR *v1alpha1br.Restore) uint64 {
	conditions := restoreCR.Status.Conditions
	if len(conditions) == 0 {
		return 0
	}

	now := time.Now()
	duration := now.Sub(conditions[0].LastTransitionTime.Time)
	return uint64(duration.Seconds())
}

func calDataRestoreStep(restoreCR *v1alpha1br.Restore) (step string, progressPercentage uint32) {
	progresses := restoreCR.Status.Progresses
	if len(progresses) == 0 {
		return "", 0
	}

	lastProgress := progresses[len(progresses)-1]
	return lastProgress.Step, uint32(lastProgress.Progress)
}

func calDataRestoreTotalProgressPercentage(restoreCR *v1alpha1br.Restore) uint32 {
	progresses := restoreCR.Status.Progresses
	if len(progresses) == 0 {
		return 0
	}

	if len(progresses) == 1 {
		return uint32(math.Ceil(float64(progresses[0].Progress) / 10))
	}

	if len(progresses) == 2 {
		return 10 + uint32(math.Ceil(float64(progresses[1].Progress)/10))
	}

	return 20 + uint32(math.Ceil(float64(progresses[2].Progress)*8/10))
}
