package service

import (
	"context"
	"fmt"

	"github.com/RafayLabs/rcloud-base/internal/dao"
	"github.com/RafayLabs/rcloud-base/internal/models"
	authzpbv1 "github.com/RafayLabs/rcloud-base/proto/types/authz"
	"github.com/casbin/casbin/v2"
	"github.com/uptrace/bun"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type AuthzService interface {
	Enforce(context.Context, *authzpbv1.EnforceRequest) (*authzpbv1.BoolReply, error)
	ListPolicies(context.Context, *authzpbv1.Policy) (*authzpbv1.Policies, error)
	CreatePolicies(context.Context, *authzpbv1.Policies) (*authzpbv1.BoolReply, error)
	DeletePolicies(context.Context, *authzpbv1.Policy) (*authzpbv1.BoolReply, error)
	ListUserGroups(context.Context, *authzpbv1.UserGroup) (*authzpbv1.UserGroups, error)
	CreateUserGroups(ctx context.Context, p *authzpbv1.UserGroups) (*authzpbv1.BoolReply, error)
	DeleteUserGroups(ctx context.Context, p *authzpbv1.UserGroup) (*authzpbv1.BoolReply, error)
	ListRolePermissionMappings(ctx context.Context, p *authzpbv1.FilteredRolePermissionMapping) (*authzpbv1.RolePermissionMappingList, error)
	CreateRolePermissionMappings(ctx context.Context, p *authzpbv1.RolePermissionMappingList) (*authzpbv1.BoolReply, error)
	DeleteRolePermissionMappings(ctx context.Context, p *authzpbv1.FilteredRolePermissionMapping) (*authzpbv1.BoolReply, error)
}

type authzService struct {
	db           *bun.DB
	enforcer     *casbin.CachedEnforcer
	mappingCache map[string][]rpmUrlAction
}

func NewAuthzService(db *bun.DB, en *casbin.CachedEnforcer) AuthzService {
	return &authzService{
		db:           db,
		enforcer:     en,
		mappingCache: make(map[string][]rpmUrlAction),
	}
}

const (
	groupGtype = "g2"
	roleGtype  = "g"
)

type rpmUrlAction struct {
	url     string
	methods []string
}

func processRpms(rpm models.ResourcePermission) []rpmUrlAction {
	urls := []rpmUrlAction{}
	for _, rurl := range rpm.ResourceUrls {
		tmp := rpmUrlAction{
			url:     rpm.BaseUrl,
			methods: make([]string, 0),
		}
		if url, ok := rurl["url"].(string); ok {
			tmp.url = tmp.url + url
		}
		if methods, ok := rurl["methods"].([]interface{}); ok {
			for _, method := range methods {
				if met, ok := method.(string); ok {
					tmp.methods = append(tmp.methods, met)
				}
			}
		}
		urls = append(urls, tmp)
	}
	for _, raurl := range rpm.ResourceActionUrls {
		tmp := rpmUrlAction{
			url:     rpm.BaseUrl,
			methods: make([]string, 0),
		}
		if url, ok := raurl["url"].(string); ok {
			tmp.url = tmp.url + url
		}
		if methods, ok := raurl["methods"].([]interface{}); ok {
			for _, method := range methods {
				if met, ok := method.(string); ok {
					tmp.methods = append(tmp.methods, met)
				}
			}
		}
		urls = append(urls, tmp)
	}
	return urls
}

func (s *authzService) cacheResourceRolePermissions(ctx context.Context) error {
	var items []models.ResourcePermission
	entities, err := dao.ListAll(ctx, s.db, &items)
	if err != nil {
		return err
	}
	if rpms, ok := entities.(*[]models.ResourcePermission); ok {
		for _, rpm := range *rpms {
			if perm, ok := s.mappingCache[rpm.Name]; ok {
				s.mappingCache[rpm.Name] = append(perm, processRpms(rpm)...)
			} else {
				s.mappingCache[rpm.Name] = processRpms(rpm)
			}
		}
	}
	return nil
}

func (s *authzService) toPolicies(policies [][]string) *authzpbv1.Policies {
	if len(policies) == 0 {
		return &authzpbv1.Policies{}
	}

	res := &authzpbv1.Policies{}
	res.Policies = make([]*authzpbv1.Policy, len(policies))
	for i := range policies {
		res.Policies[i] = &authzpbv1.Policy{
			Sub:  policies[i][0],
			Ns:   policies[i][1],
			Proj: policies[i][2],
			Org:  policies[i][3],
			Obj:  policies[i][4],
		}
	}
	return res
}

func (s *authzService) fromPolicies(policies *authzpbv1.Policies) ([][]string, error) {
	res := [][]string{}
	for i, p := range policies.GetPolicies() {
		rule := []string{p.GetSub(), p.GetNs(), p.GetProj(), p.GetOrg(), p.GetObj()}
		for _, field := range rule {
			if field == "" {
				return res, fmt.Errorf(fmt.Sprintf("index %d: policy elements do not meet definition", i))
			}
		}
		res = append(res, rule)
	}

	return res, nil
}

func (s *authzService) toUserGroups(ug [][]string) *authzpbv1.UserGroups {
	if len(ug) == 0 {
		return &authzpbv1.UserGroups{}
	}

	res := &authzpbv1.UserGroups{}
	res.UserGroups = make([]*authzpbv1.UserGroup, len(ug))
	for i := range ug {
		res.UserGroups[i] = &authzpbv1.UserGroup{
			User: ug[i][0],
			Grp:  ug[i][1],
		}
	}

	return res
}

func (s *authzService) fromUserGroups(ugs *authzpbv1.UserGroups) ([][]string, error) {
	res := [][]string{}
	for i, p := range ugs.GetUserGroups() {
		rule := []string{p.GetUser(), p.GetGrp()}
		for _, field := range rule {
			if field == "" {
				return res, fmt.Errorf(fmt.Sprintf("index %d: request elements do not meet definition", i))
			}
		}
		res = append(res, rule)
	}

	return res, nil
}

func (s *authzService) toRolePermissionMappingList(r [][]string) *authzpbv1.RolePermissionMappingList {
	if len(r) == 0 {
		return &authzpbv1.RolePermissionMappingList{}
	}

	rpms := make(map[string][]string)
	for _, rpm := range r {
		rpms[rpm[1]] = append(rpms[rpm[1]], rpm[0])
	}

	res := &authzpbv1.RolePermissionMappingList{}
	res.RolePermissionMappingList = make([]*authzpbv1.RolePermissionMapping, len(rpms))
	var i int
	for k, v := range rpms {
		i++
		res.RolePermissionMappingList[i] = &authzpbv1.RolePermissionMapping{
			Role:       k,
			Permission: v,
		}
	}

	return res
}

func (s *authzService) fromRolePermissionMappingList(ctx context.Context, r *authzpbv1.RolePermissionMappingList) ([][]string, error) {
	if len(s.mappingCache) == 0 {
		s.cacheResourceRolePermissions(ctx)
	}

	res := [][]string{}
	for i, mapping := range r.GetRolePermissionMappingList() {
		for _, permission := range mapping.GetPermission() {
			rules := [][]string{}
			for _, rpm := range s.mappingCache[permission] {
				for _, method := range rpm.methods {
					rule := []string{rpm.url, mapping.GetRole(), method}
					for _, field := range rule {
						if field == "" {
							return res, fmt.Errorf(fmt.Sprintf("index %d: mapping elements do not meet definition", i))
						}
					}
					rules = append(rules, rule)
				}
			}
			res = append(res, rules...)
		}
	}
	return res, nil
}

func (s *authzService) Enforce(ctx context.Context, req *authzpbv1.EnforceRequest) (*authzpbv1.BoolReply, error) {
	var param interface{}
	params := make([]interface{}, 0, len(req.Params))
	for index := range req.Params {
		param = req.Params[index]
		params = append(params, param)
	}

	res, err := s.enforcer.Enforce(params...)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, err.Error())
	}

	return &authzpbv1.BoolReply{Res: res}, nil
}

func (s *authzService) ListPolicies(ctx context.Context, p *authzpbv1.Policy) (*authzpbv1.Policies, error) {
	return s.toPolicies(s.enforcer.GetFilteredPolicy(0, p.GetSub(), p.GetNs(), p.GetProj(), p.GetOrg(), p.GetObj())), nil
}

func (s *authzService) CreatePolicies(ctx context.Context, p *authzpbv1.Policies) (*authzpbv1.BoolReply, error) {
	if len(p.GetPolicies()) == 0 {
		return &authzpbv1.BoolReply{Res: false}, nil
	}
	policies, err := s.fromPolicies(p)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, err.Error())
	}

	// err could be from db, policy assertions; dispatcher, watcher updates (not pertinent)
	res, err := s.enforcer.AddPolicies(policies)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &authzpbv1.BoolReply{Res: res}, nil
}

func (s *authzService) DeletePolicies(ctx context.Context, p *authzpbv1.Policy) (*authzpbv1.BoolReply, error) {
	// err could be from db, policy assertions, cache; dispatcher, watcher updates (not pertinent)
	res, err := s.enforcer.RemoveFilteredPolicy(0, p.GetSub(), p.GetNs(), p.GetProj(), p.GetOrg(), p.GetObj())
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &authzpbv1.BoolReply{Res: res}, nil
}

func (s *authzService) ListUserGroups(ctx context.Context, p *authzpbv1.UserGroup) (*authzpbv1.UserGroups, error) {
	return s.toUserGroups(s.enforcer.GetFilteredNamedGroupingPolicy(groupGtype, 0, p.GetUser(), p.GetGrp())), nil
}

func (s *authzService) CreateUserGroups(ctx context.Context, p *authzpbv1.UserGroups) (*authzpbv1.BoolReply, error) {
	if len(p.GetUserGroups()) == 0 {
		return &authzpbv1.BoolReply{Res: false}, nil
	}

	ugs, err := s.fromUserGroups(p)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, err.Error())
	}

	// err could be from db, policy assertions; dispatcher, watcher updates (not pertinent)
	res, err := s.enforcer.AddNamedGroupingPolicies(groupGtype, ugs)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	return &authzpbv1.BoolReply{Res: res}, nil
}

func (s *authzService) DeleteUserGroups(ctx context.Context, p *authzpbv1.UserGroup) (*authzpbv1.BoolReply, error) {
	// err could be from db, policy assertions, cache; dispatcher, watcher updates (not pertinent)
	res, err := s.enforcer.RemoveFilteredNamedGroupingPolicy(groupGtype, 0, p.GetUser(), p.GetGrp())
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	return &authzpbv1.BoolReply{Res: res}, nil
}

// NOTE: might need identifier per permission in list if inheritance is needed
func (s *authzService) ListRolePermissionMappings(ctx context.Context, p *authzpbv1.FilteredRolePermissionMapping) (*authzpbv1.RolePermissionMappingList, error) {
	// TODO: Change list of urls to permissions  (many to one)
	return s.toRolePermissionMappingList(s.enforcer.GetFilteredNamedGroupingPolicy(roleGtype, 0, p.GetRole())), nil
}

func (s *authzService) CreateRolePermissionMappings(ctx context.Context, p *authzpbv1.RolePermissionMappingList) (*authzpbv1.BoolReply, error) {
	if len(p.GetRolePermissionMappingList()) == 0 {
		return &authzpbv1.BoolReply{Res: false}, nil
	}

	rpms, err := s.fromRolePermissionMappingList(ctx, p)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	res, err := s.enforcer.AddNamedGroupingPolicies(roleGtype, rpms)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	return &authzpbv1.BoolReply{Res: res}, nil
}

func (s *authzService) DeleteRolePermissionMappings(ctx context.Context, p *authzpbv1.FilteredRolePermissionMapping) (*authzpbv1.BoolReply, error) {
	res, err := s.enforcer.RemoveFilteredNamedGroupingPolicy(roleGtype, 1, p.GetRole())
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	return &authzpbv1.BoolReply{Res: res}, nil
}
