import json
from collections.abc import Mapping
from typing import Any, cast

from tornado.locks import Lock
from tornado.web import Finish

from pcs import settings
from pcs.common import file_type_codes, reports
from pcs.common.async_tasks.dto import CommandDto, CommandOptionsDto
from pcs.common.booth_dto import BoothConfigAndAuthfileDto
from pcs.common.cluster_dto import ClusterDaemonsInfoDto
from pcs.common.interface.dto import to_dict
from pcs.common.pcs_cfgsync_dto import SyncConfigsDto
from pcs.common.permissions.dto import (
    PermissionEntryDto,
    PermissionMetadataDependenciesDto,
    PermissionMetadataDto,
)
from pcs.common.sbd_dto import SbdCheckResultDto
from pcs.common.str_tools import format_list
from pcs.daemon import log
from pcs.daemon.app.api_v0_tools import (
    SimplifiedResult,
    reports_to_str,
    run_library_command_in_scheduler,
)
from pcs.daemon.app.auth_provider import (
    ApiAuthProviderFactoryInterface,
    ApiAuthProviderInterface,
    NotAuthorizedException,
)
from pcs.daemon.app.common import (
    LegacyApiHandler,
    get_legacy_desired_user_from_request,
)
from pcs.daemon.async_tasks.scheduler import Scheduler
from pcs.lib.auth.tools import DesiredUser
from pcs.lib.auth.types import AuthUser
from pcs.lib.pcs_cfgsync.const import SYNCED_CONFIGS

from .common import RoutesType


class _BaseApiV0Handler(LegacyApiHandler):
    """
    Base class of handlers for the original API implemented in remote.rb
    """

    _auth_provider: ApiAuthProviderInterface
    _scheduler: Scheduler
    _real_user: AuthUser
    _desired_user: DesiredUser

    def initialize(
        self,
        api_auth_provider_factory: ApiAuthProviderFactoryInterface,
        scheduler: Scheduler,
    ) -> None:
        self._auth_provider = api_auth_provider_factory.create(self)
        self._scheduler = scheduler

    async def prepare(self) -> None:
        try:
            self._real_user = await self._auth_provider.auth_user()
        except NotAuthorizedException as e:
            raise self.unauthorized() from e

        self._desired_user = get_legacy_desired_user_from_request(
            self, log.pcsd
        )

    async def _handle_request(self) -> None:
        """
        Main method for handling requests
        """
        raise NotImplementedError()

    def _error(self, message: str, http_code: int = 400) -> Finish:
        """
        Helper method for exit request processing with an error
        """
        self.set_status(http_code)
        self.write(message)
        return Finish()

    def _check_required_params(self, required_params: set[str]) -> None:
        missing_params = required_params - set(self.request.arguments.keys())
        if missing_params:
            raise self._error(
                f"Required parameters missing: {format_list(missing_params)}"
            )

    async def _run_library_command(
        self, cmd_name: str, cmd_params: Mapping[str, Any]
    ) -> SimplifiedResult:
        """
        Helper method for calling pcs library commands
        """
        command_dto = CommandDto(
            command_name=cmd_name,
            params=dict(cmd_params),
            # the scheduler/executor handles whether the command is run with
            # real_user permissions or the effective user is used
            options=CommandOptionsDto(
                effective_username=self._desired_user.username,
                effective_groups=list(self._desired_user.groups)
                if self._desired_user.groups
                else None,
            ),
        )
        return await run_library_command_in_scheduler(
            self._scheduler, command_dto, self._real_user, self._error
        )


class ManageServicesHandler(_BaseApiV0Handler):
    _REPORT_ACTION_TO_COMMAND = {
        reports.const.SERVICE_ACTION_START: "start",
        reports.const.SERVICE_ACTION_STOP: "stop",
        reports.const.SERVICE_ACTION_ENABLE: "enable",
        reports.const.SERVICE_ACTION_DISABLE: "disable",
    }
    _SUPPORTED_COMMANDS = set(_REPORT_ACTION_TO_COMMAND.values())
    _SUPPORTED_SERVICES = {"pacemaker_remote"}

    def _check_supported_value(
        self,
        action_id: str,
        key: str,
        action_data: dict[str, str],
        supported: set[str],
    ) -> None:
        if action_data[key] not in supported:
            raise self._error(
                f"Invalid input data format: action (key: {action_id}): "
                f"unsupported '{key}' ('{action_data[key]}'), "
                f"supported are: {format_list(supported)}"
            )

    async def _handle_request(self) -> None:
        # load JSON
        self._check_required_params({"data_json"})
        try:
            actions = json.loads(self.get_argument("data_json"))
        except json.JSONDecodeError as e:
            raise self._error("Invalid input data format") from e

        # JSON structure validation
        if not isinstance(actions, dict):
            raise self._error(
                f"Invalid input data format: actions should be 'dict'. "
                f"But it is '{type(actions).__name__}': "
                f"{json.dumps(actions)}"
            )

        action_data_required_keys = ["type", "service", "command"]
        for action_id, action_data in actions.items():
            if not isinstance(action_data, dict):
                raise self._error(
                    f"Invalid input data format: action (key: {action_id}): "
                    f"should be 'dict'. But it is "
                    f"'{type(action_data).__name__}': "
                    f"{json.dumps(action_data)}"
                )
            for required_key in action_data_required_keys:
                if required_key not in action_data:
                    raise self._error(
                        f"Invalid input data format: action (key: {action_id}): "
                        f"'{required_key}' is missing"
                    )
            self._check_supported_value(
                action_id, "type", action_data, {"service_command"}
            )
            self._check_supported_value(
                action_id, "service", action_data, self._SUPPORTED_SERVICES
            )
            self._check_supported_value(
                action_id, "command", action_data, self._SUPPORTED_COMMANDS
            )

        # take actions
        commands = {action_data["command"] for action_data in actions.values()}
        if commands == {"enable", "start"}:
            cmd_name = "services.pacemaker_remote_on_local"
        elif commands == {"disable", "stop"}:
            cmd_name = "services.pacemaker_remote_off_local"
        else:
            raise self._error(
                "Invalid input data format: unsupported combination of "
                f"actions: {format_list(commands)}"
            )

        result = await self._run_library_command(cmd_name, {})
        # Report per-action success based on individual service action reports,
        # the overall result status is ignored. This is fine since the lib
        # command doesn't do anything else than service management.
        succeeded_actions = {
            self._REPORT_ACTION_TO_COMMAND[
                report_item.message.payload["action"]
            ]
            for report_item in result.reports
            if report_item.message.code
            == reports.codes.SERVICE_ACTION_SUCCEEDED
        }
        response = {
            "actions": {
                action_id: {
                    "code": (
                        "success"
                        if action_data["command"] in succeeded_actions
                        else "fail"
                    ),
                    "message": "",
                }
                for action_id, action_data in actions.items()
            }
        }
        self.write(json.dumps(response))


class ResourceManageUnmanageHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        self._check_required_params({"resource_list_json"})
        try:
            resource_list = json.loads(self.get_argument("resource_list_json"))
        except json.JSONDecodeError as e:
            raise self._error("Invalid input data format") from e
        result = await self._run_library_command(
            self._get_cmd(), dict(resource_or_tag_ids=resource_list)
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))

    @staticmethod
    def _get_cmd() -> str:
        raise NotImplementedError()


class ResourceManageHandler(ResourceManageUnmanageHandler):
    @staticmethod
    def _get_cmd() -> str:
        return "resource.manage"


class ResourceUnmanageHandler(ResourceManageUnmanageHandler):
    @staticmethod
    def _get_cmd() -> str:
        return "resource.unmanage"


class QdeviceNetGetCaCertificateHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        result = await self._run_library_command(
            "qdevice.qdevice_net_get_ca_certificate", {}
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write(result.result)


class QdeviceNetSignNodeCertificateHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        self._check_required_params({"certificate_request", "cluster_name"})
        result = await self._run_library_command(
            "qdevice.qdevice_net_sign_certificate_request",
            dict(
                certificate_request=self.get_argument("certificate_request"),
                cluster_name=self.get_body_argument("cluster_name"),
            ),
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write(result.result)


class QdeviceNetClientInitCertificateStorageHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        self._check_required_params({"ca_certificate"})
        result = await self._run_library_command(
            "qdevice.client_net_setup",
            dict(ca_certificate=self.get_argument("ca_certificate")),
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))


class QdeviceNetClientImportCertificateHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        self._check_required_params({"certificate"})
        result = await self._run_library_command(
            "qdevice.client_net_import_certificate",
            dict(certificate=self.get_argument("certificate")),
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))


class QdeviceNetClientDestroyHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        result = await self._run_library_command(
            "qdevice.client_net_destroy", {}
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))


class BoothGetConfigHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        instance_name = self.get_argument("name", None)
        result = await self._run_library_command(
            "booth.get_config_and_authfile",
            dict(instance_name=instance_name),
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))

        # Here we convert warning to an error. The overall goal was to convert
        # the ruby behaviour to python (ruby produced error) and at the same
        # time we wanted to use function which had exactly the behaviour that
        # we wanted (except it produced warning instead of error).
        # This is the compromise.
        if any(
            rep.message.code == reports.codes.BOOTH_UNSUPPORTED_FILE_LOCATION
            for rep in result.reports
        ):
            raise self._error(reports_to_str(result.reports))
        self.write(to_dict(cast(BoothConfigAndAuthfileDto, result.result)))


class CheckHostHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        result = await self._run_library_command(
            "cluster.get_host_daemons_info", {}
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write(
            self._convert_to_legacy_format(
                cast(ClusterDaemonsInfoDto, result.result)
            )
        )

    @staticmethod
    def _convert_to_legacy_format(
        command_result: ClusterDaemonsInfoDto,
    ) -> dict[str, bool | dict[str, dict[str, bool | str | None]]]:
        service_name_mapping = {"corosync-qdevice": "qdevice"}
        services_dict: dict[str, dict[str, bool | str | None]] = {
            service_name_mapping.get(
                service_status_dto.service, service_status_dto.service
            ): {
                "installed": service_status_dto.installed,
                "enabled": service_status_dto.enabled,
                "running": service_status_dto.running,
                "version": None,
            }
            for service_status_dto in command_result.services
        }

        version_mapping = {
            "corosync": command_result.versions.corosync,
            "pacemaker": command_result.versions.pacemaker,
            "pcsd": command_result.versions.pcsd,
        }

        for service_name, version_dto in version_mapping.items():
            version_str = str(version_dto)
            if service_name in services_dict and version_str != "0.0.0":
                services_dict[service_name]["version"] = version_str

        return {
            "services": services_dict,
            "cluster_configuration_exists": command_result.cluster_configuration_exists,
        }


class GetConfigsHandler(_BaseApiV0Handler):
    _FILE_TYPE_CODE_TO_LEGACY = {
        file_type_codes.PCS_SETTINGS_CONF: "pcs_settings.conf",
        file_type_codes.PCS_KNOWN_HOSTS: "known-hosts",
    }

    async def _handle_request(self) -> None:
        result = await self._run_library_command(
            "pcs_cfgsync.get_configs",
            {"cluster_name": self.get_argument("cluster_name", "")},
        )

        if any(
            rep.message.code == reports.codes.FILE_IO_ERROR
            and rep.message.payload.get("file_type_code")
            == file_type_codes.COROSYNC_CONF
            and rep.message.payload.get("operation") == "read"
            for rep in result.reports
        ):
            # corosync.conf does not exists, or we were unable to read the file
            # so we say that the node is not in cluster - same as in the old
            # implementation
            self.write({"status": "not_in_cluster"})
            return
        if any(
            rep.message.code
            == reports.codes.NODE_REPORTS_UNEXPECTED_CLUSTER_NAME
            for rep in result.reports
        ):
            self.write({"status": "wrong_cluster_name"})
            return
        if not result.success or result.result is None:
            raise self._error(reports_to_str(result.reports))

        command_result = cast(SyncConfigsDto, result.result)
        legacy_result = {
            "status": "ok",
            "cluster_name": command_result.cluster_name,
            "configs": {
                self._FILE_TYPE_CODE_TO_LEGACY[file_code]: {
                    "type": "file",
                    "text": command_result.configs.get(file_code),
                }
                for file_code in SYNCED_CONFIGS
            },
        }
        self.write(legacy_result)


class SetSyncOptionsHandler(_BaseApiV0Handler):
    _sync_config_lock: Lock

    def initialize(  # type: ignore[override]
        self,
        api_auth_provider_factory: ApiAuthProviderFactoryInterface,
        scheduler: Scheduler,
        sync_config_lock: Lock,
    ) -> None:
        super().initialize(api_auth_provider_factory, scheduler)
        self._sync_config_lock = sync_config_lock

    async def _handle_request(self) -> None:
        options = {
            key: self.get_argument(key) for key in self.request.arguments
        }

        async with self._sync_config_lock:
            result = await self._run_library_command(
                "pcs_cfgsync.update_sync_options", {"options": options}
            )

        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write("Sync thread options updated successfully")


class SetConfigsHandler(_BaseApiV0Handler):
    _REPORT_CODE_TO_RESULT = {
        reports.codes.PCS_CFGSYNC_CONFIG_ACCEPTED: "accepted",
        reports.codes.PCS_CFGSYNC_CONFIG_REJECTED: "rejected",
        reports.codes.PCS_CFGSYNC_CONFIG_SAVE_ERROR: "error",
        reports.codes.PCS_CFGSYNC_CONFIG_UNSUPPORTED: "not_supported",
    }

    _LEGACY_TO_FILE_TYPE_CODE = {
        "pcs_settings.conf": file_type_codes.PCS_SETTINGS_CONF,
        "known-hosts": file_type_codes.PCS_KNOWN_HOSTS,
    }
    _FILE_TYPE_CODE_TO_LEGACY = {
        v: k for k, v in _LEGACY_TO_FILE_TYPE_CODE.items()
    }

    _sync_config_lock: Lock

    def initialize(  # type: ignore[override]
        self,
        api_auth_provider_factory: ApiAuthProviderFactoryInterface,
        scheduler: Scheduler,
        sync_config_lock: Lock,
    ) -> None:
        super().initialize(api_auth_provider_factory, scheduler)
        self._sync_config_lock = sync_config_lock

    async def _handle_request(self) -> None:
        try:
            configs_json = json.loads(self.get_argument("configs", ""))
        except json.JSONDecodeError:
            # original handler returned 200 in this case
            self.write({"status": "bad_json"})
            return

        try:
            cluster_name = configs_json.get("cluster_name", "")
            force = bool(configs_json.get("force", False))

            configs_raw = configs_json.get("configs", {})
            configs = {}
            non_file_configs = []
            for name, data in configs_raw.items():
                if data.get("type") == "file":
                    configs[self._LEGACY_TO_FILE_TYPE_CODE.get(name, name)] = (
                        str(data.get("text", ""))
                    )
                else:
                    non_file_configs.append(name)
        except AttributeError:
            # original handler returned 200 in this case
            self.write({"status": "bad_json"})
            return

        async with self._sync_config_lock:
            result = await self._run_library_command(
                "pcs_cfgsync.set_configs",
                {
                    "cluster_name": cluster_name,
                    "configs": configs,
                    "force_flags": [reports.codes.FORCE] if force else [],
                },
            )

        if any(
            rep.message.code
            == reports.codes.NODE_REPORTS_UNEXPECTED_CLUSTER_NAME
            for rep in result.reports
        ):
            self.write({"status": "wrong_cluster_name"})
            return

        not_file_results = dict.fromkeys(non_file_configs, "not_supported")
        real_results = {
            self._FILE_TYPE_CODE_TO_LEGACY.get(
                rep.message.payload["file_type_code"],
                rep.message.payload["file_type_code"],
            ): self._REPORT_CODE_TO_RESULT[rep.message.code]
            for rep in result.reports
            if rep.message.code in self._REPORT_CODE_TO_RESULT
        }
        # default to "error" for any file that received no status report
        for file_type in configs:
            legacy_name = self._FILE_TYPE_CODE_TO_LEGACY.get(
                file_type, file_type
            )
            if legacy_name not in real_results:
                real_results[legacy_name] = "error"

        self.write({"status": "ok", "result": not_file_results | real_results})


class SetPermissionsHandler(_BaseApiV0Handler):
    """
    Input format:
    {
        "cluster_name": "name" # ignored
        "cluster": "name" # ignored
        "permissions": {
            "arbitrary-key": {
                "name": "username",
                "type": "user|group",
                "allow": {
                    "read": "1",
                    "write": "1",
                    "grant": "1",
                    "full": "1",
                }
            }
        }
    }
    """

    async def _handle_request(self) -> None:
        data_json = self.get_argument("json_data", "")

        permissions = []
        try:
            permissions_raw = json.loads(data_json)
        except (json.JSONDecodeError, TypeError) as e:
            raise self._error("{'status': 'bad_json'}") from e

        try:
            permissions = [
                {
                    "name": perm.get("name", ""),
                    "type": perm.get("type", ""),
                    "allow": [
                        perm_name
                        for perm_name, enabled in perm.get("allow", {}).items()
                        if enabled == "1"
                    ],
                }
                for perm in permissions_raw.get("permissions", {}).values()
            ]
        except AttributeError as e:
            raise self._error("{'status': 'bad_json'}") from e

        result = await self._run_library_command(
            "cluster.set_permissions", {"permissions": permissions}
        )

        for report in result.reports:
            if (
                report.message.code
                == reports.codes.NOT_AUTHORIZED_TO_CHANGE_FULL_PERMISSION
            ):
                raise self._error(http_code=403, message=report.message.message)

        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write("Permissions saved")


class GetPermissionsHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        result = await self._run_library_command("cluster.get_permissions", {})
        # Ruby compatibility: ignore errors, return empty permissions
        #
        # 403 is already returned in _run_library_command if the user does not
        # have the needed permissions to run this command, so this is safe
        user_permissions = result.result if result.success else []

        result = await self._run_library_command(
            "cluster.get_permissions_metadata", {}
        )
        permission_metadata = (
            cast(PermissionMetadataDto, result.result)
            if result.success
            else PermissionMetadataDto(
                [], [], PermissionMetadataDependenciesDto({})
            )
        )

        # Ruby compatibility: sort the permissions
        # sorted by (type, name): GROUP < USER alphabetically, then by name
        #
        # WebUI displays the permissions in the order they were sent from pcsd
        user_permissions.sort(key=lambda p: (p.type, p.name))
        self.write(
            {
                **to_dict(permission_metadata),
                "users_permissions": [
                    to_dict(cast(PermissionEntryDto, entry))
                    for entry in user_permissions
                ],
            },
        )


class KnownHostsChangeHandler(_BaseApiV0Handler):
    """
    Input format:
    {
        "known_hosts_add": {
            "hostname1": {
                "token": "auth_token_string",
                "dest_list": [
                    {"addr": "192.168.1.100", "port": 2224},
                    ...
                ]
            },
            ...
        },
        "known_hosts_remove": [
            "hostname_to_remove1",
            ...
        ]
    }
    """

    async def _handle_request(self) -> None:
        data_json = self.get_argument("data_json", "")
        try:
            hosts_raw = json.loads(data_json)
        except (json.JSONDecodeError, TypeError) as e:
            raise self._error(f"Incorrect format of request data: {e}") from e

        try:
            hosts_to_add = {
                host_name: {
                    "token": host_data.get("token", ""),
                    "dest_list": [
                        {
                            "addr": dest.get("addr") or host_name,
                            "port": (
                                dest.get("port") or settings.pcsd_default_port
                            ),
                        }
                        for dest in host_data.get("dest_list", [])
                    ],
                }
                for host_name, host_data in hosts_raw.get(
                    "known_hosts_add", {}
                ).items()
            }

            hosts_to_remove = list(hosts_raw.get("known_hosts_remove", []))
        except (AttributeError, TypeError, ValueError) as e:
            raise self._error(f"Incorrect format of request data: {e}") from e

        result = await self._run_library_command(
            "auth.known_hosts_change",
            {"hosts_to_add": hosts_to_add, "hosts_to_remove": hosts_to_remove},
        )

        if not result.success:
            raise self._error(reports_to_str(result.reports))


class CheckSbdHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        watchdog = self.get_argument("watchdog", "")
        device_list_json = self.get_argument("device_list", "[]")
        try:
            device_list = json.loads(device_list_json)
        except json.JSONDecodeError as e:
            raise self._error("Invalid input data format") from e
        if not isinstance(device_list, list) or not all(
            isinstance(device, str) for device in device_list
        ):
            raise self._error("Invalid input data format")

        result = await self._run_library_command(
            "sbd.check_sbd",
            dict(watchdog=watchdog, device_list=device_list),
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        # Not using to_dict to keep backward compatibility with older
        # pcs versions which don't expect watchdog and device_list keys
        # to be present in the response when they have no values. Also the
        # response was different than our DTOs so we make them into legacy
        # format by hand.
        sbd_status = cast(SbdCheckResultDto, result.result)
        response: dict[str, Any] = dict(
            sbd=dict(
                installed=sbd_status.sbd_service.installed,
                enabled=sbd_status.sbd_service.enabled,
                running=sbd_status.sbd_service.running,
            ),
        )
        if sbd_status.watchdog is not None:
            response["watchdog"] = dict(
                path=sbd_status.watchdog.path,
                exist=sbd_status.watchdog.exists,
                is_supported=sbd_status.watchdog.is_supported,
            )
        if sbd_status.device_list is not None:
            response["device_list"] = [
                dict(
                    path=device.path,
                    exist=device.exists,
                    block_device=device.is_block_device,
                )
                for device in sbd_status.device_list
            ]
        self.write(json.dumps(response))


class SetCorosyncConf(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        self._check_required_params({"corosync_conf"})
        corosync_conf = self.get_argument("corosync_conf")
        if not corosync_conf:
            raise self._error("Invalid corosync.conf: empty config not allowed")

        result = await self._run_library_command(
            "cluster.set_corosync_conf", {"file_content": corosync_conf}
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write("Succeeded")


class GetSbdConfigHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        result = await self._run_library_command(
            "sbd.get_node_sbd_config_text", {}
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write(result.result)


class SetSbdConfigHandler(_BaseApiV0Handler):
    async def _handle_request(self) -> None:
        self._check_required_params({"config"})
        result = await self._run_library_command(
            "sbd.set_node_sbd_config_text",
            dict(config=self.get_argument("config")),
        )
        if not result.success:
            raise self._error(reports_to_str(result.reports))
        self.write("SBD configuration saved.")


def get_routes(
    api_auth_provider_factory: ApiAuthProviderFactoryInterface,
    scheduler: Scheduler,
    sync_config_lock: Lock,
) -> RoutesType:
    def r(url: str) -> str:
        # pylint: disable=invalid-name
        return f"/remote/{url}"

    params = dict(
        api_auth_provider_factory=api_auth_provider_factory, scheduler=scheduler
    )
    return [
        # services
        (r("manage_services"), ManageServicesHandler, params),
        # resources
        (r("manage_resource"), ResourceManageHandler, params),
        (r("unmanage_resource"), ResourceUnmanageHandler, params),
        # qdevice
        (
            r("qdevice_net_client_destroy"),
            QdeviceNetClientDestroyHandler,
            params,
        ),
        (
            # /api/v1/qdevice-client-net-import-certificate/v1
            r("qdevice_net_client_import_certificate"),
            QdeviceNetClientImportCertificateHandler,
            params,
        ),
        (
            r("qdevice_net_client_init_certificate_storage"),
            QdeviceNetClientInitCertificateStorageHandler,
            params,
        ),
        (
            r("qdevice_net_get_ca_certificate"),
            QdeviceNetGetCaCertificateHandler,
            params,
        ),
        (
            # /api/v1/qdevice-qdevice-net-sign-certificate-request/v1
            r("qdevice_net_sign_node_certificate"),
            QdeviceNetSignNodeCertificateHandler,
            params,
        ),
        # booth
        (r("booth_get_config"), BoothGetConfigHandler, params),
        # cfgsync
        (r("get_configs"), GetConfigsHandler, params),
        (
            r("set_configs"),
            SetConfigsHandler,
            {**params, "sync_config_lock": sync_config_lock},
        ),
        (
            r("set_sync_options"),
            SetSyncOptionsHandler,
            {**params, "sync_config_lock": sync_config_lock},
        ),
        # permissions
        (r("set_permissions"), SetPermissionsHandler, params),
        (r("get_permissions"), GetPermissionsHandler, params),
        # known hosts
        (r("known_hosts_change"), KnownHostsChangeHandler, params),
        # check_host
        (r("check_host"), CheckHostHandler, params),
        # cluster config
        (r("set_corosync_conf"), SetCorosyncConf, params),
        # sbd
        (r("check_sbd"), CheckSbdHandler, params),
        (r("get_sbd_config"), GetSbdConfigHandler, params),
        (r("set_sbd_config"), SetSbdConfigHandler, params),
    ]
