"""SSE (Server-Sent Events) client for real-time streaming."""

from __future__ import annotations

import json
from typing import Any, Callable, Generator, Optional

import httpx

from sd_api.exceptions import StreamError
from sd_api.logging import get_logger
from sd_api.models.quote import Quote, QuoteSnapshot

logger = get_logger("stream.sse")


class SSEClient:
    """Low-level SSE client that parses text/event-stream responses.

    Usage::

        sse = SSEClient("http://127.0.0.1:5000/api/stream/quote?symbol=XAUUSD")
        for raw in sse.stream():
            print(raw)
    """

    # SSE comment lines (": ping\\n") are ignored
    _COMMENT_PREFIX = (":",)

    def __init__(
        self,
        url: str,
        *,
        timeout: float = 60.0,
        headers: Optional[dict] = None,
    ) -> None:
        self.url = url
        self.timeout = timeout
        self.headers = headers or {}
        self._client: Optional[httpx.Client] = None

    @property
    def client(self) -> httpx.Client:
        if self._client is None:
            self._client = httpx.Client(timeout=self.timeout)
        return self._client

    def close(self) -> None:
        if self._client is not None:
            self._client.close()
            self._client = None

    def stream(self) -> Generator[str, None, None]:
        """Yield raw SSE data lines from the stream.

        Yields
        ------
        str
            The text of each ``data: <payload>`` line.
        """
        try:
            with self.client.stream("GET", self.url, headers=self.headers) as resp:
                if resp.status_code != 200:
                    raise StreamError(f"SSE returned status {resp.status_code}")
                for line in resp.iter_lines():
                    stripped = line.strip()
                    if not stripped or stripped.startswith(self._COMMENT_PREFIX):
                        continue
                    if stripped.startswith("data: "):
                        yield stripped[6:]
        except httpx.TimeoutException as e:
            raise StreamError(f"SSE timeout: {e}") from e
        except httpx.HTTPError as e:
            raise StreamError(f"SSE HTTP error: {e}") from e

    def events_quote(
        self,
        symbol: str,
    ) -> Generator[Quote, None, None]:
        """Yield Quote objects from /api/stream/quote?symbol=...

        Parameters
        ----------
        symbol : str
            Product symbol (e.g. "XAUUSD").
        """
        # Reconstruct URL from the base stored in self.url
        base = "/".join(self.url.split("/")[:3])
        url = f"{base}/api/stream/quote?symbol={symbol.upper().strip()}"
        for raw in SSEClient(url, timeout=self.timeout, headers=self.headers).stream():
            if not raw:
                continue
            try:
                payload = json.loads(raw)
                yield Quote.from_api(payload)
            except (json.JSONDecodeError, KeyError, TypeError) as e:
                logger.warning(f"Failed to parse SSE quote: {e} | raw={raw[:100]}")

    def events_snapshot(
        self,
        symbols: Optional[str] = None,
    ) -> Generator[QuoteSnapshot, None, None]:
        """Yield QuoteSnapshot objects from /api/stream/quotes.

        Parameters
        ----------
        symbols : str, optional
            Comma-separated symbols to filter (e.g. "XAUUSD,EURUSD").
        """
        base = "/".join(self.url.split("/")[:3])
        url = f"{base}/api/stream/quotes"
        if symbols:
            url = f"{url}?symbols={symbols.upper().strip()}"
        for raw in SSEClient(url, timeout=self.timeout, headers=self.headers).stream():
            if not raw:
                continue
            try:
                payload = json.loads(raw)
                yield QuoteSnapshot.from_api(payload)
            except (json.JSONDecodeError, KeyError, TypeError) as e:
                logger.warning(f"Failed to parse SSE snapshot: {e} | raw={raw[:100]}")

    def __enter__(self) -> "SSEClient":
        return self

    def __exit__(self, *_: Any) -> None:
        self.close()
