from __future__ import annotations

from collections.abc import AsyncIterable, Awaitable, Callable, Iterable
from dataclasses import dataclass
from typing import Final, Literal, TypeAlias

from .compare import assert_value_parity
from .models import (
    SDKError,
    SDKReport,
    SDKStreamCompleted,
    SDKStreamFailed,
    SDKStreamReport,
    sdk_chunk,
    sdk_error_report,
)


@dataclass(frozen=True, slots=True)
class StreamCompleted:
    kind: Literal["completed"] = "completed"


@dataclass(frozen=True, slots=True)
class StreamFailed:
    phase: Literal["creation", "iteration"]
    exception_type: type[BaseException]
    error: SDKError
    kind: Literal["failed"] = "failed"


StreamTerminal: TypeAlias = StreamCompleted | StreamFailed


@dataclass(frozen=True, slots=True)
class StreamOutcome:
    wrapper_type: type[object] | None
    supports_sync_iteration: bool | None
    supports_async_iteration: bool | None
    chunks: tuple[object, ...]
    chunk_types: tuple[type[object], ...]
    terminal: StreamTerminal


ChunkNormalizer: TypeAlias = Callable[[object], object]


def drain_sync_stream(stream: Iterable[object]) -> None:
    for _ in stream:
        pass


async def drain_async_stream(stream: AsyncIterable[object]) -> None:
    async for _ in stream:
        pass


def capture_sync_stream(create: Callable[[], Iterable[object]]) -> SDKReport:
    return _stream_report(consume_sync_stream(create))


async def capture_async_stream(create: Callable[[], Awaitable[AsyncIterable[object]]]) -> SDKReport:
    return _stream_report(await consume_async_stream(create))


def _stream_report(outcome: StreamOutcome) -> SDKReport:
    terminal: Final = outcome.terminal
    if isinstance(terminal, StreamFailed) and terminal.phase == "creation":
        return terminal.error
    return SDKStreamReport(
        chunks=tuple(sdk_chunk(chunk) for chunk in outcome.chunks),
        terminal=SDKStreamFailed(error=terminal.error) if isinstance(terminal, StreamFailed) else SDKStreamCompleted(),
    )


def _failed(phase: Literal["creation", "iteration"], error: Exception) -> StreamFailed:
    return StreamFailed(
        phase=phase,
        exception_type=type(error),
        error=sdk_error_report(error),
    )


def consume_sync_stream(create: Callable[[], Iterable[object]]) -> StreamOutcome:
    try:
        stream: Final = create()
    except Exception as error:
        return StreamOutcome(
            wrapper_type=None,
            supports_sync_iteration=None,
            supports_async_iteration=None,
            chunks=(),
            chunk_types=(),
            terminal=_failed("creation", error),
        )

    chunks: list[object] = []  # mutable-ok: iterator consumption builds an ordered trace
    try:
        for chunk in stream:
            chunks.append(chunk)  # noqa: PERF402  # partial trace is required if iteration raises
    except Exception as error:
        recorded: Final = tuple(chunks)
        return StreamOutcome(
            wrapper_type=type(stream),
            supports_sync_iteration=hasattr(stream, "__iter__"),
            supports_async_iteration=hasattr(stream, "__aiter__"),
            chunks=recorded,
            chunk_types=tuple(type(chunk) for chunk in recorded),
            terminal=_failed("iteration", error),
        )
    completed_chunks: Final = tuple(chunks)
    return StreamOutcome(
        wrapper_type=type(stream),
        supports_sync_iteration=hasattr(stream, "__iter__"),
        supports_async_iteration=hasattr(stream, "__aiter__"),
        chunks=completed_chunks,
        chunk_types=tuple(type(chunk) for chunk in completed_chunks),
        terminal=StreamCompleted(),
    )


async def consume_async_stream(create: Callable[[], Awaitable[AsyncIterable[object]]]) -> StreamOutcome:
    try:
        stream: Final = await create()
    except Exception as error:
        return StreamOutcome(
            wrapper_type=None,
            supports_sync_iteration=None,
            supports_async_iteration=None,
            chunks=(),
            chunk_types=(),
            terminal=_failed("creation", error),
        )

    chunks: list[object] = []  # mutable-ok: iterator consumption builds an ordered trace
    try:
        async for chunk in stream:
            chunks.append(chunk)
    except Exception as error:
        recorded: Final = tuple(chunks)
        return StreamOutcome(
            wrapper_type=type(stream),
            supports_sync_iteration=hasattr(stream, "__iter__"),
            supports_async_iteration=hasattr(stream, "__aiter__"),
            chunks=recorded,
            chunk_types=tuple(type(chunk) for chunk in recorded),
            terminal=_failed("iteration", error),
        )
    completed_chunks: Final = tuple(chunks)
    return StreamOutcome(
        wrapper_type=type(stream),
        supports_sync_iteration=hasattr(stream, "__iter__"),
        supports_async_iteration=hasattr(stream, "__aiter__"),
        chunks=completed_chunks,
        chunk_types=tuple(type(chunk) for chunk in completed_chunks),
        terminal=StreamCompleted(),
    )


def normalize_chunk(chunk: object) -> object:
    return chunk


def assert_stream_parity(
    baseline: StreamOutcome,
    candidate: StreamOutcome,
    *,
    normalize: ChunkNormalizer = normalize_chunk,
) -> None:
    assert baseline.wrapper_type is candidate.wrapper_type
    assert baseline.supports_sync_iteration is candidate.supports_sync_iteration
    assert baseline.supports_async_iteration is candidate.supports_async_iteration
    assert baseline.chunk_types == candidate.chunk_types
    assert len(baseline.chunks) == len(candidate.chunks)
    for index, (baseline_chunk, candidate_chunk) in enumerate(zip(baseline.chunks, candidate.chunks, strict=True)):
        assert_value_parity(normalize(baseline_chunk), normalize(candidate_chunk), path=f"$.chunks[{index}]")
    assert baseline.terminal == candidate.terminal
