package rbac

import (
	"context"
	"errors"

	"github.com/tidbcloud/tiinf/sdk/rbac/common"
)

type PermissionChecker interface {
	common.AccessDecisionHandler
	SetOpened(opened bool)
	RegisterRequestHelper(requestH common.RequestHelper)
	SetResourceCascadeChecker(resourceCascadeH common.ResourceCascadeChecker)
}

type defaultPermissionChecker struct {
	opened           bool
	identityH        common.IdentityHelper
	resourceH        common.ResourceHelper
	permissionH      common.PermissionHelper
	requestHs        []common.RequestHelper
	resourceCascadeH common.ResourceCascadeChecker
}

func NewPermissionChecker(
	identityH common.IdentityHelper,
	resourceH common.ResourceHelper,
	permissionH common.PermissionHelper) PermissionChecker {

	if identityH == nil {
		panic("IdentityHelper is nil")
	}
	if resourceH == nil {
		panic("ResourceHelper is nil")
	}
	if permissionH == nil {
		panic("PermissionHelper is nil")
	}

	handler := defaultPermissionChecker{
		opened:      true,
		identityH:   identityH,
		resourceH:   resourceH,
		permissionH: permissionH,
		requestHs:   []common.RequestHelper{},
	}

	return &handler
}

func (d *defaultPermissionChecker) SetOpened(opened bool) {
	d.opened = opened
}

func (d *defaultPermissionChecker) RegisterRequestHelper(requestH common.RequestHelper) {
	if requestH != nil {
		d.requestHs = append(d.requestHs, requestH)
	}
}

func (d *defaultPermissionChecker) SetResourceCascadeChecker(resourceCascadeH common.ResourceCascadeChecker) {
	if resourceCascadeH != nil {
		d.resourceCascadeH = resourceCascadeH
	}
}

func (d *defaultPermissionChecker) Handle(ctx context.Context, param *common.AccessParam) (*common.AccessResult, error) {
	if param == nil || param.Metadata == nil {
		return nil, errors.New("access param invalid")
	}
	if !d.opened {
		return release("permission check switch not open"), nil
	}
	released, err := d.whetherReleaseRequest(ctx, param)
	if err != nil {
		return nil, err
	}
	if released {
		return release("hit release request white list"), nil
	}
	identity, err := d.identityH.GetIdentity(ctx, param)
	if err != nil {
		return nil, err
	}
	resource, err := d.resourceH.GetResource(ctx, param)
	if err != nil {
		return nil, err
	}
	// req path not match path pattern
	if resource == "" {
		return pathNotMathWithData("path not match", identity, resource), nil
	}
	matched, msg, err := whetherCascadeResourceMatch(ctx, d.resourceCascadeH, resource)
	if err != nil {
		return nil, err
	}
	if !matched {
		return interceptWithData(msg, identity, resource), nil
	}
	hasPermit, err := d.permissionH.HasPermission(ctx, identity, resource)
	if err != nil {
		return nil, err
	}
	if hasPermit {
		return releaseWithData("identity permission check passed", identity, resource), nil
	}
	return interceptWithData("identity has no permission", identity, resource), nil

}

func whetherCascadeResourceMatch(ctx context.Context, resourceCascadeH common.ResourceCascadeChecker, resource string) (matched bool, msg string, err error) {
	if resourceCascadeH == nil {
		return true, "no resource cascade checker", nil
	}
	cascadeParam := common.ResourceCascadeParam{
		ResourceUri: resource,
	}
	cascadeRet, err := resourceCascadeH.CheckCascadeResource(ctx, &cascadeParam)
	if err != nil {
		return false, "", err
	}
	return cascadeRet.Matched, cascadeRet.Msg, nil

}

func (d *defaultPermissionChecker) whetherReleaseRequest(ctx context.Context, param *common.AccessParam) (bool, error) {
	if d.requestHs != nil && len(d.requestHs) > 0 {
		for _, helper := range d.requestHs {
			released, err := helper.ReleaseRequest(ctx, param)
			if err != nil {
				return false, err
			}
			if released {
				return true, nil
			}
		}
	}
	return false, nil
}

func release(msg string) *common.AccessResult {
	return &common.AccessResult{
		Code: common.RSP_CODE_RELEASE,
		Msg:  msg,
		Data: nil,
	}
}

func intercept(msg string) *common.AccessResult {
	return &common.AccessResult{
		Code: common.RSP_CODE_INTERCEPT,
		Msg:  msg,
		Data: nil,
	}
}

func releaseWithData(msg string, identity, resource string) *common.AccessResult {
	return &common.AccessResult{
		Code: common.RSP_CODE_RELEASE,
		Msg:  msg,
		Data: map[string]string{
			"identity": identity,
			"resource": resource,
		},
	}
}

func interceptWithData(msg string, identity, resource string) *common.AccessResult {
	return &common.AccessResult{
		Code: common.RSP_CODE_INTERCEPT,
		Msg:  msg,
		Data: map[string]string{
			"identity": identity,
			"resource": resource,
		},
	}
}

func pathNotMathWithData(msg string, identity, resource string) *common.AccessResult {
	return &common.AccessResult{
		Code: common.RSP_CODE_PATH_NOT_MATCH,
		Msg:  msg,
		Data: map[string]string{
			"identity": identity,
			"resource": resource,
		},
	}
}
