"""Service provider abstraction — base class and types."""

from __future__ import annotations

from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING, Any, Awaitable, Callable

if TYPE_CHECKING:
    from notify_bridge_core.models.events import ServiceEvent
    from notify_bridge_core.templates.variables import TemplateVariableDefinition


class ServiceProviderType(str, Enum):
    """Supported service provider types."""

    IMMICH = "immich"
    GITEA = "gitea"
    PLANKA = "planka"
    SCHEDULER = "scheduler"
    NUT = "nut"
    GOOGLE_PHOTOS = "google_photos"
    WEBHOOK = "webhook"
    HOME_ASSISTANT = "home_assistant"
    BRIDGE_SELF = "bridge_self"


# Callback signature for push-style providers: a coroutine that accepts a
# parsed ServiceEvent and is expected to enqueue it for dispatch. Returning
# None keeps the contract narrow — error handling stays inside the callback.
EventEmitCallback = Callable[["ServiceEvent"], Awaitable[None]]


class ServiceProvider(ABC):
    """Abstract base class for service providers.

    A service provider connects to an external service (e.g., Immich photo server)
    and can poll for changes, producing generic ServiceEvent objects.

    Two ingest modes coexist on this base class:

    * Polling providers (Immich, NUT, Google Photos, Scheduler) implement
      :meth:`poll` and leave :attr:`supports_subscription` False.
    * Webhook providers (Gitea, Planka, generic Webhook) no-op :meth:`poll`
      and receive events out-of-band via ``api/webhooks.py``.
    * Subscription providers (Home Assistant) flip
      :attr:`supports_subscription` to True and implement :meth:`subscribe`
      to run a long-lived task that pushes events through an
      ``emit`` callback. They typically no-op :meth:`poll`.
    """

    provider_type: ServiceProviderType

    # When True, the lifecycle layer (server-side subscription manager) starts
    # a long-running task that calls :meth:`subscribe` instead of registering
    # this provider with the polling scheduler. Default False keeps the
    # legacy poll/webhook flow intact for every existing provider.
    supports_subscription: bool = False

    @abstractmethod
    async def connect(self) -> bool:
        """Connect to the service and verify connectivity.

        Returns True if connection is successful.
        """

    @abstractmethod
    async def disconnect(self) -> None:
        """Disconnect and clean up resources."""

    @abstractmethod
    async def poll(
        self,
        collection_ids: list[str],
        tracker_state: dict[str, Any],
    ) -> tuple[list[ServiceEvent], dict[str, Any]]:
        """Poll for changes in the specified collections.

        Args:
            collection_ids: IDs of collections to check.
            tracker_state: Previous state dict (opaque to caller, managed by provider).

        Returns:
            Tuple of (list of events detected, updated state dict).
        """

    async def subscribe(self, emit: EventEmitCallback) -> None:
        """Run a long-lived subscription that calls ``emit`` for each event.

        Override on providers with :attr:`supports_subscription` = True. The
        implementation is expected to:

        * Loop until cancelled (the subscription manager uses
          :func:`asyncio.Task.cancel` on shutdown).
        * Handle its own reconnect with exponential backoff — never propagate
          transient network errors to the caller.
        * Pass parsed :class:`ServiceEvent` instances to ``emit`` for
          enqueueing/dispatch. The callback is responsible for routing.

        The default implementation raises :class:`NotImplementedError` so
        accidental wiring of a polling provider into the subscription manager
        fails loudly rather than silently doing nothing.
        """
        raise NotImplementedError(
            f"{type(self).__name__} does not support subscription-based ingest"
        )

    @abstractmethod
    def get_available_variables(self) -> list[TemplateVariableDefinition]:
        """Return the template variables this provider makes available."""

    @abstractmethod
    def get_provider_config_schema(self) -> dict[str, Any]:
        """Return JSON schema for this provider's configuration fields."""

    @abstractmethod
    async def list_collections(self) -> list[dict[str, Any]]:
        """List available collections from the service.

        Returns a list of dicts with at least 'id' and 'name' keys.
        """

    @abstractmethod
    async def test_connection(self) -> dict[str, Any]:
        """Test the connection and return status info.

        Returns a dict with 'ok' (bool) and optional 'message' (str).
        """
