package api

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

	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/tiinf/sdk/auth"

	"github.com/gin-gonic/gin"
)

const AdminApiDefaultPageSize = 100

var (
	pathPattern2ResourceMap = map[string]string{
		"get:/orgs/(\\d+)/v1beta1/projects":  "account/openapi/orgs/{org_id}/list_projects",
		"post:/orgs/(\\d+)/v1beta1/projects": "account/openapi/orgs/{org_id}/create_project",
		// dbuser
		"get:/orgs/(\\d+)/projects/(\\d+)/v1beta1/clusters/(\\d+)/dbuser": "account/openapi/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/dbuser",
		// sql uesr
		"get:/orgs/(\\d+)/projects/(\\d+)/v1beta1/clusters/(\\d+)/sqlUsers":         "account/openapi/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sql_user/list",
		"post:/orgs/(\\d+)/projects/(\\d+)/v1beta1/clusters/(\\d+)/sqlUsers":        "account/openapi/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sql_user/create",
		"delete:/orgs/(\\d+)/projects/(\\d+)/v1beta1/clusters/(\\d+)/sqlUsers/\\S+": "account/openapi/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sql_user/delete",
		"patch:/orgs/(\\d+)/projects/(\\d+)/v1beta1/clusters/(\\d+)/sqlUsers/\\S+":  "account/openapi/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sql_user/update",
		"get:/orgs/(\\d+)/projects/(\\d+)/v1beta1/clusters/(\\d+)/sqlUsers/\\S+":    "account/openapi/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sql_user/query",
	}

	DefaultSuccessResp = BasicResp{
		Code:    200,
		Message: "ok",
	}
)

// XXX(bing.wang) This is used in Admin API scenario.
// Because all apiKeys or accessTokens belong to the specific org
// the path need to add a /orgs/{org_id} prefix for RBAC.
func GenAccessParam4GinWithPrefixOrgIdProjectId(c *gin.Context) (*auth.AccessParam, error) {
	ctx := c.Request.Context()
	authInfo, err := auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		return nil, err
	}
	var orgId uint64
	if authInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		orgId, err = auth.GetOrgIdFromApiKeyRsp(ctx, authInfo.ApiKeyRsp)
		if err != nil {
			return nil, err
		}
	} else if authInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		orgId = authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
	} else {
		return nil, err
	}
	uriId := SqlUserUriReq{}
	if err := c.ShouldBindUri(&uriId); err != nil {
		tilog.Warn(ctx, "GenAccessParam4GinWithPrefixOrgId ShouldBindUri err", zap.Error(err))

		return nil, errs.NewBadRequest("cluster id error in url ", nil)
	}
	clusterOrgId, clusterProjectId, err := getClusterIfo(ctx, uriId.ClusterId)
	if err != nil {
		return nil, err
	}
	if orgId != clusterOrgId {
		return nil, errs.NewBadRequest("do not have permission for this cluster", nil)
	}
	var urlPath string
	if clusterProjectId == 0 {
		urlPath = fmt.Sprintf("/orgs/%d%s", orgId, c.Request.URL.Path)
	} else {
		urlPath = fmt.Sprintf("/orgs/%d/projects/%d%s", orgId, clusterProjectId, c.Request.URL.Path)
	}
	tilog.Info(ctx, "parse request url", zap.Any("urlPath", urlPath))
	param := auth.AccessParam{
		Metadata: map[string]interface{}{
			auth.AccessMetadataPath:   urlPath,
			auth.AccessMetadataMethod: c.Request.Method,
		},
		Req: c,
	}
	return &param, nil
}

func getClusterIfo(ctx context.Context, clusterId uint64) (uint64, uint64, error) {
	var projectId uint64
	var orgId uint64
	if clusterId != 0 {
		c, err := infra.GetHandler().GetClusterById(ctx, clusterId, true, infra.SupportDBUserStatus...)

		if err != nil {
			return orgId, projectId, err
		}
		if c == nil {
			tilog.Info(ctx, "cluster is delete or not exist", zap.Any("clusterId", clusterId))
			return orgId, projectId, errs.NewBadRequest("cluster is delete or not exist", nil)
		}
		if !infra.IsSupportDBUser(ctx, c) {
			tilog.Info(ctx, "this cluster version not support sql user", zap.Any("clusterId", clusterId))
			return orgId, projectId, errs.NewBadRequest("this cluster version not support sql user", nil)
		}
		projectId = c.ProjectId
		orgId = c.TenantId
	}
	return orgId, projectId, nil
}
func doTiErr(ctx context.Context, c *gin.Context, err error, alertMsg string) {
	httpcode := http.StatusInternalServerError
	codeStr := internalServerErrorCode
	if te, ok := errs.AsTiError(err); ok {
		httpcode = te.HTTPCode()
		codeStr = errorCodeByHttpStatus(httpcode)
	} else {
		tilog.Warn(ctx, alertMsg)
	}

	writeError(ctx, c.Writer, err, httpcode, codeStr, err.Error())

}

func errorCodeByHttpStatus(status int) string {
	switch status {
	case http.StatusUnauthorized:
		return authenticateErrorCode
	case http.StatusForbidden:
		return authorizeErrorCode
	case http.StatusBadRequest:
		return paramInvalidErrorCode
	default:
		return internalServerErrorCode
	}
}

func getPageInfo(ctx context.Context, pageReq OpenApiPageReq) (int32, string) {
	var pageSize int32
	var pageToken string
	if pageReq.PageSizeUnderline != 0 {
		pageSize = pageReq.PageSizeUnderline
	} else if pageReq.PageSize != 0 {
		pageSize = pageReq.PageSize
	} else if pageReq.PageSize == 0 && pageReq.PageSizeUnderline == 0 {
		// default
		pageSize = AdminApiDefaultPageSize
		tilog.Info(ctx, fmt.Sprintf("req page size=0, set pageSize=%d", AdminApiDefaultPageSize))
	}

	if pageReq.PageToken != "" {
		pageToken = pageReq.PageToken
	} else if pageReq.PageTokenUnderline != "" {
		pageToken = pageReq.PageTokenUnderline
	}
	tilog.Info(ctx, fmt.Sprintf("pageToken=%s, pageSize=%d", pageToken, pageSize))
	return pageSize, pageToken
}
func openApiPermissionCheckResultHandleFunc(w http.ResponseWriter, r *http.Request, result *auth.AccessResult, err error) (continueRun bool) {
	tilog.Info(r.Context(), "access check result", zap.Any("result", result), zap.Any("err", err))
	if err != nil {
		w.WriteHeader(http.StatusForbidden)
		//_, _ = w.Write([]byte(fmt.Sprintf("access denied: %s", err.Error())))
		_, _ = w.Write([]byte("access denied"))
		return false

	}
	if result.Code != auth.AccessRetRelease {
		w.WriteHeader(http.StatusForbidden)
		//_, _ = w.Write([]byte(fmt.Sprintf("access denied: %s", result.Msg)))
		_, _ = w.Write([]byte("access denied"))
		return false
	}
	return true
}
