from __future__ import annotations

"""MetaTrader-powered price feed (scaffold).

The actual MT5 integration lives here so we can keep the rest of the app agnostic.
Implement the ``_connect`` and ``_fetch`` routines once MetaTrader 5 is available
on the host machine.
"""

import asyncio
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Tuple

try:
    import MetaTrader5 as mt5  # type: ignore
except Exception:  # pragma: no cover - MetaTrader library not always installed
    mt5 = None

from app.streaming.live_store import live_store


@dataclass(frozen=True)
class MTKey:
    symbol: str
    timeframe: str


class MetaTraderFeed:
    def __init__(self) -> None:
        self._tasks: Dict[MTKey, asyncio.Task] = {}
        self._pinned: set[MTKey] = set()
        self._lock = asyncio.Lock()
        self._active_counts: Dict[MTKey, int] = {}

    def get_status(self) -> list[dict]:
        out: list[dict] = []
        for key, task in self._tasks.items():
            hist = live_store.get_history(key.symbol, key.timeframe)
            last_ts = hist[-1]["time"] if hist else None
            last_iso = None
            if isinstance(last_ts, (int, float)):
                last_iso = datetime.utcfromtimestamp(int(last_ts)).isoformat() + "Z"
            out.append(
                {
                    "symbol": key.symbol,
                    "timeframe": key.timeframe,
                    "subscribers": self._active_counts.get(key, 0),
                    "last_event_time": last_iso,
                    "connected": mt5 is not None and mt5.terminal_info() is not None,
                    "poller_running": not task.done(),
                }
            )
        return out

    async def subscribe(self, symbol: str, timeframe: str = "1m") -> Tuple[asyncio.Queue, Any]:
        key = MTKey(symbol.upper().replace("/", ""), timeframe)
        queue: asyncio.Queue = asyncio.Queue(maxsize=100)
        live_store.subscribe(key.symbol, key.timeframe, queue)

        async with self._lock:
            self._active_counts[key] = self._active_counts.get(key, 0) + 1
            if key not in self._tasks:
                self._tasks[key] = asyncio.create_task(self._run_mt_poller(key))

        async def _unsubscribe() -> None:
            live_store.unsubscribe(key.symbol, key.timeframe, queue)
            async with self._lock:
                self._active_counts[key] = max(0, self._active_counts.get(key, 0) - 1)
                if self._active_counts.get(key, 0) == 0 and key not in self._pinned:
                    task = self._tasks.pop(key, None)
                    if task:
                        task.cancel()
                    self._active_counts.pop(key, None)

        return queue, _unsubscribe

    async def ensure_stream(self, symbol: str, timeframe: str = "1m") -> None:
        key = MTKey(symbol.upper().replace("/", ""), timeframe)
        async with self._lock:
            self._pinned.add(key)
            self._active_counts.setdefault(key, 0)
            if key not in self._tasks:
                self._tasks[key] = asyncio.create_task(self._run_mt_poller(key))

    async def release_stream(self, symbol: str, timeframe: str = "1m") -> None:
        """Allow external callers to drop pinning once no longer needed."""
        key = MTKey(symbol.upper().replace("/", ""), timeframe)
        async with self._lock:
            self._pinned.discard(key)
            if self._active_counts.get(key, 0) == 0:
                task = self._tasks.pop(key, None)
                if task:
                    task.cancel()
                self._active_counts.pop(key, None)

    async def _run_mt_poller(self, key: MTKey) -> None:
        if mt5 is None:
            raise RuntimeError("MetaTrader5 package not installed. Install to enable MT feed.")

        if not mt5.initialize():
            raise RuntimeError(f"Unable to initialize MetaTrader5: {mt5.last_error()}")

        interval = self._resolve_timeframe(key.timeframe)
        last_ts: int | None = None
        poll_interval = 5

        while True:
            try:
                rates = mt5.copy_rates_from_pos(key.symbol, interval, 0, 1)
                if not rates:
                    await asyncio.sleep(poll_interval)
                    continue

                row = rates[0]
                tsec = int(row["time"])
                if last_ts is not None and tsec <= last_ts:
                    await asyncio.sleep(poll_interval)
                    continue

                last_ts = tsec
                evt = {
                    "symbol": key.symbol,
                    "timeframe": key.timeframe,
                    "open_time": datetime.utcfromtimestamp(tsec).isoformat(),
                    "close_time": datetime.utcfromtimestamp(tsec).isoformat(),
                    "open": float(row["open"]),
                    "high": float(row["high"]),
                    "low": float(row["low"]),
                    "close": float(row["close"]),
                    "volume": float(row.get("tick_volume", 0.0)),
                    "is_closed": True,
                    "source": "metatrader",
                }
                live_store.ingest_bar(
                    symbol=key.symbol,
                    timeframe=key.timeframe,
                    bar={
                        "time": tsec,
                        "open": evt["open"],
                        "high": evt["high"],
                        "low": evt["low"],
                        "close": evt["close"],
                        "volume": evt["volume"],
                    },
                )

                await asyncio.sleep(poll_interval)
            except asyncio.CancelledError:
                break
            except Exception:
                await asyncio.sleep(poll_interval)

    def _resolve_timeframe(self, name: str):
        if mt5 is None:
            raise RuntimeError("MetaTrader5 package not available")
        mapping = {
            "1m": mt5.TIMEFRAME_M1,
            "5m": mt5.TIMEFRAME_M5,
            "15m": mt5.TIMEFRAME_M15,
            "30m": mt5.TIMEFRAME_M30,
            "60m": mt5.TIMEFRAME_H1,
            "1h": mt5.TIMEFRAME_H1,
        }
        return mapping.get(name.lower(), mt5.TIMEFRAME_M1)


metatrader_feed = MetaTraderFeed()
