import asyncio
import logging
from datetime import datetime, timezone
from typing import Any

from tradedesk.execution.streamer import Streamer
from tradedesk.marketdata import (
    CandleClosedEvent,
    ChartSubscription,
    MarketData,
    MarketSubscription,
)
from tradedesk.types import Candle

log = logging.getLogger(__name__)

# Optional import
try:
    from lightstreamer.client import (  # type: ignore[import-untyped]
        LightstreamerClient,
        Subscription,
    )
except Exception:  # pragma: no cover
    LightstreamerClient = None
    Subscription = None


class Lightstreamer(Streamer):
    """
    IG Lightstreamer implementation of the provider-neutral Streamer interface.

    This class encapsulates all Lightstreamer-specific wiring and translates
    incoming updates into BaseStrategy callbacks.
    """

    def __init__(self, client: Any):
        self.client = client
        self._ls_client = None
        self.heartbeat_sleep = 10

    async def connect(self) -> None:
        # Connection is established inside run() to preserve the existing flow.
        return

    async def disconnect(self) -> None:
        if self._ls_client is not None:
            try:
                self._ls_client.disconnect()
            except Exception:
                log.exception("Lightstreamer disconnect failed")

    async def run(self, consumer: Any) -> None:
        if LightstreamerClient is None or Subscription is None:
            raise RuntimeError("Lightstreamer client library not available")

        log.info(
            "Starting Lightstreamer streaming for %s subscriptions",
            len(consumer.subscriptions),
        )

        market_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
        chart_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
        loop = asyncio.get_running_loop()

        ls_client = LightstreamerClient(self.client.ls_url, "DEFAULT")
        self._ls_client = ls_client

        ls_client.connectionDetails.setUser(
            self.client.client_id or self.client.account_id or ""
        )
        ls_client.connectionDetails.setPassword(
            f"CST-{self.client.ls_cst}|XST-{self.client.ls_xst}"
        )

        log.info(
            "LS connecting to %s with clientId %s",
            self.client.ls_url,
            self.client.client_id,
        )

        market_subs = [
            s for s in consumer.subscriptions if isinstance(s, MarketSubscription)
        ]
        chart_subs = [
            s for s in consumer.subscriptions if isinstance(s, ChartSubscription)
        ]

        subscriptions = []

        if market_subs:
            market_items = [sub.get_item_name() for sub in market_subs]
            market_sub = Subscription(
                mode="MERGE",
                items=market_items,
                fields=market_subs[0].get_fields(),
            )

            class MarketListener:
                def onItemUpdate(self, update: Any) -> None:
                    try:
                        bid_str = update.getValue("BID")
                        offer_str = update.getValue("OFFER")

                        if not bid_str or not offer_str:
                            return

                        item_name = update.getItemName()
                        epic = (
                            item_name.split(":", 1)[1]
                            if ":" in item_name
                            else item_name
                        )

                        data = {
                            "type": "market",
                            "timestamp": datetime.now(timezone.utc).isoformat(
                                timespec="seconds"
                            )
                            + "Z",
                            "instrument": epic,
                            "bid": float(bid_str),
                            "offer": float(offer_str),
                            "raw": {
                                "BID": bid_str,
                                "OFFER": offer_str,
                                "UPDATE_TIME": update.getValue("UPDATE_TIME"),
                                "MARKET_STATE": update.getValue("MARKET_STATE"),
                            },
                        }

                        loop.call_soon_threadsafe(market_queue.put_nowait, data)
                    except Exception as e:
                        log.exception("Error processing market update: %s", e)

                def onSubscriptionError(self, code: Any, message: Any) -> None:
                    log.error("Market subscription error: %s - %s", code, message)

                def onSubscription(self) -> None:
                    log.info("Market subscription active")

                def onUnsubscription(self) -> None:
                    log.info("Market unsubscribed")

            market_sub.addListener(MarketListener())
            subscriptions.append(market_sub)

        if chart_subs:
            for chart_sub in chart_subs:
                item_name = chart_sub.get_item_name()
                ls_sub = Subscription(
                    mode="MERGE",
                    items=[item_name],
                    fields=chart_sub.get_fields(),
                )

                def make_chart_listener(sub: ChartSubscription) -> Any:
                    class ChartListener:
                        def onItemUpdate(self, update: Any) -> None:
                            try:
                                cons_end = update.getValue("CONS_END")
                                if cons_end != "1":
                                    return

                                ofr_open = update.getValue("OFR_OPEN")
                                ofr_high = update.getValue("OFR_HIGH")
                                ofr_low = update.getValue("OFR_LOW")
                                ofr_close = update.getValue("OFR_CLOSE")

                                bid_open = update.getValue("BID_OPEN")
                                bid_high = update.getValue("BID_HIGH")
                                bid_low = update.getValue("BID_LOW")
                                bid_close = update.getValue("BID_CLOSE")

                                if not all([ofr_close, bid_close]):
                                    return

                                open_price = (
                                    float(ofr_open or ofr_close)
                                    + float(bid_open or bid_close)
                                ) / 2
                                high_price = (
                                    float(ofr_high or ofr_close)
                                    + float(bid_high or bid_close)
                                ) / 2
                                low_price = (
                                    float(ofr_low or ofr_close)
                                    + float(bid_low or bid_close)
                                ) / 2
                                close_price = (float(ofr_close) + float(bid_close)) / 2

                                ltv = update.getValue("LTV")
                                tick_count = update.getValue("CONS_TICK_COUNT")

                                volume = float(ltv) if ltv else 0.0
                                ticks = int(tick_count) if tick_count else 0

                                data = {
                                    "type": "chart",
                                    "instrument": sub.instrument,
                                    "timeframe": sub.period,
                                    "candle": {
                                        "timestamp": update.getValue("UTM")
                                        or datetime.now(timezone.utc).isoformat(),
                                        "open": open_price,
                                        "high": high_price,
                                        "low": low_price,
                                        "close": close_price,
                                        "volume": volume,
                                        "tick_count": ticks,
                                    },
                                }

                                loop.call_soon_threadsafe(chart_queue.put_nowait, data)
                            except Exception as e:
                                log.exception("Error processing chart update: %s", e)

                        def onSubscriptionError(self, code: Any, message: Any) -> None:
                            log.error(
                                "Chart subscription error for %s: %s - %s",
                                sub.instrument,
                                code,
                                message,
                            )

                        def onSubscription(self) -> None:
                            log.info(
                                "Chart subscription active for %s %s",
                                sub.instrument,
                                sub.period,
                            )

                        def onUnsubscription(self) -> None:
                            log.info(
                                "Chart unsubscribed for %s %s",
                                sub.instrument,
                                sub.period,
                            )

                    return ChartListener()

                ls_sub.addListener(make_chart_listener(chart_sub))
                subscriptions.append(ls_sub)

        class ConnectionListener:
            def onStatusChange(self, status: Any) -> None:
                log.info("Lightstreamer connection status: %s", status)

            def onServerError(self, code: Any, message: Any) -> None:
                log.error("Lightstreamer server error: %s - %s", code, message)

        ls_client.addListener(ConnectionListener())

        ls_client.connect()
        for sub in subscriptions:
            ls_client.subscribe(sub)

        log.info("Lightstreamer subscriptions active")

        def _period_seconds(period: str) -> int:
            p = period.strip().upper()
            if p == "SECOND":
                return 1
            if p == "HOUR":
                return 60 * 60
            if p.endswith("MINUTE"):
                n = int(p[:-6])  # strip "MINUTE"
                return n * 60
            raise ValueError(f"Unsupported period for heartbeat: {period!r}")

        # Heartbeat tuning: candle subscriptions can legitimately be silent for up to one bar.
        # If we are chart-only (no tick/market updates), raise the watchdog threshold based
        # on the smallest subscribed bar to avoid false positives.
        if chart_subs and not market_subs:
            min_bar_s = min(_period_seconds(s.period) for s in chart_subs)
            tuned = max(float(consumer.watchdog_threshold), float(min_bar_s) * 1.2)
            if tuned != consumer.watchdog_threshold:
                consumer.watchdog_threshold = tuned
                log.info(
                    "Heartbeat tuned for chart-only stream: threshold=%.1fs (min_bar=%ds)",
                    consumer.watchdog_threshold,
                    min_bar_s,
                )

        async def _heartbeat_monitor() -> None:
            while True:
                await asyncio.sleep(self.heartbeat_sleep)
                delta = (
                    datetime.now(timezone.utc) - consumer.last_update
                ).total_seconds()
                if delta > consumer.watchdog_threshold:
                    log.warning(
                        "❤  Heartbeat Alert: no updates for %s in %.1fs. Connection may be stale.",
                        consumer.__class__.__name__,
                        delta,
                    )
                elif delta < self.heartbeat_sleep:
                    log.debug("❤  OK: Last update %.1fs ago", delta)

        async def market_consumer() -> None:
            while True:
                payload = await market_queue.get()
                try:
                    event = MarketData(
                        instrument=payload["instrument"],
                        bid=payload["bid"],
                        offer=payload["offer"],
                        timestamp=payload["timestamp"],
                        raw=payload["raw"],
                    )
                    await consumer._handle_event(event)
                except Exception:
                    log.exception(
                        "Unhandled exception in market_consumer for %s",
                        payload.get("instrument"),
                    )

        async def chart_consumer() -> None:
            while True:
                payload = await chart_queue.get()
                try:
                    candle_data = payload["candle"]
                    candle = Candle(**candle_data)
                    event = CandleClosedEvent(
                        instrument=payload["instrument"],
                        timeframe=payload["timeframe"],
                        candle=candle,
                    )
                    await consumer._handle_event(event)
                except Exception:
                    log.exception(
                        "Unhandled exception in chart_consumer for epic=%s period=%s payload=%r",
                        payload.get("epic"),
                        payload.get("period"),
                        payload,
                    )

        tasks = [asyncio.create_task(_heartbeat_monitor())]

        if market_subs:
            tasks.append(asyncio.create_task(market_consumer()))
        if chart_subs:
            tasks.append(asyncio.create_task(chart_consumer()))

        try:
            await asyncio.Future()
        except asyncio.CancelledError:
            log.info(
                "%s cancelled – cleaning up Lightstreamer", consumer.__class__.__name__
            )
        finally:
            for task in tasks:
                task.cancel()
            await self.disconnect()
