"""Blocklist router.

Manages external IP blocklist sources, triggers manual imports, and exposes
the import schedule and log:

* ``GET    /api/blocklists``              — list all sources
* ``POST   /api/blocklists``              — add a source
* ``GET    /api/blocklists/import``       — (reserved; use POST)
* ``POST   /api/blocklists/import``       — trigger a manual import now
* ``GET    /api/blocklists/schedule``     — get current schedule + next run
* ``PUT    /api/blocklists/schedule``     — update schedule
* ``GET    /api/blocklists/log``          — paginated import log
* ``GET    /api/blocklists/{id}``         — get a single source
* ``PUT    /api/blocklists/{id}``         — edit a source
* ``DELETE /api/blocklists/{id}``         — remove a source
* ``GET    /api/blocklists/{id}/preview`` — preview the blocklist contents

Note: static path segments (``/import``, ``/schedule``, ``/log``) are
registered *before* the ``/{id}`` routes so FastAPI resolves them correctly.

"""

from __future__ import annotations

from app.utils.logging_compat import get_logger
from fastapi import APIRouter, Depends, Query, Request, status

from app.dependencies import (
    AuthDep,
    BlocklistServiceContextDep,
    Fail2BanSocketDep,
    GeoCacheDep,
    GlobalRateLimiterDep,
    HttpSessionDep,
    SchedulerDep,
    SettingsDep,
)
from app.exceptions import (
    BadRequestError,
    BlocklistSourceAlreadyExistsError,
    BlocklistSourceNotFoundError,
    RateLimitError,
)
from app.mappers import blocklist_mappers
from app.models.blocklist import (
    BlocklistListResponse,
    BlocklistSource,
    BlocklistSourceCreate,
    BlocklistSourceUpdate,
    ImportLogListResponse,
    ImportRunResult,
    PreviewResponse,
    ScheduleConfig,
    ScheduleInfo,
)
from app.services import ban_service, blocklist_service
from app.tasks.blocklist_import import run_import_with_resources
from app.utils.constants import DEFAULT_PAGE_SIZE, RATE_LIMIT_BLOCKLIST_IMPORT_REQUESTS

router: APIRouter = APIRouter(prefix="/api/v1/blocklists", tags=["Blocklists"])

#: Rate limit bucket constants
_BLOCKLIST_IMPORT_BUCKET = "blocklist:import"
# 3600 seconds per hour
_HOUR = 3600

log = get_logger(__name__)


def _check_blocklist_import_rate_limit(
    request: Request,
    rate_limiter: GlobalRateLimiterDep,
) -> None:
    """Check rate limit for blocklist import 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(
        _BLOCKLIST_IMPORT_BUCKET, client_ip, RATE_LIMIT_BLOCKLIST_IMPORT_REQUESTS, _HOUR
    )
    if not is_allowed:
        log.warning(
            "blocklist_import_rate_limit_exceeded",
            client_ip=client_ip,
            path=request.url.path,
            retry_after=retry_after,
        )
        raise RateLimitError(
            "Rate limit exceeded for blocklist import. Please try again later.",
            retry_after_seconds=retry_after,
        )


# ---------------------------------------------------------------------------
# Source list + create
# ---------------------------------------------------------------------------


@router.get(
    "",
    response_model=BlocklistListResponse,
    summary="List all blocklist sources",
    responses={
        200: {"description": "Blocklist sources returned", "model": BlocklistListResponse},
        401: {"description": "Session missing, expired, or invalid"},
    },
)
async def list_blocklists(
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
) -> BlocklistListResponse:
    """Return all configured blocklist source definitions.

    Args:
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.

    Returns:
        :class:`~app.models.blocklist.BlocklistListResponse` with all sources.
    """
    sources = await blocklist_service.list_sources(blocklist_ctx.db)
    return BlocklistListResponse(sources=sources)


@router.post(
    "",
    response_model=BlocklistSource,
    status_code=status.HTTP_201_CREATED,
    summary="Add a new blocklist source",
    responses={
        201: {"description": "Blocklist source created", "model": BlocklistSource},
        400: {"description": "URL validation failed"},
        401: {"description": "Session missing, expired, or invalid"},
        409: {"description": "A blocklist source with this URL already exists"},
    },
)
async def create_blocklist(
    payload: BlocklistSourceCreate,
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
) -> BlocklistSource:
    """Create a new blocklist source definition.

    Args:
        payload: New source data (name, url, enabled).
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.

    Returns:
        The newly created :class:`~app.models.blocklist.BlocklistSource`.

    Raises:
        HTTPException: 400 if URL validation fails.
    """
    try:
        return await blocklist_service.create_source(
            blocklist_ctx.db, payload.name, str(payload.url), enabled=payload.enabled
        )
    except ValueError as exc:
        raise BadRequestError(str(exc)) from exc
    except BlocklistSourceAlreadyExistsError as exc:
        raise exc


# ---------------------------------------------------------------------------
# Static sub-paths — must be declared BEFORE /{id}
# ---------------------------------------------------------------------------


@router.post(
    "/import",
    response_model=ImportRunResult,
    summary="Trigger a manual blocklist import",
    dependencies=[Depends(_check_blocklist_import_rate_limit)],
    responses={
        200: {"description": "Import completed", "model": ImportRunResult},
        401: {"description": "Session missing, expired, or invalid"},
        429: {"description": "Rate limit exceeded for blocklist import"},
    },
)
async def run_import_now(
    http_session: HttpSessionDep,
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
    socket_path: Fail2BanSocketDep,
    geo_cache: GeoCacheDep,
) -> ImportRunResult:
    """Download and apply all enabled blocklist sources immediately.

    Args:
        http_session: Shared HTTP session (injected).
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.
        socket_path: Path to fail2ban Unix domain socket.
        geo_cache: Geolocation cache instance.

    Returns:
        :class:`~app.models.blocklist.ImportRunResult` with per-source
        results and aggregated counters.
    """

    return await blocklist_service.import_all(
        blocklist_ctx.db,
        http_session,
        socket_path,
        geo_is_cached=geo_cache.is_cached,
        geo_cache=geo_cache,
        ban_ip=ban_service.ban_ip,
    )


@router.get(
    "/schedule",
    response_model=ScheduleInfo,
    summary="Get the current import schedule",
    responses={
        200: {"description": "Schedule info returned", "model": ScheduleInfo},
        401: {"description": "Session missing, expired, or invalid"},
    },
)
async def get_schedule(
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
    scheduler: SchedulerDep,
) -> ScheduleInfo:
    """Return the current schedule configuration and runtime metadata.

    The ``next_run_at`` field is read from APScheduler if the job is active.

    Args:
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.
        scheduler: APScheduler instance.

    Returns:
        :class:`~app.models.blocklist.ScheduleInfo` with config and run
        times.
    """
    return await blocklist_service.get_schedule_info_with_runtime(blocklist_ctx.db, scheduler)


@router.put(
    "/schedule",
    response_model=ScheduleInfo,
    summary="Update the import schedule",
    responses={
        200: {"description": "Schedule updated", "model": ScheduleInfo},
        401: {"description": "Session missing, expired, or invalid"},
    },
)
async def update_schedule(
    payload: ScheduleConfig,
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
    scheduler: SchedulerDep,
    http_session: HttpSessionDep,
    settings: SettingsDep,
) -> ScheduleInfo:
    """Persist a new schedule configuration and reschedule the import job.

    Args:
        payload: New :class:`~app.models.blocklist.ScheduleConfig`.
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.
        scheduler: Shared APScheduler instance (injected).
        http_session: Shared HTTP session used by the scheduler job.
        settings: Current application settings used by the scheduler job.

    Returns:
        Updated :class:`~app.models.blocklist.ScheduleInfo`.
    """
    return await blocklist_service.update_schedule(
        blocklist_ctx.db,
        scheduler,
        http_session,
        settings,
        payload,
        run_import_with_resources,
    )


@router.get(
    "/log",
    response_model=ImportLogListResponse,
    summary="Get the paginated import log",
    responses={
        200: {"description": "Import log returned", "model": ImportLogListResponse},
        401: {"description": "Session missing, expired, or invalid"},
    },
)
async def get_import_log(
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
    source_id: int | None = Query(default=None, description="Filter by source id"),
    page: int = Query(default=1, ge=1, description="1-based page number."),
    page_size: int = Query(
        default=DEFAULT_PAGE_SIZE, ge=1, le=500, description="Items per page (max 500)."
    ),
) -> ImportLogListResponse:
    """Return a paginated log of all import runs.

    Args:
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.
        source_id: Optional filter — only show logs for this source.
        page: 1-based page number.
        page_size: Items per page.

    Returns:
        :class:`~app.models.blocklist.ImportLogListResponse`.
    """
    return await blocklist_service.list_import_logs(
        blocklist_ctx.db, source_id=source_id, page=page, page_size=page_size
    )


# ---------------------------------------------------------------------------
# Single source CRUD — parameterised routes AFTER static sub-paths
# ---------------------------------------------------------------------------


@router.get(
    "/{source_id}",
    response_model=BlocklistSource,
    summary="Get a single blocklist source",
    responses={
        200: {"description": "Blocklist source returned", "model": BlocklistSource},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Blocklist source not found"},
    },
)
async def get_blocklist(
    source_id: int,
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
) -> BlocklistSource:
    """Return a single blocklist source by id.

    Args:
        source_id: Primary key of the source.
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.

    Raises:
        HTTPException: 404 if the source does not exist.
    """
    source = await blocklist_service.get_source(blocklist_ctx.db, source_id)
    if source is None:
        raise BlocklistSourceNotFoundError(source_id)
    return source


@router.put(
    "/{source_id}",
    response_model=BlocklistSource,
    summary="Update a blocklist source",
    responses={
        200: {"description": "Blocklist source updated", "model": BlocklistSource},
        400: {"description": "URL validation failed"},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Blocklist source not found"},
    },
)
async def update_blocklist(
    source_id: int,
    payload: BlocklistSourceUpdate,
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
) -> BlocklistSource:
    """Update one or more fields on a blocklist source.

    Args:
        source_id: Primary key of the source to update.
        payload: Fields to update (all optional).
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.

    Raises:
        HTTPException: 400 if URL validation fails.
        HTTPException: 404 if the source does not exist.
    """
    try:
        updated = await blocklist_service.update_source(
            blocklist_ctx.db,
            source_id,
            name=payload.name,
            url=str(payload.url) if payload.url is not None else None,
            enabled=payload.enabled,
        )
    except ValueError as exc:
        raise BadRequestError(str(exc)) from exc
    if updated is None:
        raise BlocklistSourceNotFoundError(source_id)
    return updated


@router.delete(
    "/{source_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a blocklist source",
    responses={
        204: {"description": "Blocklist source deleted successfully"},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Blocklist source not found"},
    },
)
async def delete_blocklist(
    source_id: int,
    blocklist_ctx: BlocklistServiceContextDep,
    _auth: AuthDep,
) -> None:
    """Delete a blocklist source by id.

    Args:
        source_id: Primary key of the source to remove.
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.

    Raises:
        HTTPException: 404 if the source does not exist.
    """
    deleted = await blocklist_service.delete_source(blocklist_ctx.db, source_id)
    if not deleted:
        raise BlocklistSourceNotFoundError(source_id)


@router.get(
    "/{source_id}/preview",
    response_model=PreviewResponse,
    summary="Preview the contents of a blocklist source",
    responses={
        200: {"description": "Blocklist preview returned", "model": PreviewResponse},
        401: {"description": "Session missing, expired, or invalid"},
        404: {"description": "Blocklist source not found"},
        502: {"description": "URL could not be reached"},
    },
)
async def preview_blocklist(
    source_id: int,
    http_session: HttpSessionDep,
    blocklist_ctx: BlocklistServiceContextDep,
    settings: SettingsDep,
    _auth: AuthDep,
) -> PreviewResponse:
    """Download and preview a sample of a blocklist source.

    Returns the first :data:`~app.services.blocklist_service._PREVIEW_LINES`
    valid IP entries together with validation statistics.

    Args:
        source_id: Primary key of the source to preview.
        http_session: Shared HTTP session for downloading.
        blocklist_ctx: Blocklist service context containing db and repositories.
        _auth: Validated session — enforces authentication.

    Raises:
        HTTPException: 404 if the source does not exist.
        HTTPException: 502 if the URL cannot be reached.
    """
    source = await blocklist_service.get_source(blocklist_ctx.db, source_id)
    if source is None:
        raise BlocklistSourceNotFoundError(source_id)

    try:
        domain_result = await blocklist_service.preview_source(
            source.url, http_session, sample_lines=settings.blocklist_preview_max_lines
        )
        return blocklist_mappers.map_domain_preview_result_to_response(domain_result)
    except ValueError as exc:
        raise BadRequestError(f"Could not fetch blocklist: {exc}") from exc
