package permission

import (
	"bytes"
	"context"
	"errors"
	"fmt"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/rbac"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/account/thirdparty/authserver"
	"github.com/tidbcloud/auth-server/idl/pbgen/proto/provider"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

func (r *RbacPermissionSvc) HasPermissionForAccessToken(ctx context.Context, accessToken, reqUri, resourceUri string) (*PermissionCheckRet, error) {
	bm := utils.Begin(ctx, "HasPermissionForAccessToken")
	defer bm.OnExit(ctx)
	logger := tilog.WithContextL(ctx)
	logger.Info("HasPermissionForAccessToken: start permission check", zap.Any("accessToken", accessToken), zap.Any("reqUri", reqUri), zap.Any("resourceUri", resourceUri))
	if accessToken == "" || reqUri == "" || resourceUri == "" {
		return nil, errors.New("param invalid")
	}
	asRsp, err := authserver.GetIdentityByAccessToken(ctx, accessToken)
	if err != nil {
		logger.Error("HasPermissionForAccessToken: GetIdentityByAccessToken error", zap.Any("accessToken", accessToken), zap.Any("err", err))
		return nil, err
	}
	logger.Info("HasPermissionForAccessToken: get access token info", zap.Any("accessToken", accessToken), zap.Any("asRsp", asRsp))
	if asRsp.BaseResp != nil && asRsp.BaseResp.ErrCode != 0 {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       "access_token invalid",
		}
		logger.Info("HasPermissionForAccessToken not pass", zap.Any("accessToken", accessToken), zap.Any("resourceUri", resourceUri), zap.Any("reqUri", reqUri), zap.Any("ret", ret))
		return ret, nil
	}
	tokenRsp := asRsp.AccessTokenIdentity

	// get scopeType and scope_id in reqUri,   reqResource={"ORG":1369847559691517271,"PROJECT\:1369847559691517304}
	reqResource, err := parseRbacUri(reqUri, ScopeTypeMapping)
	if err != nil {
		logger.Error("HasPermissionForAccessToken: parseRbacUri error", zap.Any("reqUri", reqUri), zap.Any("err", err))
		return nil, err
	}
	logger.Info("HasPermissionForAccessToken: parse req resource", zap.Any("reqResource", reqResource))
	var assignedResource *AccessTokenRelateResource
	// grant role id
	var rbacRoleIds []entity.ID
	// grant role name
	var rbacRoleNames []string

	if tokenRsp.GrantType == provider.GrantType_DeviceCode {
		// device code:   get grant org and projectIds  from  auth
		assignedResource, err = r.getDeviceCodeAccessTokenAssignedResource(ctx, tokenRsp)

	} else if tokenRsp.GrantType == provider.GrantType_AuthorizationCode {

		// authorization code:  get grant org and projectIds  from  roleBindings
		assignedResource, err = r.getAuthorizationCodeAccessTokenAssignedResource(ctx, tokenRsp.Id, reqResource)
		rbacRoleIds = assignedResource.RoleIds
		rbacRoleNames = assignedResource.RoleNames
	}
	if err != nil {
		logger.Error("HasPermissionForAccessToken: getAccessTokenRefResource error", zap.Any("accessToken", accessToken), zap.Any("tokenRsp", tokenRsp), zap.Any("err", err))
		return nil, err
	}
	logger.Info("HasPermissionForAccessToken: get access token assigned resource", zap.Any("assignedResource", assignedResource))
	if assignedResource.OrgId == 0 && len(assignedResource.ProjectIds) == 0 {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       fmt.Sprintf("access token relate user %d do not have resource", tokenRsp.UserId),
		}
		logger.Info("HasPermissionForAccessToken not pass", zap.Any("accessToken", accessToken), zap.Any("resourceUri", resourceUri), zap.Any("reqUri", reqUri), zap.Any("ret", ret))
		return ret, nil
	}

	//  the scope_id in reqUri  is in assignedResource
	toCheckResource := getAccessTokenPermissionCheckResource(ctx, assignedResource, reqResource)
	logger.Info("HasPermissionForAccessToken: getAccessTokenPermissionCheckResource", zap.Any("toCheckResource", toCheckResource))

	if tokenRsp.GrantType == provider.GrantType_DeviceCode {
		// find  user's role in the scope (org， project)  which need to check, query user's role  scope (org， project) from roleBindings
		rbacRoleIds, rbacRoleNames, err = r.getAccessTokenPermissionCheckRoles(ctx, tokenRsp.UserId, toCheckResource)
		if err != nil {
			logger.Error("HasPermissionForAccessToken: getAccessTokenPermissionCheckRoles error", zap.Any("resourceUri", resourceUri), zap.Any("err", err))
			return nil, err
		}
	}
	logger.Info("HasPermissionForAccessToken: getAccessTokenPermissionCheckRoles result", zap.Any("accessToken", accessToken), zap.Any("rbacRoleIds", rbacRoleIds), zap.Any("rbacRoleNames", rbacRoleNames))
	if len(rbacRoleIds) == 0 {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       "access token's role is null",
		}
		logger.Info("HasPermissionForAccessToken not pass", zap.Any("accessToken", accessToken), zap.Any("resourceUri", resourceUri), zap.Any("reqUri", reqUri), zap.Any("ret", ret))
		return ret, nil
	}
	rbacPrivilege, err := dao.GetDaoHub().Rbac.GetRbacPrivilegeByResourceUri(ctx, database.DBConn.DB, resourceUri)
	if err != nil {
		logger.Error("HasPermissionForAccessToken: GetRbacPrivilegeByResourceUri error", zap.Any("resourceUri", resourceUri), zap.Any("err", err))
		return nil, err
	}
	if rbacPrivilege == nil {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       "resourceUri relate privilege not found",
		}
		logger.Info("HasPermissionForAccessToken: resourceUri relate privilege not found", zap.Any("resourceUri", resourceUri), zap.Any("ret", ret))

		return ret, nil
	}

	filter := rbac.RolePrivilegeFilter{
		RoleIDs:      rbacRoleIds,
		PrivilegeIDs: []entity.ID{rbacPrivilege.ID},
	}
	rolePrivileges, err := dao.GetDaoHub().Rbac.ListRbacRolePrivileges(ctx, database.DBConn.DB, filter)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			rolePrivileges = nil
		} else {
			logger.Error("HasPermissionForAccessToken: ListRbacRolePrivileges error", zap.Any("filter", filter), zap.Any("err", err))
			return nil, err
		}
	}
	joinRoleName := joinRoleNames(rbacRoleNames)

	if len(rolePrivileges) == 0 {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       fmt.Sprintf("access token's role (%s) relate privileges not hit resource_uri (%s)", joinRoleName, resourceUri),
		}
		logger.Info("HasPermissionForAccessToken: has no privilege", zap.Any("accessToken", accessToken), zap.Any("reqUri", reqUri), zap.Any("resourceUri", resourceUri), zap.Any("ret", ret))
		return ret, nil
	}

	ret := &PermissionCheckRet{
		HasPermission: true,
		Message:       fmt.Sprintf("access token's role (%s) relate privileges hited resource_uri (%s)", joinRoleName, resourceUri),
	}
	logger.Info("HasPermissionForAccessToken: has privilege", zap.Any("accessToken", accessToken), zap.Any("reqUri", reqUri), zap.Any("resourceUri", resourceUri), zap.Any("ret", ret))
	return ret, nil

}

func getAccessTokenPermissionCheckResource(ctx context.Context, assignedResource *AccessTokenRelateResource, reqResource map[entity.ScopeType]entity.ID) map[entity.ScopeType][]entity.ID {
	var tye2Ids = map[entity.ScopeType][]entity.ID{}
	if len(reqResource) == 0 {
		if assignedResource.OrgId != 0 {
			tye2Ids[entity.RoleBindingScopeOrg] = []entity.ID{entity.ID(assignedResource.OrgId)}
		}
		if len(assignedResource.ProjectIds) > 0 {
			var projectIds []entity.ID
			for _, pid := range assignedResource.ProjectIds {
				projectIds = append(projectIds, entity.ID(pid))
			}
			tye2Ids[entity.RoleBindingScopeProject] = projectIds
		}
	} else {
		for k, v := range reqResource {
			if k == entity.RoleBindingScopeOrg && v.Uint64() == assignedResource.OrgId {
				tye2Ids[entity.RoleBindingScopeOrg] = []entity.ID{v}
			}
			if k == entity.RoleBindingScopeProject {
				for _, pid := range assignedResource.ProjectIds {
					if pid == v.Uint64() {
						tye2Ids[entity.RoleBindingScopeProject] = []entity.ID{v}
						break
					}
				}
			}

		}
	}

	return tye2Ids
}

func (r *RbacPermissionSvc) getAccessTokenPermissionCheckRoles(ctx context.Context, userId uint64, resourceMap map[entity.ScopeType][]entity.ID) ([]entity.ID, []string, error) {
	var rbacRoleIds []entity.ID
	var rbacRoleNames []string

	if len(resourceMap) == 0 {
		return rbacRoleIds, rbacRoleNames, nil
	}
	user, err := dao.GetDaoHub().User.GetUserByID(ctx, database.DBConn.DB, userId)
	if err != nil || user == nil || user.Status != entity.UserStatusActive {
		tilog.Info(ctx, "the user's status is not active", zap.Any("userId", userId))
		return rbacRoleIds, rbacRoleNames, errs.NewBadRequest("the user's status is not active", nil)
	}
	for scopeType, scopeIds := range resourceMap {
		filter := role.RoleBindingFilter{
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{entity.ID(userId)},
			ScopeTypes: []entity.ScopeType{scopeType},
			ScopeIDs:   scopeIds,
			//Roles:      roleNames,
			Status: []string{entity.RoleBindingStatusActive},
		}
		bindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, filter)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				continue
			}
			return nil, nil, err
		}
		if len(bindings) > 0 {
			for _, binding := range bindings {
				rbacRoleNames = append(rbacRoleNames, binding.Role)
				rbacRoleIds = append(rbacRoleIds, binding.RoleID)
			}
		}
	}
	return rbacRoleIds, rbacRoleNames, nil
}

func joinRoleNames(roleNames []string) string {
	if len(roleNames) == 0 {
		return ""
	}
	var b bytes.Buffer
	b.WriteString(roleNames[0])
	for i := 1; i < len(roleNames); i++ {
		b.WriteString(", ")
		b.WriteString(roleNames[i])
	}
	return b.String()
}

type AccessTokenRelateResource struct {
	RoleNames  []string
	OrgId      uint64
	ProjectIds []uint64
	RoleIds    []entity.ID
}

// getDeviceCodeAccessTokenAssignedResource
//
//	@Description: query DeviceCodeAccessToken grant  org_id and project_ids
//	@Author wangletian 2024-03-20 17:18:54
//	@receiver r
//	@param ctx
//	@param tokenRsp
//	@return *AccessTokenRelateResource
//	@return error
func (r *RbacPermissionSvc) getDeviceCodeAccessTokenAssignedResource(ctx context.Context, tokenRsp *provider.AccessTokenIdentity) (*AccessTokenRelateResource, error) {
	tilog.Info(ctx, "getDeviceCodeAccessTokenAssignedResource")
	var assignedOrgId uint64 = 0

	orgBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{entity.ID(tokenRsp.UserId)},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(tokenRsp.OrgId)},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			orgBindings = []entity.RoleBinding{}
		} else {
			return nil, err
		}
	}
	if len(orgBindings) > 0 {
		assignedOrgId = tokenRsp.OrgId
	}

	var assignedProjectIds []uint64

	if !tokenRsp.GrantAllProjects {
		if len(tokenRsp.ProjectIds) > 0 {
			activeProjects, err := dao.GetDaoHub().Project.ListActiveProjectsByOrgID(ctx, database.DBConn.DB, tokenRsp.OrgId)
			if err != nil {
				return nil, err
			}
			var activeProjectsMap = map[uint64]entity.Project{}
			for _, ap := range activeProjects {
				activeProjectsMap[ap.ID.Uint64()] = ap
			}
			for _, pid := range tokenRsp.ProjectIds {
				if _, ok := activeProjectsMap[pid]; ok {
					assignedProjectIds = append(assignedProjectIds, pid)
				}
			}
		}
	} else {
		allActiveProjects, err := dao.GetDaoHub().Project.ListActiveProjectsByOrgID(ctx, database.DBConn.DB, tokenRsp.OrgId)
		if err != nil {
			return nil, err
		}
		var allActiveProjectIds []entity.ID
		for _, p := range allActiveProjects {
			allActiveProjectIds = append(allActiveProjectIds, p.ID)
		}
		projectBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{entity.ID(tokenRsp.UserId)},
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
			ScopeIDs:   allActiveProjectIds,
			Status:     []string{entity.RoleBindingStatusActive},
		})
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				projectBindings = []entity.RoleBinding{}
			} else {
				return nil, err
			}
		}

		for _, pb := range projectBindings {
			assignedProjectIds = append(assignedProjectIds, pb.ScopeID.Uint64())
		}

	}

	resource := AccessTokenRelateResource{
		//UserId: tokenRsp.UserId,
		OrgId: assignedOrgId,
		//RoleNames:  tokenRsp.Scopes,
		ProjectIds: assignedProjectIds,
	}

	return &resource, nil
}

// getAuthorizationCodeAccessTokenAssignedResource
// query authorization code AccessToken grant  org_id and project_ids
//
//	@Description:
//	@Author wangletian 2024-03-20 17:37:19
//	@receiver r
//	@param ctx
//	@param accessTokenId
//	@param reqScopeMap
//	@return *AccessTokenRelateResource
//	@return error
func (r *RbacPermissionSvc) getAuthorizationCodeAccessTokenAssignedResource(ctx context.Context, accessTokenId uint64, reqScopeMap map[entity.ScopeType]entity.ID) (*AccessTokenRelateResource, error) {
	tilog.Info(ctx, "getAuthorizationCodeAccessTokenAssignedResource", zap.Any("accessTokenId", accessTokenId))
	roleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetAccessToken},
		TargetIDs:  []entity.ID{entity.ID(accessTokenId)},
		//ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		//ScopeIDs: []entity.ID{entity.ID(tokenRsp.OrgId)},
		Status: []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			roleBindings = []entity.RoleBinding{}
		} else {
			return nil, err
		}
	}
	if len(roleBindings) == 0 {
		return &AccessTokenRelateResource{}, nil
	}

	var assignedOrgId uint64 = 0
	var assignedProjectIds []uint64
	var assignedRoleIds []entity.ID
	var assignedRoleNames []string
	for _, r := range roleBindings {
		dbScopeId := r.ScopeID
		// reqScope int the url  is not null, determine whether it is equal to dbScopeId
		if len(reqScopeMap) > 0 {
			reqScopeId, ok := reqScopeMap[r.ScopeType]
			if ok && dbScopeId == reqScopeId {
				// reqScope is not null,  reqScopeId must in  rolebindings  , reqScopeId must equal dbScopeId
				if r.ScopeType == entity.RoleBindingScopeOrg {
					assignedOrgId = reqScopeId.Uint64()
				} else if r.ScopeType == entity.RoleBindingScopeProject {
					assignedProjectIds = append(assignedProjectIds, reqScopeId.Uint64())
				}
				assignedRoleIds = append(assignedRoleIds, r.RoleID)
				assignedRoleNames = append(assignedRoleNames, r.Role)

			}
		} else {
			// reqScope int the url  is null(for example, static resources),   use dbScopeId
			if r.ScopeType == entity.RoleBindingScopeOrg {
				assignedOrgId = dbScopeId.Uint64()
			} else if r.ScopeType == entity.RoleBindingScopeProject {
				assignedProjectIds = append(assignedProjectIds, dbScopeId.Uint64())
			}
			assignedRoleIds = append(assignedRoleIds, r.RoleID)
			assignedRoleNames = append(assignedRoleNames, r.Role)
		}

	}

	resource := AccessTokenRelateResource{
		OrgId:      assignedOrgId,
		ProjectIds: assignedProjectIds,
		RoleIds:    assignedRoleIds,
		RoleNames:  assignedRoleNames,
	}

	return &resource, nil
}
