"""Blocklist ban executor component.

Executes bans via fail2ban for a list of IP addresses, handling errors and
logging failures.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from app.utils.logging_compat import get_logger

from app.exceptions import JailNotFoundError, JailOperationError

if TYPE_CHECKING:
    from collections.abc import Awaitable, Callable

log = get_logger(__name__)


class BanExecutor:
    """Executes bans via fail2ban for blocklist-sourced IPs."""

    def __init__(
        self,
        ban_ip: Callable[[str, str, str], Awaitable[None]],
    ) -> None:
        """Initialize the ban executor.

        Args:
            ban_ip: Async callable that bans an IP in a jail.
                Signature: async def ban_ip(socket_path: str, jail: str, ip: str) -> None
        """
        self.ban_ip = ban_ip

    async def ban_ips(
        self,
        socket_path: str,
        jail: str,
        ips: list[str],
    ) -> tuple[int, int, str | None]:
        """Ban a list of IPs in the specified fail2ban jail.

        On first JailNotFoundError, stops processing (the jail doesn't exist).
        On JailOperationError, records the error but continues with next IPs.
        Other exceptions are treated as fatal and raised.

        Args:
            socket_path: Path to fail2ban Unix socket.
            jail: Name of the fail2ban jail.
            ips: List of IP addresses to ban.

        Returns:
            Tuple of (successful bans count, failed bans count, first error or None).

        Raises:
            Exception: If an unexpected error occurs (not JailNotFoundError or
                JailOperationError).
        """
        successful = 0
        failed = 0
        first_error: str | None = None

        for ip in ips:
            try:
                await self.ban_ip(socket_path, jail, ip)
                successful += 1
            except JailNotFoundError as exc:
                # Jail doesn't exist — no point continuing
                first_error = str(exc)
                log.warning(
                    "blocklist_jail_not_found",
                    jail=jail,
                    error=str(exc),
                )
                break
            except JailOperationError as exc:
                # Individual ban failed, but continue
                failed += 1
                if first_error is None:
                    first_error = str(exc)
                log.debug("blocklist_ban_failed", ip=ip, error=str(exc))

        return successful, failed, first_error
