package server

import (
	"context"
	"fmt"
	"time"

	apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
	rbacInterfaces "code.fbi.h-da.de/danet/gosdn/controller/interfaces/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/metrics"
	"code.fbi.h-da.de/danet/gosdn/controller/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	log "github.com/sirupsen/logrus"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// RoleServer holds a JWTManager and represents a RoleServiceServer.
type RoleServer struct {
	apb.UnimplementedRoleServiceServer
	jwtManager  *rbac.JWTManager
	roleService rbacInterfaces.RoleService
}

// NewRoleServer receives a JWTManager and a RoleService and returns a new RoleServer.
func NewRoleServer(jwtManager *rbac.JWTManager, roleService rbacInterfaces.RoleService) *RoleServer {
	return &RoleServer{
		jwtManager:  jwtManager,
		roleService: roleService,
	}
}

// CreateRoles creates one are multiple new roles.
func (r RoleServer) CreateRoles(ctx context.Context, request *apb.CreateRolesRequest) (*apb.CreateRolesResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "post"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, rrole := range request.Roles {
		role := rbac.NewRole(uuid.New(), rrole.Name, rrole.Description, rrole.Permissions)

		err := r.roleService.Add(role)
		if err != nil {
			log.Error(err)
			return nil, status.Errorf(codes.Aborted, "%v", err)
		}
	}

	return &apb.CreateRolesResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}

// GetRole returns one role with its permissions found by name.
func (r RoleServer) GetRole(ctx context.Context, request *apb.GetRoleRequest) (*apb.GetRoleResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "get"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	roleID, err := uuid.Parse(request.Id)
	if err != nil {
		return nil, fmt.Errorf("could not parse role uuid")
	}

	roleData, err := r.roleService.Get(store.Query{Name: request.RoleName, ID: roleID})
	if err != nil {
		return nil, err
	}

	role := &apb.Role{
		Id:          roleData.ID().String(),
		Name:        roleData.Name(),
		Description: roleData.GetDescription(),
		Permissions: roleData.GetPermissions(),
	}

	return &apb.GetRoleResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
		Role:      role,
	}, nil
}

// GetRoles returns all roles with their permissions.
func (r RoleServer) GetRoles(ctx context.Context, request *apb.GetRolesRequest) (*apb.GetRolesResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "get"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	roleList, err := r.roleService.GetAll()
	if err != nil {
		return nil, err
	}

	roles := []*apb.Role{}
	for _, r := range roleList {
		roles = append(roles, &apb.Role{
			Id:          r.ID().String(),
			Name:        r.Name(),
			Description: r.GetDescription(),
			Permissions: r.GetPermissions(),
		})
	}

	return &apb.GetRolesResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
		Roles:     roles,
	}, nil
}

// UpdateRoles updates data of the provided roles.
func (r RoleServer) UpdateRoles(ctx context.Context, request *apb.UpdateRolesRequest) (*apb.UpdateRolesResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "post"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, role := range request.Roles {
		rid, err := uuid.Parse(role.Id)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}
		_, err = r.roleService.Get(store.Query{ID: rid})
		if err != nil {
			return nil, status.Errorf(codes.Canceled, "role not found %v", err)
		}

		roleToUpdate := rbac.NewRole(rid, role.Name, role.Description, role.Permissions)
		err = r.roleService.Update(roleToUpdate)
		if err != nil {
			return nil, status.Errorf(codes.Aborted, "could not update role %v", err)
		}
	}

	return &apb.UpdateRolesResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}

// DeletePermissionsForRole deletes the provided permissions from one role found by name.
func (r RoleServer) DeletePermissionsForRole(ctx context.Context, request *apb.DeletePermissionsForRoleRequest) (*apb.DeletePermissionsForRoleResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "delete"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	roleToUpdate, err := r.roleService.Get(store.Query{Name: request.RoleName})
	if err != nil {
		return nil, status.Errorf(codes.Canceled, "role not found %v", err)
	}

	// checks if there is at least one valid permission to delete
	// in the provided set of permissions to delete
	nonFound := true
	for _, perm := range roleToUpdate.GetPermissions() {
		for _, permToDelete := range request.PermissionsToDelete {
			if perm == permToDelete {
				nonFound = false
				break
			}
		}
		if !nonFound {
			break
		}
	}
	if nonFound {
		return nil, status.Errorf(codes.Canceled, "no fitting permissions")
	}

	// updates the existing role with the trimmed set of permissions
	roleToUpdate.RemovePermissionsFromRole(request.PermissionsToDelete)
	err = r.roleService.Update(roleToUpdate)
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "could not update role %v", err)
	}

	return &apb.DeletePermissionsForRoleResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}

// DeleteRoles deletes all the provided roles with their permissions.
func (r RoleServer) DeleteRoles(ctx context.Context, request *apb.DeleteRolesRequest) (*apb.DeleteRolesResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "delete"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, role := range request.RoleName {
		roleToDelete, err := r.roleService.Get(store.Query{Name: role})
		if err != nil {
			return nil, status.Errorf(codes.Canceled, "role not found")
		}

		err = r.roleService.Delete(roleToDelete)
		if err != nil {
			return nil, status.Errorf(codes.Aborted, "error deleting role %v", err)
		}
	}

	return &apb.DeleteRolesResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}
