package inner

import (
	"errors"
	"net/http"
	"regexp"
	"strings"

	"github.com/tidbcloud/account/internel/svc/user/session"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/central/utils"
	permissionsvc "github.com/tidbcloud/account/internel/svc/rbac/permission"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	traceHeader = "X-Debug-Trace-Id"
)

var (
	regexpStrRe       = regexp.MustCompile(permissionsvc.REGEXP_STR)
	regexpNumberRe    = regexp.MustCompile(permissionsvc.REGEXP_NUMBER)
	resourcePatternRe = regexp.MustCompile(`^(.*)\{(.+)\}(.*)$`)
)

func RegisterUserRouter(e gin.IRouter) {
	r := e.Group("/inner/account-provider/api/v1/user")

	r.POST("/authn", AuthnUserHandler)
	r.POST("/authz", AuthzUserHandler)
	// calling by gateway
	r.POST("/authn-session", AuthnSessionHandler)
}

type AuthnUserReq struct {
	Auth0Token string `json:"auth0_token"`
}

type AuthzUserReq struct {
	UserID      uint64 `json:"user_id"`
	ResourceUri string `json:"resource_uri"`
	ReqUri      string `json:"req_uri"`
}

type AuthnUserRet struct {
	CheckResult  bool      `json:"check_result"`
	CheckMessage string    `json:"check_message"`
	UserInfo     *UserInfo `json:"user_info"`
}

type UserInfo struct {
	ID        uint64 `json:"id"`
	Email     string `json:"email"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
}

func AuthnSessionHandler(c *gin.Context) {
	ctx := c.Request.Context()
	req := session.AuthnSessionParam{}
	if err := c.ShouldBindJSON(&req); err != nil {
		tilog.Info(ctx, "AuthnSessionHandler fail: bind request to json error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusBadRequest, &session.AuthnSessionRet{
			Message: "invalid param",
		})
		return
	}
	traceId := c.GetHeader(traceHeader)

	bm := utils.Begin(ctx, "AuthnSessionHandler request", zap.Any("gateway_trace", traceId), zap.Any("req", req))
	defer bm.OnExit(ctx)

	req.RenewSession = true
	ret, err := session.GetUserSessionSvc().AuthnLoginSession(ctx, &req)
	if err != nil {
		tilog.Error(ctx, "AuthnSessionHandler error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusInternalServerError, &session.AuthnSessionRet{
			Message: err.Error(),
		})
		return
	}
	httpCode := http.StatusUnauthorized
	if ret.OK {
		httpCode = http.StatusOK
	}
	// do not return user meta temporarily for each biz (o11y) call account to get
	ret.UserMeta = nil
	writeResult(c.Writer, httpCode, ret)

}

func AuthnUserHandler(c *gin.Context) {
	ctx := c.Request.Context()

	req := AuthnUserReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		tilog.Info(ctx, "AuthnUserHandler bind request to json error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusBadRequest, &AuthnUserRet{
			CheckMessage: "invalid param",
		})
		return
	}
	bm := utils.Begin(ctx, "AuthnUserHandler request", zap.Any("req", req))
	defer bm.OnExit(ctx)
	if req.Auth0Token == "" {
		writeResult(c.Writer, http.StatusBadRequest, &AuthnUserRet{
			CheckMessage: "auth0_token is blank",
		})
		return
	}
	sRet, err := session.GetUserSessionSvc().AuthnLoginSessionByToken(ctx, req.Auth0Token)
	if err != nil {
		tilog.Error(ctx, "AuthnUserHandler AuthnLoginSessionByToken error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusInternalServerError, &AuthnUserRet{
			CheckMessage: err.Error(),
		})
		return
	}
	httpCode := http.StatusUnauthorized
	if sRet.OK {
		httpCode = http.StatusOK
	}
	writeResult(c.Writer, httpCode, &AuthnUserRet{
		CheckResult:  sRet.OK,
		CheckMessage: sRet.Message,
		UserInfo: &UserInfo{
			ID:        sRet.UserMeta.ID,
			Email:     sRet.UserMeta.Email,
			FirstName: sRet.UserMeta.FirstName,
			LastName:  sRet.UserMeta.LastName,
		},
	})
}

func AuthzUserHandler(c *gin.Context) {
	ctx := c.Request.Context()

	req := AuthzUserReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		tilog.Info(ctx, "AuthzUserHandler bind request to json error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusBadRequest, &permissionsvc.PermissionCheckRet{
			Message: "invalid param",
		})
		return
	}
	bm := utils.Begin(ctx, "AuthzUserHandler request", zap.Any("req", req))
	defer bm.OnExit(ctx)
	if req.UserID == 0 || req.ResourceUri == "" || req.ReqUri == "" {
		writeResult(c.Writer, http.StatusBadRequest, &permissionsvc.PermissionCheckRet{
			Message: "invalid param",
		})
		return
	}
	if !checkReqUriMatchResourceUri(req.ReqUri, req.ResourceUri) {
		writeResult(c.Writer, http.StatusBadRequest, &permissionsvc.PermissionCheckRet{
			Message: "invalid param: req_uri not match resource_uri",
		})
		return
	}
	svc := permissionsvc.GetRbacPermissionSvc()
	checkRet, err := svc.HasPermissionForUser(ctx, req.UserID, req.ReqUri, req.ResourceUri)
	if err != nil {
		tilog.Warn(ctx, "AuthzUserHandler call permission svc HasPermissionForUser error", zap.Any("err", err))
		status := http.StatusInternalServerError
		if errors.Is(err, errs.ErrBadRequest) {
			status = http.StatusBadRequest
		}
		writeResult(c.Writer, status, &permissionsvc.PermissionCheckRet{
			Message: err.Error(),
		})
		return
	}
	if checkRet.HasPermission {
		writeResult(c.Writer, http.StatusOK, checkRet)
	} else {
		writeResult(c.Writer, http.StatusForbidden, checkRet)
	}
}

func checkReqUriMatchResourceUri(reqUri string, resourceUri string) bool {
	// serverless/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/migration/tasks/{id}:pause
	// serverless/orgs/30018/projects/2803481/clusters/10809031728936022038/migration/tasks/dmt-on2rkbquabdslbl6b26jgtbx2a:pause
	reqSlice := strings.Split(reqUri, permissionsvc.SEPARATOR)
	resourceSlice := strings.Split(resourceUri, permissionsvc.SEPARATOR)
	if len(reqSlice) != len(resourceSlice) {
		return false
	}
	for i := 0; i < len(reqSlice); i++ {
		// if meet scope_type, first check scope_id
		_, ok := permissionsvc.ScopeTypeMapping[reqSlice[i]]
		if ok && i+1 < len(reqSlice) {
			matchPattern := regexpStrRe.MatchString(resourceSlice[i+1])
			matchNumber := regexpNumberRe.MatchString(reqSlice[i+1])
			if matchPattern && !matchNumber {
				return false
			}
		}
		if reqSlice[i] != resourceSlice[i] {
			pattern := resourcePatternRe
			matches := pattern.FindStringSubmatch(resourceSlice[i])
			if matches == nil {
				return false
			}
			prefix := matches[1]
			suffix := matches[3]
			if prefix != "" && !strings.HasPrefix(reqSlice[i], prefix) {
				return false
			}
			if suffix != "" && !strings.HasSuffix(reqSlice[i], suffix) {
				return false
			}
		}
	}
	return true
}
