package rbac

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"regexp"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/pkgv2/auth/kong"
	"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/pkg/apimiddleware/grpcerror"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/consts"
)

type TiDBXAuthZService interface {
	CheckUriRbac(ctx *gin.Context)
	SetGrpcGatewayMetaData(ctx *gin.Context)
}

const projectIDLabelKey = "tidb.cloud/project"

type tidbXAuthZServiceImpl struct {
	account accountcli.Client
}

func NewTiDBXAuthZService(acc accountcli.Client) TiDBXAuthZService {
	return &tidbXAuthZServiceImpl{
		account: acc,
	}
}

func (t *tidbXAuthZServiceImpl) CheckUriRbac(ctx *gin.Context) {
	user, err := getUserFromContext(ctx)
	if err != nil {
		tilog.Error(ctx.Request.Context(), "rbac check error: get user from context failed", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	resource := getResource(ctx)
	if resource == "" {
		tilog.Error(ctx.Request.Context(), "rbac check error: get resource uri failed",
			zap.String("path", ctx.Request.URL.Path),
			zap.String("method", ctx.Request.Method),
		)
		grpcerror.NewOpenAPIError(ctx, http.StatusNotFound, consts.ErrNoRoute)
		return
	}

	var projectId string
	resource, projectId, err = resolveNextgenResourceAndProjectID(ctx, resource)
	if err != nil {
		tilog.Error(ctx.Request.Context(), "rbac check error: get project id from body failed", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusInternalServerError, consts.ErrInternal)
		return
	}

	checkPermissionParam := &authsdk.CheckPermissionParam{
		IdentityTye: convIdentityType(user.IdentityType),
		IdentityVal: convIdentityVal(user),
		ResourceUri: resource,
		ResourceParam: map[string]string{
			consts.RBACOrgIdKey: strconv.FormatUint(user.Org.Id, 10),
		},
	}

	if projectId != "" {
		checkPermissionParam.ResourceParam[consts.RBACProjectIdKey] = projectId
	}

	if checkPermissionParam.IdentityTye == "" || checkPermissionParam.IdentityVal == "" {
		tilog.Error(ctx.Request.Context(), "rbac check error: get identity type or identity value failed",
			zap.String("path", ctx.Request.URL.Path),
			zap.String("method", ctx.Request.Method),
			zap.Any("param", checkPermissionParam))
		grpcerror.NewOpenAPIError(ctx, http.StatusForbidden, consts.ErrPermissionDenied)
		return
	}

	// need tidb_id
	if strings.Contains(checkPermissionParam.ResourceUri, consts.RBACTidbIdKey) {
		tidbId, err := parseTidbIdFromPath(consts.RelativePath(ctx.Request.URL.Path))
		if err != nil {
			log.Error("failed to parse tidb id from path")
			grpcerror.NewOpenAPIError(ctx, http.StatusForbidden, consts.ErrPermissionDenied)
			return
		}
		checkPermissionParam.ResourceParam[consts.RBACTidbIdKey] = tidbId
	}
	if strings.Contains(checkPermissionParam.ResourceUri, consts.RBACResourcePoolIdKey) {
		resourcePoolID, err := parseResourcePoolIdFromPath(consts.RelativePath(ctx.Request.URL.Path))
		if err != nil {
			log.Error("failed to parse resource pool id from path")
			grpcerror.NewOpenAPIError(ctx, http.StatusForbidden, consts.ErrPermissionDenied)
			return
		}
		checkPermissionParam.ResourceParam[consts.RBACResourcePoolIdKey] = resourcePoolID
	}

	checkPermissionRet, err := authsdk.CheckIdentityPermission(ctx.Request.Context(), checkPermissionParam)
	if err != nil {
		log.Error("failed to check identity permission", zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusInternalServerError, consts.ErrPermissionDenied)
		return
	}

	if !checkPermissionRet.HasPermission {
		tilog.Error(ctx.Request.Context(), "no permission",
			zap.String("message", checkPermissionRet.Message),
			zap.Uint64(consts.OrgIdKey, user.Org.Id),
			zap.Any("user", user.User),
			zap.String("resource_uri", resource),
			zap.Error(err))
		grpcerror.NewOpenAPIError(ctx, http.StatusForbidden, consts.ErrPermissionDenied)
		return
	}
	tilog.Info(ctx.Request.Context(), "check uri rbac succeed", zap.String("path", ctx.Request.URL.Path))
}

func (t *tidbXAuthZServiceImpl) SetGrpcGatewayMetaData(ctx *gin.Context) {
	user, err := getUserFromContext(ctx)
	if err != nil {
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	if user.Org == nil {
		log.Error("failed to get org info from user",
			zap.Uint64("user id", user.User.Id))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}
	ctx.Request.Header.Set(grpcGatewayOrgIDKey, strconv.FormatUint(user.Org.Id, 10))

	if user.User == nil {
		log.Error("failed to get user info from user")
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}
	// user info
	ctx.Request.Header.Set(grpcGatewayUserIDKey, strconv.FormatUint(user.User.Id, 10))
	ctx.Request.Header.Set(grpcGatewayUserEmailKey, user.User.Email)

	ctx.Request.Header.Set(grpcGatewayProjectIDListKey, getProjectIDList(user))

	switch user.IdentityType {
	case authsdk.AuthnIdentityTypeApiKeyAK:
		ctx.Request.Header.Set(grpcGatewayXAuthType, string(kong.AuthTypeOpenAPI))
		ctx.Request.Header.Set(grpcGatewayXAuthContent, ctx.GetHeader(authsdk.XAuthContentHeader))
		ctx.Request.Header.Set(grpcGatewayPublicKeyKey, user.PublicKey)
		ctx.Request.Header.Set(grpcGatewayPublicKeyKeyId, strconv.FormatUint(user.PublicKeyId, 10))
	case authsdk.AuthnIdentityTypeAccessToken, authsdk.AuthnIdentityTypeUserToken:
		ctx.Request.Header.Set(grpcGatewayAccessTokenKey, user.AccessToken)
		ctx.Request.Header.Set(grpcGatewayXAuthMethod, authsdk.XAuthMethodValBear)
		ctx.Request.Header.Set(grpcGatewayXAuthRaw, ctx.GetHeader(authsdk.XAuthRawHeader))
	default:
		log.Error("unknown identity type", zap.String("identity type", string(user.IdentityType)))
		grpcerror.NewOpenAPIError(ctx, http.StatusUnauthorized, consts.ErrUnauthenticated)
		return
	}

	// project not required
	ctx.Request.Header.Set(grpcGatewayNoProjectRequiredKey, "true")

	// cluster id
	clusterId, _ := parseTidbIdFromPath(consts.RelativePath(ctx.Request.URL.Path))
	if clusterId != "" {
		ctx.Request.Header.Set(grpcGatewayClusterIDKey, clusterId)
	}
}

var tidbIdRegexp = regexp.MustCompile(`/tidbs/(?P<tidbId>-|\d+)`)
var resourcePoolIdRegexp = regexp.MustCompile(`/resourcePools/(?P<resourcePoolId>\d+)(?:$|[/:])`)

func parseTidbIdFromPath(path string) (string, error) {
	if index := tidbIdRegexp.SubexpIndex(consts.TidbIdKey); index != -1 {
		res := tidbIdRegexp.FindStringSubmatch(path)
		if len(res) != 0 {
			return res[index], nil
		}
	}
	return "", errors.New("tidbId not found")
}

func parseResourcePoolIdFromPath(path string) (string, error) {
	if index := resourcePoolIdRegexp.SubexpIndex("resourcePoolId"); index != -1 {
		res := resourcePoolIdRegexp.FindStringSubmatch(path)
		if len(res) != 0 {
			return res[index], nil
		}
	}
	return "", errors.New("resourcePoolId not found")
}

func resolveNextgenResourceAndProjectID(ctx *gin.Context, resource string) (scopedResource, projectID string, err error) {
	var projectScopedResource string
	switch resource {
	case "/nextgen/orgs/{org_id}/tidbs/create":
		projectScopedResource = "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/create"
	case "/nextgen/orgs/{org_id}/tidbs/restore":
		projectScopedResource = "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/restore"
	default:
		return resource, "", nil
	}

	projectId, err := getProjectIdFromBody(ctx)
	if err != nil {
		return "", "", err
	}
	if projectId == "" {
		return resource, "", nil
	}

	return projectScopedResource, projectId, nil
}

func getProjectIdFromBody(ctx *gin.Context) (string, error) {
	if ctx.Request.Body == nil {
		return "", nil
	}

	const maxRequestBodyBytes = 1 << 20 // 1 MiB; adjust as appropriate for CreateTidb payloads

	// Limit the size of the body that can be read in this middleware to avoid
	// unbounded memory usage and double-buffering of large payloads.
	ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxRequestBodyBytes)

	bodyBytes, err := io.ReadAll(ctx.Request.Body)
	if err != nil {
		// If the body is too large, abort early with 413 to avoid passing a
		// truncated body to downstream handlers.
		var maxErr *http.MaxBytesError
		if errors.As(err, &maxErr) {
			ctx.AbortWithStatus(http.StatusRequestEntityTooLarge)
		}
		return "", err
	}
	// Restore body
	ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))

	type tidbPayload struct {
		Labels map[string]string `json:"labels"`
	}

	type requestPayload struct {
		Labels map[string]string `json:"labels"`
		Tidb   tidbPayload       `json:"tidb"`
	}

	var req requestPayload
	decoder := json.NewDecoder(bytes.NewReader(bodyBytes))
	decoder.UseNumber()
	if err := decoder.Decode(&req); err != nil {
		return "", nil // Ignore JSON error, let handler handle it
	}

	if req.Labels != nil {
		if pid, ok := req.Labels[projectIDLabelKey]; ok {
			if trimmed := strings.TrimSpace(pid); trimmed != "" {
				return trimmed, nil
			}
		}
	}

	if req.Tidb.Labels != nil {
		if pid, ok := req.Tidb.Labels[projectIDLabelKey]; ok {
			if trimmed := strings.TrimSpace(pid); trimmed != "" {
				return trimmed, nil
			}
		}
	}

	return "", nil
}
