package server

import (
	"context"
	"time"

	rbacInterfaces "code.fbi.h-da.de/danet/gosdn/controller/interfaces/rbac"

	csbipb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/csbi"
	apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// AuthInterceptor provides an AuthInterceptor.
type AuthInterceptor struct {
	jwtManager  *rbac.JWTManager
	userService rbacInterfaces.UserService
	roleService rbacInterfaces.RoleService
}

// NewAuthInterceptor receives a JWTManager and a rbacMand returns a new AuthInterceptor provding gRPC Interceptor functionality.
func NewAuthInterceptor(
	jwtManager *rbac.JWTManager,
	userService rbacInterfaces.UserService,
	roleService rbacInterfaces.RoleService,
) *AuthInterceptor {
	return &AuthInterceptor{
		jwtManager:  jwtManager,
		userService: userService,
		roleService: roleService,
	}
}

// Unary returns a unary interceptor function to authenticate and authorize unary RPC calls.
func (auth *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		switch req.(type) {
		case *apb.LoginRequest:
			return handler(ctx, req)
		case *apb.LogoutRequest:
			return handler(ctx, req)
		case *csbipb.Syn:
			return handler(ctx, req)
		default:
			err := auth.authorize(ctx, info.FullMethod)
			if err != nil {
				return nil, err
			}
		}

		return handler(ctx, req)
	}
}

// Stream returns a server interceptor function to authorize stream RPC calls.
func (auth *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
	return func(
		srv interface{},
		stream grpc.ServerStream,
		info *grpc.StreamServerInfo,
		handler grpc.StreamHandler,
	) error {
		err := auth.authorize(stream.Context(), info.FullMethod)
		if err != nil {
			return err
		}

		return handler(srv, stream)
	}
}

func (auth *AuthInterceptor) authorize(ctx context.Context, method string) error {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Errorf(codes.Unauthenticated, "metadata is not provided")
	}

	// validate token and check permission here
	if len(md["authorize"]) > 0 {
		token := md["authorize"][0]
		claims, err := auth.jwtManager.GetClaimsFromToken(token)
		if err != nil {
			return err
		}

		if time.Now().Unix() > claims.ExpiresAt {
			return status.Errorf(codes.PermissionDenied, "token expired at %v, please login", time.Unix(claims.ExpiresAt, 0))
		}

		user, err := auth.userService.Get(store.Query{Name: claims.Username})
		if err != nil {
			return err
		}

		if user.GetToken() != token {
			return status.Errorf(codes.PermissionDenied, "invalid token")
		}

		err = auth.verifyPermisisonForRequestedCall(user.GetRoles(), method)
		if err != nil {
			return err
		}
	} else {
		return status.Errorf(codes.PermissionDenied, "no auth token provided")
	}

	return nil
}

func (auth *AuthInterceptor) verifyPermisisonForRequestedCall(userRoles map[string]string, requestedMethod string) error {
	for _, userRole := range userRoles {
		err := auth.verifyUserRoleAndRequestedCall(userRole, requestedMethod)
		if err != nil {
			return err
		}
	}

	return nil
}

func (auth *AuthInterceptor) verifyUserRoleAndRequestedCall(userRole, requestedMethod string) error {
	storedRoles, err := auth.roleService.GetAll()
	if err != nil {
		return err
	}

	for _, storedRole := range storedRoles {
		if userRole == storedRole.Name() {
			err := auth.compareRequestedPermissionWithRolePermissions(requestedMethod, storedRole.GetPermissions())
			if err != nil {
				return err
			}

			return nil
		}
	}

	return status.Errorf(codes.PermissionDenied, "wrong permissions")
}

func (auth *AuthInterceptor) compareRequestedPermissionWithRolePermissions(requestedMethod string, storedRolePermissions []string) error {
	for _, permission := range storedRolePermissions {
		if permission == requestedMethod {
			return nil
		}
	}

	return status.Errorf(codes.PermissionDenied, "user not authorized for this call")
}
