"""HTTP pusher with retry queue and exponential backoff."""

from __future__ import annotations

import json
import logging
import time
import urllib.error
import urllib.request
from collections import deque
from dataclasses import asdict, dataclass, field

from qc_trace.daemon.config import DaemonConfig
from qc_trace.schemas.unified import NormalizedMessage

logger = logging.getLogger("quickcall.pusher")


def _serialize_message(msg: NormalizedMessage) -> dict:
    """Serialize a NormalizedMessage to a JSON-compatible dict."""
    d = asdict(msg)
    # Remove None values for cleaner payloads
    return {k: v for k, v in d.items() if v is not None}


@dataclass
class Pusher:
    """Pushes NormalizedMessages to the ingest server with retry logic."""

    config: DaemonConfig
    _queue: deque[dict] = field(default_factory=deque, repr=False)
    _backoff: float = field(default=0.0, repr=False)
    _last_failure_time: float = field(default=0.0, repr=False)
    _consecutive_failures: int = field(default=0, repr=False)

    def push(self, messages: list[NormalizedMessage]) -> bool:
        """Push messages to ingest server. Returns True on success."""
        if not messages:
            return True

        # Respect backoff before attempting push
        if self._backoff > 0:
            time.sleep(self._backoff)

        serialized = [_serialize_message(m) for m in messages]
        success = self._post_batch(serialized)

        if success:
            self._on_success()
            self._drain_queue()
            return True
        else:
            self._enqueue(serialized)
            return False

    def _post_batch(self, batch: list[dict]) -> bool:
        """HTTP POST a batch to the ingest endpoint."""
        payload = json.dumps(batch).encode("utf-8")
        req = urllib.request.Request(
            self.config.ingest_url,
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                if resp.status < 300:
                    return True
                logger.warning("Ingest server returned %d", resp.status)
                return False
        except (urllib.error.URLError, OSError, TimeoutError) as e:
            self._on_failure(e)
            return False

    def _enqueue(self, batch: list[dict]) -> None:
        """Add failed batch to retry queue, dropping oldest if full."""
        dropped = 0
        for item in batch:
            if len(self._queue) >= self.config.retry_queue_max:
                self._queue.popleft()
                dropped += 1
            self._queue.append(item)
        if dropped:
            logger.warning(
                "Retry queue full — dropped %d oldest message(s)", dropped
            )

    def _drain_queue(self) -> None:
        """Try to send queued messages after a successful push."""
        while self._queue:
            batch: list[dict] = []
            while self._queue and len(batch) < self.config.batch_size:
                batch.append(self._queue.popleft())

            if batch:
                if not self._post_batch(batch):
                    # Re-enqueue at the front and stop draining
                    for item in reversed(batch):
                        self._queue.appendleft(item)
                    break

    def _on_success(self) -> None:
        """Reset backoff state on success."""
        self._backoff = 0.0
        self._consecutive_failures = 0
        self._last_failure_time = 0.0

    def _on_failure(self, error: Exception) -> None:
        """Handle a push failure with backoff tracking."""
        now = time.monotonic()
        self._consecutive_failures += 1

        if self._last_failure_time == 0.0:
            self._last_failure_time = now

        # Calculate backoff
        self._backoff = min(
            self.config.retry_backoff_base * (2 ** (self._consecutive_failures - 1)),
            self.config.retry_backoff_max,
        )

        elapsed = now - self._last_failure_time
        if elapsed > self.config.retry_timeout:
            logger.error(
                "Persistent push failure for %.0fs: %s. Continuing to collect.",
                elapsed,
                error,
            )
        else:
            logger.warning(
                "Push failed (attempt %d, backoff %.1fs): %s",
                self._consecutive_failures,
                self._backoff,
                error,
            )

    def report_progress(
        self,
        file_path: str,
        source: str,
        last_line_read: int,
        content_hash: str | None = None,
    ) -> bool:
        """POST file progress to /api/file-progress. Returns True on success."""
        url = self.config.ingest_url.replace("/ingest", "/api/file-progress")
        payload = json.dumps({
            "raw_file_path": file_path,
            "source": source,
            "last_line_read": last_line_read,
            "content_hash": content_hash,
        }).encode("utf-8")
        req = urllib.request.Request(
            url,
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                return resp.status < 300
        except (urllib.error.URLError, OSError, TimeoutError) as e:
            logger.warning("Failed to report file progress for %s: %s", file_path, e)
            return False

    @property
    def current_backoff(self) -> float:
        """Current backoff delay in seconds."""
        return self._backoff

    @property
    def queue_size(self) -> int:
        """Number of messages in retry queue."""
        return len(self._queue)

    def has_queued(self) -> bool:
        """Whether there are queued messages waiting to be sent."""
        return len(self._queue) > 0
