"""ChannelMonitor — async iterator + bot runner.

Polls channels in parallel, analyzes via pluggable analyzer,
yields MonitorAlert objects. Optionally auto-notifies admins via bot.
"""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING

from ._config import MonitorConfig
from ._notifier import Notifier
from ._poller import PollFn, poll_all_channels
from ._types import MonitorAlert

if TYPE_CHECKING:
    from ._analyzer import AnalyzerFn

logger = logging.getLogger(__name__)


class ChannelMonitor:
    """Async iterator that yields MonitorAlert from monitored Telegram channels.

    Usage as async iterator:
        async with monitor:
            async for alert in monitor:
                print(alert.summary)

    Usage as bot (auto-notify admins):
        await monitor.run()
    """

    def __init__(
        self,
        config: MonitorConfig,
        poll_fn: PollFn,
        analyzer: AnalyzerFn,
        notifier: Notifier | None = None,
    ):
        self._config = config
        self._poll_fn = poll_fn
        self._analyzer = analyzer
        self._notifier = notifier
        self._queue: asyncio.Queue[MonitorAlert] = asyncio.Queue()
        self._stop_event = asyncio.Event()
        self._task: asyncio.Task | None = None
        self._offsets: dict[str, int] = {}
        self._started = False
        self._alert_count = 0
        self._llm_calls = 0

    # ── Context manager ───────────────────────────────────────────────────

    async def __aenter__(self):
        await self.start()
        return self

    async def __aexit__(self, *exc):
        await self.stop()

    # ── Async iterator ────────────────────────────────────────────────────

    def __aiter__(self):
        return self

    async def __anext__(self) -> MonitorAlert:
        if not self._started:
            await self.start()
        while True:
            if self._stop_event.is_set() and self._queue.empty():
                raise StopAsyncIteration
            try:
                return await asyncio.wait_for(self._queue.get(), timeout=1.0)
            except asyncio.TimeoutError:
                if self._stop_event.is_set():
                    raise StopAsyncIteration
                if self._task and self._task.done():
                    exc = self._task.exception()
                    if exc:
                        raise exc
                    raise StopAsyncIteration
                continue

    # ── Lifecycle ─────────────────────────────────────────────────────────

    async def start(self) -> None:
        """Initialize channel offsets and start background polling loop."""
        if self._started:
            return
        self._started = True

        # Init offsets — poll each channel for 1 message to find starting point
        for ch in self._config.channels:
            try:
                msgs = await self._poll_fn(self._config.account, ch, min_id=0, limit=1)
                last = msgs[-1] if msgs else None
                self._offsets[ch] = (last.message_id if hasattr(last, "message_id") else last["message_id"]) if last else 0
                logger.info("Monitor: %s offset=%d", ch, self._offsets[ch])
            except Exception as e:
                logger.warning("Monitor: failed to init %s: %s", ch, e)

        if not self._offsets:
            raise RuntimeError("No channels could be initialized")

        self._task = asyncio.create_task(self._run_loop())

    async def stop(self) -> None:
        """Signal the monitor to stop and wait for background task."""
        self._stop_event.set()
        if self._task and not self._task.done():
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
        logger.info(
            "Monitor stopped. alerts=%d llm_calls=%d",
            self._alert_count, self._llm_calls,
        )

    # ── Bot mode ──────────────────────────────────────────────────────────

    async def run(self) -> None:
        """Run as bot: poll → analyze → notify admins. Blocks until cancelled.

        This is the simplest way to use the monitor:
            await monitor.run()
        """
        async with self:
            async for alert in self:
                if self._notifier:
                    await self._notifier.notify(alert)

    # ── Properties ────────────────────────────────────────────────────────

    @property
    def stats(self) -> dict:
        return {
            "alerts": self._alert_count,
            "llm_calls": self._llm_calls,
            "offsets": dict(self._offsets),
            "running": self._started and not self._stop_event.is_set(),
        }

    # ── Internal ──────────────────────────────────────────────────────────

    async def _run_loop(self) -> None:
        """Background polling + analysis loop."""
        cfg = self._config
        sem = asyncio.Semaphore(cfg.max_concurrent_llm)

        while not self._stop_event.is_set():
            await asyncio.sleep(cfg.interval)

            # Poll all channels in parallel
            new_by_channel = await poll_all_channels(
                self._poll_fn, cfg.account, self._offsets,
                cfg.batch_size, cfg.min_text_length, cfg.max_text_length,
            )

            if not new_by_channel:
                continue

            # Analyze all channels in parallel (semaphore-limited)
            tasks = [
                self._analyze_channel(sem, ch, msgs)
                for ch, msgs in new_by_channel.items()
            ]
            await asyncio.gather(*tasks, return_exceptions=True)

    async def _analyze_channel(
        self,
        sem: asyncio.Semaphore,
        channel: str,
        messages: list[dict],
    ) -> None:
        """Analyze messages for one channel and push alerts to queue."""
        async with sem:
            self._llm_calls += 1
            logger.debug("Analyzing %d messages from %s", len(messages), channel)
            try:
                alerts = await self._analyzer(
                    channel, messages, self._config.topics,
                )
            except Exception as e:
                logger.error("Analyzer error [%s]: %s", channel, e)
                return

        for alert in alerts:
            self._alert_count += 1
            await self._queue.put(alert)
