from __future__ import annotations

from typing import Annotated

from fastapi import APIRouter, Depends, Path, Query, Request, status

from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep, GlobalRateLimiterDep
from app.mappers import config_mappers
from app.models.config import (
    FilterConfig,
    FilterCreateRequest,
    FilterListResponse,
    FilterUpdateRequest,
)
from app.services import filter_config_service
from app.utils.constants import (
    RATE_LIMIT_FILTER_CREATE_REQUESTS,
    RATE_LIMIT_FILTER_DELETE_REQUESTS,
    RATE_LIMIT_FILTER_UPDATE_REQUESTS,
)

router: APIRouter = APIRouter(prefix="/filters", tags=["Filter Config"])

_MINUTE = 60

_FILTER_UPDATE_BUCKET = "filter:update"
_FILTER_CREATE_BUCKET = "filter:create"
_FILTER_DELETE_BUCKET = "filter:delete"


def _check_filter_update_rate_limit(
    request: Request,
    rate_limiter: GlobalRateLimiterDep,
) -> None:
    """Check rate limit for filter update operations."""
    from app.utils.client_ip import get_client_ip

    settings = request.app.state.settings
    client_ip = get_client_ip(request, trusted_proxies=settings.trusted_proxies)
    is_allowed, retry_after = rate_limiter.check_allowed_for_bucket(
        _FILTER_UPDATE_BUCKET, client_ip, RATE_LIMIT_FILTER_UPDATE_REQUESTS, _MINUTE
    )
    if not is_allowed:
        from app.exceptions import RateLimitError
        from app.utils.logging_compat import get_logger

        log = get_logger(__name__)
        log.warning(
            "filter_update_rate_limit_exceeded",
            client_ip=client_ip,
            path=request.url.path,
            retry_after=retry_after,
        )
        raise RateLimitError(
            "Rate limit exceeded for filter update operations. Please try again later.",
            retry_after_seconds=retry_after,
        )


def _check_filter_create_rate_limit(
    request: Request,
    rate_limiter: GlobalRateLimiterDep,
) -> None:
    """Check rate limit for filter create operations."""
    from app.utils.client_ip import get_client_ip

    settings = request.app.state.settings
    client_ip = get_client_ip(request, trusted_proxies=settings.trusted_proxies)
    is_allowed, retry_after = rate_limiter.check_allowed_for_bucket(
        _FILTER_CREATE_BUCKET, client_ip, RATE_LIMIT_FILTER_CREATE_REQUESTS, _MINUTE
    )
    if not is_allowed:
        from app.exceptions import RateLimitError
        from app.utils.logging_compat import get_logger

        log = get_logger(__name__)
        log.warning(
            "filter_create_rate_limit_exceeded",
            client_ip=client_ip,
            path=request.url.path,
            retry_after=retry_after,
        )
        raise RateLimitError(
            "Rate limit exceeded for filter create operations. Please try again later.",
            retry_after_seconds=retry_after,
        )


def _check_filter_delete_rate_limit(
    request: Request,
    rate_limiter: GlobalRateLimiterDep,
) -> None:
    """Check rate limit for filter delete operations."""
    from app.utils.client_ip import get_client_ip

    settings = request.app.state.settings
    client_ip = get_client_ip(request, trusted_proxies=settings.trusted_proxies)
    is_allowed, retry_after = rate_limiter.check_allowed_for_bucket(
        _FILTER_DELETE_BUCKET, client_ip, RATE_LIMIT_FILTER_DELETE_REQUESTS, _MINUTE
    )
    if not is_allowed:
        from app.exceptions import RateLimitError
        from app.utils.logging_compat import get_logger

        log = get_logger(__name__)
        log.warning(
            "filter_delete_rate_limit_exceeded",
            client_ip=client_ip,
            path=request.url.path,
            retry_after=retry_after,
        )
        raise RateLimitError(
            "Rate limit exceeded for filter delete operations. Please try again later.",
            retry_after_seconds=retry_after,
        )


_FilterNamePath = Annotated[
    str,
    Path(description='Filter base name, e.g. ``sshd`` or ``sshd.conf``.'),
]


@router.get(
    "",
    response_model=FilterListResponse,
    summary="List all available filters with active/inactive status",
    responses={
        200: {"description": "Filter list returned", "model": FilterListResponse},
        401: {"description": "Session missing, expired, or invalid"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def list_filters(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
) -> FilterListResponse:
    """Return all filters discovered in ``filter.d/`` with active/inactive status.

    Scans ``{config_dir}/filter.d/`` for ``.conf`` files, merges any
    corresponding ``.local`` overrides, and cross-references each filter's
    name against the ``filter`` fields of currently running jails to determine
    whether it is active.

    Active filters (those used by at least one running jail) are sorted to the
    top of the list; inactive filters follow.  Both groups are sorted
    alphabetically within themselves.

    Args:
        request: FastAPI request object.
        _auth: Validated session — enforces authentication.

    Returns:
        :class:`~app.models.config.FilterListResponse` with all discovered
        filters.
    """
    domain_result = await filter_config_service.list_filters(config_dir, socket_path)
    # Sort: active first (by name), then inactive (by name).
    domain_result.items.sort(key=lambda f: (not f.active, f.name.lower()))
    return config_mappers.map_domain_filter_list_to_response(domain_result)




@router.get(
    "/{name}",
    response_model=FilterConfig,
    summary="Return full parsed detail for a single filter",
    responses={
        200: {"description": "Filter config returned", "model": FilterConfig},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Filter not found in filter.d/"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def get_filter(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: Annotated[str, Path(description="Filter base name, e.g. ``sshd`` or ``sshd.conf``.")],
) -> FilterConfig:
    """Return the full parsed configuration and active/inactive status for one filter.

    Reads ``{config_dir}/filter.d/{name}.conf``, merges any corresponding
    ``.local`` override, and annotates the result with ``active``,
    ``used_by_jails``, ``source_file``, and ``has_local_override``.

    Args:
        request: FastAPI request object.
        _auth: Validated session — enforces authentication.
        name: Filter base name (with or without ``.conf`` extension).

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

    Raises:
        HTTPException: 404 if the filter is not found in ``filter.d/``.
        HTTPException: 502 if fail2ban is unreachable.
    """
    return await filter_config_service.get_filter(config_dir, socket_path, name)


# ---------------------------------------------------------------------------
# Filter write endpoints (Task 2.2)
# ---------------------------------------------------------------------------


_FilterNamePath = Annotated[
    str,
    Path(description="Filter base name, e.g. ``sshd`` or ``sshd.conf``."),
]


@router.put(
    "/{name}",
    response_model=FilterConfig,
    summary="Update a filter's .local override with new regex/pattern values",
    dependencies=[Depends(_check_filter_update_rate_limit)],
    responses={
        200: {"description": "Filter updated", "model": FilterConfig},
        400: {"description": "Invalid filter name"},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Filter not found"},
        422: {"description": "Regex pattern failed to compile"},
        429: {"description": "Rate limit exceeded for filter update operations"},
        500: {"description": "Failed to write .local file"},
    },
)
async def update_filter(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _FilterNamePath,
    body: FilterUpdateRequest,
    reload: bool = Query(default=False, description="Reload fail2ban after writing."),
) -> FilterConfig:
    """Update a filter's ``[Definition]`` fields by writing a ``.local`` override.

    All regex patterns are validated before writing. Validation includes:

    - **Length limit**: Patterns must not exceed 1000 characters (prevents DoS)
    - **Compilation timeout**: Pattern compilation must complete within 2 seconds
      (prevents ReDoS attacks via catastrophic backtracking)
    - **Syntax validation**: Patterns must be valid Python regex

    The original ``.conf`` file is never modified. Fields left as ``null`` in the
    request body are kept at their current values.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Filter base name (with or without ``.conf`` extension).
        body: Partial update — ``failregex``, ``ignoreregex``, ``datepattern``,
              ``journalmatch``.
        reload: When ``true``, trigger a fail2ban reload after writing.

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

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 400 if any regex pattern exceeds 1000 characters.
        HTTPException: 400 if any regex pattern times out during compilation (ReDoS).
        HTTPException: 422 if any regex pattern fails to compile.
        HTTPException: 404 if the filter does not exist.
        HTTPException: 500 if writing the ``.local`` file fails.
    """
    return await filter_config_service.update_filter(config_dir, socket_path, name, body, do_reload=reload)




@router.post(
    "",
    response_model=FilterConfig,
    status_code=status.HTTP_201_CREATED,
    summary="Create a new user-defined filter",
    dependencies=[Depends(_check_filter_create_rate_limit)],
    responses={
        201: {"description": "Filter created", "model": FilterConfig},
        400: {"description": "Invalid filter name or regex too long"},
        401: {"description": "Session missing, expired, or invalid"},
        409: {"description": "Filter already exists"},
        422: {"description": "Regex pattern failed to compile"},
        429: {"description": "Rate limit exceeded for filter create operations"},
        500: {"description": "Failed to write .local file"},
    },
)
async def create_filter(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    body: FilterCreateRequest,
    reload: bool = Query(default=False, description="Reload fail2ban after creating."),
) -> FilterConfig:
    """Create a new user-defined filter at ``filter.d/{name}.local``.

    The filter is created as a ``.local`` file so it can coexist safely with
    shipped ``.conf`` files.  Returns 409 if a ``.conf`` or ``.local`` for
    the requested name already exists.

    All regex patterns are validated before writing. Validation includes:

    - **Length limit**: Patterns must not exceed 1000 characters (prevents DoS)
    - **Compilation timeout**: Pattern compilation must complete within 2 seconds
      (prevents ReDoS attacks via catastrophic backtracking)
    - **Syntax validation**: Patterns must be valid Python regex

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        body: Filter name and ``[Definition]`` fields.
        reload: When ``true``, trigger a fail2ban reload after creating.

    Returns:
        :class:`~app.models.config.FilterConfig` for the new filter.

    Raises:
        HTTPException: 400 if the name contains invalid characters.
        HTTPException: 400 if any regex pattern exceeds 1000 characters.
        HTTPException: 400 if any regex pattern times out during compilation (ReDoS).
        HTTPException: 409 if the filter already exists.
        HTTPException: 422 if any regex pattern is invalid.
        HTTPException: 500 if writing fails.
    """
    return await filter_config_service.create_filter(config_dir, socket_path, body, do_reload=reload)




@router.delete(
    "/{name}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a user-created filter's .local file",
    dependencies=[Depends(_check_filter_delete_rate_limit)],
    responses={
        204: {"description": "Filter deleted successfully"},
        400: {"description": "Invalid filter name"},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Filter not found"},
        409: {"description": "Filter is a shipped default (conf-only)"},
        429: {"description": "Rate limit exceeded for filter delete operations"},
        500: {"description": "Failed to delete .local file"},
    },
)
async def delete_filter(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    name: _FilterNamePath,
) -> None:
    """Delete a user-created filter's ``.local`` override file.

    Shipped ``.conf``-only filters cannot be deleted (returns 409).  When
    both a ``.conf`` and a ``.local`` exist, only the ``.local`` is removed.
    When only a ``.local`` exists (user-created filter), the file is deleted
    entirely.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Filter base name.

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if the filter does not exist.
        HTTPException: 409 if the filter is a shipped default (conf-only).
        HTTPException: 500 if deletion fails.
    """
    await filter_config_service.delete_filter(config_dir, name)




# ---------------------------------------------------------------------------
# Action discovery endpoints (Task 3.1)
# ---------------------------------------------------------------------------



