package auth

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

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type GenAccessParam4GinFunc func(c *gin.Context) (*AccessParam, error)

func GenAccessParam4GinByDefault(c *gin.Context) *AccessParam {
	param := AccessParam{
		Metadata: map[string]interface{}{
			AccessMetadataPath:   c.Request.URL.Path,
			AccessMetadataMethod: c.Request.Method,
		},
		Req: c,
	}
	return &param
}

// 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 GenAccessParam4GinWithPrefixOrgId(c *gin.Context) (*AccessParam, error) {
	authInfo, err := GetIdentityInfoFromGinContext(c)
	if err != nil {
		return nil, err
	}
	var orgId uint64
	if authInfo.IdentityType == AuthnIdentityTypeApiKeyAK {
		orgId, _ = GetOrgIdFromApiKeyRsp(c.Request.Context(), authInfo.ApiKeyRsp)
		if orgId == 0 {
			orgId, _ = GetAccessOrgIdFromGinContext(c)
		}
	} else if authInfo.IdentityType == AuthnIdentityTypeAccessToken {
		orgId = authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
	} else if authInfo.IdentityType == AuthnIdentityTypeUserToken {
		orgId, _ = GetAccessOrgIdFromGinContext(c)
	} else {
		return nil, errors.New("invalid authn identity type")
	}
	urlPath := fmt.Sprintf("/orgs/%d%s", orgId, c.Request.URL.Path)
	param := AccessParam{
		Metadata: map[string]interface{}{
			AccessMetadataPath:   urlPath,
			AccessMetadataMethod: c.Request.Method,
		},
		Req: c,
	}
	return &param, nil
}

type PermissionCheckResultHandleFunc func(w http.ResponseWriter, r *http.Request, result *AccessResult, err error) (continueRun bool)

func defaultPermissionCheckResultHandleFunc(w http.ResponseWriter, r *http.Request, result *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.Header().Set("Content-Type", "application/json")
		jsonResp, _ := json.Marshal(map[string]string{
			"error": err.Error(),
		})
		_, _ = w.Write(jsonResp)
		return false

	}
	if result.Code != AccessRetRelease {
		w.WriteHeader(http.StatusForbidden)
		w.Header().Set("Content-Type", "application/json")
		jsonResp, _ := json.Marshal(map[string]string{
			"error": result.Msg,
		})
		_, _ = w.Write(jsonResp)
		return false
	}
	return true
}

type GenAccessParam4GrpcGatewayFunc func(ctx context.Context) (*AccessParam, error)

func GenAccessParam4GrpcGateway(ctx context.Context) *AccessParam {
	param := AccessParam{
		Metadata: map[string]interface{}{
			AccessMetadataPath:   GetHttpMetadataFromGrpcContext(ctx, AccessMetadataPath),
			AccessMetadataMethod: GetHttpMetadataFromGrpcContext(ctx, AccessMetadataMethod),
		},
		Req: ctx,
	}
	return &param
}

func GenAccessParam4GrpcGatewayWithPrefixOrgId(ctx context.Context) (*AccessParam, error) {
	authInfo, err := GetIdentityInfoFromGrpcContext(ctx)
	if err != nil {
		return nil, err
	}
	var orgId uint64
	if authInfo.IdentityType == AuthnIdentityTypeApiKeyAK {
		orgId, _ = GetOrgIdFromApiKeyRsp(ctx, authInfo.ApiKeyRsp)
		if orgId == 0 {
			orgId, _ = GetAccessOrgIdFromGrpcContext(ctx)
		}
	} else if authInfo.IdentityType == AuthnIdentityTypeAccessToken {
		orgId = authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
	} else if authInfo.IdentityType == AuthnIdentityTypeUserToken {
		orgId, _ = GetAccessOrgIdFromGrpcContext(ctx)
	} else {
		return nil, errors.New("invalid authn identity type")
	}
	param := AccessParam{
		Metadata: map[string]interface{}{
			AccessMetadataPath:   fmt.Sprintf("/orgs/%d%s", orgId, GetHttpMetadataFromGrpcContext(ctx, AccessMetadataPath)),
			AccessMetadataMethod: GetHttpMetadataFromGrpcContext(ctx, AccessMetadataMethod),
		},
		Req: ctx,
	}
	return &param, nil
}
