"""Streaming result export — NDJSON, SSE, and paginated output.

Provides iterator-based streaming for large result sets with
progress callbacks, chunked output, and Server-Sent Events format.
Stdlib-only (Tier 0 compliant).
"""

from __future__ import annotations

import json
import time
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
from typing import IO


@dataclass
class StreamProgress:
    """Progress tracker for streaming operations."""

    total: int = 0
    processed: int = 0
    started_at: float = field(default_factory=time.time)
    errors: int = 0

    @property
    def elapsed_ms(self) -> float:
        return (time.time() - self.started_at) * 1000

    @property
    def percent(self) -> float:
        if self.total <= 0:
            return 0.0
        return min(100.0, (self.processed / self.total) * 100)

    @property
    def rate(self) -> float:
        """Records per second."""
        elapsed = self.elapsed_ms / 1000
        if elapsed <= 0:
            return 0.0
        return self.processed / elapsed

    def to_dict(self) -> dict:
        return {
            "total": self.total,
            "processed": self.processed,
            "percent": round(self.percent, 1),
            "elapsed_ms": round(self.elapsed_ms, 1),
            "rate_per_sec": round(self.rate, 1),
            "errors": self.errors,
        }


ProgressCallback = Callable[[StreamProgress], None]


def stream_ndjson(
    records: Iterator[dict] | list[dict],
    output: IO[str] | None = None,
    on_progress: ProgressCallback | None = None,
    total: int = 0,
) -> list[str]:
    """Stream records as newline-delimited JSON.

    If output is provided, writes to it. Always returns the lines.
    Calls on_progress after each record if provided.
    """
    progress = StreamProgress(total=total)
    lines: list[str] = []
    for record in records:
        line = json.dumps(record, default=str)
        lines.append(line)
        if output is not None:
            output.write(line + "\n")
        progress.processed += 1
        if on_progress:
            on_progress(progress)
    return lines


def stream_sse(
    records: Iterator[dict] | list[dict],
    output: IO[str] | None = None,
    event_type: str = "result",
    on_progress: ProgressCallback | None = None,
    total: int = 0,
) -> list[str]:
    """Stream records as Server-Sent Events (SSE).

    Format per record::

        event: result
        data: {"key": "value"}

    Ends with a done event.
    """
    progress = StreamProgress(total=total)
    events: list[str] = []

    for record in records:
        data = json.dumps(record, default=str)
        event = f"event: {event_type}\ndata: {data}\n"
        events.append(event)
        if output is not None:
            output.write(event + "\n")
        progress.processed += 1
        if on_progress:
            on_progress(progress)

    # Done sentinel
    done_data = json.dumps(progress.to_dict(), default=str)
    done_event = f"event: done\ndata: {done_data}\n"
    events.append(done_event)
    if output is not None:
        output.write(done_event + "\n")

    return events


def paginate(
    records: list[dict],
    page: int = 1,
    page_size: int = 50,
) -> dict:
    """Paginate a list of records.

    Returns a dict with page metadata and the page slice.
    """
    if page < 1:
        page = 1
    if page_size < 1:
        page_size = 50

    total = len(records)
    total_pages = max(1, (total + page_size - 1) // page_size)
    page = min(page, total_pages)

    start = (page - 1) * page_size
    end = start + page_size
    page_records = records[start:end]

    return {
        "page": page,
        "page_size": page_size,
        "total_records": total,
        "total_pages": total_pages,
        "has_next": page < total_pages,
        "has_prev": page > 1,
        "records": page_records,
    }


def chunked_processor(
    records: list[dict],
    handler: Callable[[dict], dict | None],
    chunk_size: int = 100,
    on_progress: ProgressCallback | None = None,
    on_chunk: Callable[[list[dict]], None] | None = None,
) -> list[dict]:
    """Process records in chunks with progress callbacks.

    handler: called per-record, returns result dict or None for skip.
    on_chunk: called after each chunk with that chunk's results.
    """
    progress = StreamProgress(total=len(records))
    all_results: list[dict] = []
    chunk_results: list[dict] = []

    for record in records:
        try:
            result = handler(record)
            if result is not None:
                chunk_results.append(result)
                all_results.append(result)
        except Exception:
            progress.errors += 1

        progress.processed += 1

        if len(chunk_results) >= chunk_size:
            if on_chunk:
                on_chunk(chunk_results)
            chunk_results = []

        if on_progress:
            on_progress(progress)

    # Flush remaining chunk
    if chunk_results and on_chunk:
        on_chunk(chunk_results)

    return all_results


class ResultStream:
    """High-level streaming interface for result sets.

    Wraps records with format selection and progress tracking::

        stream = ResultStream(records, total=1000)
        for line in stream.iter_ndjson():
            send_to_client(line)
    """

    def __init__(
        self,
        records: Iterator[dict] | list[dict],
        total: int = 0,
        on_progress: ProgressCallback | None = None,
    ) -> None:
        self._records = records
        self._total = total
        self._on_progress = on_progress
        self._progress = StreamProgress(total=total)

    @property
    def progress(self) -> StreamProgress:
        return self._progress

    def iter_ndjson(self) -> Iterator[str]:
        """Yield NDJSON lines one at a time."""
        for record in self._records:
            line = json.dumps(record, default=str)
            self._progress.processed += 1
            if self._on_progress:
                self._on_progress(self._progress)
            yield line

    def iter_sse(self, event_type: str = "result") -> Iterator[str]:
        """Yield SSE-formatted events one at a time."""
        for record in self._records:
            data = json.dumps(record, default=str)
            self._progress.processed += 1
            if self._on_progress:
                self._on_progress(self._progress)
            yield f"event: {event_type}\ndata: {data}\n"

        done = json.dumps(self._progress.to_dict(), default=str)
        yield f"event: done\ndata: {done}\n"

    def collect(self) -> list[dict]:
        """Consume iterator and return all records as list."""
        if isinstance(self._records, list):
            return self._records
        results = list(self._records)
        self._progress.processed = len(results)
        return results

    def to_json(self, indent: int = 2) -> str:
        """Collect all records and return as JSON array string."""
        records = self.collect()
        return json.dumps(records, indent=indent, default=str)


__all__ = [
    "ProgressCallback",
    "ResultStream",
    "StreamProgress",
    "chunked_processor",
    "paginate",
    "stream_ndjson",
    "stream_sse",
]
