package br_svc

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/console"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/service"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	"github.com/tidbcloud/serverless-service/internal/errorcode"
	"github.com/tidbcloud/serverless-service/internal/utils"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

var (
	consoleTracer                     = otel.Tracer("br.console")
	ListBackupsOrderByField           = []string{"backup_time"}
	ListRecycledInstancesOrderByField = []string{"delete_time"}
)

type backupRestoreConsoleService struct {
	br service.BackupRestoreService
	console.UnsafeBackupRestoreServiceServer
}

func NewBackupRestoreConsoleService(br service.BackupRestoreService) console.BackupRestoreServiceServer {
	return &backupRestoreConsoleService{
		br: br,
	}
}

func (b *backupRestoreConsoleService) ListBackups(
	ctx context.Context,
	request *console.ListBackupsRequest,
) (_ *console.ListBackupsResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.ListBackups")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
		attribute.String("PageSize", fmt.Sprint(request.PageSize)),
		attribute.String("PageToken", request.PageToken),
	)

	pager := utils.NewPagerNumberPager()
	err = pager.Normalize(request)
	if err != nil {
		return nil, err
	}

	// check orderBy
	err = utils.CheckOrderBy(request, ListBackupsOrderByField)
	if err != nil {
		return nil, bizerr.New(
			errorcode.ErrCodeInvalidOrderby,
			"%s Error: %s",
			errorcode.BackupErrorMessages[errorcode.ErrCodeInvalidOrderby],
			err.Error(),
		)
	}
	// list backups
	backupVos, total, err := b.br.ListBackups(ctx, &vo.ListUserBackupsOpt{
		ClusterId: request.ClusterId,
		TenantId:  request.OrgId,
		PageSize:  int32(pager.PageSize()),
		Offset:    int64(pager.Offset()),
		OrderBy:   request.OrderBy,
	})
	if err != nil {
		logger.Error("list backups failed", zap.Error(err))
		return nil, err
	}
	// build response
	backups := make([]*console.Backup, 0, len(backupVos))
	for _, backupVo := range backupVos {
		backups = append(backups, backupVo.ConvertToConsole())
	}
	nextPageToken := pager.NextPageToken(total)
	return &console.ListBackupsResponse{
		Backups:       backups,
		NextPageToken: nextPageToken,
		TotalSize:     total,
	}, nil
}

func (b *backupRestoreConsoleService) GetBackup(
	ctx context.Context,
	request *console.GetBackupRequest,
) (_ *console.Backup, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.GetBackup")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
		attribute.String("backupId", request.BackupId),
	)

	backupVo, err := b.br.GetBackup(ctx, request.OrgId, request.ClusterId, request.BackupId)
	if err != nil {
		logger.Error("get backup failed", zap.Error(err))
		return nil, err
	}

	return backupVo.ConvertToConsole(), nil
}

func (b *backupRestoreConsoleService) ListRecycledInstances(
	ctx context.Context,
	request *console.ListRecycledInstancesRequest,
) (_ *console.ListRecycledInstancesResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.ListRecycledInstances")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("orgId", request.OrgId),
		attribute.String("PageSize", fmt.Sprint(request.PageSize)),
		attribute.String("PageToken", request.PageToken),
		attribute.String("RegionIds", strings.Join(request.RegionIds, ",")),
		attribute.String("ProjectIds", strings.Join(request.ProjectIds, ",")),
	)

	pager := utils.NewPagerNumberPager()
	err = pager.Normalize(request)
	if err != nil {
		return nil, err
	}

	err = utils.CheckOrderBy(request, ListRecycledInstancesOrderByField)
	if err != nil {
		return nil, bizerr.New(
			errorcode.ErrCodeInvalidOrderby,
			"%s Error: %s",
			errorcode.BackupErrorMessages[errorcode.ErrCodeInvalidOrderby],
			err.Error(),
		)
	}

	var startTime *time.Time
	if request.StartTime != nil {
		t := request.StartTime.AsTime()
		startTime = &t
	}
	var endTime *time.Time
	if request.EndTime != nil {
		t := request.EndTime.AsTime()
		endTime = &t
	}

	recycledInstances, total, err := b.br.ListRecycledInstances(ctx, &vo.ListRecycledInstancesOpt{
		TenantId:   request.OrgId,
		PageSize:   int32(pager.PageSize()),
		Offset:     int64(pager.Offset()),
		OrderBy:    convertRecycledInstanceOrderBy(request.OrderBy),
		RegionIds:  request.RegionIds,
		ProjectIds: request.ProjectIds,
		StartTime:  startTime,
		EndTime:    endTime,
	})
	if err != nil {
		logger.Error("list recycled instances failed", zap.Error(err))
		return nil, err
	}

	items := make([]*console.RecycledInstance, 0, len(recycledInstances))
	for _, item := range recycledInstances {
		items = append(items, item.ConvertToConsole())
	}
	nextPageToken := pager.NextPageToken(total)
	return &console.ListRecycledInstancesResponse{
		Items:         items,
		NextPageToken: nextPageToken,
		TotalSize:     total,
	}, nil
}

func (b *backupRestoreConsoleService) GetRecycledInstance(
	ctx context.Context,
	request *console.GetRecycledInstanceRequest,
) (_ *console.RecycledInstance, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.GetRecycledInstance")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("orgId", request.OrgId),
		attribute.String("clusterId", request.ClusterId),
	)

	item, err := b.br.GetRecycledInstance(ctx, request.OrgId, request.ClusterId)
	if err != nil {
		logger.Error("get recycled instance failed", zap.Error(err))
		return nil, err
	}
	return item.ConvertToConsole(), nil
}

func (b *backupRestoreConsoleService) ListRecycledInstanceBackups(
	ctx context.Context,
	request *console.ListRecycledInstanceBackupsRequest,
) (_ *console.ListRecycledInstanceBackupsResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.ListRecycledInstanceBackups")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("orgId", request.OrgId),
		attribute.String("clusterId", request.ClusterId),
		attribute.String("PageSize", fmt.Sprint(request.PageSize)),
		attribute.String("PageToken", request.PageToken),
	)

	pager := utils.NewPagerNumberPager()
	err = pager.Normalize(request)
	if err != nil {
		return nil, err
	}

	err = utils.CheckOrderBy(request, ListBackupsOrderByField)
	if err != nil {
		return nil, bizerr.New(
			errorcode.ErrCodeInvalidOrderby,
			"%s Error: %s",
			errorcode.BackupErrorMessages[errorcode.ErrCodeInvalidOrderby],
			err.Error(),
		)
	}

	backupVos, total, err := b.br.ListRecycledInstanceBackups(ctx, &vo.ListUserBackupsOpt{
		TenantId:  request.OrgId,
		ClusterId: request.ClusterId,
		PageSize:  int32(pager.PageSize()),
		Offset:    int64(pager.Offset()),
		OrderBy:   request.OrderBy,
	})
	if err != nil {
		logger.Error("list recycled instance backups failed", zap.Error(err))
		return nil, err
	}

	backups := make([]*console.Backup, 0, len(backupVos))
	for _, backupVo := range backupVos {
		backups = append(backups, backupVo.ConvertToConsole())
	}
	nextPageToken := pager.NextPageToken(total)
	return &console.ListRecycledInstanceBackupsResponse{
		Backups:       backups,
		NextPageToken: nextPageToken,
		TotalSize:     total,
	}, nil
}

func (b *backupRestoreConsoleService) DeleteRecycledInstanceBackup(
	ctx context.Context,
	request *console.DeleteRecycledInstanceBackupRequest,
) (resp *console.DeleteRecycledInstanceBackupResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.DeleteRecycledInstanceBackup")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
		go recordDeleteRecycledInstanceBackupAuditLog(ctx, request, err)
	}()
	span.SetAttributes(
		attribute.String("orgId", request.OrgId),
		attribute.String("clusterId", request.ClusterId),
		attribute.String("backupId", request.BackupId),
	)

	if err := b.br.DeleteRecycledInstanceBackup(ctx, request.OrgId, request.ClusterId, request.BackupId); err != nil {
		logger.Error("delete recycled instance backup failed", zap.Error(err))
		return nil, err
	}
	return &console.DeleteRecycledInstanceBackupResponse{}, nil
}

func convertRecycledInstanceOrderBy(orderBy string) string {
	if orderBy == "" {
		return ""
	}
	return strings.ReplaceAll(orderBy, "delete_time", "deleted_at")
}

func (b *backupRestoreConsoleService) Restore(
	ctx context.Context,
	request *console.RestoreRequest,
) (resp *console.RestoreResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.Restore")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
		go b.recordRestoreAuditLog(ctx, request, resp, err)
	}()

	restoreVo, err := buildRestore(request)
	if err != nil {
		logger.Error("build restore failed", zap.Error(err))
		return nil, err
	}
	clusterId, err := b.br.Restore(ctx, restoreVo)
	if err != nil {
		logger.Error("restore failed", zap.Error(err))
		return nil, err
	}
	return &console.RestoreResponse{ClusterId: clusterId}, nil
}

func buildRestore(request *console.RestoreRequest) (*vo.Restore, error) {
	if request == nil {
		return nil, bizerr.New(errorcode.ErrCodeEmptyRestoreRequest, "")
	}
	restore := &vo.Restore{
		TenantId:  request.OrgId,
		ProjectId: &request.ProjectId,
	}
	switch request.GetSource().(type) {
	case *console.RestoreRequest_BackupId:
		backupId := request.GetBackupId()
		if backupId == "" {
			return nil, bizerr.New(errorcode.ErrCodeEmptyBackupID, "")
		}
		restore.BackupId = &backupId
	case *console.RestoreRequest_PointInTime:
		pitr := request.GetPointInTime()
		if pitr == nil || pitr.ClusterId == "" {
			return nil, bizerr.New(errorcode.ErrCodeEmptyClusterIDInPITR, "")
		}
		if pitr.BackupTime == nil {
			return nil, bizerr.New(errorcode.ErrCodeEmptyBackupTimeInPITR, "")
		}
		_, err := strconv.ParseUint(pitr.ClusterId, 10, 64)
		if err != nil {
			return nil, bizerr.New(errorcode.ErrCodeInvalidClusterID, "")
		}
		restore.PointInTime = &vo.PointInTime{
			BackupTime: pitr.BackupTime.AsTime(),
			ClusterId:  pitr.ClusterId,
		}
	default:
		return nil, bizerr.New(errorcode.ErrCodeEmptyRestoreRequest, "")
	}

	switch request.GetType() {
	case console.RestoreType_INPLACE:
		restore.RestoreType = constant.RestoreTypeInPlace
	case console.RestoreType_NEW:
		restore.RestoreType = constant.RestoreTypeNew
		restore.RestoreCluster = &vo.RestoreCluster{}
		if request.Cluster == nil || request.Cluster.DisplayName == "" {
			return nil, bizerr.New(errorcode.ErrCodeRestoreToNewWithoutClusterInfo, "")
		}
		restore.RestoreCluster.DisplayName = request.Cluster.DisplayName
		if request.Cluster.SpendingLimit != nil {
			if request.Cluster.SpendingLimit.Monthly < 0 {
				return nil, bizerr.New(bizerr.ErrCodeParams, "Invalid Spending Limit")
			}
			restore.RestoreCluster.SpendingLimitMonthly = request.Cluster.SpendingLimit.Monthly
		}
		if request.Cluster.AutoScaling != nil {
			restore.RestoreCluster.AutoScaling = &vo.AutoScaling{
				MinRcu: request.Cluster.AutoScaling.MinRcu,
				MaxRcu: request.Cluster.AutoScaling.MaxRcu,
			}
		}
		if request.Cluster.HighAvailabilityType == console.RestoreClusterHighAvailabilityType_REGIONAL {
			restore.RestoreCluster.ZoneRedundantEnabled = true
		}
	default:
		return nil, bizerr.New(errorcode.ErrCodeUnsupportedRestoreType, "")
	}
	return restore, nil
}

func (b *backupRestoreConsoleService) recordRestoreAuditLog(
	ctx context.Context,
	req *console.RestoreRequest,
	resp *console.RestoreResponse,
	err error,
) {
	logger := log.WithContextL(ctx)
	if !config.Global().ServerConfig.EnableAuditLog {
		logger.Info("audit log is disabled")
		return
	}
	// build audit log detail
	auditlogDetail := RestoreAuditLogDetail{}
	if err != nil {
		be, ok := err.(*bizerr.BizErr)
		if ok {
			auditlogDetail.Error = be.Message
		} else {
			auditlogDetail.Error = "internal error"
		}
	}
	if req.GetBackupId() != "" {
		auditlogDetail.RestoreMode = &RestoreMode{
			Snapshot: &Snapshot{
				BackupID: req.GetBackupId(),
			},
		}
	} else if req.GetPointInTime() != nil {
		auditlogDetail.RestoreMode = &RestoreMode{
			PointInTime: &PointInTime{
				ClusterID:  req.GetPointInTime().ClusterId,
				BackupTime: req.GetPointInTime().BackupTime.AsTime(),
			},
		}
	}
	if resp != nil && resp.ClusterId != "" {
		auditlogDetail.TargetClusterID = resp.ClusterId
	}
	details, err := ConvertToAuditLogDetail(auditlogDetail)
	if err != nil {
		logger.Error("failed to build audit log detail", zap.Error(err))
		return
	}
	eventType := auditlog.EventRecoverFromBackup
	if req.GetPointInTime() != nil {
		eventType = auditlog.EventRestoreFromPITR
	}
	// build audit log event
	orgId, err := strconv.ParseUint(req.OrgId, 10, 64)
	if err != nil {
		logger.Error("failed to parse org id", zap.Error(err))
		return
	}
	projectId, err := strconv.ParseUint(req.ProjectId, 10, 64)
	if err != nil {
		logger.Error("failed to parse project id", zap.Error(err))
		return
	}
	event := auditlog.NewEvent(ctx, eventType).
		SetResult(auditlogDetail.Error == "").
		SetOrgID(orgId).
		SetProjectID(projectId).
		SetDetails(details.Build())
	clusterID := ""
	if req.GetPointInTime() != nil {
		clusterID = req.GetPointInTime().GetClusterId()
	} else if resp != nil && resp.ClusterId != "" {
		clusterID = resp.ClusterId
	}
	if clusterID != "" {
		if clusterIDUint64, e := strconv.ParseUint(clusterID, 10, 64); e == nil {
			event.SetClusterID(clusterIDUint64)
		} else {
			logger.Error("failed to parse cluster id", zap.Error(e))
		}
	}
	// submit audit log
	err = event.Submit()
	if err != nil {
		logger.Error("failed to submit audit log", zap.Error(err))
		return
	}
}

func (b *backupRestoreConsoleService) GetBackupRetention(
	ctx context.Context,
	request *console.GetBackupRetentionReq,
) (_ *console.GetBackupRetentionResp, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.GetBackupRetention")
	logger := log.WithContextL(ctx)
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)

	backupRetention, err := b.br.GetBackupRetention(ctx, request.OrgId, request.ClusterId)
	if err != nil {
		logger.Error("get backup retention failed", zap.Error(err))
		return nil, err
	}

	return &console.GetBackupRetentionResp{
		RetentionHours: int32(backupRetention.Hours()),
	}, nil
}

func (b *backupRestoreConsoleService) GetRestoreProgress(
	ctx context.Context,
	request *console.GetRestoreProgressRequest,
) (_ *console.GetRestoreProgressResponse, err error) {
	ctx, span := consoleTracer.Start(ctx, "backupRestoreConsoleService.GetRestoreProgress")
	defer func() {
		tracing.EndSpan(span, err)
		err = errorcode.PrepareBackupErrorMessage(ctx, err)
	}()
	span.SetAttributes(
		attribute.String("clusterId", request.ClusterId),
	)
	if request.ClusterId == "" {
		return nil, bizerr.New(errorcode.ErrCodeEmptyClusterID, "")
	}
	logger := log.WithContextL(ctx).With(zap.String("clusterId", request.ClusterId))

	restoreVos, _, err := b.br.ListRestores(ctx, &vo.ListRestoresOpt{
		ClusterId: request.ClusterId,
		TenantId:  request.OrgId,
		PageSize:  1,
		Offset:    0,
		OrderBy:   "created_at desc",
	})
	if err != nil {
		logger.Error("list restores failed", zap.Error(err))
		return nil, err
	}
	if len(restoreVos) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "no restore job found")
	}
	logger.Info("list restores success")
	restoreVo := restoreVos[0]
	resp := &console.GetRestoreProgressResponse{
		Percent:   float32(restoreVo.Percent),
		SizeBytes: float64(restoreVo.SizeBytes),
		State:     restoreVo.GetConsoleState(),
	}
	return resp, nil
}
