"""Bybit long/short ACCOUNT-ratio adapter — the RETAIL-POSITIONING signal, off Bybit v5 public market data.

Why this signal: Bybit publishes the fraction of ACCOUNTS that are long vs short per coin. Bybit is a
retail-heavy venue, so the account ratio is a clean read on retail positioning — the input to the contrarian
"fade-the-crowd" / momentum "informed-flow" positioning edge. History on the account-ratio endpoint goes
back to ≥ 2023-01.

Endpoint (HTTP is INJECTED so tests run with no network):
  GET /v5/market/account-ratio?category=linear&symbol=<S>&period=1d&limit=200[&endTime=]
    -> result.list[] of {symbol, buyRatio, sellRatio, timestamp(ms)}, newest-first.
  buyRatio = fraction of accounts LONG. period=1d is already daily, so there is exactly one row per UTC day
  and NO intra-day aggregation is needed (unlike funding's 8h→daily sum) — the daily value IS buyRatio.

`long_ratio_history(symbol, *, start_ms=None) -> {epoch_day: buy_ratio}` paginates BACKWARD via `endTime`
(same shape as BybitFunding.funding_history) down to a floor (default ≈ 2023-01, the endpoint's history
start), stopping on the first empty page or once the oldest row falls below the floor. epoch_day =
timestamp_ms // 86_400_000.
"""
from __future__ import annotations

import datetime as _dt
import json
import time
import urllib.request
from collections.abc import Callable
from typing import Any

from fxhnt.application.bybit_concurrency import with_rate_limit_backoff

_API = "https://api.bybit.com"
_PAGE_LIMIT = 200      # max rows/page on account-ratio

# Backstop floor for the backward pagination: ≈ 2023-01-01 UTC in ms. The account-ratio endpoint's history
# starts ~2023, so this is the natural lower bound at which the backward walk terminates.
BYBIT_RATIO_FLOOR_MS = int(_dt.datetime(2023, 1, 1, tzinfo=_dt.UTC).timestamp() * 1000)


def _http_get_json(url: str, tries: int = 4) -> Any:
    """Default JSON fetcher (urllib, retry+backoff) — mirrors the other free-data adapters' `_get`."""
    for a in range(tries):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
            with urllib.request.urlopen(req, timeout=40) as r:
                return json.loads(r.read())
        except Exception:  # noqa: BLE001 — transient HTTP; retry then re-raise
            if a == tries - 1:
                raise
            time.sleep(2 * (a + 1))
    raise RuntimeError("unreachable")


class BybitAccountRatio:
    def __init__(self, *, fetch: Callable[[str], Any] = _http_get_json,
                 sleep: Callable[[float], None] = time.sleep,
                 backoff_base: float = 0.5, backoff_tries: int = 5) -> None:
        # fetch(url) -> parsed JSON dict (injected for tests). Wrapped with rate-limit backoff so that under
        # higher ingest concurrency a Bybit 429 / rate-limit retCode (10006/10018) is retried with
        # exponential backoff instead of failing — `sleep` is the (injectable) backoff sleeper too, so tests
        # passing a no-op sleep never actually wait.
        self._fetch = with_rate_limit_backoff(
            fetch, sleep=sleep, base=backoff_base, tries=backoff_tries)
        self._sleep = sleep          # throttle between pages in production; no-op-able in tests

    def long_ratio_history(self, symbol: str, *, start_ms: int | None = None) -> dict[int, float]:
        """{epoch_day: buy_ratio} for `symbol`, where buy_ratio = fraction of accounts LONG on that UTC day
        (period=1d → one row/day, no aggregation).

        Paginates BACKWARD via `endTime`: fetch newest page, then set endTime = oldest row's ts − 1 and loop,
        stopping on the first empty page or when the oldest row falls below `start_ms`
        (default BYBIT_RATIO_FLOOR_MS ≈ 2023-01). Last-write-wins on a duplicate (coin, day) — there is at
        most one row per day, so this only guards page-boundary repeats."""
        floor = BYBIT_RATIO_FLOOR_MS if start_ms is None else int(start_ms)
        daily: dict[int, float] = {}
        seen: set[int] = set()                 # guard against duplicate ts across page boundaries
        end_ms: int | None = None              # first page: no cursor (newest)
        while True:
            url = (f"{_API}/v5/market/account-ratio?category=linear&symbol={symbol}"
                   f"&period=1d&limit={_PAGE_LIMIT}")
            if end_ms is not None:
                url += f"&endTime={end_ms}"
            data = self._fetch(url)
            rows = data.get("result", {}).get("list", [])
            if not rows:
                break
            oldest_ms = end_ms if end_ms is not None else None
            for row in rows:
                ts_ms = int(row["timestamp"])
                if oldest_ms is None or ts_ms < oldest_ms:
                    oldest_ms = ts_ms
                if ts_ms < floor or ts_ms in seen:
                    continue
                seen.add(ts_ms)
                daily[ts_ms // 86_400_000] = float(row["buyRatio"])
            if oldest_ms is None or oldest_ms <= floor:
                break                          # walked back to the floor → done
            next_end = oldest_ms - 1
            if end_ms is not None and next_end >= end_ms:
                break                          # cursor not advancing → avoid an infinite loop
            end_ms = next_end
            self._sleep(0.1)                   # gentle throttle; tests inject a no-op sleep
        return daily
