package rbac

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/patrickmn/go-cache"
)

const (
	CacheKey4ResourceUri2RoleId = "rbac_key_resource_uri_2_role_id"
	CacheKey4RoleId2ResourceUri = "rbac_key_role_id_2_resource_uri"
	CacheKey4RoleName2RoleId    = "rbac_key_role_name_2_role_id"
	CacheKey4RoleId2RoleName    = "rbac_key_role_id_2_role_name"
)

const (
	defaultExpiration = 3 * time.Hour
	cleanupInterval   = 60 * time.Second
)

var (
	LocalCache4RBAC = cache.New(defaultExpiration, cleanupInterval)
)

func GetRoleIdsByResourceUriFromCache(ctx context.Context, resourceUri string) (map[uint64]struct{}, bool) {
	cacheRet, exist := GetAndLoadCache4RBAC(ctx, CacheKey4ResourceUri2RoleId)
	if !exist {
		tilog.Warn(ctx, fmt.Sprintf("%s local cache not exist", CacheKey4ResourceUri2RoleId))
		return map[uint64]struct{}{}, false
	}
	resourceUri2RoleIdMap, ok := cacheRet.(map[string]map[uint64]struct{})
	if !ok {
		tilog.Error(ctx, fmt.Sprintf("%s local cache type invalid", CacheKey4ResourceUri2RoleId))
		return map[uint64]struct{}{}, false
	}
	roleIdMap, ok := resourceUri2RoleIdMap[resourceUri]
	if !ok {
		tilog.Warn(ctx, fmt.Sprintf("%s local cache not found %s relate role_id", CacheKey4ResourceUri2RoleId, resourceUri))
		return map[uint64]struct{}{}, false
	}
	tilog.Info(ctx, fmt.Sprintf("%s hit cache", CacheKey4ResourceUri2RoleId), zap.Any("resource_uri", resourceUri), zap.Any("roleIdMap", roleIdMap))
	return roleIdMap, true
}

// ForEachResourceUriByRoleIds iterates the cached resource uris of roleIds
// without copying the underlying shared maps. fn returns false to stop early.
// The bool return reports whether any role had privileges in cache.
func ForEachResourceUriByRoleIds(ctx context.Context, roleIds []uint64, fn func(resourceUri string) bool) bool {
	cacheRet, exist := GetAndLoadCache4RBAC(ctx, CacheKey4RoleId2ResourceUri)
	if !exist {
		tilog.Warn(ctx, fmt.Sprintf("%s local cache not exist", CacheKey4RoleId2ResourceUri))
		return false
	}
	roleId2ResourceUriMap, ok := cacheRet.(map[uint64]map[string]struct{})
	if !ok {
		tilog.Error(ctx, fmt.Sprintf("%s local cache type invalid", CacheKey4RoleId2ResourceUri))
		return false
	}

	found := false
	for _, roleId := range roleIds {
		resourceUriMap, exist := roleId2ResourceUriMap[roleId]
		if !exist {
			continue
		}
		found = true
		for resUri := range resourceUriMap {
			if !fn(resUri) {
				return true
			}
		}
	}
	return found
}

func GetRoleIdByRoleNameFromCache(ctx context.Context, roleName string) (uint64, bool) {
	cacheRet, exist := GetAndLoadCache4RBAC(ctx, CacheKey4RoleName2RoleId)
	if !exist {
		tilog.Warn(ctx, fmt.Sprintf("%s local cache not exist", CacheKey4RoleName2RoleId))
		return 0, false
	}
	roleName2RoleIdMap, ok := cacheRet.(map[string]uint64)
	if !ok {
		tilog.Error(ctx, fmt.Sprintf("%s local cache type invalid", CacheKey4RoleName2RoleId))
		return 0, false
	}

	roleId, exist := roleName2RoleIdMap[roleName]
	if !exist {
		return 0, false
	}
	tilog.Info(ctx, fmt.Sprintf("%s hit cache", CacheKey4RoleName2RoleId), zap.Any("roleName", roleName), zap.Any("roleId", roleId))
	return roleId, true
}

func GetRoleNameByRoleIdFromCache(ctx context.Context, roleId uint64) (string, bool) {
	cacheRet, exist := GetAndLoadCache4RBAC(ctx, CacheKey4RoleId2RoleName)
	if !exist {
		tilog.Warn(ctx, fmt.Sprintf("%s local cache not exist", CacheKey4RoleId2RoleName))
		return "", false
	}
	roleId2RoleNameMap, ok := cacheRet.(map[uint64]string)
	if !ok {
		tilog.Error(ctx, fmt.Sprintf("%s local cache type invalid", CacheKey4RoleId2RoleName))
		return "", false
	}

	roleName, exist := roleId2RoleNameMap[roleId]
	if !exist {
		return "", false
	}
	tilog.Info(ctx, fmt.Sprintf("%s hit cache", CacheKey4RoleId2RoleName), zap.Any("roleName", roleName), zap.Any("roleId", roleId))
	return roleName, true
}

func GetAndLoadCache4RBAC(ctx context.Context, key string) (interface{}, bool) {
	cacheRet, exist := LocalCache4RBAC.Get(key)
	if exist {
		return cacheRet, true
	}
	tilog.Info(ctx, fmt.Sprintf("%s local cache not exist, go load cache", key))
	var err error
	if key == CacheKey4RoleName2RoleId {
		err = LoadRbacRoles(ctx)
	} else {
		err = LoadRbacRolePrivileges(ctx)
	}
	if err != nil {
		tilog.Info(ctx, fmt.Sprintf("%s local cache not exist, load cache error", key), zap.Any("err", err))
		return nil, false
	}
	return LocalCache4RBAC.Get(key)
}

func LoadRbacRoles(ctx context.Context) error {
	roles, err := dao.NewDaoHub().Rbac.ListRbacRoles(ctx, database.DBConn.DB)
	if err != nil {
		tilog.Error(ctx, "LoadRbacRoles ListRbacRoles error", zap.Any("err", err))
		return err
	}
	var roleName2RoleIdMap = map[string]uint64{}
	var roleId2RoleNameMap = map[uint64]string{}
	for _, role := range roles {
		roleName2RoleIdMap[role.Name] = role.ID.Uint64()
		roleId2RoleNameMap[role.ID.Uint64()] = role.Name
	}
	LocalCache4RBAC.SetDefault(CacheKey4RoleName2RoleId, roleName2RoleIdMap)
	LocalCache4RBAC.SetDefault(CacheKey4RoleId2RoleName, roleId2RoleNameMap)

	tilog.Info(ctx, fmt.Sprintf("LoadRbacRoles set %s success", CacheKey4RoleName2RoleId), zap.Any("roleName2RoleIdMap", roleName2RoleIdMap))
	return nil
}

func LoadRbacRolePrivileges(ctx context.Context) error {
	defer func() {
		// recover from panic if one occurred. Set err to nil otherwise.
		if err := recover(); err != nil {
			tilog.Error(ctx, "panic occurred", zap.Any("panic", err))
		}
	}()

	err := LoadRbacRoles(ctx)
	if err != nil {
		return err
	}

	cacheRet, exist := GetAndLoadCache4RBAC(ctx, CacheKey4RoleName2RoleId)
	if !exist {
		tilog.Warn(ctx, "LoadRbacRolePrivileges not found roles, ignore")
		return nil
	}
	roleName2RoleIdMap, ok := cacheRet.(map[string]uint64)
	if !ok {
		tilog.Error(ctx, fmt.Sprintf("%s local cache type invalid", CacheKey4RoleName2RoleId))
		return fmt.Errorf("%s local cache type invalid", CacheKey4RoleName2RoleId)
	}

	var roleId2ResourceUriMap = make(map[uint64]map[string]struct{})
	var resourceUri2RoleIdMap = make(map[string]map[uint64]struct{})

	hub := dao.NewDaoHub()
	for _, roleId := range roleName2RoleIdMap {
		privIds, err := hub.Rbac.ListRbacPrivilegesIDsByRoleID(ctx, database.DBConn.DB, []uint64{roleId})
		if err != nil {
			tilog.Error(ctx, "LoadRbacRolePrivileges ListRbacPrivilegesIDsByRoleID error", zap.Any("roleId", roleId), zap.Any("err", err))
			return err
		}
		privs, err := hub.Rbac.ListRbacPrivilegesByIDs(ctx, database.DBConn.DB, privIds)
		if err != nil {
			tilog.Error(ctx, "LoadRbacRolePrivileges ListRbacPrivilegesByIDs error", zap.Any("privIds", privIds), zap.Any("err", err))
			return err
		}

		resourceUriMap, ok := roleId2ResourceUriMap[roleId]
		if !ok {
			resourceUriMap = make(map[string]struct{})
		}

		for _, priv := range privs {
			resourceUriMap[priv.ResourceUri] = struct{}{}

			roleIdMap, exist := resourceUri2RoleIdMap[priv.ResourceUri]
			if !exist {
				roleIdMap = make(map[uint64]struct{})
			}
			roleIdMap[roleId] = struct{}{}
			resourceUri2RoleIdMap[priv.ResourceUri] = roleIdMap
		}

		roleId2ResourceUriMap[roleId] = resourceUriMap
	}

	LocalCache4RBAC.SetDefault(CacheKey4ResourceUri2RoleId, resourceUri2RoleIdMap)
	LocalCache4RBAC.SetDefault(CacheKey4RoleId2ResourceUri, roleId2ResourceUriMap)

	tilog.Info(ctx, "LoadRbacRolePrivileges set roleId relate resourceUri success")
	return nil
}

func TimingLoadRbacResource() {
	go func() {
		ctx := context.Background()
		for {
			tilog.Info(ctx, "start load rbac resource")
			_ = LoadRbacRolePrivileges(ctx)

			time.Sleep(time.Minute * 5)
		}
	}()
}
