"""File-based fail2ban configuration router.

Provides endpoints to list, view, edit, and create fail2ban configuration
files directly on the filesystem (``jail.d/``, ``filter.d/``, ``action.d/``).

Endpoints:
* ``GET  /api/config/jail-files``                  — list all jail config files
* ``GET  /api/config/jail-files/{filename}``        — get one jail config file (with content)
* ``PUT  /api/config/jail-files/{filename}``        — overwrite a jail config file
* ``PUT  /api/config/jail-files/{filename}/enabled`` — enable/disable a jail config
* ``GET  /api/config/filters/{name}/raw``          — get one filter file raw content
* ``PUT  /api/config/filters/{name}/raw``           — update a filter file (raw content)
* ``POST /api/config/filters/raw``                 — create a new filter file (raw content)
* ``GET  /api/config/filters/{name}/parsed``        — parse a filter file into a structured model
* ``PUT  /api/config/filters/{name}/parsed``        — update a filter file from a structured model
* ``GET  /api/config/actions``                     — list all action files
* ``GET  /api/config/actions/{name}/raw``          — get one action file (raw content)
* ``PUT  /api/config/actions/{name}/raw``          — update an action file (raw content)
* ``POST /api/config/actions``                     — create a new action file
* ``GET  /api/config/actions/{name}/parsed``        — parse an action file into a structured model
* ``PUT  /api/config/actions/{name}/parsed``        — update an action file from a structured model

Note: ``GET /api/config/filters`` (enriched list) and
``GET /api/config/filters/{name}`` (full parsed detail) are handled by the
config router (``config.py``), which is registered first and therefore takes
precedence.  Raw-content read/write variants are at ``/filters/{name}/raw``
and ``POST /filters/raw``.
"""

from __future__ import annotations

from typing import Annotated

from fastapi import APIRouter, HTTPException, Path, status

from app.dependencies import AuthDep, Fail2BanConfigDirDep
from app.models.config import (
    ActionConfig,
    ActionConfigUpdate,
    FilterConfig,
    FilterConfigUpdate,
    JailFileConfig,
    JailFileConfigUpdate,
)
from app.models.file_config import (
    ConfFileContent,
    ConfFileCreateRequest,
    ConfFilesResponse,
    ConfFileUpdateRequest,
    JailConfigFileContent,
    JailConfigFileEnabledUpdate,
    JailConfigFilesResponse,
)
from app.services import raw_config_io_service
from app.exceptions import (
    ConfigDirError,
    ConfigFileExistsError,
    ConfigFileNameError,
    ConfigFileNotFoundError,
    ConfigFileWriteError,
)

router: APIRouter = APIRouter(prefix="/api/config", tags=["Config"])

# ---------------------------------------------------------------------------
# Path type aliases
# ---------------------------------------------------------------------------

_FilenamePath = Annotated[
    str, Path(description="Config filename including extension (e.g. ``sshd.conf``).")
]
_NamePath = Annotated[
    str, Path(description="Base name with or without extension (e.g. ``sshd`` or ``sshd.conf``).")
]

# ---------------------------------------------------------------------------
# Error helpers
# ---------------------------------------------------------------------------


def _not_found(filename: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail=f"Config file not found: {filename!r}",
    )


def _bad_request(message: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail=message,
    )


def _conflict(filename: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_409_CONFLICT,
        detail=f"Config file already exists: {filename!r}",
    )


def _service_unavailable(message: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
        detail=message,
    )


# ---------------------------------------------------------------------------
# Jail config file endpoints (Task 4a)
# ---------------------------------------------------------------------------


@router.get(
    "/jail-files",
    response_model=JailConfigFilesResponse,
    summary="List all jail config files",
)
async def list_jail_config_files(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
) -> JailConfigFilesResponse:
    """Return metadata for every ``.conf`` and ``.local`` file in ``jail.d/``.

    The ``enabled`` field reflects the value of the ``enabled`` key inside the
    file (defaulting to ``true`` when the key is absent).

    Args:
        config_dir: Config directory path injected from application settings.
        _auth: Validated session — enforces authentication.

    Returns:
        :class:`~app.models.file_config.JailConfigFilesResponse`.
    """
    try:
        return await raw_config_io_service.list_jail_config_files(config_dir)
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.get(
    "/jail-files/{filename}",
    response_model=JailConfigFileContent,
    summary="Return a single jail config file with its content",
)
async def get_jail_config_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    filename: _FilenamePath,
) -> JailConfigFileContent:
    """Return the metadata and raw content of one jail config file.

    Args:
        request: Incoming request.
        _auth: Validated session.
        filename: Filename including extension (e.g. ``sshd.conf``).

    Returns:
        :class:`~app.models.file_config.JailConfigFileContent`.

    Raises:
        HTTPException: 400 if *filename* is unsafe.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        return await raw_config_io_service.get_jail_config_file(config_dir, filename)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(filename) from None
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/jail-files/{filename}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Overwrite a jail.d config file with new raw content",
)
async def write_jail_config_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    filename: _FilenamePath,
    body: ConfFileUpdateRequest,
) -> None:
    """Overwrite the raw content of an existing jail.d config file.

    The change is written directly to disk.  You must reload fail2ban
    (``POST /api/config/reload``) separately for the change to take effect.

    Args:
        request: Incoming request.
        _auth: Validated session.
        filename: Filename of the jail config file (e.g. ``sshd.conf``).
        body: New raw file content.

    Raises:
        HTTPException: 400 if *filename* is unsafe or content is invalid.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.write_jail_config_file(config_dir, filename, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(filename) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/jail-files/{filename}/enabled",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Enable or disable a jail configuration file",
)
async def set_jail_config_file_enabled(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    filename: _FilenamePath,
    body: JailConfigFileEnabledUpdate,
) -> None:
    """Set the ``enabled = true/false`` key inside a jail config file.

    The change modifies the file on disk.  You must reload fail2ban
    (``POST /api/config/reload``) separately for the change to take effect.

    Args:
        request: Incoming request.
        _auth: Validated session.
        filename: Filename of the jail config file (e.g. ``sshd.conf``).
        body: New enabled state.

    Raises:
        HTTPException: 400 if *filename* is unsafe or the operation fails.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.set_jail_config_enabled(
            config_dir, filename, body.enabled
        )
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(filename) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.post(
    "/jail-files",
    response_model=ConfFileContent,
    status_code=status.HTTP_201_CREATED,
    summary="Create a new jail.d config file",
)
async def create_jail_config_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    body: ConfFileCreateRequest,
) -> ConfFileContent:
    """Create a new ``.conf`` file in ``jail.d/``.

    Args:
        request: Incoming request.
        _auth: Validated session.
        body: :class:`~app.models.file_config.ConfFileCreateRequest` with name and content.

    Returns:
        :class:`~app.models.file_config.ConfFileContent` with the created file metadata.

    Raises:
        HTTPException: 400 if the name is unsafe or the content exceeds the size limit.
        HTTPException: 409 if a file with that name already exists.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        filename = await raw_config_io_service.create_jail_config_file(config_dir, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileExistsError:
        raise _conflict(body.name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc

    return ConfFileContent(
        name=body.name,
        filename=filename,
        content=body.content,
    )


# ---------------------------------------------------------------------------
# Filter file endpoints (Task 4d)
# ---------------------------------------------------------------------------


@router.get(
    "/filters/{name}/raw",
    response_model=ConfFileContent,
    summary="Return a filter definition file's raw content",
)
async def get_filter_file_raw(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
) -> ConfFileContent:
    """Return the raw content of a filter definition file.

    This endpoint provides direct access to the file bytes for the raw
    config editor.  For structured parsing with active/inactive status use
    ``GET /api/config/filters/{name}`` (served by the config router).

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name with or without extension (e.g. ``sshd`` or ``sshd.conf``).

    Returns:
        :class:`~app.models.file_config.ConfFileContent`.

    Raises:
        HTTPException: 400 if *name* is unsafe.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        return await raw_config_io_service.get_filter_file(config_dir, name)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/filters/{name}/raw",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Update a filter definition file (raw content)",
)
async def write_filter_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
    body: ConfFileUpdateRequest,
) -> None:
    """Overwrite the content of an existing filter definition file.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name with or without extension.
        body: New file content.

    Raises:
        HTTPException: 400 if *name* is unsafe or content exceeds the size limit.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.write_filter_file(config_dir, name, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.post(
    "/filters/raw",
    status_code=status.HTTP_201_CREATED,
    response_model=ConfFileContent,
    summary="Create a new filter definition file (raw content)",
)
async def create_filter_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    body: ConfFileCreateRequest,
) -> ConfFileContent:
    """Create a new ``.conf`` file in ``filter.d/``.

    Args:
        request: Incoming request.
        _auth: Validated session.
        body: Name and initial content for the new file.

    Returns:
        The created :class:`~app.models.file_config.ConfFileContent`.

    Raises:
        HTTPException: 400 if *name* is invalid or content exceeds limit.
        HTTPException: 409 if a file with that name already exists.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        filename = await raw_config_io_service.create_filter_file(config_dir, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileExistsError:
        raise _conflict(body.name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc

    return ConfFileContent(
        name=body.name,
        filename=filename,
        content=body.content,
    )


# ---------------------------------------------------------------------------
# Action file endpoints (Task 4e)
# ---------------------------------------------------------------------------


@router.get(
    "/actions",
    response_model=ConfFilesResponse,
    summary="List all action definition files",
)
async def list_action_files(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
) -> ConfFilesResponse:
    """Return a list of every ``.conf`` and ``.local`` file in ``action.d/``.

    Args:
        request: Incoming request.
        _auth: Validated session.

    Returns:
        :class:`~app.models.file_config.ConfFilesResponse`.
    """
    try:
        return await raw_config_io_service.list_action_files(config_dir)
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.get(
    "/actions/{name}/raw",
    response_model=ConfFileContent,
    summary="Return an action definition file with its content",
)
async def get_action_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
) -> ConfFileContent:
    """Return the content of an action definition file.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name with or without extension.

    Returns:
        :class:`~app.models.file_config.ConfFileContent`.

    Raises:
        HTTPException: 400 if *name* is unsafe.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        return await raw_config_io_service.get_action_file(config_dir, name)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/actions/{name}/raw",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Update an action definition file",
)
async def write_action_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
    body: ConfFileUpdateRequest,
) -> None:
    """Overwrite the content of an existing action definition file.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name with or without extension.
        body: New file content.

    Raises:
        HTTPException: 400 if *name* is unsafe or content exceeds the size limit.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.write_action_file(config_dir, name, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.post(
    "/actions",
    status_code=status.HTTP_201_CREATED,
    response_model=ConfFileContent,
    summary="Create a new action definition file",
)
async def create_action_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    body: ConfFileCreateRequest,
) -> ConfFileContent:
    """Create a new ``.conf`` file in ``action.d/``.

    Args:
        request: Incoming request.
        _auth: Validated session.
        body: Name and initial content for the new file.

    Returns:
        The created :class:`~app.models.file_config.ConfFileContent`.

    Raises:
        HTTPException: 400 if *name* is invalid or content exceeds limit.
        HTTPException: 409 if a file with that name already exists.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        filename = await raw_config_io_service.create_action_file(config_dir, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileExistsError:
        raise _conflict(body.name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc

    return ConfFileContent(
        name=body.name,
        filename=filename,
        content=body.content,
    )


# ---------------------------------------------------------------------------
# Parsed filter endpoints (Task 2.1)
# ---------------------------------------------------------------------------


@router.get(
    "/filters/{name}/parsed",
    response_model=FilterConfig,
    summary="Return a filter file parsed into a structured model",
)
async def get_parsed_filter(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
) -> FilterConfig:
    """Parse a filter definition file and return its structured fields.

    The file is read from ``filter.d/``, parsed as fail2ban INI format, and
    returned as a :class:`~app.models.config.FilterConfig` JSON object.  This
    is the input model for the form-based filter editor (Task 2.3).

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name (e.g. ``sshd`` or ``sshd.conf``).

    Returns:
        :class:`~app.models.config.FilterConfig`.

    Raises:
        HTTPException: 400 if *name* is unsafe.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        return await raw_config_io_service.get_parsed_filter_file(config_dir, name)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/filters/{name}/parsed",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Update a filter file from a structured model",
)
async def update_parsed_filter(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
    body: FilterConfigUpdate,
) -> None:
    """Apply a partial structured update to a filter definition file.

    Fields set to ``null`` in the request body are left unchanged.  The file is
    re-serialized to fail2ban INI format after merging.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name of the filter to update.
        body: Partial :class:`~app.models.config.FilterConfigUpdate`.

    Raises:
        HTTPException: 400 if *name* is unsafe or content exceeds the size limit.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.update_parsed_filter_file(config_dir, name, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


# ---------------------------------------------------------------------------
# Parsed action endpoints (Task 3.1)
# ---------------------------------------------------------------------------


@router.get(
    "/actions/{name}/parsed",
    response_model=ActionConfig,
    summary="Return an action file parsed into a structured model",
)
async def get_parsed_action(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
) -> ActionConfig:
    """Parse an action definition file and return its structured fields.

    The file is read from ``action.d/``, parsed as fail2ban INI format, and
    returned as a :class:`~app.models.config.ActionConfig` JSON object.  This
    is the input model for the form-based action editor (Task 3.3).

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name (e.g. ``iptables`` or ``iptables.conf``).

    Returns:
        :class:`~app.models.config.ActionConfig`.

    Raises:
        HTTPException: 400 if *name* is unsafe.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        return await raw_config_io_service.get_parsed_action_file(config_dir, name)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/actions/{name}/parsed",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Update an action file from a structured model",
)
async def update_parsed_action(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    name: _NamePath,
    body: ActionConfigUpdate,
) -> None:
    """Apply a partial structured update to an action definition file.

    Fields set to ``null`` in the request body are left unchanged.  The file is
    re-serialized to fail2ban INI format after merging.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Base name of the action to update.
        body: Partial :class:`~app.models.config.ActionConfigUpdate`.

    Raises:
        HTTPException: 400 if *name* is unsafe or content exceeds the size limit.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.update_parsed_action_file(config_dir, name, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(name) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


# ---------------------------------------------------------------------------
# Parsed jail file endpoints (Task 6.1)
# ---------------------------------------------------------------------------


@router.get(
    "/jail-files/{filename}/parsed",
    response_model=JailFileConfig,
    summary="Return a jail.d file parsed into a structured model",
)
async def get_parsed_jail_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    filename: _NamePath,
) -> JailFileConfig:
    """Parse a jail.d config file and return its structured fields.

    The file is read from ``jail.d/``, parsed as fail2ban INI format, and
    returned as a :class:`~app.models.config.JailFileConfig` JSON object.  This
    is the input model for the form-based jail file editor (Task 6.2).

    Args:
        request: Incoming request.
        _auth: Validated session.
        filename: Filename including extension (e.g. ``sshd.conf``).

    Returns:
        :class:`~app.models.config.JailFileConfig`.

    Raises:
        HTTPException: 400 if *filename* is unsafe.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        return await raw_config_io_service.get_parsed_jail_file(config_dir, filename)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(filename) from None
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc


@router.put(
    "/jail-files/{filename}/parsed",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Update a jail.d file from a structured model",
)
async def update_parsed_jail_file(
    config_dir: Fail2BanConfigDirDep,
    _auth: AuthDep,
    filename: _NamePath,
    body: JailFileConfigUpdate,
) -> None:
    """Apply a partial structured update to a jail.d config file.

    Fields set to ``null`` in the request body are left unchanged.  The file is
    re-serialized to fail2ban INI format after merging.

    Args:
        request: Incoming request.
        _auth: Validated session.
        filename: Filename including extension (e.g. ``sshd.conf``).
        body: Partial :class:`~app.models.config.JailFileConfigUpdate`.

    Raises:
        HTTPException: 400 if *filename* is unsafe or content exceeds size limit.
        HTTPException: 404 if the file does not exist.
        HTTPException: 503 if the config directory is unavailable.
    """
    try:
        await raw_config_io_service.update_parsed_jail_file(config_dir, filename, body)
    except ConfigFileNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigFileNotFoundError:
        raise _not_found(filename) from None
    except ConfigFileWriteError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigDirError as exc:
        raise _service_unavailable(str(exc)) from exc
