package rbac

import (
	"context"
	"fmt"
	"strconv"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"

	"github.com/Kong/go-pdk"
	"github.com/pingcap/errors"
	sdkauth "github.com/tidbcloud/tiinf/sdk/auth"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/pingcap-inc/clinic/pkg/api_gateway/auth/api_key"
	"github.com/pingcap-inc/clinic/pkg/api_gateway/auth/utils"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/rbac"
)

type PermissionChecker interface {
	Handle(kong *pdk.PDK, obj any) (bool, error)
}

type ClinicRBACChecker struct {
	cli                rbac.CasbinClient
	resourceUri        *resourceUri
	whitelistRequest   *whitelistRequest
	userDebugwlRequest *whitelistRequest
}

func NewCasbinClient(rbacEndpoint string) func() (rbac.CasbinClient, error) {
	return func() (rbac.CasbinClient, error) {
		conn, err := grpc.NewClient(
			rbacEndpoint,
			grpc.WithTransportCredentials(insecure.NewCredentials()),
			grpc.WithChainUnaryInterceptor(),
		)
		if err != nil {
			return nil, err
		}

		return rbac.NewCasbinClient(conn), nil
	}
}

func NewClinicRBACChecker(cliFun func() (rbac.CasbinClient, error), resourceMap map[string]string, whitelist, userdebugList []string) (*ClinicRBACChecker, error) {
	resourceUri, err := NewResourceUri(resourceMap)
	if err != nil {
		return nil, err
	}

	whitelistRequest, err := NewWhitelistRequest(whitelist)
	if err != nil {
		return nil, err
	}
	userdebugRequest, err := NewWhitelistRequest(userdebugList)
	if err != nil {
		return nil, err
	}

	rbacCli, err := cliFun()
	if err != nil {
		return nil, err
	}

	if rbacCli == nil {
		return nil, fmt.Errorf("empty rbac client")
	}

	return &ClinicRBACChecker{
		cli:                rbacCli,
		resourceUri:        resourceUri,
		whitelistRequest:   whitelistRequest,
		userDebugwlRequest: userdebugRequest,
	}, nil
}

func (p *ClinicRBACChecker) Handle(kong *pdk.PDK, obj any) (bool, error) {
	user, ok := obj.(*session.CtxUser)
	if !ok {
		return false, errors.New("unexpect user type")
	}

	path, err := utils.GetURI(kong)
	if err != nil {
		return false, err
	}

	method, err := utils.GetMethod(kong)
	if err != nil {
		return false, err
	}

	allow, err := p.whitelistRequest.Request(path, method)
	if err != nil {
		return false, err
	}

	if allow {
		userdebug, err := p.userDebugwlRequest.Request(path, method)
		if err != nil {
			return false, err
		}
		if userdebug {
			if user == nil {
				return false, errors.New(api_key.InvalidUser)
			}
			orgID, err := kong.Request.GetHeader("x-org-id")
			if err != nil {
				return false, err
			}
			clusterID, err := kong.Request.GetHeader("x-cluster-id")
			if err != nil {
				return false, err
			}
			enabled, err := datasource.CloudClusterIsUserDebugAuthEnable(user, orgID)
			if err != nil {
				return false, err
			}
			if enabled {
				_, err := datasource.CloudClusterIsAccessDashboardAuthorized(user, orgID, clusterID, user.Username)
				if err != nil {
					_ = kong.Log.Err("auth userdebug failed ", err.Error())
					return false, errors.New(`{"error":true,"message":"No access rights, please contract the administrator for authorization"}`)
				}
			}
		}
		return true, nil
	}

	rUri, err := p.resourceUri.Get(path, method)
	if err != nil {
		_ = kong.Log.Err("resource uri get failed, ", err.Error())
		return false, err
	}
	if rUri == "" {
		// For paths not in resourceMap, allow internal users to proceed
		// This allows internal-only endpoints to work without RBAC config changes
		if user != nil && user.Internal {
			_ = kong.Log.Info("[clinic_auth] internal user allowed for unmapped path: ", path)
			return true, nil
		}
		return false, errors.New("not found resource uri")
	}

	if user == nil {
		return false, errors.New(api_key.InvalidUser)
	}

	resp, err := p.cli.Enforce(context.Background(), &rbac.EnforceRequest{
		User: &rbac.Identity{
			Id:       uint32(user.UserID),
			Username: user.Username,
			Email:    user.Email,
			Internal: user.Internal,
		},
		Uri: rUri,
	})
	if err != nil {
		return false, err
	}

	return resp.Res, nil
}

type DbaasRBACChecker struct {
	resourceUri      *resourceUri
	whitelistRequest *whitelistRequest
}

func NewDbaasRBACChecker(resourceMap map[string]string, whitelist []string) (*DbaasRBACChecker, error) {
	resourceUri, err := NewResourceUri(resourceMap)
	if err != nil {
		return nil, err
	}

	whitelistRequest, err := NewWhitelistRequest(whitelist)
	if err != nil {
		return nil, err
	}

	return &DbaasRBACChecker{
		resourceUri:      resourceUri,
		whitelistRequest: whitelistRequest,
	}, nil
}

func (checker *DbaasRBACChecker) Handle(kong *pdk.PDK, obj any) (bool, error) {
	user, ok := obj.(*account.User)
	if !ok {
		return false, errors.New("unexpect user type")
	}

	path, err := utils.GetURI(kong)
	if err != nil {
		return false, err
	}

	method, err := utils.GetMethod(kong)
	if err != nil {
		return false, err
	}

	allow, err := checker.whitelistRequest.Request(path, method)
	if err != nil {
		return false, err
	}

	if allow {
		return true, nil
	}

	resolved, err := checker.resourceUri.GetResolved(path, method)
	if err != nil {
		_ = kong.Log.Err("resource uri get failed, ", err.Error())
		return false, err
	}
	if resolved == nil || resolved.resourceUri == "" {
		return false, errors.New("not found resource uri")
	}

	if user == nil {
		return false, errors.New(api_key.InvalidUser)
	}

	resp, err := sdkauth.CheckIdentityPermission(context.Background(), &sdkauth.CheckPermissionParam{
		IdentityTye:   sdkauth.AccessIdentityTypeUserID,
		IdentityVal:   strconv.FormatUint(user.Id, 10),
		ResourceUri:   resolved.resourceUri,
		ResourceParam: resolved.params,
	})
	if err != nil {
		return false, err
	}
	if !resp.HasPermission {
		_ = kong.Log.Warn("[dbaas_auth] rbac permission denied: ", resp.Message)
	}
	return resp.HasPermission, nil
}
