from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from threading import Condition, Event, Thread
from typing import Any, Literal
import time

EventKind = Literal["data", "system", "control"]
BackpressurePolicy = Literal["block", "drop_oldest", "drop_newest"]


@dataclass(slots=True)
class EventEnvelope:
    run_id: str | None
    request_id: str | None
    seq: int
    kind: EventKind
    payload: Any
    timestamp: float

    @property
    def request_ref_id(self) -> str | None:
        """Compatibility alias of request_id for request-ref terminology."""
        return self.request_id


@dataclass(slots=True)
class TerminalEnvelope:
    run_id: str | None
    request_id: str | None
    final_seq: int
    reason: str
    timestamp: float
    terminal: bool = True

    @property
    def request_ref_id(self) -> str | None:
        """Compatibility alias of request_id for request-ref terminology."""
        return self.request_id


@dataclass(slots=True)
class GapNotice:
    run_id: str | None
    request_id: str | None
    seq: int
    reason: str
    timestamp: float
    kind: str = "gap"

    @property
    def request_ref_id(self) -> str | None:
        """Compatibility alias of request_id for request-ref terminology."""
        return self.request_id


class OrderedEventSubscription:
    def __init__(self, stop_event: Event, thread: Thread):
        self._stop_event = stop_event
        self._thread = thread

    def cancel(self) -> None:
        self._stop_event.set()

    def join(self, timeout: float | None = None) -> bool:
        self._thread.join(timeout=timeout)
        return not self._thread.is_alive()

    def is_alive(self) -> bool:
        return self._thread.is_alive()


class OrderedEventChannel:
    """
    Request-scoped ordered event channel with terminal barrier semantics.

    Notes:
    - Order is enforced by `seq` and consumed in strictly increasing sequence.
    - `publish_terminal(final_seq)` is visible only after `<= final_seq` are
      either delivered as data or surfaced as explicit `GapNotice`.
    - Backpressure strategy is configurable per channel.
    """

    def __init__(
        self,
        *,
        run_id: str | None = None,
        request_id: str | None = None,
        capacity: int = 256,
        backpressure: BackpressurePolicy = "block",
        terminal_hold_s: float = 0.05,
    ):
        if capacity <= 0:
            raise ValueError("capacity must be a positive integer.")
        if backpressure not in {"block", "drop_oldest", "drop_newest"}:
            raise ValueError(f"Unsupported backpressure policy: {backpressure}")
        if terminal_hold_s < 0:
            raise ValueError("terminal_hold_s must be non-negative.")

        self.run_id = run_id
        self.request_id = request_id
        self.capacity = int(capacity)
        self.backpressure = backpressure
        self.terminal_hold_s = float(terminal_hold_s)

        self._cv = Condition()
        self._events: dict[int, EventEnvelope] = {}
        self._gap_reasons: dict[int, str] = {}
        self._next_write_seq = 1
        self._next_read_seq = 1
        self._terminal: TerminalEnvelope | None = None
        self._terminal_marked_at_monotonic: float | None = None
        self._terminal_emitted = False
        self._closed = False
        self._cancelled = False

        self._stats = {
            "published_total": 0,
            "delivered_total": 0,
            "dropped_total": 0,
            "gap_total": 0,
            "blocked_duration_ms": 0.0,
            "terminal_wait_ms": 0.0,
        }

    def publish(
        self,
        envelope: EventEnvelope,
    ) -> bool:
        if envelope.seq <= 0:
            raise ValueError("envelope.seq must be a positive integer.")
        if envelope.kind not in {"data", "system", "control"}:
            raise ValueError(f"Unsupported envelope kind: {envelope.kind}")

        with self._cv:
            return self._publish_locked(envelope)

    def publish_data(
        self,
        payload: Any,
        *,
        kind: EventKind = "data",
        seq: int | None = None,
        timestamp: float | None = None,
    ) -> int:
        if kind not in {"data", "system", "control"}:
            raise ValueError(f"Unsupported kind: {kind}")
        ts = time.time() if timestamp is None else float(timestamp)
        with self._cv:
            if seq is None:
                seq = self._next_write_seq
                self._next_write_seq += 1
            envelope = EventEnvelope(
                run_id=self.run_id,
                request_id=self.request_id,
                seq=int(seq),
                kind=kind,
                payload=payload,
                timestamp=ts,
            )
            self._publish_locked(envelope)
            return envelope.seq

    def publish_terminal(
        self,
        *,
        final_seq: int | None = None,
        reason: str = "done",
        timestamp: float | None = None,
    ) -> None:
        ts = time.time() if timestamp is None else float(timestamp)
        with self._cv:
            if final_seq is None:
                final_seq = self._next_write_seq - 1
            final_seq = int(final_seq)
            if final_seq < 0:
                raise ValueError("final_seq must be >= 0.")
            if self._terminal is None:
                self._terminal = TerminalEnvelope(
                    run_id=self.run_id,
                    request_id=self.request_id,
                    final_seq=final_seq,
                    reason=reason,
                    timestamp=ts,
                )
                self._terminal_marked_at_monotonic = time.monotonic()
            else:
                if final_seq > self._terminal.final_seq:
                    self._terminal.final_seq = final_seq
                if reason:
                    self._terminal.reason = reason
                self._terminal.timestamp = ts
            self._cv.notify_all()

    def read(
        self,
        timeout: float | None = 5.0,
        *,
        from_seq: int | None = None,
    ) -> EventEnvelope | TerminalEnvelope | GapNotice | None:
        timeout_s = None if timeout is None else max(0.0, float(timeout))
        deadline = None if timeout_s is None else (time.monotonic() + timeout_s)
        with self._cv:
            if from_seq is not None:
                from_seq = int(from_seq)
                if from_seq > self._next_read_seq:
                    self._next_read_seq = from_seq

            while True:
                event = self._pop_next_locked(force_gap_on_close=False)
                if event is not None:
                    return event

                # Fail-open: once closed/cancelled, synthesize missing gaps toward
                # terminal convergence instead of hanging forever.
                if self._closed or self._cancelled:
                    event = self._pop_next_locked(force_gap_on_close=True)
                    if event is not None:
                        return event
                    return None

                wait_timeout: float | None
                if deadline is not None:
                    remaining = deadline - time.monotonic()
                    if remaining <= 0:
                        return None
                    wait_timeout = remaining
                else:
                    wait_timeout = None

                # When terminal is pending but still in hold window, wake up at
                # hold expiry to avoid sleeping until the caller timeout.
                wait_started = self._terminal_marked_at_monotonic
                if (
                    self._terminal is not None
                    and not self._terminal_emitted
                    and wait_started is not None
                    and self.terminal_hold_s > 0
                ):
                    hold_remaining = self.terminal_hold_s - (time.monotonic() - wait_started)
                    if hold_remaining > 0:
                        if wait_timeout is None or hold_remaining < wait_timeout:
                            wait_timeout = hold_remaining

                self._cv.wait(timeout=wait_timeout)

    def subscribe(
        self,
        handler: Callable[[EventEnvelope | TerminalEnvelope | GapNotice], Any],
        *,
        from_seq: int | None = None,
        poll_timeout: float = 0.2,
    ) -> OrderedEventSubscription:
        stop_event = Event()

        def _worker() -> None:
            first = True
            while not stop_event.is_set():
                item = self.read(
                    timeout=poll_timeout,
                    from_seq=from_seq if first else None,
                )
                first = False
                if item is None:
                    if self.closed():
                        break
                    continue
                handler(item)
                if isinstance(item, TerminalEnvelope):
                    break

        thread = Thread(target=_worker, daemon=True)
        thread.start()
        return OrderedEventSubscription(stop_event, thread)

    def close(self) -> None:
        with self._cv:
            self._closed = True
            self._cv.notify_all()

    def cancel(self) -> None:
        with self._cv:
            self._cancelled = True
            self._cv.notify_all()

    def closed(self) -> bool:
        with self._cv:
            return self._closed or self._cancelled

    def last_published_seq(self) -> int:
        with self._cv:
            return max(0, self._next_write_seq - 1)

    def terminal_final_seq(self) -> int | None:
        with self._cv:
            if self._terminal is None:
                return None
            return self._terminal.final_seq

    def terminal_emitted(self) -> bool:
        with self._cv:
            return self._terminal_emitted

    def stats(self) -> dict[str, float | int]:
        with self._cv:
            stats = dict(self._stats)
            stats["queued"] = len(self._events)
            stats["next_read_seq"] = self._next_read_seq
            stats["last_published_seq"] = max(0, self._next_write_seq - 1)
            stats["terminal_final_seq"] = self._terminal.final_seq if self._terminal else None
            return stats

    def _publish_locked(self, envelope: EventEnvelope) -> bool:
        if self._closed or self._cancelled:
            return False

        if envelope.seq < self._next_read_seq:
            # Already consumed.
            return False

        if envelope.seq >= self._next_write_seq:
            self._next_write_seq = envelope.seq + 1

        if envelope.seq in self._events:
            # Idempotent duplicate.
            return True

        if self.backpressure == "block":
            blocked_since: float | None = None
            while len(self._events) >= self.capacity and not (self._closed or self._cancelled):
                if blocked_since is None:
                    blocked_since = time.monotonic()
                self._cv.wait(timeout=0.1)
            if blocked_since is not None:
                self._stats["blocked_duration_ms"] += (
                    (time.monotonic() - blocked_since) * 1000.0
                )
            if self._closed or self._cancelled:
                return False
        elif self.backpressure == "drop_oldest":
            while len(self._events) >= self.capacity:
                oldest_seq = min(self._events.keys())
                self._events.pop(oldest_seq, None)
                self._mark_gap_locked(oldest_seq, "drop_oldest")
                self._stats["dropped_total"] += 1
        elif self.backpressure == "drop_newest":
            if len(self._events) >= self.capacity:
                self._mark_gap_locked(envelope.seq, "drop_newest")
                self._stats["dropped_total"] += 1
                return False

        self._events[envelope.seq] = envelope
        if self._terminal is not None and envelope.seq > self._terminal.final_seq:
            # Terminal was observed before late event visibility.
            self._terminal.final_seq = envelope.seq
        self._stats["published_total"] += 1
        self._cv.notify_all()
        return True

    def _mark_gap_locked(self, seq: int, reason: str) -> None:
        if seq < self._next_read_seq:
            return
        if seq in self._events:
            return
        self._gap_reasons.setdefault(seq, reason)

    def _pop_next_locked(self, *, force_gap_on_close: bool) -> EventEnvelope | TerminalEnvelope | GapNotice | None:
        expected_seq = self._next_read_seq

        data = self._events.pop(expected_seq, None)
        if data is not None:
            self._next_read_seq = expected_seq + 1
            self._stats["delivered_total"] += 1
            self._cv.notify_all()
            return data

        gap_reason = self._gap_reasons.pop(expected_seq, None)
        if gap_reason is not None:
            self._next_read_seq = expected_seq + 1
            self._stats["gap_total"] += 1
            self._stats["delivered_total"] += 1
            self._cv.notify_all()
            return GapNotice(
                run_id=self.run_id,
                request_id=self.request_id,
                seq=expected_seq,
                reason=gap_reason,
                timestamp=time.time(),
            )

        terminal = self._terminal
        if terminal is None or self._terminal_emitted:
            return None

        if expected_seq <= terminal.final_seq:
            if force_gap_on_close:
                self._next_read_seq = expected_seq + 1
                self._stats["gap_total"] += 1
                self._stats["delivered_total"] += 1
                return GapNotice(
                    run_id=self.run_id,
                    request_id=self.request_id,
                    seq=expected_seq,
                    reason="missing_before_terminal",
                    timestamp=time.time(),
                )
            return None

        wait_started = self._terminal_marked_at_monotonic
        if wait_started is not None and self.terminal_hold_s > 0 and not force_gap_on_close:
            waited = time.monotonic() - wait_started
            if waited < self.terminal_hold_s:
                return None
            self._stats["terminal_wait_ms"] += waited * 1000.0
        elif wait_started is not None:
            self._stats["terminal_wait_ms"] += (time.monotonic() - wait_started) * 1000.0

        self._terminal_emitted = True
        self._stats["delivered_total"] += 1
        return terminal


__all__ = [
    "EventEnvelope",
    "TerminalEnvelope",
    "GapNotice",
    "BackpressurePolicy",
    "OrderedEventChannel",
    "OrderedEventSubscription",
]
