package rbac

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type ResourceRelationSvcIface interface {
	GetResourceRelations(ctx context.Context, resourceType entity.ScopeType, resourceId uint64) ([]entity.ResourceRelation, error)
	UpsertClusterRelations(ctx context.Context, req *OperClusterRelationReq) error
	DeleteClusterRelations(ctx context.Context, clusterId uint64) error
}

type OperClusterRelationReq struct {
	ClusterId uint64
	OrgId     uint64
	ProjectId uint64
}

var (
	resourceRelationSvcInitOnce sync.Once
	resourceRelationSvc         ResourceRelationSvcIface
)

const (
	maxRetryCount     = 5
	retryIntervalBase = 10 * time.Millisecond
)

func isDeadlockError(err error) bool {
	if err == nil {
		return false
	}
	errStr := fmt.Sprintf("%+v", err)
	return strings.Contains(errStr, "1213") ||
		strings.Contains(errStr, "Deadlock") ||
		strings.Contains(errStr, "deadlock")
}

type ResourceRelationSvc struct {
}

func GetResourceRelationSvc() ResourceRelationSvcIface {
	resourceRelationSvcInitOnce.Do(func() {
		resourceRelationSvc = &ResourceRelationSvc{}
	})
	return resourceRelationSvc
}

func (s *ResourceRelationSvc) GetResourceRelations(ctx context.Context, resourceType entity.ScopeType, resourceId uint64) ([]entity.ResourceRelation, error) {
	// add cache later
	return dao.GetDaoHub().ResourceRelation.GetResourceRelationsByResource(ctx, database.DBConn.DB, string(resourceType), resourceId)
}

func (s *ResourceRelationSvc) UpsertClusterRelations(ctx context.Context, req *OperClusterRelationReq) error {
	if req.ClusterId == 0 {
		return errs.NewBadRequest("cluster_id is invalid", nil)
	}

	var lastErr error
	for retryCount := 0; retryCount < maxRetryCount; retryCount++ {
		if retryCount > 0 {
			time.Sleep(retryIntervalBase * time.Duration(retryCount))
			tilog.Warn(ctx, "UpsertClusterRelations retry",
				zap.Int("retry_count", retryCount),
				zap.Any("req", req),
				zap.Any("last_err", lastErr))
		}

		relations := make([]*entity.ResourceRelation, 0, 2)

		if req.OrgId != 0 {
			relations = append(relations, &entity.ResourceRelation{
				ResourceType: entity.RoleBindingScopeTiDB.String(),
				ResourceID:   req.ClusterId,
				ParentType:   entity.RoleBindingScopeOrg.String(),
				ParentID:     req.OrgId,
			})
		}

		if req.ProjectId != 0 {
			relations = append(relations, &entity.ResourceRelation{
				ResourceType: entity.RoleBindingScopeTiDB.String(),
				ResourceID:   req.ClusterId,
				ParentType:   entity.RoleBindingScopeProject.String(),
				ParentID:     req.ProjectId,
			})
		}

		if len(relations) == 0 {
			return nil
		}

		sort.Slice(relations, func(i, j int) bool {
			return relations[i].ParentType < relations[j].ParentType
		})

		err := dao.GetDaoHub().ResourceRelation.BatchUpsertResourceRelations(ctx, database.DBConn.DB, relations)
		if err != nil {
			err = errs.NewInternalUnknown("batch upsert cluster relations error", err)
		}

		if err == nil {
			for _, relation := range relations {
				tilog.Info(ctx, "upsert cluster relation success", zap.Any("relation", relation))
			}
			return nil
		}

		lastErr = err
		if !isDeadlockError(err) {
			tilog.Error(ctx, "UpsertClusterRelations non-deadlock error", zap.Any("req", req), zap.Any("err", err))
			return err
		}
	}

	tilog.Error(ctx, "UpsertClusterRelations error after retries",
		zap.Any("req", req),
		zap.Any("err", lastErr),
		zap.Int("retry_count", maxRetryCount))
	return lastErr
}

func (s *ResourceRelationSvc) DeleteClusterRelations(ctx context.Context, clusterId uint64) error {
	if clusterId == 0 {
		return errs.NewBadRequest("cluster_id is invalid", nil)
	}

	err := database.DBConn.DB.Transaction(func(tx *gorm.DB) error {
		err := dao.GetDaoHub().ResourceRelation.DeleteResourceRelationsByResource(ctx, tx, entity.RoleBindingScopeTiDB.String(), []uint64{clusterId})
		if err != nil {
			return errs.NewInternalUnknown("delete cluster relations error", err)
		}
		tilog.Info(ctx, "delete cluster relation success", zap.Any("cluster_id", clusterId))
		_, err = dao.GetDaoHub().Role.DeleteRoleBindingByScopeInfo(ctx, tx, entity.RoleBindingScopeTiDB, clusterId, "")
		if err != nil {
			return errs.NewInternalUnknown("delete role bindings error", err)
		}
		tilog.Info(ctx, "delete cluster role binding success", zap.Any("cluster_id", clusterId))
		return nil
	})

	if err != nil {
		tilog.Error(ctx, "DeleteClusterRelations error", zap.Any("cluster_id", clusterId), zap.Any("err", err))
	}
	return err
}
