"""Channel-agnostic reply indicator lifecycle helpers."""

from __future__ import annotations

import asyncio
from contextlib import suppress
from dataclasses import dataclass
from typing import Awaitable, Callable

AsyncCallback = Callable[[], Awaitable[None]]
ErrorCallback = Callable[[Exception], None]


@dataclass(slots=True)
class ReplyIndicatorCallbacks:
    """Lifecycle hooks for one reply indicator session."""

    on_reply_start: AsyncCallback
    on_idle: AsyncCallback | None = None
    on_cleanup: AsyncCallback | None = None


class _ReplyIndicatorStartGuard:
    """Guard repeated start failures from spamming the channel."""

    def __init__(
        self,
        *,
        is_sealed: Callable[[], bool],
        on_start_error: ErrorCallback,
        max_consecutive_failures: int,
        on_trip: Callable[[], None],
    ) -> None:
        self._is_sealed = is_sealed
        self._on_start_error = on_start_error
        self._max_consecutive_failures = max(1, max_consecutive_failures)
        self._on_trip = on_trip
        self._consecutive_failures = 0
        self._tripped = False

    async def run(self, start: AsyncCallback) -> str:
        """Attempt one start call and return the guard state."""
        if self._is_sealed() or self._tripped:
            return "skipped"
        try:
            await start()
        except Exception as exc:
            self._consecutive_failures += 1
            self._on_start_error(exc)
            if self._consecutive_failures >= self._max_consecutive_failures:
                self._tripped = True
                self._on_trip()
                return "tripped"
            return "failed"
        self._consecutive_failures = 0
        return "started"

    def reset(self) -> None:
        self._consecutive_failures = 0
        self._tripped = False

    def is_tripped(self) -> bool:
        return self._tripped


def create_reply_indicator_callbacks(
    *,
    start: AsyncCallback,
    stop: AsyncCallback | None = None,
    on_start_error: ErrorCallback,
    on_stop_error: ErrorCallback | None = None,
    keepalive_interval_ms: int = 3_000,
    max_consecutive_failures: int = 2,
    max_duration_ms: int = 60_000,
) -> ReplyIndicatorCallbacks:
    """Create a keepalive-based reply indicator controller."""
    keepalive_task: asyncio.Task[None] | None = None
    ttl_task: asyncio.Task[None] | None = None
    closed = False
    stop_sent = False

    def stop_keepalive() -> None:
        nonlocal keepalive_task
        task = keepalive_task
        keepalive_task = None
        if task and not task.done() and task is not asyncio.current_task():
            task.cancel()

    def stop_ttl() -> None:
        nonlocal ttl_task
        task = ttl_task
        ttl_task = None
        if task and not task.done() and task is not asyncio.current_task():
            task.cancel()

    start_guard = _ReplyIndicatorStartGuard(
        is_sealed=lambda: closed,
        on_start_error=on_start_error,
        max_consecutive_failures=max_consecutive_failures,
        on_trip=stop_keepalive,
    )

    async def fire_start() -> str:
        return await start_guard.run(start)

    async def keepalive_loop() -> None:
        while not closed:
            await asyncio.sleep(max(keepalive_interval_ms, 1) / 1000)
            if closed:
                break
            await fire_start()
            if start_guard.is_tripped():
                break

    async def ttl_loop() -> None:
        await asyncio.sleep(max(max_duration_ms, 1) / 1000)
        if not closed:
            await fire_stop()

    def start_keepalive() -> None:
        nonlocal keepalive_task
        if keepalive_interval_ms <= 0 or keepalive_task is not None or closed:
            return
        keepalive_task = asyncio.create_task(keepalive_loop())

    def start_ttl() -> None:
        nonlocal ttl_task
        if max_duration_ms <= 0 or ttl_task is not None or closed:
            return
        ttl_task = asyncio.create_task(ttl_loop())

    async def fire_stop() -> None:
        nonlocal closed, stop_sent
        closed = True
        stop_keepalive()
        stop_ttl()
        if keepalive_task:
            with suppress(asyncio.CancelledError):
                await keepalive_task
        if ttl_task:
            with suppress(asyncio.CancelledError):
                await ttl_task
        if stop is None or stop_sent:
            return
        stop_sent = True
        try:
            await stop()
        except Exception as exc:
            (on_stop_error or on_start_error)(exc)

    async def on_reply_start() -> None:
        nonlocal stop_sent
        if closed:
            return
        stop_sent = False
        stop_keepalive()
        stop_ttl()
        start_guard.reset()
        await fire_start()
        if start_guard.is_tripped():
            return
        start_keepalive()
        start_ttl()

    return ReplyIndicatorCallbacks(
        on_reply_start=on_reply_start,
        on_idle=fire_stop,
        on_cleanup=fire_stop,
    )
