package helperimpl

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"sync"
	"time"

	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/client"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/rbac/common"

	"github.com/patrickmn/go-cache"

	"google.golang.org/grpc/credentials/insecure"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"google.golang.org/grpc"
)

var (
	globalClient     account.AccountAPIServiceClient
	globalClientInit sync.Once
)

var DefaultCachedPermissionHelper = NewCachedPermissionHelper()
var DefaultApiKeyPermissionHelper = NewApiKeyPermissionHelper()

type AttributeSettablePermissionHelper interface {
	common.PermissionHelper
	SetReadTimeout(timeout time.Duration)
	SetEndpoint(endpoint string)
}

type defaultPermissionHelper struct {
	endpoint    string
	readTimeout time.Duration
}

func NewDefaultPermissionHelper() AttributeSettablePermissionHelper {
	return &defaultPermissionHelper{
		endpoint:    common.DEFAULT_ENDPOINT,
		readTimeout: common.DEFAULT_TIMEOUT,
	}
}

func (d *defaultPermissionHelper) SetEndpoint(endpoint string) {
	if endpoint != "" {
		d.endpoint = endpoint
	}
}

func (d *defaultPermissionHelper) SetReadTimeout(timeout time.Duration) {
	d.readTimeout = timeout
}

func (d *defaultPermissionHelper) HasPermission(ctx context.Context, identity, resource string) (bool, error) {
	identInt, err := strconv.ParseUint(identity, 10, 64)
	if err != nil {
		return false, errors.New(fmt.Sprintf("defaultPermissionHelper: %s", err.Error()))
	}
	req := account.RbacPermissionRequest{
		UserId:      identInt,
		ResourceUri: resource,
	}

	timeout := d.readTimeout
	if timeout == 0 {
		timeout = common.DEFAULT_TIMEOUT
	}
	tctx, cf := context.WithTimeout(context.Background(), timeout)
	defer cf()

	rsp, err := d.getClient().HasPermissionForUser(tctx, &req)
	if err != nil {
		return false, errors.New(fmt.Sprintf("defaultPermissionHelper: %s", err.Error()))
	}
	if !rsp.AuthResult {
		tilog.Info(ctx, fmt.Sprintf("defaultPermissionHelper: has no permission: %s", rsp.BaseResp.ErrMsg))
	}
	return rsp.AuthResult, nil
}

func SetGlobalClient(accountClient *account.AccountAPIServiceClient) {
	globalClientInit.Do(func() {
		tilog.PlainInfo("Set SetGlobalClient for account-provider-client")
	})
	globalClient = *accountClient
}

func (d *defaultPermissionHelper) getClient() account.AccountAPIServiceClient {
	globalClientInit.Do(func() {
		conn, err := grpc.Dial(d.endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
		if err != nil {
			panic(fmt.Sprintf("defaultPermissionHelper: dial account-svc error: %s", err))
		}
		globalClient = account.NewAccountAPIServiceClient(conn)
	})
	return globalClient
}

type CachePermissionHelper interface {
	AttributeSettablePermissionHelper
	SetUseCache(useCache bool)
	SetCacheExpiration(expiration time.Duration)
}

type cachedPermissionHelper struct {
	helper     AttributeSettablePermissionHelper
	useCache   bool
	cache      *cache.Cache
	expiration time.Duration
}

func NewCachedPermissionHelper() CachePermissionHelper {
	return &cachedPermissionHelper{
		helper:     NewDefaultPermissionHelper(),
		useCache:   false,
		cache:      cache.New(5*time.Minute, 60*time.Second),
		expiration: 0,
	}
}

func (d *cachedPermissionHelper) SetEndpoint(endpoint string) {
	if endpoint != "" {
		d.helper.SetEndpoint(endpoint)
	}
}

func (d *cachedPermissionHelper) SetReadTimeout(timeout time.Duration) {
	d.helper.SetReadTimeout(timeout)
}

func (d *cachedPermissionHelper) SetUseCache(useCache bool) {
	d.useCache = useCache
}

func (d *cachedPermissionHelper) SetCacheExpiration(expiration time.Duration) {
	d.expiration = expiration
}

func (d *cachedPermissionHelper) HasPermission(ctx context.Context, identity, resource string) (bool, error) {
	if !d.useCache {
		return d.helper.HasPermission(ctx, identity, resource)
	}
	key := fmt.Sprintf(common.PERMISSION_CACHE_KEY_FORMATTER, identity, resource)
	value, _ := d.cache.Get(key)
	if value != nil {
		if hasPermission, ok := value.(bool); ok {
			return hasPermission, nil
		}
	}
	hasPermission, err := d.helper.HasPermission(ctx, identity, resource)
	if err != nil {
		return false, err
	}
	d.cache.Set(key, hasPermission, d.expiration)
	return hasPermission, nil
}

type apiKeyPermissionHelper struct {
	endpoint    string
	readTimeout time.Duration
}

func NewApiKeyPermissionHelper() AttributeSettablePermissionHelper {
	return &apiKeyPermissionHelper{
		endpoint:    common.DEFAULT_ENDPOINT,
		readTimeout: common.DEFAULT_TIMEOUT,
	}
}

func (h *apiKeyPermissionHelper) SetEndpoint(endpoint string) {
	if endpoint != "" {
		h.endpoint = endpoint
	}
}

func (h *apiKeyPermissionHelper) SetReadTimeout(timeout time.Duration) {
	h.readTimeout = timeout
}

func (h *apiKeyPermissionHelper) HasPermission(ctx context.Context, identity, resource string) (bool, error) {
	if identity == "" {
		return false, errors.New("identity is blank")
	}
	if resource == "" {
		return false, errors.New("resource is blank")
	}
	req := account.RbacIdentityPermissionRequest{
		Identity:     identity,
		IdentityType: account.RbacIdentityType_RBAC_IDENTITY_TYPE_APIKEY,
		ResourceUri:  resource,
	}

	timeout := h.readTimeout
	if timeout == 0 {
		timeout = common.DEFAULT_TIMEOUT
	}
	tctx, cf := context.WithTimeout(context.Background(), timeout)
	defer cf()

	rsp, err := h.getClient().HasPermissionForIdentity(tctx, &req)
	if err != nil {
		return false, errors.New(fmt.Sprintf("apiKeyPermissionHelper: %s", err.Error()))
	}
	if !rsp.AuthResult {
		tilog.Info(ctx, fmt.Sprintf("apiKeyPermissionHelper: has no permission: %s", rsp.BaseResp.ErrMsg))
	}
	return rsp.AuthResult, nil
}

func (h *apiKeyPermissionHelper) getClient() account.AccountAPIServiceClient {
	globalClientInit.Do(func() {
		conn, err := grpc.Dial(h.endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
		if err != nil {
			panic(fmt.Sprintf("apiKeyPermissionHelper: dial account-svc error: %s", err))
		}
		globalClient = account.NewAccountAPIServiceClient(conn)
	})
	return globalClient
}

type customPermissionHelper struct {
	client      account.AccountAPIServiceClient
	readTimeout time.Duration
}

func NewCustomClientPermissionHelper(client account.AccountAPIServiceClient) AttributeSettablePermissionHelper {
	if client == nil {
		panic("customPermissionHelper: client is nil")
	}
	return &customPermissionHelper{
		client:      client,
		readTimeout: common.DEFAULT_TIMEOUT,
	}
}

func NewCustomEndpointPermissionHelper(endpoint string) AttributeSettablePermissionHelper {
	if endpoint == "" {
		panic("customPermissionHelper: endpoint is empty")
	}
	conn, err := tigrpc.NewClientConn(endpoint, tigrpc.WithDefault())
	if err != nil {
		panic(fmt.Sprintf("customPermissionHelper: dial account-svc error: %s", err))
	}
	client := account.NewAccountAPIServiceClient(conn)

	return &customPermissionHelper{
		client:      client,
		readTimeout: common.DEFAULT_TIMEOUT,
	}
}

func (d *customPermissionHelper) SetEndpoint(endpoint string) {
	panic("customClientPermissionHelper: not support to change")

}

func (d *customPermissionHelper) SetReadTimeout(timeout time.Duration) {
	d.readTimeout = timeout
}

func (d *customPermissionHelper) HasPermission(ctx context.Context, identity, resource string) (bool, error) {
	identInt, err := strconv.ParseUint(identity, 10, 64)
	if err != nil {
		return false, errors.New(fmt.Sprintf("customPermissionHelper: %s", err.Error()))
	}
	req := account.RbacPermissionRequest{
		UserId:      identInt,
		ResourceUri: resource,
	}

	timeout := d.readTimeout
	if timeout == 0 {
		timeout = common.DEFAULT_TIMEOUT
	}
	tctx, cf := context.WithTimeout(context.Background(), timeout)
	defer cf()

	rsp, err := d.client.HasPermissionForUser(tctx, &req)
	if err != nil {
		return false, errors.New(fmt.Sprintf("customPermissionHelper: %s", err.Error()))
	}
	if !rsp.AuthResult {
		tilog.Info(ctx, fmt.Sprintf("customPermissionHelper: has no permission: %s", rsp.BaseResp.ErrMsg))
	}
	return rsp.AuthResult, nil
}
