package auth

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

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

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

func AuthnIdentity(ctx context.Context, identity *IdentityAuthParam) (*IdentityAuthResult, error) {
	f, ok := identityAuthnFs[identity.IdentityType]
	if !ok {
		return nil, errors.New(fmt.Sprintf("identity type [%s] not support", identity.IdentityType))
	}
	return f(ctx, &IdentityVal{
		Value:   identity.IdentityValue,
		Options: identity.Options,
	})
}

var identityAuthnFs = map[AuthnIdentityType]IdentityAuthnFunc{
	AuthnIdentityTypeUserToken:   AuthnUserToken,
	AuthnIdentityTypeApiKeyAK:    AuthnApiKeyAK,
	AuthnIdentityTypeAccessToken: AuthnAccessToken,
}

type IdentityAuthnFunc func(ctx context.Context, in *IdentityVal) (*IdentityAuthResult, error)

func AuthnUserToken(ctx context.Context, in *IdentityVal) (*IdentityAuthResult, error) {
	if in.Options.EnableLog {
		tilog.Info(ctx, "AuthnIdentity AuthnUserToken start", zap.Any("in", in))
	}
	if in.Value == "" {
		return nil, errors.New("identity value is empty")
	}

	client, err := getStoredAccountClient()
	if err != nil {
		return nil, err
	}
	tctx, cf := context.WithTimeout(ctx, accountReadTimeout)
	defer cf()
	rsp, err := client.GetUserByToken(tctx, &account.GetUserByTokenRequest{
		Token: in.Value,
	})
	if in.Options.EnableLog {
		tilog.Info(ctx, "AuthnIdentity AuthnUserToken GetUserByToken result", zap.Any("in", in), zap.Any("rsp", rsp), zap.Any("err", err))
	}
	if err != nil {
		return nil, err
	}
	var authnErr error = nil
	if rsp.BaseResp.ErrCode != 0 {
		authnErr = errors.New(rsp.BaseResp.ErrMsg)
	} else if rsp.User.Status != account.UserStatus_USER_STATUS_ACTIVE {
		authnErr = errors.New("user status not active")
	}

	ret := genIdentityAuthResult(AuthnIdentityTypeUserToken, in, rsp, authnErr)
	if in.Options.EnableLog {
		tilog.Info(ctx, "AuthnIdentity AuthnUserToken end", zap.Any("in", in), zap.Any("ret", ret))
	}
	return ret, err
}

func genIdentityAuthResult(tye AuthnIdentityType, in *IdentityVal, rsp interface{}, err error) *IdentityAuthResult {
	info := &AuthnIdentityInfo{
		IdentityType: tye,
		IdentityVal:  in.Value,
		RawIdentity:  rsp,
	}
	if tye == AuthnIdentityTypeUserToken {
		userTokenRsp := rsp.(*account.GetUserByTokenResponse)
		info.UserTokenRsp = userTokenRsp
		if err == nil {
			info.IdentityID = userTokenRsp.User.Id
			info.OrgID = in.Options.AccessOrgID
		}
	} else if tye == AuthnIdentityTypeApiKeyAK {
		apiKeyRsp := rsp.(*account.GetApiKeyByAccessKeyRsp)
		info.ApiKeyRsp = apiKeyRsp
		if err == nil {
			info.IdentityID = apiKeyRsp.ApiKey.Id
			orgId, _ := GetOrgIdFromApiKeyRsp(context.Background(), apiKeyRsp)
			if orgId == 0 {
				orgId = in.Options.AccessOrgID
			}
			info.OrgID = orgId
		}
	} else if tye == AuthnIdentityTypeAccessToken {
		accessTokenRsp := rsp.(*account.GetIdentityByAccessTokenResp)
		info.AccessTokenRsp = accessTokenRsp
		if err == nil {
			info.IdentityID = accessTokenRsp.AccessTokenIdentity.UserId
			info.OrgID = accessTokenRsp.AccessTokenIdentity.OrgId
		}
	}

	ret := IdentityAuthResult{
		AuthRet:      AuthnRetOK,
		AuthMessage:  "OK",
		IdentityInfo: info,
	}
	if err != nil {
		ret.AuthRet = AuthnRetFail
		ret.AuthMessage = err.Error()
	}

	return &ret
}

func AuthnApiKeyAK(ctx context.Context, in *IdentityVal) (*IdentityAuthResult, error) {
	if in.Options.EnableLog {
		tilog.Info(ctx, "AuthnIdentity AuthnApiKeyAK start", zap.Any("in", in))
	}
	if in.Value == "" {
		return nil, errors.New("identity value is empty")
	}

	client, err := getStoredAccountClient()
	if err != nil {
		return nil, err
	}
	tctx, cf := context.WithTimeout(ctx, accountReadTimeout)
	defer cf()
	rsp, err := client.GetApiKeyByAccessKey(tctx, &account.GetApiKeyByAccessKeyReq{
		AccessKey: in.Value,
	})
	if in.Options.EnableLog {
		tilog.Info(ctx, "AuthnIdentity AuthnApiKeyAK GetApiKeyByAccessKey result", zap.Any("in", in), zap.Any("rsp", rsp), zap.Any("err", err))
	}
	if err != nil {
		return nil, err
	}
	var authnErr error = nil
	if rsp.BaseResp.ErrCode != 0 {
		authnErr = errors.New(rsp.BaseResp.ErrMsg)
	}

	ret := genIdentityAuthResult(AuthnIdentityTypeApiKeyAK, in, rsp, authnErr)
	if in.Options.EnableLog {
		tilog.Info(ctx, "AuthnIdentity AuthnApiKeyAK end", zap.Any("in", in), zap.Any("ret", ret))
	}
	return ret, err
}

func AuthnAccessToken(ctx context.Context, in *IdentityVal) (*IdentityAuthResult, error) {
	if in.Value == "" {
		return nil, errors.New("identity value is empty")
	}

	client, err := getStoredAccountClient()
	if err != nil {
		return nil, err
	}
	tctx, cf := context.WithTimeout(ctx, accountReadTimeout)
	defer cf()
	rsp, err := client.GetIdentityByAccessToken(tctx, &account.GetIdentityByAccessTokenReq{
		RawAccessToken: in.Value,
	})
	if err != nil {
		if in.Options.EnableLog {
			tilog.Info(ctx, "AuthnIdentity AuthnAccessToken err", zap.Any("err", err))
		}
		return nil, err
	}

	var authnErr error = nil
	if rsp.BaseResp.ErrCode != 0 {
		authnErr = errors.New(rsp.BaseResp.ErrMsg)
		if in.Options.EnableLog {
			tilog.Info(ctx, "AuthnIdentity AuthnAccessToken fail", zap.Any("baseResp", rsp.BaseResp))
		}
	}
	ret := genIdentityAuthResult(AuthnIdentityTypeAccessToken, in, rsp, authnErr)
	return ret, err
}

var (
	accountClient      account.AccountAPIServiceClient
	accountClientInit  sync.Once
	accountEndpoint    = "account-provider-grpc:10000"
	accountReadTimeout = time.Second * 5

	/*authServerClient     provider.AuthServerServiceClient
	authServerClientInit sync.Once
	authServerEndpoint   = "authserver-provider-service:10000"*/
)

func InitAccountClientByClient(client account.AccountAPIServiceClient, readTimeout time.Duration) {
	if readTimeout != 0 {
		accountReadTimeout = readTimeout
	}
	GetAndStoreAccountClient(client, "")
}

func InitAccountClientByEndpoint(endpoint string, readTimeout time.Duration) {
	if readTimeout != 0 {
		accountReadTimeout = readTimeout
	}
	GetAndStoreAccountClient(nil, endpoint)
}

// GetAndStoreAccountClient if changed endpoint, it needs to restart server to activate the account client
func GetAndStoreAccountClient(defaultClient account.AccountAPIServiceClient, endpoint string) account.AccountAPIServiceClient {
	accountClientInit.Do(func() {
		if defaultClient != nil {
			accountClient = defaultClient
		} else {
			accountClient = newAccountClient(endpoint)
		}
	})
	return accountClient
}

func newAccountClient(endpoint string) account.AccountAPIServiceClient {
	if endpoint == "" {
		endpoint = accountEndpoint
	}
	conn, err := tigrpc.NewClientConn(endpoint, tigrpc.WithDefault())
	if err != nil {
		panic(fmt.Sprintf("dial account-provider-svc [%s] error: %s", endpoint, err))
	}
	return account.NewAccountAPIServiceClient(conn)
}

func getStoredAccountClient() (account.AccountAPIServiceClient, error) {
	if accountClient == nil {
		return nil, errors.New("account client has not been inited.")
	}
	return accountClient, nil
}

func GetOrgIdFromApiKeyRsp(ctx context.Context, apiKeyRsp *account.GetApiKeyByAccessKeyRsp) (uint64, error) {
	for _, item := range apiKeyRsp.GetResourceInfos() {
		if item.GetScopeType() == "ORG" {
			return item.GetResourceId(), nil
		}
	}
	return 0, errors.New("no orgId found in ApiKeyRsp")
}

func GetOrgIdFromAccessTokenRsp(accessTokenRsp *account.GetIdentityByAccessTokenResp) uint64 {
	return accessTokenRsp.AccessTokenIdentity.OrgId
}

func ListProjectsFromApiKeyRsp(ctx context.Context, apiKeyRsp *account.GetApiKeyByAccessKeyRsp) ([]*account.Project, error) {
	defaultAccountClient, err := getStoredAccountClient()
	if err != nil {
		return nil, err
	}
	orgId, err := GetOrgIdFromApiKeyRsp(ctx, apiKeyRsp)
	if err != nil {
		return nil, err
	}
	in := &account.ListProjectsByOrgsRequest{
		OrgIds: []uint64{orgId},
	}
	resp, err := defaultAccountClient.ListProjectsByOrgs(ctx, in)
	if err != nil {
		return nil, err
	}
	for _, item := range resp.GetProjects() {
		if item.OrgId != orgId {
			continue
		}
		return item.Projects, nil
	}
	return nil, err
}

type IdentityAuthParam struct {
	IdentityType  AuthnIdentityType
	IdentityValue string
	Options       IdentityAuthOption
}

type IdentityAuthOption struct {
	EnableLog   bool
	AccessOrgID uint64
}

type IdentityAuthResult struct {
	AuthRet     int32
	AuthMessage string

	IdentityInfo *AuthnIdentityInfo
}

type AuthnIdentityInfo struct {
	IdentityType   AuthnIdentityType
	IdentityVal    string
	IdentityID     uint64
	OrgID          uint64
	RawIdentity    interface{}
	UserTokenRsp   *account.GetUserByTokenResponse
	ApiKeyRsp      *account.GetApiKeyByAccessKeyRsp
	AccessTokenRsp *account.GetIdentityByAccessTokenResp
}

type IdentityVal struct {
	Value   string
	Options IdentityAuthOption
}
