"""Minimal asynchronous component contracts for pipeline nodes."""

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any, TypeAlias

import asyncio

from .envelope import Envelope


DEFAULT_INPUT_PORT = "input"
DEFAULT_OUTPUT_PORT = "output"


@dataclass(frozen=True, slots=True)
class Emission:
    """One envelope emitted on a named output port."""

    port: str
    envelope: Envelope[Any]

    def __post_init__(self) -> None:
        if not self.port:
            raise ValueError("emission port must not be empty")
        if not isinstance(self.envelope, Envelope):
            raise TypeError("emission envelope must be an Envelope")


OutputItem: TypeAlias = Envelope[Any] | Emission
OperatorResult: TypeAlias = (
    OutputItem | Iterable[OutputItem] | AsyncIterable[OutputItem] | None
)


@dataclass(frozen=True, slots=True)
class ComponentContext:
    """Runtime information made available during component setup."""

    pipeline_id: str
    node_id: str
    shutdown_event: asyncio.Event
    metadata: Mapping[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not self.pipeline_id:
            raise ValueError("pipeline_id must not be empty")
        if not self.node_id:
            raise ValueError("node_id must not be empty")
        object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))


@dataclass(frozen=True, slots=True)
class HealthReport:
    """Small transport-neutral health response for supervisors."""

    healthy: bool = True
    detail: str = ""


class Component(ABC):
    """Base lifecycle shared by sources, operators and sinks.

    Hooks are intentionally no-ops by default.  Implementations should acquire
    expensive resources in ``setup`` or ``start`` and release them in ``stop``.
    The runtime calls each hook at most once per start/stop cycle.
    """

    async def setup(self, context: ComponentContext) -> None:
        """Validate configuration and allocate resources."""

    async def start(self) -> None:
        """Start background work after every node has completed setup."""

    async def stop(self) -> None:
        """Release resources; implementations should make this idempotent."""

    async def health(self) -> HealthReport:
        """Return the component's current health."""

        return HealthReport()


class Source(Component, ABC):
    """A component that produces envelopes from an external or synthetic input."""

    @abstractmethod
    def messages(self) -> AsyncIterator[OutputItem]:
        """Return an async iterator of envelopes or named emissions.

        Yielding an :class:`Envelope` uses :data:`DEFAULT_OUTPUT_PORT`.
        Implementations normally define this method as an async generator.
        """

        raise NotImplementedError


class Operator(Component, ABC):
    """A component that transforms an envelope and may emit zero or more results."""

    @abstractmethod
    async def process(
        self,
        envelope: Envelope[Any],
        input_port: str = DEFAULT_INPUT_PORT,
    ) -> OperatorResult:
        """Process one envelope received on ``input_port``.

        A plain envelope is emitted on :data:`DEFAULT_OUTPUT_PORT`; use
        :class:`Emission` for a different output port.  Iterables and async
        iterables are supported for zero-to-many transforms.
        """

        raise NotImplementedError


class Sink(Component, ABC):
    """A terminal component that sends messages to an external destination."""

    @abstractmethod
    async def consume(
        self,
        envelope: Envelope[Any],
        input_port: str = DEFAULT_INPUT_PORT,
    ) -> None:
        """Consume one envelope received on ``input_port``."""

        raise NotImplementedError


async def iter_emissions(
    result: OperatorResult,
    *,
    default_port: str = DEFAULT_OUTPUT_PORT,
) -> AsyncIterator[Emission]:
    """Normalize a component result into named emissions.

    The helper performs strict element validation so a component accidentally
    returning raw bytes or a model object fails near the offending node instead
    of much later in a connector.
    """

    if not default_port:
        raise ValueError("default_port must not be empty")

    if result is None:
        return
    if isinstance(result, Envelope):
        yield Emission(default_port, result)
        return
    if isinstance(result, Emission):
        yield result
        return

    if isinstance(result, AsyncIterable):
        async for item in result:
            yield _as_emission(item, default_port)
        return

    if isinstance(result, Iterable):
        for item in result:
            yield _as_emission(item, default_port)
        return

    raise TypeError(
        "component output must be an Envelope, Emission, iterable, "
        "async iterable, or None"
    )


def _as_emission(item: OutputItem, default_port: str) -> Emission:
    if isinstance(item, Emission):
        return item
    if isinstance(item, Envelope):
        return Emission(default_port, item)
    raise TypeError("component output iterable contains an invalid item")


__all__ = [
    "Component",
    "ComponentContext",
    "DEFAULT_INPUT_PORT",
    "DEFAULT_OUTPUT_PORT",
    "Emission",
    "HealthReport",
    "Operator",
    "OperatorResult",
    "OutputItem",
    "Sink",
    "Source",
    "iter_emissions",
]
