package auth

import (
	"context"
	"errors"
	"fmt"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type PermissionCheckerCommonParam struct {
	// IdentityPlugin attributes
	IdentityF GetIdentityFunc
	// ResourcePlugin attributes
	PathPattern2ResourceMap map[string]string
	// PermissionPlugin attributes
	GoDDBPermitCheck bool
	// RequestPlugin attributes
	PathWhitelist []string
	// ResourceCascadePlugin attributes
	ResourceCascadeF CheckCascadeResourceFunc
	ResourceCascadeP ResourceCascadePlugin
	// create access param attributes
	GenAccessParam4GinF         GenAccessParam4GinFunc
	GenAccessParam4GrpcGatewayF GenAccessParam4GrpcGatewayFunc
	// the func to exec after permission check for gin/http
	HttpReqCheckRetHandleF PermissionCheckResultHandleFunc
	// Options attributes
	EnableLog            bool // enable the log print
	ClosePermissionCheck bool // close permission check, if set true, all request' permission can not be checked
}

func NewPermissionCheckerWithCommonParam(commonParam *PermissionCheckerCommonParam) (PermissionChecker, error) {
	if commonParam == nil {
		return nil, errors.New("commonParam is nil")
	}
	// identity
	if commonParam.IdentityF == nil {
		return nil, errors.New("IdentityF is nil")
	}
	identityH := commonParam.IdentityF

	// resource
	if len(commonParam.PathPattern2ResourceMap) == 0 {
		return nil, errors.New("PathPattern2ResourceMap is empty")
	}
	// use metadata store resource map
	_, err := validateAndFormatResourceMap(commonParam.PathPattern2ResourceMap)
	if err != nil {
		return nil, err
	}
	resourceH, err := NewMetadataStoreAllResourcePlugin(AccessMetadataResourceMapKey)
	if err != nil {
		return nil, err
	}

	// permission
	var permissionH PermissionPlugin
	if commonParam.GoDDBPermitCheck {
		permissionH = DefaultRegionApiKeyPermissionHelper
	} else {
		permissionH, _ = NewDefaultPermissionPlugin()
	}

	// request
	// use metadata store whitelist
	_, err = validateAndFormatWhitelist(commonParam.PathWhitelist...)
	if err != nil {
		return nil, err
	}
	requestH, err := NewMetadataStoreWhitelistRequestPlugin(AccessMetadataRequestWhitelistKey)
	if err != nil {
		return nil, err
	}
	var requestHs = []RequestPlugin{requestH}

	// resource cascade
	var resourceCascadeH ResourceCascadePlugin
	if commonParam.ResourceCascadeF != nil {
		resourceCascadeH = commonParam.ResourceCascadeF
	} else if commonParam.ResourceCascadeP != nil {
		resourceCascadeH = commonParam.ResourceCascadeP
	}

	// options
	options := PermissionCheckerOption{
		PermissionCheckClosed: commonParam.ClosePermissionCheck,
		EnableLog:             commonParam.EnableLog,
	}

	return NewPermissionChecker(identityH, resourceH, permissionH, requestHs, resourceCascadeH, options)
}

// --------- checker -------------------

type PermissionChecker interface {
	AccessDecisionHandler
}

type defaultPermissionChecker struct {
	Options          PermissionCheckerOption
	IdentityH        IdentityPlugin
	ResourceH        ResourcePlugin
	PermissionH      PermissionPlugin
	RequestHs        []RequestPlugin
	ResourceCascadeH ResourceCascadePlugin
}

func NewPermissionChecker(identityH IdentityPlugin,
	resourceH ResourcePlugin,
	permissionH PermissionPlugin,
	requestHs []RequestPlugin,
	resourceCascadeH ResourceCascadePlugin,
	options PermissionCheckerOption) (PermissionChecker, error) {
	if identityH == nil {
		return nil, errors.New("IdentityPlugin is nil")
	}
	if resourceH == nil {
		return nil, errors.New("ResourcePlugin is nil")
	}
	if permissionH == nil {
		return nil, errors.New("PermissionPlugin is nil")
	}

	checker := defaultPermissionChecker{
		Options:          options,
		IdentityH:        identityH,
		ResourceH:        resourceH,
		PermissionH:      permissionH,
		RequestHs:        []RequestPlugin{},
		ResourceCascadeH: resourceCascadeH,
	}

	if len(requestHs) > 0 {
		checker.RequestHs = append(checker.RequestHs, requestHs...)
	}

	return &checker, nil
}

func (d *defaultPermissionChecker) Handle(ctx context.Context, param *AccessParam) (*AccessResult, error) {
	if d.Options.EnableLog {
		tilog.Info(ctx, "authPermissionChecker start")
	}

	if param == nil || param.Metadata == nil {
		if d.Options.EnableLog {
			tilog.Warn(ctx, "authPermissionChecker error: access param invalid")
		}
		return nil, errors.New("access param invalid")
	}

	if d.Options.EnableLog {
		for k, v := range param.Metadata {
			tilog.Info(ctx, "authPermissionChecker metadata key value", zap.Any("key", k), zap.Any("value", v))
		}
	}

	if d.Options.PermissionCheckClosed {
		if d.Options.EnableLog {
			tilog.Info(ctx, "authPermissionChecker release: permission check switch not open")
		}
		return release("permission check switch not open", nil), nil
	}

	releaseRet, err := d.whetherReleaseRequest(ctx, param)
	if err != nil {
		if d.Options.EnableLog {
			tilog.Warn(ctx, "authPermissionChecker error: whetherReleaseRequest check error", zap.Any("err", err))
		}
		return nil, err
	}
	if releaseRet.Released {
		if d.Options.EnableLog {
			tilog.Info(ctx, fmt.Sprintf("authPermissionChecker release: %s", releaseRet.Message))
		}
		return release(releaseRet.Message, nil), nil
	}

	identityRet, err := d.IdentityH.GetIdentity(ctx, param)
	if d.Options.EnableLog {
		tilog.Info(ctx, "authPermissionChecker GetIdentity result", zap.Any("identity", identityRet), zap.Any("err", err))
	}
	if err != nil {
		if d.Options.EnableLog {
			tilog.Warn(ctx, "authPermissionChecker error: GetIdentity error", zap.Any("err", err))
		}
		return nil, err
	}

	resourceRet, err := d.ResourceH.GetResource(ctx, param)
	if d.Options.EnableLog {
		tilog.Info(ctx, "authPermissionChecker GetResource result", zap.Any("resource", resourceRet), zap.Any("err", err))
	}
	if err != nil {
		if d.Options.EnableLog {
			tilog.Warn(ctx, "authPermissionChecker error: GetResource error", zap.Any("err", err))
		}
		return nil, err
	}

	cascadeRet, err := whetherCascadeResourceMatch(ctx, d.ResourceCascadeH, resourceRet)
	if err != nil {
		if d.Options.EnableLog {
			tilog.Warn(ctx, "authPermissionChecker error: whetherCascadeResourceMatch check error", zap.Any("resource", resourceRet), zap.Any("err", err))
		}
		return nil, err
	}
	if !cascadeRet.Matched {
		if d.Options.EnableLog {
			tilog.Info(ctx, "authPermissionChecker intercept: whetherCascadeResourceMatch check not matched", zap.Any("resource", resourceRet), zap.Any("cascadeRet", cascadeRet))
		}
		return intercept(cascadeRet.Message, nil), nil
	}

	permissionParam := &PermissionParam{
		IdentityType: identityRet.IdentityType,
		IdentityVal:  identityRet.IdentityValue,
		ResourceUri:  resourceRet.ResourceUri,
		ReqUri:       resourceRet.ReqUri,
	}
	permissionRet, err := d.PermissionH.HasPermission(ctx, permissionParam)
	if d.Options.EnableLog {
		tilog.Info(ctx, "authPermissionChecker HasPermission result", zap.Any("permissionParam", permissionParam), zap.Any("permissionRet", permissionRet), zap.Any("err", err))
	}
	if err != nil {
		if d.Options.EnableLog {
			tilog.Warn(ctx, "authPermissionChecker error: HasPermission check error", zap.Any("permissionParam", permissionParam), zap.Any("err", err))
		}
		return nil, err
	}

	data := genReturnData(permissionParam)

	if permissionRet.HasPermission {
		tilog.Info(ctx, fmt.Sprintf("authPermissionChecker release: %s", permissionRet.Message), zap.Any("permissionParam", permissionParam))
		return release("identity permission check passed", data), nil
	}

	tilog.Info(ctx, fmt.Sprintf("authPermissionChecker intercept: %s", permissionRet.Message), zap.Any("permissionParam", permissionParam))
	return intercept(fmt.Sprintf("identity has no permission: %s", permissionRet.Message), data), nil
}

func whetherCascadeResourceMatch(ctx context.Context, resourceCascadeH ResourceCascadePlugin, resourceRet *ResourceRet) (*ResourceCascadeRet, error) {
	if resourceCascadeH == nil {
		ret := ResourceCascadeRet{
			Matched: true,
			Message: "not set resource cascade plugin",
		}
		return &ret, nil
	}
	cascadeParam := ResourceCascadeParam{
		ReqUri: resourceRet.ReqUri,
	}
	cascadeRet, err := resourceCascadeH.CheckCascadeResource(ctx, &cascadeParam)
	if err != nil {
		return nil, err
	}
	return cascadeRet, nil

}

func (d *defaultPermissionChecker) whetherReleaseRequest(ctx context.Context, param *AccessParam) (*RequestRet, error) {
	if d.RequestHs != nil && len(d.RequestHs) > 0 {
		for _, h := range d.RequestHs {
			ret, err := h.ReleaseRequest(ctx, param)
			if err != nil {
				return ret, err
			}
			if ret.Released {
				return ret, nil
			}
		}
	}
	ret := RequestRet{
		Released: false,
		Message:  "not hit release logic",
	}
	return &ret, nil
}

func genReturnData(p *PermissionParam) map[string]string {
	return map[string]string{
		"identity_tye": string(p.IdentityType),
		"identity_val": p.IdentityVal,
		"resource_uri": p.ResourceUri,
		"req_uri":      p.ReqUri,
	}
}

func release(msg string, data interface{}) *AccessResult {
	return &AccessResult{
		Code: AccessRetRelease,
		Msg:  msg,
		Data: data,
	}
}

func intercept(msg string, data interface{}) *AccessResult {
	return &AccessResult{
		Code: AccessRetIntercept,
		Msg:  msg,
		Data: data,
	}
}
