package backup

import (
	"context"
	"fmt"
	"time"

	"github.com/gogo/protobuf/types"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/grpc/grpcli"
	"github.com/pingcap/dbaas/central/model/backup"
	"github.com/pingcap/dbaas/central/model/util"
	"github.com/pingcap/dbaas/central/service"
	"github.com/pingcap/dbaas/central/service/cvt"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/idl/pbgen/proto/central"
	"github.com/tidbcloud/idl/pbgen/proto/core"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/tracing"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
)

var Tracer = otel.Tracer("central/service/backup")

type backupService struct {
	central.UnimplementedBackupServiceServer
	service.OperationsService
	backupModel backup.Model
}

var _ central.BackupServiceServer = (*backupService)(nil)

// NewService constructs a backupService
func NewService(s service.OperationsService, model backup.Model) central.BackupServiceServer {
	return &backupService{OperationsService: s, backupModel: model}
}

func (s *backupService) userBackup(ctx context.Context) backup.UserContext {
	user := s.CurrentUser(ctx)
	return s.backupModel.User(ctx, user)
}

func (s *backupService) Log(ctx context.Context) *zap.Logger {
	return s.userBackup(ctx).Log()
}

func (s *backupService) List(ctx context.Context, req *central.BackupListReq) (_ *central.Backups, err error) {
	ctx, span := Tracer.Start(ctx, "backupService.List")
	defer tracing.EndSpan(span, err)
	logger := s.Log(ctx)
	context := s.userBackup(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	err = context.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	rdb := db.NewProvider().ReadDB(ctx)
	cluster, err := dbutil.GetClusterByID(rdb, orgID, projectID, entity.ID(req.ClusterId))
	if err != nil {
		return nil, err
	}
	if cluster.ProjectID != projectID {
		return nil, errcode.NewInvalidInputErr(fmt.Errorf("cluster %d not in project %d", req.ClusterId, req.ProjectId))
	}

	listResp, err := grpcli.ListBackupsOfCluster(ctx, req.ClusterId)
	if err != nil {
		return nil, err
	}
	items := make([]*central.Backup, len(listResp.Backups))
	_, chileSpan := Tracer.Start(ctx, "List.parseBackup")
	for i, each := range listResp.Backups {
		item := &central.Backup{
			Id: each.Id,
			Cluster: &central.BackupCluster{
				ClusterId:         each.ClusterId,
				ClusterComponents: util.CoreClusterComponents2centralClusterComponents(each.Components),
			},
			Name:           each.Name,
			Description:    each.Description,
			CreatedAt:      nil, // fill later
			Size:           uint64(each.SizeBytes),
			Status:         cvt.CoreBackupStatus2Central(each.Status),
			Type:           cvt.CoreBackupType2Central(each.Type),
			ExpirationTime: nil, // fill later
		}

		createdAt, err := types.TimestampProto(time.Unix(each.CreatedAt, 0))
		if err != nil {
			logger.Error("failed to parse backup's CreatedAt", zap.Uint64("clusterID", req.ClusterId), zap.Error(err))
		} else {
			item.CreatedAt = createdAt
		}
		if each.ExpirationTime != nil {
			expirationTime, err := types.TimestampProto(time.Unix(*each.ExpirationTime, 0))
			if err != nil {
				logger.Error("failed to parse backup's ExpirationTime", zap.Uint64("clusterID", req.ClusterId), zap.Error(err))
			} else {
				item.ExpirationTime = expirationTime
			}
		}

		items[i] = item
	}
	chileSpan.End()

	return &central.Backups{
		Items: service.Pagination(req.Page, req.PerPage, items).([]*central.Backup),
		Total: uint32(len(items))}, nil
}

// CreateBackupSchedule creates a schedule for manual backup or auto backup setting
func (s *backupService) CreateBackupSchedule(ctx context.Context, req *central.BackupCreateReq) (_ *central.BackupScheduleId, err error) {
	ctx, span := Tracer.Start(ctx, "backupService.CreateBackupSchedule")
	defer func() { tracing.EndSpan(span, err) }()
	logger := log.WithContextL(ctx)
	context := s.userBackup(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	err = context.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}
	span.AddEvent("CheckOrgIDAndProjectID")

	var (
		backup               backup.Backup
		createBackupRequired dbutil.CreateBackupRequired
	)

	switch req.BackupType {
	case central.BackupType_AUTO:
		createBackupRequired = dbutil.CreateBackupRequired{
			Description: "_",
			ClusterID:   entity.ID(req.ClusterId),
			MaxBackups:  req.MaxBackups,
			Schedule:    req.BackupTime,
			Method:      entity.BackupMethod_Full,
			Type:        entity.BackupType_Auto,
		}
		backup, err = context.UpsertAutoBackup(ctx, orgID, projectID, createBackupRequired)
		if err != nil {
			logger.Error("failed to upsert auto backup", zap.Uint64("clusterID", req.ClusterId), zap.Error(err))
			return nil, err
		}
	case central.BackupType_MANUAL:
		if err := validateManualBackupCreateReq(req); err != nil {
			return nil, errcode.NewInvalidInputErr(err)
		}
		createBackupRequired = dbutil.CreateBackupRequired{
			Name:        req.Name,
			Description: "_",
			ClusterID:   entity.ID(req.ClusterId),
			Method:      entity.BackupMethod_Full,
			Type:        entity.BackupType_Manual,
		}
		backup, err = context.CreateManualBackup(ctx, orgID, projectID, createBackupRequired)
		if err != nil {
			logger.Error("failed to create manual backup", zap.Uint64("clusterID", req.ClusterId), zap.Error(err))
			return nil, err
		}
	default:
		return nil, errcode.NewInvalidInputErr(errors.New("backup type not supported"))
	}
	span.AddEvent("CreateBackup")

	return &central.BackupScheduleId{Id: uint64(backup.ID)}, nil
}

func (s *backupService) GetAutoSetting(ctx context.Context, req *central.AutoBackupSettingReq) (*central.AutoBackupSetting, error) {
	logger := log.WithContextL(ctx)
	context := s.userBackup(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	err := context.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	clusterID := entity.ID(req.GetClusterId())
	setting, err := context.GetAutoSetting(orgID, projectID, clusterID)
	if err != nil {
		logger.Error("failed to get auto backup settings", zap.Uint64("clusterID", req.ClusterId), zap.Error(err))
		return setting, err
	}
	return setting, nil
}

// Delete marks a backup as deleted.
// A record of the backup will still exist.
func (s *backupService) Delete(ctx context.Context, req *central.BackupReq) (*types.Empty, error) {
	context := s.userBackup(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	clusterID := entity.ID(req.ClusterId)
	backupID := entity.ID(req.Id)
	err := context.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	err = context.Delete(orgID, projectID, clusterID, backupID)
	if err != nil {
		logger := s.Log(ctx)
		logger.Error("failed to delete backup of cluster", zap.Uint64("clusterID", req.ClusterId), zap.Uint64("backupID", req.Id), zap.Error(err))
		return &types.Empty{}, errcode.NewInternalErr(errors.New("failed to delete backup"))
	}
	return &types.Empty{}, nil
}

// Restore restores a backup to a new cluster
func (s *backupService) Restore(ctx context.Context, restoreReq *central.BackupRestoreReq) (_ *central.RestoreResp, err error) {
	ctx, span := Tracer.Start(ctx, "backupService.Restore")
	defer func() { tracing.EndSpan(span, err) }()
	logger := log.WithContextL(ctx)
	resp, err := s.restoreImpl(ctx, restoreReq)
	if err != nil {
		logger.Error("failed to restore", zap.Uint64("clusterID", restoreReq.ClusterId), zap.Uint64("backupID", restoreReq.Id), zap.Error(err))
		return resp, err
	}
	return resp, err
}

func (s *backupService) restoreImpl(ctx context.Context, restoreReq *central.BackupRestoreReq) (_ *central.RestoreResp, err error) {
	ctx, span := Tracer.Start(ctx, "backupService.restoreImpl")
	defer func() { tracing.EndSpan(span, err) }()
	if err := validateBackupRestoreReq(restoreReq); err != nil {
		return nil, errcode.NewInvalidInputErr(err)
	}
	context := s.userBackup(ctx)
	orgID := entity.ID(restoreReq.OrgId)
	projectID := entity.ID(restoreReq.ProjectId)
	clusterID := entity.ID(restoreReq.ClusterId)
	backupID := entity.ID(restoreReq.Id)
	err = context.CheckOrgIDAndProjectIDAndClusterIDAndBackupID(orgID, projectID, clusterID, backupID)
	if err != nil {
		return nil, err
	}

	coreReq := &core.RestoreClusterReq{
		BackupId:           restoreReq.Id,
		OperatorUserId:     s.CurrentUser(ctx).ID.Uint64(),
		ReplicationId:      nil,
		RegionName:         nil,
		ClusterName:        restoreReq.ClusterName,
		ClusterDescription: "",
		ClusterPassword:    restoreReq.Password,
		Components:         cvt.ClusterComponentsCentral2Core(restoreReq.Components),
		Port:               restoreReq.Port,
		BaseReq:            nil,
	}

	coreResp, err := grpcli.RestoreCluster(ctx, coreReq)
	if err != nil {
		return nil, err
	}

	return &central.RestoreResp{
		Id:        coreResp.RestoreId,
		ClusterId: coreResp.ClusterId,
	}, nil
}

func (s *backupService) DeleteDeletedClusterAndBackups(ctx context.Context, req *central.ClusterIdReq) (*types.Empty, error) {
	context := s.userBackup(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	clusterID := entity.ID(req.ClusterId)
	err := context.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	err = context.DeleteDeletedClusterAndBackups(orgID, projectID, clusterID)
	if err != nil {
		logger := s.Log(ctx)
		logger.Error("failed to delete deleted cluster and backups", zap.Uint64("clusterID", req.ClusterId), zap.Error(err))
		return &types.Empty{}, errcode.NewInternalErr(errors.New("failed to delete deleted cluster and backups"))
	}
	return &types.Empty{}, nil
}
