package auth

import (
	"cmp"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"slices"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/idl/pbgen/proto/openapi"
	"github.com/tidbcloud/pkgv2/auth/kong"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"

	accountgrpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/account"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

var (
	ErrMessageAPIKeyRole = "The v1beta API is restricted to the role of \"Organization Owner\" only. We recommend using the v1beta1 API for broader roles access."
)

func getApiKeyUser(ctx *gin.Context, oaAccess *account.GetOpenAPIAccessResp) (*User, error) {
	if oaAccess == nil {
		tilog.Error(ctx.Request.Context(), "failed to get auth info", zap.Error(errors.New("failed to get auth info")))
		return nil, errors.New("failed to get auth info")
	}

	publicKey, err := getOpenAPIPublicKey(ctx)
	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get public key", zap.Error(err))
		return nil, errors.New("failed to retrieve public_key")
	}

	if oaAccess.Org == nil {
		tilog.Error(ctx.Request.Context(), "failed to get org id of public key", zap.String("publicKey", publicKey))
		return nil, fmt.Errorf("failed to retrieve tenantID: public key: %s", publicKey)
	}

	if len(oaAccess.Projects) == 0 {
		tilog.Error(ctx.Request.Context(), "failed to get projects of public key", zap.String("publicKey", publicKey))
		return nil, fmt.Errorf("failed to retrieve projectID: public key: %s", publicKey)
	}

	u, err := accountgrpc.GetOrCreateSystemUser(ctx, &account.GetOrCreateSystemUserRequest{OrgId: oaAccess.Org.Id})
	if err != nil {
		tilog.Error(ctx.Request.Context(), "failed to get or create system user", zap.Error(err))
		return nil, errors.New("failed to get user by api key")
	}

	orgRoles := oaAccess.Org.OrgRoles
	if !slices.Contains(orgRoles, acl.OrgRole_ORG_ROLE_OWNER) {
		tilog.Error(
			ctx.Request.Context(),
			"check permission failed",
			zap.String("publicKey", publicKey),
			zap.Any("orgRoles", orgRoles),
		)
		return nil, errors.New(ErrMessageAPIKeyRole)
	}

	user := &User{
		User:      u.User,
		PublicKey: publicKey,
		Projects:  make([]*account.Project, 0, len(oaAccess.Projects)),
		Org:       &account.Org{Id: oaAccess.Org.Id},
	}

	for _, id := range oaAccess.Projects {
		user.Projects = append(user.Projects, &account.Project{Id: id.Id, Status: ProjectStatusActive})
	}

	slices.SortFunc(user.Projects, func(a, b *account.Project) int {
		return cmp.Compare(a.Id, b.Id)
	})

	if len(user.Projects) == 0 {
		tilog.Error(ctx.Request.Context(), "no project in this organization", zap.String("publicKey", publicKey))
		return nil, errors.New("no project in this organization")
	}
	return user, nil
}

func getOpenAPIPublicKey(ctx *gin.Context) (string, error) {
	authType := ctx.GetHeader("X-Auth-Type")
	if authType != string(kong.AuthTypeOpenAPI) {
		return "", fmt.Errorf("unexpected auth type: %s", authType)
	}

	authContent := ctx.GetHeader("X-Auth-Content")
	result := new(kong.OpenAPIAuthResult)
	err := json.Unmarshal([]byte(authContent), result)
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal: auth content: %s, err: %w", authContent, err)
	}

	publicKey := result.PublicKey
	if publicKey == "" {
		return "", fmt.Errorf("failed to retrieve public_key: auth content: %s", authContent)
	}
	return publicKey, nil
}

// CheckApiKeyRole is a middleware that checks if the API key has the required role to access the v1beta API.
// Only API Key with the "Organization Owner" role are allowed to access the v1beta API.
// PRD: https://pingcap.feishu.cn/wiki/BTDUwZcS1iYYKdkWZ6icFLNcnjc
func CheckApiKeyRole() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		a, err := tigin.GetCurrentAuthInfo(ctx)
		if err != nil {
			tilog.Error(ctx.Request.Context(), "failed to get auth info", zap.Error(err))
			_ = ctx.AbortWithError(http.StatusForbidden, errors.New("failed to get auth info"))
			return
		}

		if a.AuthType == tigin.CURRENT_APIKEY {
			_, err = getApiKeyUser(ctx, a.OpenAPIAccess)
			if err != nil {
				tilog.Error(ctx.Request.Context(), "failed to get user by api key", zap.Error(err))
				errMsg := &openapi.ErrorResponse{
					Code:    int32(bizerr.ErrCodePermission),
					Message: err.Error(),
				}
				ctx.AbortWithStatusJSON(http.StatusForbidden, errMsg)
				return
			}
		}

		ctx.Next()
	}
}
