"""Market Data API client for Python (standard library only; `pip install websockets` for streaming).

    from marketapi import MarketAPI
    api = MarketAPI("mdp_live_...")
    print(api.latest(["EURUSD", "XAUUSD"]))
    for bar in api.bars("EURUSD", "H1", "2026-09-01", "2026-09-08"):   # pages automatically
        print(bar["time"], bar["close"])
"""
from __future__ import annotations

import datetime as _dt
import json
import time
import urllib.error
import urllib.parse
import urllib.request

__version__ = "1.0.0"
DEFAULT_BASE = "https://api.marketapi.app/v1"


class MarketAPIError(Exception):
    def __init__(self, status: int, code: str, detail: str, request_id: str = ""):
        super().__init__(f"{status} {code}: {detail}")
        self.status, self.code, self.detail, self.request_id = status, code, detail, request_id


def _iso(t) -> str:
    if isinstance(t, _dt.datetime):
        t = t if t.tzinfo else t.replace(tzinfo=_dt.timezone.utc)
        return t.astimezone(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    if isinstance(t, _dt.date):
        return f"{t.isoformat()}T00:00:00Z"
    s = str(t)
    return f"{s}T00:00:00Z" if len(s) == 10 else s


class MarketAPI:
    def __init__(self, api_key: str | None = None, base_url: str = DEFAULT_BASE, timeout: float = 30, retries: int = 3):
        self.api_key, self.base_url, self.timeout, self.retries = api_key, base_url.rstrip("/"), timeout, retries

    # ---- low level
    def get(self, path: str, params: dict | None = None, auth: bool = True) -> dict:
        q = {k: (",".join(v) if isinstance(v, (list, tuple)) else ("true" if v is True else v))
             for k, v in (params or {}).items() if v is not None and v is not False}
        url = f"{self.base_url}{path}" + (f"?{urllib.parse.urlencode(q)}" if q else "")
        headers = {"Accept": "application/json", "User-Agent": f"marketapi-python/{__version__}"}
        if auth:
            if not self.api_key:
                raise MarketAPIError(401, "unauthenticated", "an API key is required for this endpoint")
            headers["Authorization"] = f"Bearer {self.api_key}"
        for attempt in range(self.retries + 1):
            try:
                with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=self.timeout) as r:
                    return json.loads(r.read() or b"{}")
            except urllib.error.HTTPError as e:
                try:
                    body = json.loads(e.read() or b"{}")
                except ValueError:
                    body = {}
                if (e.code == 429 or e.code >= 500) and attempt < self.retries:
                    time.sleep(float(e.headers.get("Retry-After") or 2 ** attempt))
                    continue
                raise MarketAPIError(e.code, body.get("code", "http_error"), body.get("detail", str(e)),
                                     body.get("request_id", "")) from None
            except urllib.error.URLError as e:
                if attempt < self.retries:
                    time.sleep(2 ** attempt)
                    continue
                raise MarketAPIError(0, "network_error", str(e.reason)) from None
        raise MarketAPIError(0, "network_error", "no response")

    def _pages(self, path: str, params: dict):
        params = dict(params)
        while True:
            page = self.get(path, params)
            yield from page.get("data", [])
            if not page.get("next_cursor"):
                return
            params["cursor"] = page["next_cursor"]

    # ---- endpoints
    def latest(self, symbols, require_live: bool = False) -> list[dict]:
        return self.get("/prices/latest", {"symbols": list(symbols), "require_live": require_live})["data"]

    def bars(self, symbol: str, timeframe: str, start, end, limit: int = 10000):
        return self._pages("/bars", {"symbol": symbol, "timeframe": timeframe, "from": _iso(start), "to": _iso(end),
                                     "limit": limit})

    def ticks(self, symbol: str, start, end, limit: int = 10000):
        return self._pages("/ticks", {"symbol": symbol, "from": _iso(start), "to": _iso(end), "limit": limit})

    def symbols(self, q: str | None = None, asset_class: str | None = None):
        return self._pages("/symbols", {"q": q, "asset_class": asset_class})

    def symbol(self, name: str) -> dict:
        return self.get(f"/symbols/{urllib.parse.quote(name)}")

    def market_status(self, symbols) -> list[dict]:
        return self.get("/market-status", {"symbols": list(symbols)})["data"]

    def account(self) -> dict:
        return self.get("/account")

    def free_latest(self, symbols) -> list[dict]:
        return self.get("/free/prices/latest", {"symbols": list(symbols)}, auth=False)["data"]

    async def stream(self, symbols):
        """Async generator of quote messages: async for q in api.stream(["EURUSD"]): ..."""
        import websockets  # pip install websockets
        url = self.base_url.replace("https://", "wss://").replace("http://", "ws://") + "/stream"
        hdr = {"Authorization": f"Bearer {self.api_key}"}
        try:
            new_api = int(websockets.__version__.split(".")[0]) >= 14
        except (AttributeError, ValueError):
            new_api = True
        # websockets 14+ calls it additional_headers; older versions extra_headers
        opts = {"additional_headers": hdr} if new_api else {"extra_headers": hdr}
        async with websockets.connect(url, **opts) as ws:
            await ws.send(json.dumps({"action": "subscribe", "symbols": list(symbols)}))
            async for raw in ws:
                msg = json.loads(raw)
                if msg.get("type") == "quote":
                    yield msg
                elif msg.get("type") == "error":
                    raise MarketAPIError(400, msg.get("code", "stream_error"), msg.get("detail", ""))
