"""Dashboard router.

Provides the ``GET /api/dashboard/status`` endpoint that returns the cached
fail2ban server health snapshot.  The snapshot is maintained by the
background health-check task and refreshed every 30 seconds.

Also provides ``GET /api/dashboard/bans`` for the dashboard ban-list table,
``GET /api/dashboard/bans/by-country`` for country aggregation,
``GET /api/dashboard/bans/trend`` for time-bucketed ban counts, and
``GET /api/dashboard/bans/by-jail`` for per-jail ban counts.
"""

from __future__ import annotations

from typing import Literal

from fastapi import APIRouter, Query

from app import __version__
from app.dependencies import (
    AuthDep,
    BanServiceContextDep,
    Fail2BanSocketDep,
    GeoCacheDep,
    HttpSessionDep,
    ServerStatusDep,
    SettingsDep,
)
from app.mappers import (
    map_domain_ban_trend_to_response,
    map_domain_bans_by_country_to_response,
    map_domain_bans_by_jail_to_response,
    map_domain_dashboard_ban_list_to_response,
)
from app.models._common import TimeRange
from app.models.ban import (
    BanOrigin,
    BansByCountryResponse,
    BansByJailResponse,
    BanTrendResponse,
    DashboardBanListResponse,
)
from app.models.server import ServerStatus, ServerStatusResponse
from app.services import ban_service
from app.utils.constants import DEFAULT_PAGE_SIZE

router: APIRouter = APIRouter(prefix="/api/v1/dashboard", tags=["Dashboard"])

# ---------------------------------------------------------------------------
# Default pagination constants
# ---------------------------------------------------------------------------

_DEFAULT_RANGE: TimeRange = "24h"


@router.get(
    "/status",
    response_model=ServerStatusResponse,
    summary="Return the cached fail2ban server status",
    responses={
        200: {"description": "Server status returned", "model": ServerStatusResponse},
        401: {"description": "Session missing, expired, or invalid"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def get_server_status(
    server_status: ServerStatusDep,
    _auth: AuthDep,
) -> ServerStatusResponse:
    """Return the most recent fail2ban health snapshot.

    The snapshot is populated by a background task that runs every 30 seconds.
    If the task has not yet executed a placeholder ``online=False`` status is
    returned so the response is always well-formed.

    Args:
        server_status: Cached fail2ban server health snapshot (injected).
        _auth: Validated session — enforces authentication on this endpoint.

    Returns:
        :class:`~app.models.server.ServerStatusResponse` containing the
        current health snapshot.
    """
    cached: ServerStatus = server_status
    cached.version = __version__
    return ServerStatusResponse(status=cached)


@router.get(
    "/bans",
    response_model=DashboardBanListResponse,
    summary="Return a paginated list of recent bans",
    responses={
        200: {"description": "Ban list returned", "model": DashboardBanListResponse},
        401: {"description": "Session missing, expired, or invalid"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def get_dashboard_bans(
    _auth: AuthDep,
    ban_ctx: BanServiceContextDep,
    socket_path: Fail2BanSocketDep,
    http_session: HttpSessionDep,
    geo_cache: GeoCacheDep,
    settings: SettingsDep,
    range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
    source: Literal["fail2ban", "archive"] = Query(
        default="fail2ban",
        description="Data source: 'fail2ban' or 'archive'.",
    ),
    page: int = Query(default=1, ge=1, description="1-based page number."),
    page_size: int = Query(default=DEFAULT_PAGE_SIZE, ge=1, description="Items per page."),
    origin: BanOrigin | None = Query(
        default=None,
        description="Filter by ban origin: 'blocklist' or 'selfblock'. Omit for all.",
    ),
) -> DashboardBanListResponse:
    """Return a paginated list of bans within the selected time window.

    Reads from the fail2ban database and enriches each entry with
    geolocation data (country, ASN, organisation) from the ip-api.com
    free API.  Results are sorted newest-first.  Geo lookups are served
    from the in-memory cache only; no database writes occur during this
    GET request.

    Args:
        _auth: Validated session dependency.
        ban_ctx: Ban service context containing db and repository.
        socket_path: Path to fail2ban Unix domain socket.
        http_session: Shared HTTP session for geolocation.
        geo_cache: Geolocation cache instance.
        range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or
            ``"365d"``.
        page: 1-based page number.
        page_size: Maximum items per page (1–500).
        origin: Optional filter by ban origin.

    Returns:
        :class:`~app.models.ban.DashboardBanListResponse` with paginated
        ban items and the total count for the selected window.
    """
    domain_result = await ban_service.list_bans(
        socket_path,
        range,
        source=source,
        page=page,
        page_size=page_size,
        max_page_size=settings.max_page_size,
        http_session=http_session,
        app_db=ban_ctx.db,
        geo_cache=geo_cache,
        origin=origin,
    )
    return map_domain_dashboard_ban_list_to_response(domain_result)


@router.get(
    "/bans/by-country",
    response_model=BansByCountryResponse,
    summary="Return ban counts aggregated by country",
    responses={
        200: {"description": "Ban counts by country returned", "model": BansByCountryResponse},
        401: {"description": "Session missing, expired, or invalid"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def get_bans_by_country(
    _auth: AuthDep,
    ban_ctx: BanServiceContextDep,
    socket_path: Fail2BanSocketDep,
    http_session: HttpSessionDep,
    geo_cache: GeoCacheDep,
    range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
    source: Literal["fail2ban", "archive"] = Query(
        default="fail2ban",
        description="Data source: 'fail2ban' or 'archive'.",
    ),
    origin: BanOrigin | None = Query(
        default=None,
        description="Filter by ban origin: 'blocklist' or 'selfblock'. Omit for all.",
    ),
    country_code: str | None = Query(
        default=None,
        description="ISO alpha-2 country code to filter companion rows.",
    ),
) -> BansByCountryResponse:
    """Return ban counts aggregated by ISO country code.

    Uses SQL aggregation (``GROUP BY ip``) and batch geo-resolution to handle
    10 000+ banned IPs efficiently.  Returns a ``{country_code: count}`` map
    and the 200 most recent raw ban rows for the companion access table.  Geo
    lookups are served from the in-memory cache only; no database writes occur
    during this GET request.

    Args:
        _auth: Validated session dependency.
        ban_ctx: Ban service context containing db and repository.
        socket_path: Path to fail2ban Unix domain socket.
        http_session: Shared HTTP session for geolocation.
        geo_cache: Geolocation cache instance.
        range: Time-range preset.
        origin: Optional filter by ban origin.

    Returns:
        :class:`~app.models.ban.BansByCountryResponse` with per-country
        aggregation and the companion ban list.
    """
    domain_result = await ban_service.bans_by_country(
        socket_path,
        range,
        source=source,
        http_session=http_session,
        geo_cache_lookup=geo_cache.lookup_cached_only,
        geo_cache=geo_cache,
        app_db=ban_ctx.db,
        origin=origin,
        country_code=country_code,
    )
    return map_domain_bans_by_country_to_response(domain_result)


@router.get(
    "/bans/trend",
    response_model=BanTrendResponse,
    summary="Return ban counts aggregated into time buckets",
    responses={
        200: {"description": "Ban trend data returned", "model": BanTrendResponse},
        401: {"description": "Session missing, expired, or invalid"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def get_ban_trend(
    _auth: AuthDep,
    ban_ctx: BanServiceContextDep,
    socket_path: Fail2BanSocketDep,
    range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
    source: Literal["fail2ban", "archive"] = Query(
        default="fail2ban",
        description="Data source: 'fail2ban' or 'archive'.",
    ),
    origin: BanOrigin | None = Query(
        default=None,
        description="Filter by ban origin: 'blocklist' or 'selfblock'. Omit for all.",
    ),
) -> BanTrendResponse:
    """Return ban counts grouped into equal-width time buckets.

    Each bucket represents a contiguous time interval within the selected
    window.  All buckets are returned — empty buckets (zero bans) are
    included so the frontend always receives a complete, gap-free series
    suitable for rendering a continuous area or line chart.

    Bucket sizes:

    * ``24h``  → 1-hour buckets  (24 total)
    * ``7d``   → 6-hour buckets  (28 total)
    * ``30d``  → 1-day buckets   (30 total)
    * ``365d`` → 7-day buckets   (~53 total)

    Args:
        _auth: Validated session dependency.
        ban_ctx: Ban service context containing db and repository.
        socket_path: Path to fail2ban Unix domain socket.
        range: Time-range preset.
        origin: Optional filter by ban origin.

    Returns:
        :class:`~app.models.ban.BanTrendResponse` with the ordered bucket
        list and the bucket-size label.
    """
    domain_result = await ban_service.ban_trend(
        socket_path,
        range,
        source=source,
        app_db=ban_ctx.db,
        origin=origin,
    )
    return map_domain_ban_trend_to_response(domain_result)


@router.get(
    "/bans/by-jail",
    response_model=BansByJailResponse,
    summary="Return ban counts aggregated by jail",
    responses={
        200: {"description": "Ban counts by jail returned", "model": BansByJailResponse},
        401: {"description": "Session missing, expired, or invalid"},
        502: {"description": "fail2ban unreachable"},
    },
)
async def get_bans_by_jail(
    _auth: AuthDep,
    ban_ctx: BanServiceContextDep,
    socket_path: Fail2BanSocketDep,
    range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
    source: Literal["fail2ban", "archive"] = Query(
        default="fail2ban",
        description="Data source: 'fail2ban' or 'archive'.",
    ),
    origin: BanOrigin | None = Query(
        default=None,
        description="Filter by ban origin: 'blocklist' or 'selfblock'. Omit for all.",
    ),
) -> BansByJailResponse:
    """Return ban counts grouped by jail name for the selected time window.

    Queries the fail2ban database and returns a list of jails sorted by
    ban count descending.  This endpoint is intended for the dashboard jail
    distribution bar chart.

    Args:
        _auth: Validated session dependency.
        ban_ctx: Ban service context containing db and repository.
        socket_path: Path to fail2ban Unix domain socket.
        range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or
            ``"365d"``.
        origin: Optional filter by ban origin.

    Returns:
        :class:`~app.models.ban.BansByJailResponse` with per-jail counts
        sorted descending and the total for the selected window.
    """
    domain_result = await ban_service.bans_by_jail(
        socket_path,
        range,
        source=source,
        app_db=ban_ctx.db,
        origin=origin,
    )
    return map_domain_bans_by_jail_to_response(domain_result)
