"""
StrategyEventBridge — receives internal strategy events from subprocesses
and forwards them to the user-facing EventPublisher.

Architecture:
    Strategy subprocess ──PUSH──> [IPC socket] ──PULL──> StrategyEventBridge
                                                            └─> EventPublisher
                                                                  ├── XPUB (informational)
                                                                  └── DEALER (critical)
"""
from __future__ import annotations

import asyncio
import logging
import time
from typing import Optional

import zmq
import zmq.asyncio

from .events import StrategyEvent, StrategyEventType, IPC_ENDPOINT

log = logging.getLogger(__name__)

# How long without a heartbeat before a strategy is considered dead (seconds)
HEARTBEAT_TIMEOUT = 60.0


class StrategyEventBridge:
    """
    Binds a ZMQ PULL socket and relays strategy events to EventPublisher.

    Also monitors heartbeats to detect crashed strategy subprocesses.
    """

    def __init__(self, event_publisher, strategy_lifecycle=None):
        """
        Args:
            event_publisher: dexorder.events.publisher.EventPublisher instance
            strategy_lifecycle: StrategyLifecycleManager (optional) for marking crashed strategies
        """
        self._publisher = event_publisher
        self._lifecycle = strategy_lifecycle
        self._ctx: Optional[zmq.asyncio.Context] = None
        self._socket: Optional[zmq.asyncio.Socket] = None
        self._task: Optional[asyncio.Task] = None
        self._heartbeat_task: Optional[asyncio.Task] = None
        self._last_heartbeat: dict[str, float] = {}  # strategy_name -> timestamp
        self._running = False

    async def start(self) -> None:
        """Bind PULL socket and start receive loop."""
        self._ctx = zmq.asyncio.Context.instance()
        self._socket = self._ctx.socket(zmq.PULL)
        self._socket.bind(IPC_ENDPOINT)
        self._running = True
        self._task = asyncio.create_task(self._receive_loop())
        self._heartbeat_task = asyncio.create_task(self._heartbeat_monitor())
        log.info("StrategyEventBridge started on %s", IPC_ENDPOINT)

    async def stop(self) -> None:
        """Stop receive loop and close socket."""
        self._running = False
        for task in [self._task, self._heartbeat_task]:
            if task:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass
        if self._socket:
            self._socket.close()
        log.info("StrategyEventBridge stopped")

    def notify_strategy_started(self, strategy_name: str) -> None:
        """Called by lifecycle manager when a strategy subprocess starts."""
        self._last_heartbeat[strategy_name] = time.time()

    def notify_strategy_stopped(self, strategy_name: str) -> None:
        """Called by lifecycle manager when a strategy is deactivated."""
        self._last_heartbeat.pop(strategy_name, None)

    async def _receive_loop(self) -> None:
        while self._running:
            try:
                raw = await asyncio.wait_for(self._socket.recv(), timeout=1.0)
                event = StrategyEvent.deserialize(raw)
                await self._handle_event(event)
            except asyncio.TimeoutError:
                continue
            except asyncio.CancelledError:
                raise
            except Exception as e:
                log.error("Error receiving strategy event: %s", e)

    async def _handle_event(self, event: StrategyEvent) -> None:
        """Translate internal StrategyEvent to UserEvent and publish."""
        from dexorder.events.types import EventType, Priority
        from dexorder.events.publisher import UserEvent, DeliverySpec

        name = event.strategy_name

        if event.event_type == StrategyEventType.HEARTBEAT:
            self._last_heartbeat[name] = time.time()
            return  # heartbeats are not forwarded to the user

        # Map to UserEvent types
        type_map = {
            StrategyEventType.STARTED:          (EventType.STRATEGY_STARTED,   Priority.INFORMATIONAL),
            StrategyEventType.STOPPED:          (EventType.STRATEGY_STOPPED,   Priority.INFORMATIONAL),
            StrategyEventType.ORDER_SUBMITTED:  (EventType.ORDER_PLACED,       Priority.NORMAL),
            StrategyEventType.ORDER_FILLED:     (EventType.ORDER_FILLED,       Priority.CRITICAL),
            StrategyEventType.POSITION_UPDATE:  (EventType.POSITION_UPDATED,   Priority.INFORMATIONAL),
            StrategyEventType.PNL_UPDATE:       (EventType.STRATEGY_LOG,       Priority.INFORMATIONAL),
            StrategyEventType.ERROR:            (EventType.STRATEGY_ERROR,     Priority.CRITICAL),
            StrategyEventType.LOG:              (EventType.STRATEGY_LOG,       Priority.INFORMATIONAL),
        }
        et, priority = type_map.get(event.event_type, (EventType.STRATEGY_LOG, Priority.INFORMATIONAL))

        payload = {"strategy_name": name, **event.payload}

        delivery = (
            DeliverySpec.critical() if priority == Priority.CRITICAL
            else DeliverySpec.informational()
        )

        try:
            from dexorder.events.types import UserEvent as UE
            await self._publisher.publish(UE(
                event_type=et,
                payload=payload,
                delivery=delivery,
            ))
        except Exception as e:
            log.error("Failed to publish strategy event %s: %s", event.event_type, e)

    async def _heartbeat_monitor(self) -> None:
        """Periodically check for strategies that stopped sending heartbeats."""
        while self._running:
            try:
                await asyncio.sleep(30)
                now = time.time()
                for name, last_seen in list(self._last_heartbeat.items()):
                    if now - last_seen > HEARTBEAT_TIMEOUT:
                        log.warning("Strategy '%s' missed heartbeat, marking as crashed", name)
                        self._last_heartbeat.pop(name, None)
                        if self._lifecycle:
                            await self._lifecycle.mark_crashed(name)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                log.error("Heartbeat monitor error: %s", e)
