package auth

import (
	"context"
	"fmt"
	"net/http"
	"strconv"

	"braces.dev/errtrace"
	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	authsdk "github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"

	accountcli "github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/adapter/account"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/apimiddleware/grpcerror"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/config"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/consts"
)

type TiDBXAuthNService interface {
	RemoveApiKeyHeader(ctx *gin.Context)
	GetIdentityInfo() func(ctx *gin.Context)

	TransTokenUser(ctx *gin.Context)
	TransOpenAPIUser(ctx *gin.Context)
}

type tidbXAuthNServiceImpl struct {
	account accountcli.Client
}

func NewTiDBXAuthNService(acc accountcli.Client) TiDBXAuthNService {
	return &tidbXAuthNServiceImpl{
		account: acc,
	}
}

func (t tidbXAuthNServiceImpl) RemoveApiKeyHeader(ctx *gin.Context) {
	ctx.Request.Header.Del("X-Auth-Method")
	ctx.Request.Header.Del("X-Auth-Content")
	ctx.Next()
}

func (t tidbXAuthNServiceImpl) GetIdentityInfo() func(ctx *gin.Context) {
	if config.Global().Auth.LocalMode {
		return func(ctx *gin.Context) {}
	}

	return authsdk.GinGetIdentityInfoInterceptor(&authsdk.IdentityAuthOption{
		EnableLog: false,
	})
}

func (t tidbXAuthNServiceImpl) TransTokenUser(ctx *gin.Context) {
	identityInfo, err := authsdk.GetIdentityInfoFromGinContext(ctx)
	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get identity info", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	if identityInfo == nil {
		tilog.Error(ctx.Request.Context(), "identity info is nil")
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	var user *entity.User
	switch identityInfo.IdentityType {
	case authsdk.AuthnIdentityTypeUserToken:
		user, err = t.transUserTokenRspToUser(ctx, identityInfo.UserTokenRsp)
	case authsdk.AuthnIdentityTypeAccessToken:
		user, err = t.transAccessTokenRspToUser(ctx, identityInfo.AccessTokenRsp)
	default:
		tilog.Error(ctx.Request.Context(), "unknown identity type",
			zap.String("identityType", string(identityInfo.IdentityType)),
		)
		err = fmt.Errorf("unknown identity type")
	}

	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get user", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	ctx.Set(consts.XAuthUser, user)
	tilog.Info(ctx.Request.Context(), "get user successfully", zap.String("path", ctx.Request.URL.Path))
}

func (t tidbXAuthNServiceImpl) TransOpenAPIUser(ctx *gin.Context) {
	identityInfo, err := authsdk.GetIdentityInfoFromGinContext(ctx)
	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get identity info", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	if identityInfo == nil {
		tilog.Error(ctx.Request.Context(), "identity info is nil")
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	var user *entity.User
	switch identityInfo.IdentityType {
	case authsdk.AuthnIdentityTypeApiKeyAK:
		user, err = t.transAPIKeyRspToUser(ctx, identityInfo.ApiKeyRsp)
	default:
		tilog.Error(ctx.Request.Context(), "unknown identity type",
			zap.String("identityType", string(identityInfo.IdentityType)),
		)
		err = fmt.Errorf("unknown identity type")
	}

	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get user", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	ctx.Set(consts.XAuthUser, user)
	tilog.Info(ctx.Request.Context(), "get user successfully", zap.String("path", ctx.Request.URL.Path))
}

func (t tidbXAuthNServiceImpl) transUserTokenRspToUser(
	ctx *gin.Context,
	userTokenRsp *account.GetUserByTokenResponse) (*entity.User, error) {
	if userTokenRsp == nil {
		return nil, errtrace.Wrap(fmt.Errorf("user token response is nil"))
	}
	user := &entity.User{
		User:         userTokenRsp.User,
		IdentityType: authsdk.AuthnIdentityTypeUserToken,
	}

	orgId := ctx.Query(consts.OrgIdKey)
	if orgId == "" {
		tilog.Error(ctx.Request.Context(), "failed to get orgId from query param")
		return nil, errtrace.Wrap(fmt.Errorf("failed to get orgId from query param"))
	}

	id, err := strconv.ParseUint(orgId, 10, 64)
	if err != nil {
		return nil, errtrace.Wrap(fmt.Errorf("failed to get orgId from query param: %w", err))
	}
	user.Org = &account.Org{
		Id: id,
	}

	projects, err := t.listProjectsByOrgId(ctx, id)
	if err != nil {
		return nil, err
	}
	if len(projects) == 0 {
		tilog.Warn(
			ctx.Request.Context(),
			"no project found",
		)
	}
	user.Projects = projects

	return user, nil
}

func (t tidbXAuthNServiceImpl) transAccessTokenRspToUser(
	ctx *gin.Context,
	accessTokenRsp *account.GetIdentityByAccessTokenResp) (*entity.User, error) {
	if accessTokenRsp == nil || accessTokenRsp.AccessTokenIdentity == nil {
		return nil, errtrace.Wrap(fmt.Errorf("access token or access token identity is nil"))
	}

	user := &entity.User{
		User: &account.User{
			Id:    accessTokenRsp.AccessTokenIdentity.GetUserId(),
			Email: accessTokenRsp.AccessTokenIdentity.GetUserEmail(),
		},
		AccessToken: accessTokenRsp.AccessTokenIdentity.RawAccessToken,
		Org: &account.Org{
			Id: accessTokenRsp.AccessTokenIdentity.GetOrgId(),
		},
		IdentityType: authsdk.AuthnIdentityTypeAccessToken,
	}

	orgId := authsdk.GetOrgIdFromAccessTokenRsp(accessTokenRsp)

	projects, err := t.listProjectsByOrgId(ctx, orgId)
	if err != nil {
		return nil, err
	}
	if len(projects) == 0 {
		tilog.Warn(
			ctx.Request.Context(),
			"no project found",
		)
	}
	user.Projects = projects

	return user, nil
}

func (t tidbXAuthNServiceImpl) transAPIKeyRspToUser(ctx *gin.Context, apiKeyRsp *account.GetApiKeyByAccessKeyRsp) (*entity.User, error) {
	if apiKeyRsp == nil || apiKeyRsp.ApiKey == nil || len(apiKeyRsp.ResourceInfos) == 0 {
		return nil, errtrace.Wrap(fmt.Errorf("api key response or api key or resource infos is nil"))
	}

	orgId, err := authsdk.GetOrgIdFromApiKeyRsp(ctx, apiKeyRsp)
	if err != nil {
		tilog.Error(
			ctx.Request.Context(),
			"failed to get org id of public key",
			zap.String("publicKey", apiKeyRsp.ApiKey.GetAccessKey()),
			zap.Error(err),
		)
		return nil, fmt.Errorf("failed to get org id public key: %s, %w", apiKeyRsp.ApiKey.GetAccessKey(), err)
	}

	projects, err := getProjectsByAPIKeyScope(ctx.Request.Context(), t.account, apiKeyRsp)
	if err != nil {
		tilog.Error(
			ctx.Request.Context(),
			"failed to get projects of public key",
			zap.String("publicKey", apiKeyRsp.ApiKey.GetAccessKey()),
			zap.Error(err),
		)
		return nil, fmt.Errorf("failed to get projects public key: %s, %w", apiKeyRsp.ApiKey.GetAccessKey(), err)
	}

	u, err := t.account.GetOrCreateSystemUser(ctx, orgId)
	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get or create system user", zap.Error(err))
		return nil, errtrace.Wrap(fmt.Errorf("failed to get or create system user: %w", err))
	}

	user := &entity.User{
		User:         u,
		PublicKey:    apiKeyRsp.ApiKey.GetAccessKey(),
		PublicKeyId:  apiKeyRsp.ApiKey.GetId(),
		Projects:     projects,
		Org:          &account.Org{Id: orgId},
		IdentityType: authsdk.AuthnIdentityTypeApiKeyAK,
	}

	return user, nil
}

func (t tidbXAuthNServiceImpl) listProjectsByOrgId(ctx context.Context, orgId uint64) ([]*account.Project, error) {
	in := &account.ListProjectsByOrgsRequest{
		OrgIds: []uint64{orgId},
	}
	resp, err := t.account.GetAccountClient().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, nil
}
