// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package models

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/tristate"
	"yunion.io/x/pkg/util/netutils"
	"yunion.io/x/pkg/util/rbacscope"
	"yunion.io/x/pkg/utils"
	"yunion.io/x/sqlchemy"

	api "yunion.io/x/onecloud/pkg/apis/identity"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/keystone/options"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/auth"
	"yunion.io/x/onecloud/pkg/util/rbacutils"
	"yunion.io/x/onecloud/pkg/util/stringutils2"
)

type SRolePolicyManager struct {
	db.SResourceBaseManager
}

var RolePolicyManager *SRolePolicyManager

func init() {
	RolePolicyManager = &SRolePolicyManager{
		SResourceBaseManager: db.NewResourceBaseManager(
			SRolePolicy{},
			"rolepolicy_tbl",
			"rolepolicy",
			"rolepolicies",
		),
	}
	RolePolicyManager.SetVirtualObject(RolePolicyManager)
}

type SRolePolicy struct {
	db.SResourceBase

	// 角色ID, 主键
	RoleId string `width:"128" charset:"ascii" primary:"true" list:"domain" create:"domain_optional"`
	// 项目ID，主键
	ProjectId string `width:"128" charset:"ascii" primary:"true" list:"domain" create:"domain_optional"`
	// 权限ID, 主键
	PolicyId string `width:"128" charset:"ascii" primary:"true" list:"domain" create:"domain_required"`
	// 是否需要认证
	Auth tristate.TriState `default:"true" list:"domain" create:"domain_optional"`
	// 匹配的IP白名单
	Ips string `list:"domain" create:"domain_optional" update:"domain"`
	// 匹配开始时间
	ValidSince time.Time `list:"domain" create:"domain_optional" update:"domain"`
	// 匹配结束时间
	ValidUntil time.Time `list:"domain" create:"domain_optional" update:"domain"`
}

func (manager *SRolePolicyManager) newRecord(ctx context.Context, roleId, projectId, policyId string, auth tristate.TriState, ips []netutils.IPV4Prefix, validSince, validUntil time.Time) error {
	if len(roleId) == 0 {
		return errors.Wrap(httperrors.ErrNotEmpty, "roleId")
	}
	if len(policyId) == 0 {
		return errors.Wrap(httperrors.ErrNotEmpty, "policyId")
	}
	rpg := SRolePolicy{}
	rpg.RoleId = roleId
	rpg.ProjectId = projectId
	rpg.PolicyId = policyId
	rpg.Auth = auth
	ipStrs := make([]string, len(ips))
	for i, ipprefix := range ips {
		ipStrs[i] = ipprefix.String()
	}
	rpg.Ips = strings.Join(ipStrs, rbacutils.IP_PREFIX_SEP)
	rpg.ValidSince = validSince
	rpg.ValidUntil = validUntil
	rpg.SetModelManager(manager, &rpg)
	err := RolePolicyManager.TableSpec().InsertOrUpdate(ctx, &rpg)
	if err != nil {
		log.Errorf("insert role policy fail %s", err)
		return errors.Wrap(err, "insert role policy")
	}
	return nil
}

func (manager *SRolePolicyManager) deleteRecord(ctx context.Context, roleId, projectId, policyId string) error {
	rpg := SRolePolicy{}
	rpg.RoleId = roleId
	rpg.ProjectId = projectId
	rpg.PolicyId = policyId
	rpg.SetModelManager(manager, &rpg)
	_, err := db.Update(&rpg, func() error {
		return rpg.MarkDelete()
	})
	if err != nil && errors.Cause(err) != sql.ErrNoRows {
		return errors.Wrap(err, "Update")
	}
	return nil
}

func (rp *SRolePolicy) GetId() string {
	return fmt.Sprintf("%s:%s:%s", rp.RoleId, rp.ProjectId, rp.PolicyId)
}

func (rp *SRolePolicy) GetName() string {
	return getRolePolicyName(rp.GetRole(), rp.GetProject(), rp.GetPolicy())
}

func getRolePolicyName(role *SRole, project *SProject, policy *SPolicy) string {
	names := make([]string, 0)
	if role != nil {
		names = append(names, role.GetName())
	}
	if project != nil {
		names = append(names, project.GetName())
	}
	if policy != nil {
		names = append(names, policy.GetName())
	}
	return strings.Join(names, "/")
}

func (rp *SRolePolicy) GetRole() *SRole {
	role, err := RoleManager.FetchById(rp.RoleId)
	if err != nil {
		log.Errorf("RoleManaget.FetchById %s fail %s", rp.RoleId, err)
		return nil
	}
	return role.(*SRole)
}

func (rp *SRolePolicy) GetProject() *SProject {
	if len(rp.ProjectId) == 0 {
		return nil
	}
	project, err := ProjectManager.FetchById(rp.ProjectId)
	if err != nil {
		log.Errorf("ProjectManager.FetchById %s fail %s", rp.ProjectId, err)
		return nil
	}
	return project.(*SProject)
}

func (rp *SRolePolicy) GetPolicy() *SPolicy {
	policy, err := PolicyManager.FetchById(rp.PolicyId)
	if err != nil {
		log.Errorf("PolicyManaget.FetchById %s fail %s", rp.PolicyId, err)
		return nil
	}
	return policy.(*SPolicy)
}

func (manager *SRolePolicyManager) NamespaceScope() rbacscope.TRbacScope {
	return PolicyManager.NamespaceScope()
}

func (manager *SRolePolicyManager) FilterByOwner(ctx context.Context, q *sqlchemy.SQuery, man db.FilterByOwnerProvider, userCred mcclient.TokenCredential, owner mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery {
	policyQ := PolicyManager.Query()
	policyQ = PolicyManager.FilterByOwner(ctx, policyQ, PolicyManager, userCred, owner, scope)
	subq := policyQ.SubQuery()
	q = q.Join(subq, sqlchemy.Equals(q.Field("policy_id"), subq.Field("id")))
	return q
}

func (manager *SRolePolicyManager) ListItemFilter(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.RolePolicyListInput,
) (*sqlchemy.SQuery, error) {
	var err error
	q, err = manager.SResourceBaseManager.ListItemFilter(ctx, q, userCred, query.ResourceBaseListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SResourceBaseManager.ListItemFilter")
	}
	if len(query.RoleIds) > 0 {
		for i := range query.RoleIds {
			role, err := RoleManager.FetchByIdOrName(ctx, userCred, query.RoleIds[i])
			if err != nil {
				if errors.Cause(err) == sql.ErrNoRows {
					return nil, errors.Wrapf(httperrors.ErrResourceNotFound, "%s %s", RoleManager.Keyword(), query.RoleIds[i])
				} else {
					return nil, errors.Wrap(err, "RoleManager.FetchByIdOrName")
				}
			}
			query.RoleIds[i] = role.GetId()
		}
		q = q.Filter(sqlchemy.OR(
			sqlchemy.IsNullOrEmpty(q.Field("role_id")),
			sqlchemy.In(q.Field("role_id"), query.RoleIds),
		))
	}
	if len(query.ProjectId) > 0 {
		project, err := ProjectManager.FetchByIdOrName(ctx, userCred, query.ProjectId)
		if err != nil {
			if errors.Cause(err) == sql.ErrNoRows {
				return nil, errors.Wrapf(httperrors.ErrResourceNotFound, "%s %s", ProjectManager.Keyword(), query.ProjectId)
			} else {
				return nil, errors.Wrap(err, "ProjectManager.FetchByIdOrName")
			}
		}
		q = q.Filter(sqlchemy.OR(
			sqlchemy.IsNullOrEmpty(q.Field("project_id")),
			sqlchemy.Equals(q.Field("project_id"), project.GetId()),
		))
	}
	if len(query.PolicyId) > 0 {
		policy, err := PolicyManager.FetchByIdOrName(ctx, userCred, query.PolicyId)
		if err != nil {
			if errors.Cause(err) == sql.ErrNoRows {
				return nil, errors.Wrapf(httperrors.ErrResourceNotFound, "%s %s", PolicyManager.Keyword(), query.PolicyId)
			} else {
				return nil, errors.Wrap(err, "PolicyManager.FetchByIdOrName")
			}
		}
		q = q.Filter(sqlchemy.OR(
			sqlchemy.IsNullOrEmpty(q.Field("policy_id")),
			sqlchemy.Equals(q.Field("policy_id"), policy.GetId()),
		))
	}
	if query.Auth != nil {
		if *query.Auth {
			q = q.IsTrue("auth")
		} else {
			q = q.IsFalse("auth")
		}
	}
	return q, nil
}

func (manager *SRolePolicyManager) OrderByExtraFields(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.RolePolicyListInput,
) (*sqlchemy.SQuery, error) {
	var err error

	q, err = manager.SResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.ResourceBaseListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SResourceBaseManager.OrderByExtraFields")
	}

	return q, nil
}

func (manager *SRolePolicyManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
	var err error

	q, err = manager.SResourceBaseManager.QueryDistinctExtraField(q, field)
	if err == nil {
		return q, nil
	}

	return q, httperrors.ErrNotFound
}

func (manager *SRolePolicyManager) FetchCustomizeColumns(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	objs []interface{},
	fields stringutils2.SSortedStrings,
	isList bool,
) []api.RolePolicyDetails {
	rows := make([]api.RolePolicyDetails, len(objs))
	resRows := manager.SResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	roleIds := make([]string, 0)
	projectIds := make([]string, 0)
	policyIds := make([]string, 0)
	for i := range rows {
		rows[i] = api.RolePolicyDetails{
			ResourceBaseDetails: resRows[i],
		}
		rp := objs[i].(*SRolePolicy)
		roleIds = append(roleIds, rp.RoleId)
		projectIds = append(projectIds, rp.ProjectId)
		policyIds = append(policyIds, rp.PolicyId)
	}

	roleMap := make(map[string]SRole)
	err := db.FetchModelObjectsByIds(RoleManager, "id", roleIds, &roleMap)
	if err != nil {
		log.Errorf("db.FetchModelObjectsByIds RoleManager fail %s", err)
		return rows
	}
	projectMap := make(map[string]SProject)
	err = db.FetchModelObjectsByIds(ProjectManager, "id", projectIds, &projectMap)
	if err != nil {
		log.Errorf("db.FetchModelObjectsByIds ProjectManager fail %s", err)
		return rows
	}
	policyMap := make(map[string]SPolicy)
	err = db.FetchModelObjectsByIds(PolicyManager, "id", policyIds, &policyMap)
	if err != nil {
		log.Errorf("db.FetchModelObjectsByIds PolicyManager fail %s", err)
		return rows
	}

	for i := range rows {
		rp := objs[i].(*SRolePolicy)
		var role *SRole
		if obj, ok := roleMap[rp.RoleId]; ok {
			role = &obj
		}
		var project *SProject
		if obj, ok := projectMap[rp.ProjectId]; ok {
			project = &obj
		}
		var policy *SPolicy
		if obj, ok := policyMap[rp.PolicyId]; ok {
			policy = &obj
		}
		rows[i].Id = rp.GetId()
		rows[i].Name = getRolePolicyName(role, project, policy)
		if role != nil {
			rows[i].Role = role.GetName()
		}
		if project != nil {
			rows[i].Project = project.GetName()
		}
		if policy != nil {
			rows[i].Policy = policy.GetName()
			rows[i].Scope = policy.Scope
			rows[i].Description = policy.Description
		}
	}

	return rows
}

func (manager *SRolePolicyManager) getMatchPolicyIds(userCred rbacutils.IRbacIdentity, tm time.Time) ([]string, error) {
	isGuest := true
	if userCred != nil && len(userCred.GetProjectId()) > 0 && len(userCred.GetRoleIds()) > 0 && !auth.IsGuestToken(userCred) {
		isGuest = false
	}
	return manager.getMatchPolicyIds2(isGuest, userCred.GetRoleIds(), userCred.GetProjectId(), userCred.GetLoginIp(), tm)
}

func (manager *SRolePolicyManager) getMatchPolicyIds2(isGuest bool, roleIds []string, pid string, loginIp string, tm time.Time) ([]string, error) {
	q := manager.Query()
	if !isGuest {
		if len(roleIds) > 0 {
			q = q.Filter(sqlchemy.OR(
				sqlchemy.IsNullOrEmpty(q.Field("role_id")),
				sqlchemy.In(q.Field("role_id"), roleIds),
			))
		}
		if len(pid) > 0 {
			q = q.Filter(sqlchemy.OR(
				sqlchemy.IsNullOrEmpty(q.Field("project_id")),
				sqlchemy.Equals(q.Field("project_id"), pid),
			))
		}
	} else {
		q = q.IsFalse("auth")
	}
	rps := make([]SRolePolicy, 0)
	err := db.FetchModelObjects(manager, q, &rps)
	if err != nil && errors.Cause(err) != sql.ErrNoRows {
		return nil, errors.Wrap(err, "FetchPolicies")
	}
	policyIds := stringutils2.NewSortedStrings(nil)
	// filter by login IP
	for _, rp := range rps {
		if len(loginIp) > 0 && !rp.MatchIP(loginIp) {
			continue
		}
		if !tm.IsZero() && !rp.MatchTime(tm) {
			continue
		}
		policyIds = stringutils2.Append(policyIds, rp.PolicyId)
	}
	return policyIds, nil
}

func appendPolicy(ctx context.Context, names map[rbacscope.TRbacScope][]string, policies rbacutils.TPolicyGroup, scope rbacscope.TRbacScope, policyName string, nameOnly bool) (map[rbacscope.TRbacScope][]string, rbacutils.TPolicyGroup, error) {
	if utils.IsInStringArray(policyName, names[scope]) {
		return names, policies, nil
	}
	policyObj, err := PolicyManager.FetchByName(ctx, nil, policyName)
	if err != nil && errors.Cause(err) != sql.ErrNoRows {
		return nil, nil, errors.Wrapf(err, "FetchPolicy %s", policyName)
	}
	if policyObj == nil {
		// empty policy
		return names, policies, nil
	}
	names[scope] = append(names[scope], policyName)
	if !nameOnly {
		data, err := policyObj.(*SPolicy).getPolicy()
		if err != nil {
			return nil, nil, errors.Wrap(err, "getPolicy")
		}
		policies[scope] = append(policies[scope], *data)
	}
	return names, policies, nil
}

func (manager *SRolePolicyManager) GetMatchPolicyGroup(userCred rbacutils.IRbacIdentity, tm time.Time, nameOnly bool) (map[rbacscope.TRbacScope][]string, rbacutils.TPolicyGroup, error) {
	policyIds, err := manager.getMatchPolicyIds(userCred, tm)
	if err != nil {
		return nil, nil, errors.Wrap(err, "getMatchPolicyIds")
	}
	return manager.GetPolicyGroupByIds(policyIds, nameOnly)
}

type sUserProjectPair struct {
	userId    string
	projectId string
}

func (p sUserProjectPair) GetProjectId() string {
	return p.projectId
}

func (p sUserProjectPair) GetRoleIds() []string {
	if len(p.userId) == 0 || len(p.projectId) == 0 {
		return nil
	}
	roles, _ := AssignmentManager.FetchUserProjectRoles(p.userId, p.projectId)
	ret := make([]string, len(roles))
	for i := range roles {
		ret[i] = roles[i].Id
	}
	return ret
}

func (p sUserProjectPair) GetUserId() string {
	return p.userId
}

func (p sUserProjectPair) GetLoginIp() string {
	return ""
}

func (p sUserProjectPair) GetTokenString() string {
	if len(p.userId) == 0 {
		return auth.GUEST_TOKEN
	}
	return p.userId
}

func (manager *SRolePolicyManager) GetMatchPolicyGroupByInput(ctx context.Context, userId, projectId string, tm time.Time, nameOnly bool) (map[rbacscope.TRbacScope][]string, rbacutils.TPolicyGroup, error) {
	return manager.GetMatchPolicyGroupByCred(
		ctx,
		sUserProjectPair{
			userId:    userId,
			projectId: projectId,
		},
		tm, nameOnly,
	)
}

func (manager *SRolePolicyManager) GetMatchPolicyGroupByCred(ctx context.Context, userCred api.IRbacIdentityWithUserId, tm time.Time, nameOnly bool) (map[rbacscope.TRbacScope][]string, rbacutils.TPolicyGroup, error) {
	names, policies, err := manager.GetMatchPolicyGroup(userCred, tm, nameOnly)
	if err != nil {
		return nil, nil, errors.Wrap(err, "GetMatchPolicyGroup")
	}
	if !options.Options.EnableDefaultDashboardPolicy {
		return names, policies, nil
	}
	userId := userCred.GetUserId()
	if len(userId) == 0 {
		// anonymous access
		log.Debugf("anomymouse accessed policies: %s", jsonutils.Marshal(names))
		return names, policies, nil
	}
	usr, err := UserManager.fetchUserById(userId)
	if err != nil {
		return nil, nil, errors.Wrapf(err, "user id %s of userCred not found", userId)
	}
	// append dashboard policy only when there are matched policies
	if len(names) > 0 && usr.AllowWebConsole.IsTrue() {
		// add web console policy
		for scope := range names {
			consolePolicyName := ""
			switch scope {
			case rbacscope.ScopeSystem:
				consolePolicyName = options.Options.SystemDashboardPolicy
			case rbacscope.ScopeDomain:
				consolePolicyName = options.Options.DomainDashboardPolicy
			case rbacscope.ScopeProject:
				consolePolicyName = options.Options.ProjectDashboardPolicy
			}
			if len(consolePolicyName) == 0 {
				continue
			}
			names, policies, err = appendPolicy(ctx, names, policies, scope, consolePolicyName, nameOnly)
			if err != nil {
				return nil, nil, errors.Wrapf(err, "appendConsolePolicy %s %s", scope, consolePolicyName)
			}
		}
	}
	return names, policies, nil
}

func (manager *SRolePolicyManager) GetMatchPolicyGroup2(isGuest bool, roleIds []string, pid string, loginIp string, tm time.Time, nameOnly bool) (map[rbacscope.TRbacScope][]string, rbacutils.TPolicyGroup, error) {
	policyIds, err := manager.getMatchPolicyIds2(isGuest, roleIds, pid, loginIp, tm)
	if err != nil {
		return nil, nil, errors.Wrap(err, "getMatchPolicyIds")
	}
	return manager.GetPolicyGroupByIds(policyIds, nameOnly)
}

func (manager *SRolePolicyManager) GetPolicyGroupByIds(policyIds []string, nameOnly bool) (map[rbacscope.TRbacScope][]string, rbacutils.TPolicyGroup, error) {
	names := make(map[rbacscope.TRbacScope][]string)
	var group rbacutils.TPolicyGroup
	if !nameOnly {
		group = rbacutils.TPolicyGroup{}
	}
	for _, id := range policyIds {
		policyObj, err := PolicyManager.FetchById(id)
		if err != nil {
			if errors.Cause(err) == sql.ErrNoRows {
				continue
			}
			return nil, nil, errors.Wrapf(err, "FetchPolicy %s", id)
		}
		policy := policyObj.(*SPolicy)
		if policy.Enabled.IsFalse() {
			// skip disabled policy
			continue
		}
		if scopeName, ok := names[policy.Scope]; !ok {
			names[policy.Scope] = []string{policy.Name}
		} else {
			names[policy.Scope] = append(scopeName, policy.Name)
		}
		if !nameOnly {
			data, err := policy.getPolicy()
			if err != nil {
				return nil, nil, errors.Wrap(err, "getPolicy")
			}
			if set, ok := group[policy.Scope]; !ok {
				group[policy.Scope] = rbacutils.TPolicySet{*data}
			} else {
				group[policy.Scope] = append(set, *data)
			}
		}
	}
	return names, group, nil
}

func (rp *SRolePolicy) MatchIP(ipstr string) bool {
	result := rbacutils.MatchIPStrings(rp.Ips, ipstr)
	return result
}

func (rp *SRolePolicy) MatchTime(tm time.Time) bool {
	if !rp.ValidSince.IsZero() && tm.Before(rp.ValidSince) {
		return false
	}
	if !rp.ValidUntil.IsZero() && tm.After(rp.ValidUntil) {
		return false
	}
	return true
}

func (manager *SRolePolicyManager) fetchByRoleId(roleId string) ([]SRolePolicy, error) {
	q := manager.Query().Equals("role_id", roleId)
	rps := make([]SRolePolicy, 0)
	err := db.FetchModelObjects(manager, q, &rps)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModelObjects")
	}
	return rps, nil
}

func (manager *SRolePolicyManager) fetchByPolicyId(policyId string) ([]SRolePolicy, error) {
	q := manager.Query().Equals("policy_id", policyId)
	rps := make([]SRolePolicy, 0)
	err := db.FetchModelObjects(manager, q, &rps)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModelObjects")
	}
	return rps, nil
}

func (manager *SRolePolicyManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
	parts := strings.Split(idStr, ":")
	if len(parts) == 3 {
		return q.Equals("role_id", parts[0]).Equals("project_id", parts[1]).Equals("policy_id", parts[2])
	} else {
		return q.Equals("ips", idStr)
	}
}

func (manager *SRolePolicyManager) FilterByNotId(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
	parts := strings.Split(idStr, ":")
	if len(parts) == 3 {
		return q.Filter(sqlchemy.OR(
			sqlchemy.NotEquals(q.Field("role_id"), parts[0]),
			sqlchemy.NotEquals(q.Field("project_id"), parts[1]),
			sqlchemy.NotEquals(q.Field("policy_id"), parts[2]),
		))
	} else {
		return q
	}
}

func (manager *SRolePolicyManager) FilterByName(q *sqlchemy.SQuery, name string) *sqlchemy.SQuery {
	return manager.FilterById(q, name)
}

func (manager *SRolePolicyManager) isBootstrapRolePolicy() (bool, error) {
	cnt, err := manager.Query().CountWithError()
	if err != nil {
		return false, errors.Wrap(err, "CountWithError")
	}
	return (cnt == 0), nil
}
