package rbac

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/casbin/casbin/v2"
	"github.com/casbin/casbin/v2/model"
	defaultrolemanager "github.com/casbin/casbin/v2/rbac/default-role-manager"
	"github.com/casbin/casbin/v2/util"
	xormadapter "github.com/casbin/xorm-adapter/v2"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/config"
	model2 "github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/stream"
)

// rbac domain: sub domain obj act eft(default allow)

/*
	1. create one tenant
	2. create role clusterid*
	3. policy and inherit like this:
		p, admin, org1, user_data, read
		p, admin, org1, user_data, write
		p, admin, org1, user_data, delete

		p, editor, org1, cluster_data, read
		p, editor, org1, cluster_data, write

		p, viewer, org1, cluster_data, read

		role inherit
		g, alice, admin, org1
		g, alice, editor, org1

		data inherit
		g2, {cluster_id}, cluster_data, org1
*/

const (
	rbac_model = "rbac_model.conf"
	polices    = "init_policy.toml"
)

const (
	DEFAULT_ROLEID = 0
	SEPARATOR      = "/"
	REGEXP_NUMBER  = "^\\d{1,}$"
	REGEXP_STR     = "\\{.+\\}"
)

type RbacMock struct{}

func (r *RbacMock) InitOrgRBAC(_ uint) error             { return nil }
func (r *RbacMock) AttachRoleForUser(_, _, _ uint) error { return nil }
func (r *RbacMock) Auth(_ uint, _ bool, _ Resource, _ Acting, _ uint) (bool, error) {
	return true, nil
}
func (r *RbacMock) GetRolesOfUserInOrg(_, _ uint) []uint { return nil }
func (r *RbacMock) DeleteOrg(_ uint) error               { return nil }
func (r *RbacMock) GetUserAccessOrgs(_ uint) []uint      { return nil }
func (r *RbacMock) GetAllUsersInOrg(_ uint) []uint       { return nil }
func (r *RbacMock) DeleteRoleForUser(_, _, _ uint) error { return nil }
func (r *RbacMock) GetOneUserAccessOrg(_ uint) uint      { return 0 }
func (r *RbacMock) GetEnforcer() casbin.IEnforcer {
	e, _ := casbin.NewEnforcer("./testdata/rbac_model.conf", "./testdata/policy.csv")
	return e
}

type Auth interface {
	// biz related api
	InitOrgRBAC(orgID uint) error
	AttachRoleForUser(orgID, userID, roleID uint) error
	Auth(userID uint, internal bool, resource Resource, act Acting, orgID uint) (bool, error)
	GetRolesOfUserInOrg(userID, orgID uint) []uint
	DeleteOrg(orgId uint) error
	GetUserAccessOrgs(userId uint) []uint
	GetAllUsersInOrg(orgID uint) []uint
	DeleteRoleForUser(orgID, userID, roleID uint) error
	GetOneUserAccessOrg(userId uint) uint
	// just to access the real enforcer
	GetEnforcer() casbin.IEnforcer
}

type AuthCasbin struct {
	enforcer *casbin.Enforcer
}

func InitAuth(dbCfg *config.DBConfig, modelPath string) (*AuthCasbin, error) {
	// adapter, err := xormadapter.NewAdapterWithTableName("mysql", cfg.DBConfig.DSN(), "casbin_rule", "", true)
	adapter, err := xormadapter.NewAdapter("mysql", dbCfg.DSN(), true)
	if err != nil {
		return nil, errors.Annotate(err, "init casbin table failed")
	}

	var mo model.Model
	if modelPath == "" {
		// Default model.
		mo, err = model.NewModelFromString(defaultRbacModelConf)
	} else {
		mo, err = model.NewModelFromFile(filewrapper(modelPath, rbac_model))
	}
	if err != nil {
		return nil, errors.Annotate(err, "init model failed")
	}

	e, err := casbin.NewEnforcer(mo, adapter)
	if err != nil {
		return nil, errors.Annotate(err, "init enforcer failed")
	}

	e.GetRoleManager().(*defaultrolemanager.RoleManager).AddMatchingFunc("matcher", util.KeyMatch2)
	// N.B. do not add DomainMatchingFunc with * logic before calling BuildLinkedRoles

	e.AddNamedMatchingFunc("g", "", util.KeyMatch)
	e.AddFunction("actMatch", ActingMatchFunc)
	e.AddFunction("domMatch", DomainMatchFunc)
	if err := loadDefaultPolicy(); err != nil {
		return nil, errors.Annotate(err, "load policy failed")
	}

	return &AuthCasbin{enforcer: e}, nil
}

func (au *AuthCasbin) GetEnforcer() casbin.IEnforcer {
	return au.enforcer
}

// TODO: can remove
// DeleteRoleForUser will not delete role for pingcaper role
func (au *AuthCasbin) DeleteRoleForUser(orgID, userID, roleID uint) error {
	ok, err := au.enforcer.DeleteRoleForUserInDomain(getUserSubject(userID), getRoleSubject(roleID), Tenant(orgID).toString())
	if ok {
		log.Info("delete role", zap.Uint("org", orgID), zap.Uint("user", userID))
	}
	return err
}

// DeleteUserInOrganization will not delete role for pingcaper role
func (au *AuthCasbin) DeleteUserInOrganization(orgID, userID uint) error {
	ok, err := au.enforcer.DeleteRolesForUser(getUserSubject(userID), Tenant(orgID).toString())
	if ok {
		log.Info("delete role", zap.Uint("org", orgID), zap.Uint("user", userID))
	}
	return err
}

func (au *AuthCasbin) DeleteOrg(orgId uint) error {
	res, err := au.enforcer.DeleteDomains(Tenant(orgId).toString())
	if err != nil {
		return err
	}
	if !res {
		return errors.Errorf("delete role inherit failed")
	}
	if err := model2.DeleteRoleByOrg(orgId); err != nil {
		return err
	}
	return nil
}

// TODO: can remove, no caller call this function
func (au *AuthCasbin) GetOneUserAccessOrg(userId uint) uint {
	policy := au.enforcer.GetFilteredGroupingPolicy(0, getUserSubject(userId))
	if len(policy) == 0 {
		return 0
	}

	p := policy[0]
	if len(p) != 3 {
		return 0
	}

	id, err := strconv.ParseUint(p[2], 10, 64)
	if err != nil {
		log.Error("parse org id failed", zap.Any("policy", p))
		return 0
	}

	return uint(id)
}

// GetUserAccessOrgs return empty slice if user only has pingcaper role.
func (au *AuthCasbin) GetUserAccessOrgs(userId uint) []uint {
	policy := au.enforcer.GetFilteredGroupingPolicy(0, getUserSubject(userId))
	orgMap := make(map[string]struct{})

	orgs := make([]uint, 0)
	stream.OfSlice(policy).Filter(func(p []string) bool {
		if len(p) != 3 {
			return false
		}
		if strings.HasSuffix(p[2], "pingcaper") {
			return false
		}
		_, ok := orgMap[p[2]]
		if !ok {
			orgMap[p[2]] = struct{}{}
		}
		return !ok
	}).Map(func(p []string) uint {
		id, err := strconv.ParseUint(p[2], 10, 64)
		if err != nil {
			log.Error("parse org id failed", zap.Any("policy", p))
			return 0
		}

		return uint(id)
	}).Filter(func(id uint) bool {
		return id > 0
	}).CollectEmptySlice(&orgs)
	// check whether reply domain is match string
	return orgs
}

func (au *AuthCasbin) Enforce(ctx context.Context, request *EnforceRequest) (*BoolReply, error) {
	//  orgs/1/clusters/read
	reqSlice := strings.Split(request.Uri, SEPARATOR)
	length := len(reqSlice)
	// for now, clinic rbac server only support orgs resource.
	if length != 4 || reqSlice[0] != "orgs" {
		return nil, fmt.Errorf("invalid resource uri format, uri=%s", request.Uri)
	}

	org, err := strconv.ParseUint(reqSlice[1], 10, 64)
	if err != nil {
		return nil, err
	}
	// TODO
	action := reqSlice[3]

	// TODO: action should be string type
	ok, err := au.Auth(uint(request.User.Id), request.User.Internal, Resource(reqSlice[2]), Acting(action), uint(org))
	if err != nil {
		return nil, err
	}

	return &BoolReply{
		Res: ok,
	}, nil
}

// TODO: internal auth should be removed, add admin role to all internal user
func (au *AuthCasbin) Auth(userID uint, internal bool, resource Resource, act Acting, orgID uint) (bool, error) {
	allow, err := au.enforcer.Enforce(getUserSubject(userID), Tenant(orgID).toString(), string(resource), act.ToString())
	if err != nil {
		return false, err
	}
	if !allow {
		log.Warn("enforcer return false", zap.String("r.sub", getUserSubject(userID)), zap.String("r.dom", Tenant(orgID).toString()), zap.String("r.obj", string(resource)), zap.String("r.act", act.ToString()))
	}
	return allow, nil
}

func (au *AuthCasbin) GetRolesOfUserInOrg(userID, orgID uint) []uint {
	roles, err := au.enforcer.GetImplicitRolesForUser(getUserSubject(userID), Tenant(orgID).toString())
	if err != nil {
		log.Error("get roles for user failed", zap.Uint("user", userID), zap.Uint("org", orgID))
		return nil
	}

	results := make([]uint, 0)
	// do not include pingcaper role
	stream.OfSlice(roles).Filter(func(r string) bool {
		if !strings.HasPrefix(r, roleSubjectPrefix) {
			return false
		}
		if strings.HasSuffix(r, "pingcaper") {
			return false
		}
		return true
	}).Map(func(r string) uint {
		idStr := strings.TrimPrefix(r, roleSubjectPrefix)
		id, err := strconv.ParseUint(idStr, 10, 64)
		if err != nil {
			log.Error("convert role id failed", zap.Uint("user", userID), zap.Uint("org", orgID))
			return 0
		}
		return uint(id)
	}).Filter(func(id uint) bool {
		return id > 0
	}).CollectEmptySlice(&results)

	return results
}

func filewrapper(path, name string) string {
	return fmt.Sprintf("%s/%s", path, name)
}

func (au *AuthCasbin) InitOrgRBAC(orgID uint) error {
	policies := make([][]string, 0)

	roles := make(map[string]uint)
	for _, r := range defaultOrgLevelRoles {
		err := model2.InsertRole(r, orgID, r, func(roleID uint) error {
			roles[r] = roleID
			return nil
		})
		if err != nil {
			log.Error("insert role failed", zap.Error(err))
			return err
		}
	}
	// actually they should be in a transaction
	stream.OfSlice(defaultOrgLevelPolicy).Map(func(p PolicyOrg) []string {
		p.Role = getRoleSubject(roles[p.Role])
		p.Tenant = Tenant(orgID)
		return p.toArray()
	}).CollectEmptySlice(&policies)

	ok, err := au.enforcer.AddPolicies(policies)
	if err != nil {
		log.Error("enforcer add policies failed", zap.Error(err))
		return err
	}
	if ok {
		log.Info("the default policy add successful", zap.Uint("org", orgID))
	}

	groups := make([][]string, 0)
	stream.OfSlice(defaultOrgLevelGroupPolicy).Map(func(g GroupPolicy) []string {
		g.Item = getRoleSubject(roles[g.Item])
		g.Role = getRoleSubject(roles[g.Role])
		g.Tenant = Tenant(orgID)

		return g.toArray()
	}).CollectEmptySlice(&groups)

	ok, err = au.enforcer.AddGroupingPolicies(groups)
	if err != nil {
		log.Error("enforcer add group policies failed", zap.Error(err))
	}
	if ok {
		log.Info("the default group add successful", zap.Uint("org", orgID))
	}
	return err
}

func (au *AuthCasbin) AttachRoleForUser(orgID, userID, roleID uint) error {
	ok, err := au.enforcer.AddRoleForUserInDomain(getUserSubject(userID), getRoleSubject(roleID), Tenant(orgID).toString())
	if ok {
		log.Info("add role for user", zap.Uint("org", orgID), zap.Uint("user", userID), zap.Uint("role", roleID))
	}
	return err
}

// GetAllUsersInOrgs does not reutrn any pingcaper users
func (au *AuthCasbin) GetAllUsersInOrg(orgID uint) []uint {
	ids := make([]uint, 0)

	items := au.enforcer.GetAllUsersByDomain(Tenant(orgID).toString())
	stream.OfSlice(items).Filter(func(i string) bool {
		return strings.HasPrefix(i, userSubjectPrefix)
	}).Map(func(i string) uint {
		idStr := strings.TrimPrefix(i, userSubjectPrefix)
		id, err := strconv.ParseUint(idStr, 10, 64)
		if err != nil {
			log.Error("convert role id failed", zap.Uint("org", orgID))
			return 0
		}
		return uint(id)
	}).Filter(func(id uint) bool {
		return id > 0
	}).CollectEmptySlice(&ids)

	return ids
}
