package api

import (
	"context"
	"fmt"
	"strings"

	accounthandler "github.com/tidbcloud/account/thirdparty/account"

	"github.com/tidbcloud/account/internel/svc/apikey"
	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/internel/controllers/openapi"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/sso"
	"github.com/tidbcloud/account/internel/svc/orgsso"
	"github.com/tidbcloud/tiinf/sdk/rbac/helperimpl"

	"github.com/gin-gonic/gin"
	"github.com/imulab/go-scim/pkg/v2/handlerutil"
	"github.com/imulab/go-scim/pkg/v2/spec"
	"github.com/tidbcloud/account/central/server"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/rbac"
	"github.com/tidbcloud/tiinf/sdk/rbac/common"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.uber.org/zap"
	"google.golang.org/grpc/metadata"
)

const (
	scimAccessApiKey = "scim_access_api_key"

	XAuthMethodHeader  = "X-Auth-Method"
	XAuthRawHeader     = "X-Auth-Raw"
	XAuthContentHeader = "X-Auth-Content"

	XAuthMethodValBear   = "bear"
	XAuthMethodValBasic  = "basic"
	XAuthMethodValDigest = "digest"
)

var (
	ErrUnAuthenticated = &spec.Error{Status: 401, Type: "Unauthenticated"}
	ErrAccessDenied    = &spec.Error{Status: 403, Type: "Access denied"}
)

var checker rbac.PermissionChecker

var scimPathPatternResourceMap = map[string]string{
	"/orgs/(\\d+)/scim/v2/ServiceProviderConfig": "account/scim/orgs/{org_id}/service_provider_config",
	"/orgs/(\\d+)/scim/v2/Schemas":               "account/scim/orgs/{org_id}/list_schemas",
	"/orgs/(\\d+)/scim/v2/Schemas/\\S+":          "account/scim/orgs/{org_id}/get_schema",
	"/orgs/(\\d+)/scim/v2/ResourceTypes":         "account/scim/orgs/{org_id}/list_resource_types",
	"/orgs/(\\d+)/scim/v2/ResourceTypes/\\S+":    "account/scim/orgs/{org_id}/get_resource_type",

	"get:/orgs/(\\d+)/scim/v2/Users":         "account/scim/orgs/{org_id}/list_users",
	"post:/orgs/(\\d+)/scim/v2/Users":        "account/scim/orgs/{org_id}/create_user",
	"get:/orgs/(\\d+)/scim/v2/Users/\\S+":    "account/scim/orgs/{org_id}/get_user",
	"put:/orgs/(\\d+)/scim/v2/Users/\\S+":    "account/scim/orgs/{org_id}/put_user",
	"patch:/orgs/(\\d+)/scim/v2/Users/\\S+":  "account/scim/orgs/{org_id}/patch_user",
	"delete:/orgs/(\\d+)/scim/v2/Users/\\S+": "account/scim/orgs/{org_id}/delete_user",

	"get:/orgs/(\\d+)/scim/v2/Groups":         "account/scim/orgs/{org_id}/list_groups",
	"post:/orgs/(\\d+)/scim/v2/Groups":        "account/scim/orgs/{org_id}/create_group",
	"get:/orgs/(\\d+)/scim/v2/Groups/\\S+":    "account/scim/orgs/{org_id}/get_group",
	"put:/orgs/(\\d+)/scim/v2/Groups/\\S+":    "account/scim/orgs/{org_id}/put_group",
	"patch:/orgs/(\\d+)/scim/v2/Groups/\\S+":  "account/scim/orgs/{org_id}/patch_group",
	"delete:/orgs/(\\d+)/scim/v2/Groups/\\S+": "account/scim/orgs/{org_id}/delete_group",
}

func scimProvisionCheckFunc(c *gin.Context) {
	ctx := c.Request.Context()
	orgId, _ := getOrgId(c)

	svc, _ := orgsso.GetOrgSsoSvc()
	methods, err := svc.ListAuthMethods(ctx, sso.OrgAuthMethodFilter{
		OrgIds: []uint64{orgId},
		Types:  []entity.AuthMethodType{entity.AuthMethodTypeSAML},
		Status: []entity.EntityStatus{entity.Enable},
	})
	if err != nil {
		tilog.Error(ctx, "ScimSwitchCheckFunc ListAuthMethods error", zap.Any("orgID", orgId), zap.Any("err", err))
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: org enable scim check failed", ErrAccessDenied))
		c.Abort()
		return
	}
	if len(methods) == 0 {
		tilog.Warn(ctx, "ScimSwitchCheckFunc org not enable saml", zap.Any("orgID", orgId))
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: org not enable saml", ErrAccessDenied))
		c.Abort()
		return
	}
	if !methods[0].ScimProvision {
		tilog.Warn(ctx, "ScimSwitchCheckFunc org not enable scim", zap.Any("orgID", orgId))
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: org not enable scim", ErrAccessDenied))
		c.Abort()
		return
	}
	c.Next()
}

func scimReceiveHttpRequest(c *gin.Context) {
	tilog.Info(c.Request.Context(), fmt.Sprintf("Scim receive http requst %s: %s", c.Request.Method, c.Request.URL.Path), zap.Any("header", c.Request.Header))
	c.Next()
	tilog.Info(c.Request.Context(), fmt.Sprintf("Scim return http request %d", c.Writer.Status()))
}

func scimAuthFunc(c *gin.Context) {
	authMethodHeader := c.Request.Header.Get(XAuthMethodHeader)
	// Bearer token
	if authMethodHeader == XAuthMethodValBear {
		authRaw := c.Request.Header.Get(XAuthRawHeader)
		keyRsp, err := scimAuthBearerToken(c.Request.Context(), authRaw)
		if err != nil {
			tilog.Info(c.Request.Context(), "ScimAuthFunc scimAuthBearerToken error", zap.Any("authRaw", authRaw), zap.Any("err", err))
			_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: Authorizate failed", ErrUnAuthenticated))
			c.Abort()
			return
		}
		c.Set(scimAccessApiKey, keyRsp)
		tilog.Info(c.Request.Context(), "ScimAuthFunc auth bearer token success", zap.Any("authRaw", authRaw))
		c.Next()
		return
	}
	// Api Key
	if authMethodHeader == XAuthMethodValBasic || authMethodHeader == XAuthMethodValDigest {
		authContent := c.Request.Header.Get(XAuthContentHeader)
		keyRsp, err := scimAuthApiKey(c.Request.Context(), authContent)
		if err != nil {
			tilog.Info(c.Request.Context(), "ScimAuthFunc scimAuthApiKey error", zap.Any("authContent", authContent), zap.Any("err", err))
			_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: Authorizate failed", ErrUnAuthenticated))
			c.Abort()
			return
		}
		c.Set(scimAccessApiKey, keyRsp)
		tilog.Info(c.Request.Context(), "ScimAuthFunc auth api key success", zap.Any("authContent", authContent), zap.Any("keyRsp", keyRsp))
		c.Next()
		return
	}
	// unknown header
	tilog.Info(c.Request.Context(), fmt.Sprintf("ScimAuthFunc unknown %s header: %s", XAuthMethodHeader, authMethodHeader))
	_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: Authorizate failed", ErrUnAuthenticated))
	c.Abort()

}

func scimAuthBearerToken(ctx context.Context, authRaw string) (*account.GetApiKeyByAccessKeyRsp, error) {
	tilog.Info(ctx, "ScimAuthBearerToken start", zap.Any("authRaw", authRaw))
	bearerPrefix := "Bearer "
	hasPrefix := strings.HasPrefix(authRaw, bearerPrefix)
	if !hasPrefix {
		return nil, errs.NewBadRequest("invalid bearer token", nil)
	}
	token := strings.TrimPrefix(authRaw, bearerPrefix)

	ak, sk, err := apikey.ParseBearerToken(ctx, token)
	tilog.Info(ctx, "ScimAuthBearerToken ParseBearerToken result", zap.Any("ak", ak), zap.Any("sk", sk), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	rsp, err := accounthandler.GetHandler().GetApiKeyByAccessKey(ctx, &account.GetApiKeyByAccessKeyReq{
		AccessKey: ak,
	})
	tilog.Info(ctx, "ScimAuthBearerToken GetApiKeyByAccessKey result", zap.Any("ak", ak), zap.Any("rsp", rsp), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if rsp.ApiKey.SecretKey != sk {
		return nil, errs.NewUnauthorized("sk invalid", nil)
	}
	return rsp, nil
}

func scimAuthApiKey(ctx context.Context, authContent string) (*account.GetApiKeyByAccessKeyRsp, error) {
	tilog.Info(ctx, "ScimAuthApiKey start", zap.Any("authContent", authContent))
	if authContent == "" {
		return nil, errs.NewBadRequest("authContent is blank", nil)
	}
	req := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{
		"X-Auth-Content": authContent,
	}))
	ctx, err := server.AuthFuncOpenAPI(req)
	if err != nil {
		return nil, err
	}
	ctxVal := ctx.Value(openapi.OpenAPIAccessKey)
	keyRsp := ctxVal.(*account.GetApiKeyByAccessKeyRsp)
	return keyRsp, nil
}

func scimRbacFunc(c *gin.Context) {
	param := useadapter.GenAccessParam4Gin(c)
	ctx := c.Request.Context()

	result, err := checker.Handle(ctx, param)
	tilog.Info(ctx, "ScimRbacFunc permission check finished", zap.Any("result", result), zap.Any("err", err), zap.Any("param", param))

	if err != nil {
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: Access check failed", ErrAccessDenied))
		c.Abort()
		return
	}
	if result.Code != common.RSP_CODE_RELEASE {
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: Access denied", ErrAccessDenied))
		c.Abort()
		return
	}

	c.Next()
}

func initPermissionChecker() {
	var identityF common.GetIdentityFunc = scimGetIdentityFunc
	resourceH := helperimpl.NewMapStoreResourceHelper(scimPathPatternResourceMap)
	permissionH := helperimpl.DefaultApiKeyPermissionHelper

	checker = rbac.NewPermissionChecker(identityF, resourceH, permissionH)
}

func scimGetIdentityFunc(_ context.Context, param *common.AccessParam) (string, error) {
	c := param.Req.(*gin.Context)
	cValue, _ := c.Get(scimAccessApiKey)
	keyRsp, _ := cValue.(*account.GetApiKeyByAccessKeyRsp)
	return keyRsp.ApiKey.AccessKey, nil
}
