package controller

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	userapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"

	"github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/pkg/controller/auth"
	coresvccli "github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	httputil "github.com/tidbcloud/devops/pkg/infra/utils/http"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	larkcard "github.com/larksuite/oapi-sdk-go/v3/card"
	larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type AuthMiddlewareBuilder struct {
	auth *auth.Auth
}

func NewAuthMiddlewareBuilder(auth *auth.Auth) *AuthMiddlewareBuilder {
	return &AuthMiddlewareBuilder{
		auth: auth,
	}
}

func (b *AuthMiddlewareBuilder) APIMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		var identity *auth.AuthnResult
		var skipAuthz bool

		token := GetAccessTokenFromContext(c)
		apiToken := GetAPITokenFromContext(c)
		key := GetAPIKeyFromContext(c)

		// authn
		var err error
		switch {
		case token != "":
			identity, skipAuthz, err = b.auth.AuthnFromAccessToken(c, token)
		case apiToken != "":
			identity, skipAuthz, err = b.auth.AuthnFromAPIToken(c, apiToken, c.ClientIP())
		case key != "":
			identity, skipAuthz, err = b.auth.AuthnFromAPIKey(c, key)
		default:
			err = fmt.Errorf("miss credential")
		}
		if err != nil {
			FailWithCode(c, err, authnErrorCode(err))
			return
		}

		b.setIdentityContext(c, identity)

		if skipAuthz {
			c.Next()
			return
		}
		uri := c.FullPath()
		if c.FullPath() == "/api/v1/devops/billing-proxy/*any" ||
			c.FullPath() == "/api/v1/devops/aiops/proxy/*any" ||
			c.FullPath() == "/api/v1/devops-external/aiops/proxy/*any" {
			uri = c.Request.URL.Path
		}
		b.authz(c, uri, identity)

		c.Next()
	}
}

func authnErrorCode(err error) api.ErrCode {
	if status.Code(err) == codes.PermissionDenied {
		return api.ErrCodePermissionDenied
	}
	return api.ErrCodeUnauthenticated
}

func Soc2Middleware(userCli userapiv1.UserServiceClient) gin.HandlerFunc {
	return func(c *gin.Context) {
		email := c.GetString(consts.CtxUserEmail)
		if email == "" {
			c.Next()
			return
		}

		resp, err := userCli.GetUser(c, &userapiv1.GetUserRequest{Email: email})
		if err != nil {
			Fail(c, err)
			return
		}
		if !resp.User.Soc2Compliant {
			FailWithCode(c, fmt.Errorf("user %s is not soc2 compliant", email), api.ErrCodePermissionDenied)
			return
		}

		c.Next()
	}
}

func (b *AuthMiddlewareBuilder) authz(c *gin.Context, uri string, identity *auth.AuthnResult) {
	allow, err := b.auth.Authz(c, identity, uri, c.Request.Method)
	if err != nil {
		FailWithCode(c, err, api.ErrCodePermissionDenied)
		return
	}
	if !allow {
		FailWithCode(c, fmt.Errorf("permission denied"), api.ErrCodePermissionDenied)
		return
	}
}

func (b *AuthMiddlewareBuilder) setIdentityContext(c *gin.Context, identity *auth.AuthnResult) {
	profile := identity.PrincipalProfile
	c.Set(consts.CtxOperatorType, profile.OperatorType)
	c.Set(consts.CtxOperator, profile.OperatorName)
	c.Set(consts.CtxUserEmail, profile.UserEmail)
	c.Set(consts.CtxUserProfile, profile)
	if identity.APIKey != nil {
		c.Set(consts.CtxAPIKeyProfile, identity.APIKey)
		c.Set(consts.CtxRoleScope, profile.AggregatedRoleScope)
	}
}

// TenantScopeMiddleware enforces the API key's role tenant restriction
// against the tenant_id path param.
func TenantScopeMiddleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		scope, ok := roleScopeFromCtx(ctx)
		if !ok || !isRestricted(scope) {
			ctx.Next()
			return
		}
		tenantID, err := strconv.ParseUint(ctx.Param("tenant_id"), 10, 64)
		if err != nil {
			FailWithCode(ctx, fmt.Errorf("tenant scope is required for this api key"), api.ErrCodePermissionDenied)
			ctx.Abort()
			return
		}
		if !tenantAllowedByScope(scope, tenantID) {
			FailWithCode(ctx, fmt.Errorf("tenant %d is not accessible with this api key (allowed: %v)", tenantID, scope.AllowedTenantIds), api.ErrCodePermissionDenied)
			ctx.Abort()
			return
		}
		ctx.Next()
	}
}

// ClusterScopeMiddleware enforces the API key's role tenant restriction
// against the cluster_id path param (resolved to its tenant via coreSvcCli).
func ClusterScopeMiddleware(coreSvcCli *coresvccli.CoreSvcCli) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		scope, ok := roleScopeFromCtx(ctx)
		if !ok || !isRestricted(scope) {
			ctx.Next()
			return
		}
		clusterID, err := strconv.ParseUint(ctx.Param("cluster_id"), 10, 64)
		if err != nil {
			FailWithCode(ctx, fmt.Errorf("cluster scope is required for this api key"), api.ErrCodePermissionDenied)
			ctx.Abort()
			return
		}
		tenantID, err := resolveClusterTenant(ctx, coreSvcCli, clusterID)
		if err != nil {
			Fail(ctx, err)
			ctx.Abort()
			return
		}
		if !tenantAllowedByScope(scope, tenantID) {
			FailWithCode(ctx, fmt.Errorf("cluster %d (tenant %d) is not accessible with this api key (allowed: %v)", clusterID, tenantID, scope.AllowedTenantIds), api.ErrCodePermissionDenied)
			ctx.Abort()
			return
		}
		ctx.Next()
	}
}

// resolveClusterTenant returns the owning tenant id of the given id, which
// the devops API path /clusters/:cluster_id overloads to address either a
// real cluster or a resource pool. It tries GetCluster first and falls back
// to GetResourcePool on NotFound, mirroring GetClusterCore.
func resolveClusterTenant(ctx context.Context, coreSvcCli *coresvccli.CoreSvcCli, clusterID uint64) (uint64, error) {
	cluster, err := coreSvcCli.Mgmt.GetCluster(ctx, clusterID)
	if err == nil {
		return cluster.GetOrganizationId(), nil
	}
	if apie, ok := errs.AsAPIError(err); !ok || apie.ErrCode() != api.ErrCodeNotFound {
		return 0, fmt.Errorf("failed to get cluster %d: %w", clusterID, err)
	}
	rp, err := coreSvcCli.Mgmt.GetResourcePool(ctx, clusterID)
	if err != nil {
		return 0, fmt.Errorf("failed to get resource pool %d: %w", clusterID, err)
	}
	rpp := rp.GetResourcePool()
	if rpp.GetOwnerType() != nextgenv1.ResourcePool_ORGANIZATION {
		return 0, fmt.Errorf("resource pool %d is not owned by an organization", clusterID)
	}
	return rpp.GetOwnerId(), nil
}

// roleScopeFromCtx returns the bound role scope that authorized the current
// request, and whether a scope was set on the context at all. Callers that
// were not subjected to role-scope authn return (nil, false), which check
// helpers treat as "no restriction". A nil scope with ok=true means the
// request was authorized without any tenant restriction.
func roleScopeFromCtx(ctx *gin.Context) (*userapiv1.PrincipalRoleScope, bool) {
	v, ok := ctx.Get(consts.CtxRoleScope)
	if !ok {
		return nil, false
	}
	scope, _ := v.(*userapiv1.PrincipalRoleScope)
	return scope, true
}

// isRestricted reports whether the role scope carries a non-empty tenant
// allow-list. nil scope or empty AllowedTenantIds means unrestricted.
func isRestricted(scope *userapiv1.PrincipalRoleScope) bool {
	return scope != nil && len(scope.AllowedTenantIds) > 0
}

func tenantAllowedByScope(scope *userapiv1.PrincipalRoleScope, tenantID uint64) bool {
	if scope == nil {
		return true
	}
	for _, id := range scope.AllowedTenantIds {
		if id == tenantID {
			return true
		}
	}
	return false
}

// IsTenantRestricted reports whether the current request is subject to a
// tenant restriction. List-style handlers use this to require an explicit
// tenant_id filter from restricted callers.
func IsTenantRestricted(ctx *gin.Context) bool {
	scope, ok := roleScopeFromCtx(ctx)
	return ok && isRestricted(scope)
}

// CheckTenantScope checks whether the given tenantID is allowed by the
// API key's role scope. Returns nil for non-API-key callers and for API
// keys whose role has no tenant restriction.
func CheckTenantScope(ctx *gin.Context, tenantID uint64) error {
	scope, ok := roleScopeFromCtx(ctx)
	if !ok || !isRestricted(scope) {
		return nil
	}
	if !tenantAllowedByScope(scope, tenantID) {
		return fmt.Errorf("tenant %d is not accessible with this api key (allowed: %v)", tenantID, scope.AllowedTenantIds)
	}
	return nil
}

// CheckParamsScope enforces per-role tenant scope on every tenant- or
// resource-identifying entry in a flat operation params map. *All* present
// ids are checked (not just the first one) so a restricted caller can't
// sneak a disallowed cluster/pool past the gate by also setting an allowed
// tenant_id. Used by both operation and approval handlers, which carry the
// same param keys ("tenant_id", "cluster_id", "resource_pool_id").
func CheckParamsScope(ctx *gin.Context, coreSvcCli *coresvccli.CoreSvcCli, params map[string]string) error {
	if s, ok := params["resource_pool_id"]; ok {
		if id, err := strconv.ParseUint(s, 10, 64); err == nil {
			if err := CheckResourcePoolScope(ctx, coreSvcCli, id); err != nil {
				return err
			}
		}
	}
	if s, ok := params["tenant_id"]; ok {
		if id, err := strconv.ParseUint(s, 10, 64); err == nil {
			if err := CheckTenantScope(ctx, id); err != nil {
				return err
			}
		}
	}
	if s, ok := params["cluster_id"]; ok {
		if id, err := strconv.ParseUint(s, 10, 64); err == nil {
			if err := CheckClusterScope(ctx, coreSvcCli, id); err != nil {
				return err
			}
		}
	}
	return nil
}

// CheckResourcePoolScope resolves resource_pool_id directly instead of
// pretending it is a tenant cluster. Organization-owned pools still inherit
// the owner tenant scope. Shared pools are platform-wide targets and require
// an unrestricted API key until a dedicated shared-pool role scope exists.
func CheckResourcePoolScope(ctx *gin.Context, coreSvcCli *coresvccli.CoreSvcCli, resourcePoolID uint64) error {
	scope, ok := roleScopeFromCtx(ctx)
	if !ok || !isRestricted(scope) {
		return nil
	}
	rp, err := coreSvcCli.Mgmt.GetResourcePool(ctx, resourcePoolID)
	if err != nil {
		return err
	}
	rpp := rp.GetResourcePool()
	switch rpp.GetOwnerType() {
	case nextgenv1.ResourcePool_ORGANIZATION:
		tenantID := rpp.GetOwnerId()
		if !tenantAllowedByScope(scope, tenantID) {
			return fmt.Errorf("resource pool %d (tenant %d) is not accessible with this api key (allowed: %v)", resourcePoolID, tenantID, scope.AllowedTenantIds)
		}
	case nextgenv1.ResourcePool_SHARED:
		return fmt.Errorf("shared resource pool %d is not accessible with a tenant-restricted api key", resourcePoolID)
	default:
		return fmt.Errorf("resource pool %d has unsupported owner type %s", resourcePoolID, rpp.GetOwnerType().String())
	}
	return nil
}

// CheckClusterScope resolves the tenant of a cluster (or resource pool,
// see resolveClusterTenant) and checks it against the API key's role scope.
func CheckClusterScope(ctx *gin.Context, coreSvcCli *coresvccli.CoreSvcCli, clusterID uint64) error {
	scope, ok := roleScopeFromCtx(ctx)
	if !ok || !isRestricted(scope) {
		return nil
	}
	tenantID, err := resolveClusterTenant(ctx, coreSvcCli, clusterID)
	if err != nil {
		return err
	}
	if !tenantAllowedByScope(scope, tenantID) {
		return fmt.Errorf("cluster %d (tenant %d) is not accessible with this api key (allowed: %v)", clusterID, tenantID, scope.AllowedTenantIds)
	}
	return nil
}

func CorsMiddleware() gin.HandlerFunc {
	return cors.New(cors.Config{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{"PUT", "PATCH", "GET", "POST", "DELETE"},
		AllowHeaders:     []string{"Origin", "Authorization", "X-Requested-With", "Content-Type", "Accept", "Idempotency-Key"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
		AllowOriginFunc: func(origin string) bool {
			return true
		},
		MaxAge: 12 * time.Hour,
	})
}

func RecoveryMiddleware(logger logr.Logger) gin.HandlerFunc {
	return gin.CustomRecovery(func(c *gin.Context, e interface{}) {
		if err, ok := e.(error); ok {
			logger.Error(err, "recovered a panic")
		} else {
			logger.Error(fmt.Errorf("%v", e), "recovered a panic")
		}
		Fail(c, fmt.Errorf("internal server error"))
	})
}

type LarkMiddlewareBuiler struct {
	EncryptKey        string
	VerificationToken string
	Logger            logr.Logger

	CardEventProxyKey   string
	CardEventProxyAddrs map[string]string
}

func (b *LarkMiddlewareBuiler) EventMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		rawBody, err := FetchBody(c)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// decrypt
		plain, err := b.larkDecrypt(rawBody)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// parse
		body := &larkevent.EventV2Body{}
		err = json.Unmarshal(plain, body)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// handle challenge
		if larkevent.ReqType(body.Type) == larkevent.ReqTypeChallenge {
			c.JSON(http.StatusOK, gin.H{"challenge": body.Challenge})
			c.Abort()
			return
		}

		// verify sign
		err = b.verifySignV2(c, rawBody)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// set cardAction to context
		c.Set(consts.CtxLarkEvent, body)
		c.Next()
	}
}

// CardMiddleware parse and verify lark card event.
//
// It will set cardAction to context.
func (b *LarkMiddlewareBuiler) CardMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		rawBody, err := FetchBody(c)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// decrypt
		plain, err := b.larkDecrypt(rawBody)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// parse
		cardAction := &larkcard.CardAction{}
		err = json.Unmarshal(plain, cardAction)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// handle challenge
		if larkevent.ReqType(cardAction.Type) == larkevent.ReqTypeChallenge {
			c.JSON(http.StatusOK, gin.H{"challenge": cardAction.Challenge})
			c.Abort()
			return
		}

		// proxy
		if cardAction.Action != nil {
			if proxyValueAny, exist := cardAction.Action.Value[b.CardEventProxyKey]; exist { // proxy key exist
				if addr, exist := b.CardEventProxyAddrs[proxyValueAny.(string)]; exist { // addr exist
					// proxy to other service
					b.Logger.Info("proxy a card event to other service", "addr", addr, "card_event", cardAction)
					httputil.Proxy(c,
						[]httputil.RequestOpts{
							httputil.ReplaceScheme("https"),
							httputil.ReplaceHost(addr),
						},
						[]httputil.ProxyOpts{httputil.WithErrorLogger(b.Logger)},
					)
					c.Abort()
					return
				}

			}
		}

		// verify sign
		err = b.verifySign(c, rawBody)
		if err != nil {
			FailWithCode(c, err, api.ErrCodeInternal)
			return
		}

		// set cardAction to context
		c.Set(consts.CtxLarkCardAction, cardAction)
		c.Next()
	}
}

func (b *LarkMiddlewareBuiler) larkDecrypt(bs []byte) ([]byte, error) {
	var plain []byte
	var encrypt larkevent.EventEncryptMsg
	err := json.Unmarshal(bs, &encrypt)
	if err != nil {
		return nil, err
	}
	if encrypt.Encrypt != "" {
		if b.EncryptKey == "" {
			return nil, fmt.Errorf("encrypt_key not found")
		}
		plain, err = larkevent.EventDecrypt(encrypt.Encrypt, b.EncryptKey)
		if err != nil {
			return nil, err
		}
	} else {
		plain = bs
	}

	return plain, nil
}

// For lark event, we use the v2 signature verification.
// https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/encrypt-key-encryption-configuration-case
func (b *LarkMiddlewareBuiler) verifySignV2(c *gin.Context, rawBody []byte) error {
	if b.EncryptKey == "" {
		return nil
	}

	requestTimestamp := c.GetHeader(larkevent.EventRequestTimestamp)
	requestNonce := c.GetHeader(larkevent.EventRequestNonce)
	sourceSign := c.GetHeader(larkevent.EventSignature)

	targetSign := larkevent.Signature(requestTimestamp, requestNonce, b.EncryptKey, string(rawBody))

	if targetSign == sourceSign {
		return nil
	}
	return fmt.Errorf("the result of signature verification failed")
}

// For lark card event, we use the v1 signature verification.
// Ref: https://open.feishu.cn/document/common-capabilities/message-card/add-card-interaction/message-card-security-verification
func (b *LarkMiddlewareBuiler) verifySign(c *gin.Context, rawBody []byte) error {
	if b.VerificationToken == "" {
		return nil
	}

	requestTimestamp := c.GetHeader(larkevent.EventRequestTimestamp)
	requestNonce := c.GetHeader(larkevent.EventRequestNonce)
	sourceSign := c.GetHeader(larkevent.EventSignature)

	targetSign := larkcard.Signature(requestTimestamp, requestNonce, b.VerificationToken, string(rawBody))

	if targetSign == sourceSign {
		return nil
	}
	return fmt.Errorf("the result of signature verification failed")
}
