package inner

import (
	"encoding/json"
	"errors"
	"net/http"

	"github.com/tidbcloud/tiinf/errs"

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

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

	// authentication
	r.POST("/authn", AuthnApiKeyHandler)
	// authorization
	r.POST("/authz", AuthzApiKeyHandler)
}

type AuthzApiKeyReq struct {
	PublicKey   string `json:"public_key"`
	ReqUri      string `json:"req_uri"`
	ResourceUri string `json:"resource_uri"`
}

type AuthApiKeyReq struct {
	ApiKey      string `json:"api_key"`
	ReqUri      string `json:"req_uri"`
	ResourceUri string `json:"resource_uri"`
}

type AuthApikeyRet struct {
	PublicKey     string                              `json:"public_key"`
	AuthRet       bool                                `json:"auth_ret"`
	ResourceInfos []apikeysvc.AuthnApiKeyResourceInfo `json:"resource_infos,omitempty"`
	Message       string                              `json:"message"`
}

func AuthnApiKeyHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "AuthnApiKeyHandler")
	defer bm.OnExit(ctx)

	handlerRet := &AuthApikeyRet{
		Message: "invalid param",
	}
	req := AuthApiKeyReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		tilog.Info(ctx, "AuthnApiKeyHandler bind request to json error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusBadRequest, handlerRet)
		return
	}
	tilog.Info(ctx, "AuthnApiKeyHandler request", zap.Bool("api_key_present", req.ApiKey != ""))
	if req.ApiKey == "" {
		handlerRet.Message = "api_key is blank"
		writeResult(c.Writer, http.StatusBadRequest, handlerRet)
		return
	}
	ak, sk, err := apikeysvc.ParseInnerApiKey(ctx, req.ApiKey)
	if err != nil {
		tilog.Info(ctx, "AuthnApiKeyHandler ParseInnerApiKey error", zap.Error(err))
		handlerRet.Message = err.Error()
		writeResult(c.Writer, http.StatusBadRequest, handlerRet)
		return
	}
	handlerRet.PublicKey = ak

	authnRet, err := apikeysvc.AuthnApiKey(ctx, ak, sk)
	if err != nil {
		tilog.Info(ctx, "AuthnApiKeyHandler AuthnApiKey error", zap.Error(err))
		handlerRet.Message = err.Error()
		writeResult(c.Writer, http.StatusInternalServerError, handlerRet)
		return
	}
	tilog.Info(ctx, "AuthnApiKeyHandler authn result", zap.Any("ak", ak), zap.Any("apiKey", req.ApiKey), zap.Any("ret", authnRet))

	httpStatus := http.StatusUnauthorized
	if authnRet.AuthRet {
		httpStatus = http.StatusOK
	}
	handlerRet.AuthRet = authnRet.AuthRet
	handlerRet.ResourceInfos = authnRet.ResourceInfos
	handlerRet.Message = authnRet.Message
	writeResult(c.Writer, httpStatus, handlerRet)

}

func writeResult(rw http.ResponseWriter, status int, ret interface{}) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(status)

	raw, _ := json.Marshal(ret)
	_, _ = rw.Write(raw)
}

func AuthzApiKeyHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "AuthzApiKeyHandler")
	defer bm.OnExit(ctx)

	handlerRet := &AuthApikeyRet{
		Message: "invalid param",
	}

	req := AuthzApiKeyReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		tilog.Info(ctx, "AuthzApiKeyHandler bind request to json error", zap.Any("err", err))
		writeResult(c.Writer, http.StatusBadRequest, handlerRet)
		return
	}
	tilog.Info(ctx, "AuthzApiKeyHandler request", zap.Any("req", req))
	if req.PublicKey == "" || req.ResourceUri == "" || req.ReqUri == "" {
		writeResult(c.Writer, http.StatusBadRequest, handlerRet)
		return
	}
	if !checkReqUriMatchResourceUri(req.ReqUri, req.ResourceUri) {
		handlerRet.Message = "invalid param: req_uri not match resource_uri"
		writeResult(c.Writer, http.StatusBadRequest, handlerRet)
		return
	}
	handlerRet.PublicKey = req.PublicKey

	svc := permission.GetRbacPermissionSvc()
	authzRet, err := svc.HasPermissionForApiKey(ctx, req.PublicKey, req.ReqUri, req.ResourceUri)
	if err != nil {
		tilog.Info(ctx, "AuthzApiKeyHandler call permission svc HasPermissionForApiKey error", zap.Any("req", req), zap.Any("err", err))
		status := http.StatusInternalServerError
		if errors.Is(err, errs.ErrBadRequest) {
			status = http.StatusBadRequest
		}
		handlerRet.Message = err.Error()
		writeResult(c.Writer, status, handlerRet)
		return
	}
	tilog.Info(ctx, "AuthzApiKeyHandler authz result", zap.Any("req", req), zap.Any("ret", authzRet))

	httpStatus := http.StatusForbidden
	if authzRet.HasPermission {
		httpStatus = http.StatusOK
	}
	handlerRet.AuthRet = authzRet.HasPermission
	handlerRet.Message = authzRet.Message
	writeResult(c.Writer, httpStatus, handlerRet)
}
