"""Core dispatch layer for sending messages through registered providers.

Public API:
    send()        -- synchronous multi-provider dispatch
    async_send()  -- asynchronous multi-provider dispatch

Both functions accept a message followed by keyword arguments whose names
correspond to registered provider names
(e.g. `slack=True`, `discord={"username": "bot"}`).

Unrecognised keyword arguments are silently ignored,
so callers can freely forward broader `**kwargs` without
needing to filter them first.
"""

import asyncio

from .exceptions import ConfigurationError, ProviderError
from .models import Message
from .providers.decorators import normalise_message
from .utils.provider import iter_providers


@normalise_message
def send(message: str | Message, **kwargs) -> None:
    """Send a message synchronously via one or more providers.

    Each keyword argument corresponds to a provider name. The value controls
    whether the provider is used and how it is configured.

    Args:
        message: Message text or Message object.
        **kwargs: Provider configurations keyed by provider name.

    Raises:
        ConfigurationError: If no providers are used or configuration is invalid.
        ProviderError: (Indirectly) if a provider raises internally.

    Examples:
        send("Hello", slack=True)
        send("Hi", slack=True, discord={"username": "bot"})
    """
    used: bool = False

    errors = []

    for spec, msg, provider_kwargs in iter_providers(message, kwargs):  # type: ignore[arg-type]
        try:
            spec.send(msg, **provider_kwargs)
            used = True
        except Exception as e:
            errors.append(e)

    if errors:
        raise ProviderError(
            "Providers failed:\n" + "\n".join(f"- {type(e).__name__}: {e}" for e in errors)
        )

    if not used:
        raise ConfigurationError("No providers used.")


@normalise_message
async def async_send(message: str | Message, **kwargs) -> None:
    """Send a message asynchronously via one or more providers.

    Providers that implement `async_send` are awaited directly.
    Synchronous providers are executed in a thread pool.

    Args:
        message: Message text or Message object.
        **kwargs: Provider configurations keyed by provider name.

    Raises:
        ConfigurationError: If no providers are used.
        ProviderError: If one or more providers fail.

    Examples:
        async_send("Hello", slack=True)
        async_send("Hi", slack=True, discord={"username": "bot"})
    """
    tasks = []

    for spec, msg, provider_kwargs in iter_providers(message, kwargs):  # type: ignore[arg-type]
        if spec.async_send:
            tasks.append(spec.async_send(msg, **provider_kwargs))
        else:
            tasks.append(asyncio.to_thread(spec.send, msg, **provider_kwargs))

    if not tasks:
        raise ConfigurationError("No providers used.")

    results = await asyncio.gather(*tasks, return_exceptions=True)

    errors = [r for r in results if isinstance(r, BaseException)]
    if errors:
        raise ProviderError(
            "Providers failed:\n" + "\n".join(f"- {type(e).__name__}: {e}" for e in errors)
        )
