package permission

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/tidbcloud/account/internel/svc/rbac"

	"github.com/tidbcloud/account/commons/metrics"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"gorm.io/gorm"
)

func isImpersonatedUser(ctx context.Context, userID uint64, logpre string) (uint64, bool, error) {
	ts := time.Now()
	defer metrics.RecPermissionCheckMetrics(logpre, "is_impersonated_user", ts, false)

	impersonatedUser, isImpersonated, err := dao.GetDaoHub().User.GetImpersonatedUser(ctx, database.DBConn.DB, userID)
	tilog.Info(ctx, fmt.Sprintf("%s: get impersonated user result", logpre), zap.Any("impersonatedUser", impersonatedUser), zap.Any("isImpersonated", isImpersonated), zap.Any("err", err))
	if err != nil {
		return 0, false, err
	}
	if isImpersonated {
		return impersonatedUser.TenantID.Uint64(), true, nil
	}
	return 0, isImpersonated, nil
}

func isScopeResourceMatch(ctx context.Context, scopeMap map[entity.ScopeType]entity.ID) (bool, string, error) {
	if len(scopeMap) < 2 {
		return true, "", nil
	}

	_, eProjectAccessScope := scopeMap[entity.RoleBindingScopeProject]

	for curScope, curID := range scopeMap {
		group, ok := scopeMatchGroup[curScope]
		if !ok {
			continue
		}
		for pScope, f := range group {
			//orgs/org_id/projects/project_id/tidbs/tidb_id
			if curScope == entity.RoleBindingScopeTiDB && pScope == entity.RoleBindingScopeOrg && eProjectAccessScope {
				continue
			}
			pID, ok := scopeMap[pScope]
			if !ok {
				continue
			}
			matched, msg, err := f(ctx, curID.Uint64(), pID.Uint64())
			if err != nil {
				return false, msg, err
			}
			if !matched {
				return false, msg, nil
			}
		}
	}
	return true, "", nil
}

var scopeMatchGroup = map[entity.ScopeType]map[entity.ScopeType]scopeMatchFunc{
	entity.RoleBindingScopeProject: {
		entity.RoleBindingScopeOrg: checkProjectBelongToOrg,
	},
	entity.RoleBindingScopeTiDB: {
		entity.RoleBindingScopeOrg:     checkTiDBBelongToOrg,
		entity.RoleBindingScopeProject: checkTiDBBelongToProject,
	},
}

type scopeMatchFunc func(ctx context.Context, curID, parentID uint64) (bool, string, error)

func checkProjectBelongToOrg(ctx context.Context, projectID, orgID uint64) (bool, string, error) {
	tilog.Info(ctx, "check project belong to org", zap.Any("project_id", projectID), zap.Any("org_id", orgID))
	project, err := dao.GetDaoHub().Project.GetProjectByID(ctx, database.DBConn.DB, projectID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, "not found project entity", nil
		}
		return false, "get project entity error", err
	}
	if project == nil {
		return false, "not found project entity", nil
	}
	if project.Status != entity.ProjectStatusActive {
		return false, "project is not active", nil
	}
	if project.TenantID.Uint64() == orgID {
		return true, "", nil
	}
	return false, "project relate org is not the access org", nil
}

func checkTiDBBelongToOrg(ctx context.Context, clusterID, orgID uint64) (bool, string, error) {
	tilog.Info(ctx, "check tidb belong to org", zap.Any("cluster_id", clusterID), zap.Any("org_id", orgID))
	resRefs, err := rbac.GetResourceRelationSvc().GetResourceRelations(ctx, entity.RoleBindingScopeTiDB, clusterID)
	if err != nil {
		return false, "get resource relation error", err
	}
	for _, ref := range resRefs {
		if ref.ParentType == entity.RoleBindingScopeOrg.String() && ref.ParentID == orgID {
			return true, "", nil
		}
	}
	return false, "not found tidb org relation", nil
}

func checkTiDBBelongToProject(ctx context.Context, clusterID, projectID uint64) (bool, string, error) {
	tilog.Info(ctx, "check tidb belong to project", zap.Any("cluster_id", clusterID), zap.Any("project_id", projectID))
	resRefs, err := rbac.GetResourceRelationSvc().GetResourceRelations(ctx, entity.RoleBindingScopeTiDB, clusterID)
	if err != nil {
		return false, "get resource relation error", err
	}
	for _, ref := range resRefs {
		if ref.ParentType == entity.RoleBindingScopeProject.String() && ref.ParentID == projectID {
			return true, "", nil
		}
	}
	return false, "not found tidb project relation", nil
}
