"""Reusable service mixins for BigBrotr.

All service extensions live here as mixin classes. Each mixin uses
cooperative multiple inheritance (``super().__init__(**kwargs)``) so
that initialization is handled automatically via the MRO — no
explicit ``_init_*()`` calls are needed in service constructors.

See Also:
    [BaseService][bigbrotr.core.base_service.BaseService]: The base class
        that mixin classes are composed with via multiple inheritance.
    [NetworksConfig][bigbrotr.services.common.configs.NetworksConfig]:
        Provides ``max_tasks`` values consumed by
        [NetworkSemaphoresMixin][bigbrotr.services.common.mixins.NetworkSemaphoresMixin].
"""

from __future__ import annotations

import asyncio
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

from bigbrotr.models.constants import NetworkType


if TYPE_CHECKING:
    import geoip2.database

    from .configs import NetworksConfig


@dataclass(slots=True)
class ChunkProgress:
    """Tracks progress of a chunk-based processing cycle.

    All counters are reset at the start of each cycle via ``reset()``.
    Use ``record()`` after processing each chunk to update counters.

    Attributes:
        started_at: Timestamp when the cycle started (``time.time()``).
        total: Total items to process in this cycle.
        processed: Items processed so far.
        succeeded: Items that succeeded.
        failed: Items that failed.
        chunks: Number of chunks completed.

    Note:
        ``started_at`` uses ``time.time()`` for Unix timestamps (used in
        SQL comparisons), while ``elapsed`` uses ``time.monotonic()`` for
        accurate duration measurement unaffected by clock adjustments.

    See Also:
        [ChunkProgressMixin][bigbrotr.services.common.mixins.ChunkProgressMixin]:
            Mixin that exposes a ``chunk_progress`` attribute of this type.
    """

    started_at: float = field(default=0.0)
    _monotonic_start: float = field(default=0.0, repr=False)
    total: int = field(default=0)
    processed: int = field(default=0)
    succeeded: int = field(default=0)
    failed: int = field(default=0)
    chunks: int = field(default=0)

    def reset(self) -> None:
        """Reset all counters and set ``started_at`` to the current time."""
        self.started_at = time.time()
        self._monotonic_start = time.monotonic()
        self.total = 0
        self.processed = 0
        self.succeeded = 0
        self.failed = 0
        self.chunks = 0

    def record(self, succeeded: int, failed: int) -> None:
        """Record the results of one processed chunk.

        Args:
            succeeded: Number of items that succeeded in this chunk.
            failed: Number of items that failed in this chunk.
        """
        self.processed += succeeded + failed
        self.succeeded += succeeded
        self.failed += failed
        self.chunks += 1

    @property
    def remaining(self) -> int:
        """Number of items left to process."""
        return self.total - self.processed

    @property
    def elapsed(self) -> float:
        """Seconds elapsed since processing started, rounded to 1 decimal."""
        return round(time.monotonic() - self._monotonic_start, 1)


class ChunkProgressMixin:
    """Mixin providing chunk-based processing progress tracking.

    Services that process items in chunks compose this mixin to get
    a ``chunk_progress`` attribute with counters and timing. Initialization
    is automatic via ``__init__``.

    See Also:
        [ChunkProgress][bigbrotr.services.common.mixins.ChunkProgress]:
            The dataclass this mixin manages.
        [Validator][bigbrotr.services.validator.Validator],
        [Monitor][bigbrotr.services.monitor.Monitor]: Services that
            compose this mixin.

    Examples:
        ```python
        class MyService(ChunkProgressMixin, BaseService[MyConfig]):
            async def run(self):
                self.chunk_progress.reset()
                ...
                self.chunk_progress.record(succeeded=len(ok), failed=len(err))
        ```
    """

    chunk_progress: ChunkProgress

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.chunk_progress = ChunkProgress()


#: Network types that support concurrent relay connections.
OPERATIONAL_NETWORKS: tuple[NetworkType, ...] = (
    NetworkType.CLEARNET,
    NetworkType.TOR,
    NetworkType.I2P,
    NetworkType.LOKI,
)


class NetworkSemaphores:
    """Per-network concurrency semaphores.

    Creates an ``asyncio.Semaphore`` for each operational
    [NetworkType][bigbrotr.models.constants.NetworkType] (clearnet, Tor,
    I2P, Lokinet) to cap the number of simultaneous connections.

    See Also:
        [NetworksConfig][bigbrotr.services.common.configs.NetworksConfig]:
            Provides ``max_tasks`` per network type.
    """

    __slots__ = ("_map",)

    def __init__(self, networks: NetworksConfig) -> None:
        self._map: dict[NetworkType, asyncio.Semaphore] = {
            nt: asyncio.Semaphore(networks.get(nt).max_tasks) for nt in OPERATIONAL_NETWORKS
        }

    def get(self, network: NetworkType) -> asyncio.Semaphore | None:
        """Look up the concurrency semaphore for a network type.

        Returns:
            The semaphore, or ``None`` for non-operational networks
            (LOCAL, UNKNOWN).
        """
        return self._map.get(network)


class NetworkSemaphoresMixin:
    """Mixin providing per-network concurrency semaphores.

    Exposes a ``network_semaphores`` attribute of type
    [NetworkSemaphores][bigbrotr.services.common.mixins.NetworkSemaphores],
    initialized from the ``networks`` keyword argument.

    Services must pass ``networks=config.networks`` in their
    ``super().__init__()`` call.

    See Also:
        [Validator][bigbrotr.services.validator.Validator],
        [Monitor][bigbrotr.services.monitor.Monitor],
        [Synchronizer][bigbrotr.services.synchronizer.Synchronizer]:
            Services that compose this mixin for bounded concurrency.
    """

    network_semaphores: NetworkSemaphores

    def __init__(self, **kwargs: Any) -> None:
        networks: NetworksConfig = kwargs.pop("networks")
        super().__init__(**kwargs)
        self.network_semaphores = NetworkSemaphores(networks)


class GeoReaders:
    """GeoIP database reader container for city and ASN lookups.

    Manages the lifecycle of ``geoip2.database.Reader`` instances.
    Reader initialization is offloaded to a thread via ``open()`` to
    avoid blocking the event loop.

    Attributes:
        city: GeoLite2-City reader for geolocation lookups, or ``None``.
        asn: GeoLite2-ASN reader for network info lookups, or ``None``.

    See Also:
        [GeoReaderMixin][bigbrotr.services.common.mixins.GeoReaderMixin]:
            Mixin that exposes a ``geo_readers`` attribute of this type.
    """

    __slots__ = ("asn", "city")

    def __init__(self) -> None:
        self.city: geoip2.database.Reader | None = None
        self.asn: geoip2.database.Reader | None = None

    async def open(
        self,
        *,
        city_path: str | None = None,
        asn_path: str | None = None,
    ) -> None:
        """Open GeoIP readers from file paths via ``asyncio.to_thread``.

        Args:
            city_path: Path to GeoLite2-City database. ``None`` to skip.
            asn_path: Path to GeoLite2-ASN database. ``None`` to skip.
        """
        import geoip2.database as geoip2_db  # noqa: PLC0415  # runtime import

        if city_path:
            self.city = await asyncio.to_thread(geoip2_db.Reader, city_path)
        if asn_path:
            self.asn = await asyncio.to_thread(geoip2_db.Reader, asn_path)

    def close(self) -> None:
        """Close readers and set to ``None``. Idempotent."""
        if self.city:
            self.city.close()
            self.city = None
        if self.asn:
            self.asn.close()
            self.asn = None


class GeoReaderMixin:
    """Mixin providing GeoIP database reader lifecycle management.

    Exposes a ``geo_readers`` attribute of type
    [GeoReaders][bigbrotr.services.common.mixins.GeoReaders].

    Note:
        Call ``geo_readers.close()`` in a ``finally`` block or ``__aexit__``.

    See Also:
        [Monitor][bigbrotr.services.monitor.Monitor]: The service that
            composes this mixin for NIP-66 geo/net checks.
    """

    geo_readers: GeoReaders

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.geo_readers = GeoReaders()
