"""Async streaming ingestion pipeline with backpressure."""

from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass, field
from typing import Any

from neurovault.pipeline.hooks import HookManager

logger = logging.getLogger(__name__)


@dataclass
class IngestionItem:
    """A single item flowing through the pipeline."""

    content: str
    source: str
    metadata: dict[str, Any] = field(default_factory=dict)
    importance: float = 0.5
    timestamp: float = field(default_factory=time.time)
    embedding: list[float] | None = None


class StreamingPipeline:
    """Async ingestion pipeline with backpressure and batching."""

    MAX_QUEUE_SIZE = 1000
    BATCH_SIZE = 16
    BATCH_TIMEOUT = 2.0

    def __init__(self, content_filter, embedding_engine, deduplicator, storage, bridge, hooks=None):
        self._filter = content_filter
        self._embedding = embedding_engine
        self._dedup = deduplicator
        self._storage = storage
        self._bridge = bridge
        self._hooks = hooks or HookManager()
        self._queue: asyncio.Queue[IngestionItem] = asyncio.Queue(maxsize=self.MAX_QUEUE_SIZE)
        self._loop: asyncio.AbstractEventLoop | None = None
        self._running = False
        self._process_task: asyncio.Task | None = None
        self._stats = {"ingested": 0, "filtered": 0, "deduplicated": 0, "errors": 0}

    async def start(self) -> None:
        """Start the pipeline processing loop."""
        if self._running:
            return
        self._loop = asyncio.get_running_loop()
        self._running = True
        self._process_task = asyncio.create_task(self._process_loop())

    async def stop(self) -> None:
        """Stop pipeline processing loop."""
        self._running = False
        if self._process_task is not None:
            await asyncio.wait([self._process_task], timeout=1.0)
            self._process_task = None
        self._loop = None

    async def submit(self, content: str, source: str, metadata: dict | None = None, importance: float = 0.5) -> None:
        """Submit an item to the pipeline."""
        item = IngestionItem(content=content, source=source, metadata=metadata or {}, importance=importance)
        try:
            await asyncio.wait_for(self._queue.put(item), timeout=5.0)
        except asyncio.TimeoutError:
            logger.warning("Pipeline backpressure: queue full, dropping item from %s", source)
            self._stats["filtered"] += 1

    def submit_sync(self, content: str, source: str, metadata: dict | None = None, importance: float = 0.5) -> None:
        """Synchronous submit for use from non-async sensor threads."""
        item = IngestionItem(content=content, source=source, metadata=metadata or {}, importance=importance)
        if self._loop is not None and self._loop.is_running():

            def _enqueue() -> None:
                try:
                    self._queue.put_nowait(item)
                except asyncio.QueueFull:
                    self._stats["filtered"] += 1

            self._loop.call_soon_threadsafe(_enqueue)
            return

        try:
            self._queue.put_nowait(item)
        except asyncio.QueueFull:
            self._stats["filtered"] += 1

    async def _process_loop(self) -> None:
        while self._running:
            batch = await self._collect_batch()
            if batch:
                await self._process_batch(batch)

    async def _collect_batch(self) -> list[IngestionItem]:
        batch: list[IngestionItem] = []
        deadline = time.time() + self.BATCH_TIMEOUT

        while len(batch) < self.BATCH_SIZE:
            remaining = max(0.01, deadline - time.time())
            try:
                item = await asyncio.wait_for(self._queue.get(), timeout=remaining)
                batch.append(item)
            except asyncio.TimeoutError:
                break

        return batch

    async def _process_batch(self, batch: list[IngestionItem]) -> None:
        filtered: list[IngestionItem] = []
        for item in batch:
            await self._hooks.run("pre_ingest", item)
            result, _reason = self._filter.filter(item.content, item.source)
            if result is None:
                self._stats["filtered"] += 1
                continue
            item.content = result
            filtered.append(item)

        if not filtered:
            return

        texts = [item.content for item in filtered]
        try:
            embeddings = self._embedding.embed_batch(texts)
            for item, emb in zip(filtered, embeddings):
                item.embedding = emb
        except Exception as exc:
            logger.warning("Batch embedding failed: %s", exc)
            self._stats["errors"] += len(filtered)
            return

        stored: list[IngestionItem] = []
        for item in filtered:
            if item.embedding:
                dup = self._dedup.check_duplicate(item.content, item.embedding)
                if dup and dup.get("action") == "skip":
                    self._stats["deduplicated"] += 1
                    continue
            stored.append(item)

        for item in stored:
            try:
                memory = self._bridge.store_memory(
                    content=item.content,
                    source=item.source,
                    importance=item.importance,
                    metadata=item.metadata,
                )
                self._stats["ingested"] += 1
                await self._hooks.run("post_ingest", item, memory)
            except Exception as exc:
                logger.warning("Store failed: %s", exc)
                self._stats["errors"] += 1

    @property
    def stats(self) -> dict[str, int]:
        """Runtime ingestion pipeline stats."""
        return dict(self._stats)
