// Package backup encapsulates all backup functionality
package backup

import (
	"context"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	"github.com/tidbcloud/dataflow-service/internal/bizutils"
	"github.com/tidbcloud/dataflow-service/internal/db"
	"github.com/tidbcloud/dataflow-service/internal/db/dbutil"
	"github.com/tidbcloud/dataflow-service/internal/grpc/grpcli"
	"github.com/tidbcloud/dataflow-service/pkg/account/accountdb"
	"github.com/tidbcloud/pkgv2/log"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

//revive:enable:expd

// Model is created with NewModel
type Model struct {
	log *zap.Logger
}

var model *Model

// NewModel returns a Model
func NewModel(log *zap.Logger) Model {
	model = &Model{
		log: log,
	}
	return *model
}

// UserContext attaches a User to a Context.
// This helps audit the User
type UserContext struct {
	Model
	tenantAndUser *accountdb.User
	log           *zap.Logger
}

// BgContext is meant for the system worker/controller
type BgContext UserContext

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

func (m Model) Log() *zap.Logger {
	return m.log
}

// User returns a UserContext
func (m Model) User(ctx context.Context, user *accountdb.User) UserContext {
	return UserContext{
		Model:         m,
		tenantAndUser: user,
		log:           m.log.With(zap.Uint64("tenantID", user.TenantID), zap.Uint64("userID", user.ID), zap.String("module", "backup")).With(log.FieldsFromContext(ctx)...),
	}
}

// BgContext returns a BgContext
func (m Model) BgContext(tenantID entity.ID) (BgContext, error) {
	systemUser, err := accountdb.GetOrCreateSystemUser(tenantID.Uint64())
	if err != nil {
		return BgContext{}, err
	}

	return BgContext{
		m,
		&accountdb.User{User: *systemUser, TenantID: tenantID.Uint64()},
		m.log.With(zap.Uint64("tenantID", tenantID.Uint64()), zap.Uint64("userID", systemUser.ID), zap.String("module", "backup")),
	}, nil
}

func (m Model) GetRestoreByCluster(ctx context.Context, clusterID entity.ID) (*entity.Restore, error) {
	tx := db.GetDBWithContext(ctx)
	return dbutil.GetRestoreByClusterIDWithoutPreloadCluster(tx, clusterID)
}

func (c UserContext) Log() *zap.Logger {
	return c.log
}

func (c UserContext) CheckOrgIDAndProjectID(orgID, projectID entity.ID) error {
	user := c.tenantAndUser
	if user.IsImpersonated {
		return nil
	}

	if orgID.Uint64() != user.TenantID {
		return errcode.NewForbiddenErr(errors.New("org_id parameter is inconsistent with the user's org ID"))
	}

	return dbutil.CheckProjectID(user.Projects, projectID)
}

func (c BgContext) Log() *zap.Logger {
	return c.log
}

// GetLogBackupsClusters gets all normal clusters with log backups enabled.
func (m Model) GetLogBackupsClusters(ctx context.Context, tx *gorm.DB) ([]*core.Cluster, error) {
	backups, err := dbutil.ListActiveLogBackups(tx)
	if err != nil {
		return nil, errors.Trace(err)
	}
	if len(backups) == 0 {
		return nil, nil
	}
	clusters := make([]*core.Cluster, 0, len(backups))
	clusterIDs := make([]uint64, 0, len(backups))
	for _, b := range backups {
		clusterIDs = append(clusterIDs, b.ClusterID.Uint64())
	}

	clusterResp, err := grpcli.ListClustersById(ctx, clusterIDs)
	if err != nil {
		return nil, errors.Annotatef(err, "get cluster %v error", clusterIDs)
	}
	for _, c := range clusterResp.Clusters {
		if !bizutils.ClusterDeletedOrCleared(c) {
			clusters = append(clusters, c)
		}
	}
	return clusters, nil
}
