# app/src/market_data/acquisition/adapters/dzengi/parser.py

from __future__ import annotations

from collections.abc import Mapping, Sequence

from src.market_data.acquisition.adapters.dzengi.models import (
    DzengiExchangeInfoPayload,
    DzengiExchangeInfoResponse,
    DzengiExchangeInfoSymbol,
    DzengiInstrumentFilter,
    DzengiJsonNumber,
    DzengiJsonScalar,
    DzengiKline,
    DzengiKlinesResponse,
    DzengiLotSizeFilter,
    DzengiMinNotionalFilter,
    DzengiRateLimit,
    DzengiRawNumeric,
    DzengiUnknownFilter,
    DzengiTicker24hrResponse,
    DzengiWebSocketOhlcEvent,
    DzengiWebSocketQuoteResponse,
)
from src.market_data.acquisition.exceptions import (
    CandleParseError,
    CandleWebSocketParseError,
    InstrumentReferenceParseError,
    QuoteParseError,
)
from src.market_data.acquisition.validation.schema import (
    ValidatedCandlesDocument,
    ValidatedExchangeInfoDocument,
    ValidatedQuoteDocument,
    ValidatedWebSocketOhlcDocument,
    ValidatedWebSocketQuoteDocument,
)


def parse_exchange_info(
    document: ValidatedExchangeInfoDocument,
) -> DzengiExchangeInfoResponse:
    """
    Преобразовать структурно проверенный exchangeInfo в raw-модели Dzengi.

    Функция не выполняет schema validation, предметную валидацию,
    нормализацию символов или преобразование в Instrument.
    """

    payload = document.payload

    return DzengiExchangeInfoResponse(
        status=_optional_string(
            document.status,
            path="$.status",
        ),
        correlation_id=_optional_string(
            document.correlation_id,
            path="$.correlationId",
        ),
        payload=DzengiExchangeInfoPayload(
            timezone=_optional_string(
                payload.get("timezone"),
                path="$.payload.timezone",
            ),
            server_time=_optional_int(
                payload.get("serverTime"),
                path="$.payload.serverTime",
            ),
            rate_limits=_parse_rate_limits(
                payload.get("rateLimits"),
                path="$.payload.rateLimits",
            ),
            exchange_filters=_parse_exchange_filters(
                payload.get("exchangeFilters"),
                path="$.payload.exchangeFilters",
            ),
            symbols=_parse_symbols(
                payload["symbols"],
                path="$.payload.symbols",
            ),
        ),
    )


def _parse_symbols(
    value: object,
    *,
    path: str,
) -> tuple[DzengiExchangeInfoSymbol, ...]:
    items = _require_sequence(value, path=path)

    symbols: list[DzengiExchangeInfoSymbol] = []

    for index, item in enumerate(items):
        item_path = f"{path}[{index}]"
        mapping = _require_mapping(item, path=item_path)
        symbols.append(_parse_symbol(mapping, path=item_path))

    return tuple(symbols)


def _parse_symbol(
    item: Mapping[str, object],
    *,
    path: str,
) -> DzengiExchangeInfoSymbol:
    return DzengiExchangeInfoSymbol(
        symbol=_required_string(
            item.get("symbol"),
            path=f"{path}.symbol",
        ),
        name=_required_string(
            item.get("name"),
            path=f"{path}.name",
        ),
        status=_required_string(
            item.get("status"),
            path=f"{path}.status",
        ),
        asset_type=_optional_string(
            item.get("assetType"),
            path=f"{path}.assetType",
        ),
        base_asset=_required_string(
            item.get("baseAsset"),
            path=f"{path}.baseAsset",
        ),
        base_asset_precision=_optional_int(
            item.get("baseAssetPrecision"),
            path=f"{path}.baseAssetPrecision",
        ),
        quote_asset=_required_string(
            item.get("quoteAsset"),
            path=f"{path}.quoteAsset",
        ),
        quote_asset_id=_optional_string(
            item.get("quoteAssetId"),
            path=f"{path}.quoteAssetId",
        ),
        quote_precision=_optional_int(
            item.get("quotePrecision"),
            path=f"{path}.quotePrecision",
        ),
        order_types=_optional_string_tuple(
            item.get("orderTypes"),
            path=f"{path}.orderTypes",
        ),
        filters=_parse_instrument_filters(
            item.get("filters"),
            path=f"{path}.filters",
        ),
        market_modes=_optional_string_tuple(
            item.get("marketModes"),
            path=f"{path}.marketModes",
        ),
        market_type=_required_string(
            item.get("marketType"),
            path=f"{path}.marketType",
        ),
        country=_optional_string(
            item.get("country"),
            path=f"{path}.country",
        ),
        sector=_optional_string(
            item.get("sector"),
            path=f"{path}.sector",
        ),
        industry=_optional_string(
            item.get("industry"),
            path=f"{path}.industry",
        ),
        trading_hours=_optional_string(
            item.get("tradingHours"),
            path=f"{path}.tradingHours",
        ),
        tick_size=_optional_json_number(
            item.get("tickSize"),
            path=f"{path}.tickSize",
        ),
        tick_value=_optional_json_number(
            item.get("tickValue"),
            path=f"{path}.tickValue",
        ),
        trading_fee=_optional_json_number(
            item.get("tradingFee"),
            path=f"{path}.tradingFee",
        ),
        exchange_fee=_optional_json_number(
            item.get("exchangeFee"),
            path=f"{path}.exchangeFee",
        ),
        long_rate=_optional_json_number(
            item.get("longRate"),
            path=f"{path}.longRate",
        ),
        short_rate=_optional_json_number(
            item.get("shortRate"),
            path=f"{path}.shortRate",
        ),
        swap_charge_interval=_optional_int(
            item.get("swapChargeInterval"),
            path=f"{path}.swapChargeInterval",
        ),
        min_sl_gap=_optional_json_number(
            item.get("minSLGap"),
            path=f"{path}.minSLGap",
        ),
        max_sl_gap=_optional_json_number(
            item.get("maxSLGap"),
            path=f"{path}.maxSLGap",
        ),
        min_tp_gap=_optional_json_number(
            item.get("minTPGap"),
            path=f"{path}.minTPGap",
        ),
        max_tp_gap=_optional_json_number(
            item.get("maxTPGap"),
            path=f"{path}.maxTPGap",
        ),
    )


def _parse_rate_limits(
    value: object,
    *,
    path: str,
) -> tuple[DzengiRateLimit, ...]:
    if value is None:
        return ()

    items = _require_sequence(value, path=path)
    rate_limits: list[DzengiRateLimit] = []

    for index, item in enumerate(items):
        item_path = f"{path}[{index}]"
        mapping = _require_mapping(item, path=item_path)

        rate_limits.append(
            DzengiRateLimit(
                interval=_required_string(
                    mapping.get("interval"),
                    path=f"{item_path}.interval",
                ),
                interval_num=_required_int(
                    mapping.get("intervalNum"),
                    path=f"{item_path}.intervalNum",
                ),
                limit=_required_int(
                    mapping.get("limit"),
                    path=f"{item_path}.limit",
                ),
                rate_limit_type=_required_string(
                    mapping.get("rateLimitType"),
                    path=f"{item_path}.rateLimitType",
                ),
            )
        )

    return tuple(rate_limits)


def _parse_exchange_filters(
    value: object,
    *,
    path: str,
) -> tuple[DzengiUnknownFilter, ...]:
    if value is None:
        return ()

    items = _require_sequence(value, path=path)
    filters: list[DzengiUnknownFilter] = []

    for index, item in enumerate(items):
        item_path = f"{path}[{index}]"
        mapping = _require_mapping(item, path=item_path)

        filters.append(
            _parse_unknown_filter(
                mapping,
                path=item_path,
                filter_type_required=False,
            )
        )

    return tuple(filters)


def _parse_instrument_filters(
    value: object,
    *,
    path: str,
) -> tuple[DzengiInstrumentFilter, ...]:
    if value is None:
        return ()

    items = _require_sequence(value, path=path)
    filters: list[DzengiInstrumentFilter] = []

    for index, item in enumerate(items):
        item_path = f"{path}[{index}]"
        mapping = _require_mapping(item, path=item_path)

        filter_type = _required_string(
            mapping.get("filterType"),
            path=f"{item_path}.filterType",
        )

        if filter_type == "LOT_SIZE":
            filters.append(
                DzengiLotSizeFilter(
                    filter_type=filter_type,
                    min_qty=_optional_raw_numeric(
                        mapping.get("minQty"),
                        path=f"{item_path}.minQty",
                    ),
                    max_qty=_optional_raw_numeric(
                        mapping.get("maxQty"),
                        path=f"{item_path}.maxQty",
                    ),
                    step_size=_optional_raw_numeric(
                        mapping.get("stepSize"),
                        path=f"{item_path}.stepSize",
                    ),
                )
            )
            continue

        if filter_type == "MIN_NOTIONAL":
            filters.append(
                DzengiMinNotionalFilter(
                    filter_type=filter_type,
                    min_notional=_optional_raw_numeric(
                        mapping.get("minNotional"),
                        path=f"{item_path}.minNotional",
                    ),
                )
            )
            continue

        filters.append(
            _parse_unknown_filter(
                mapping,
                path=item_path,
                filter_type_required=True,
            )
        )

    return tuple(filters)


def _parse_unknown_filter(
    mapping: Mapping[str, object],
    *,
    path: str,
    filter_type_required: bool,
) -> DzengiUnknownFilter:
    if filter_type_required:
        filter_type = _required_string(
            mapping.get("filterType"),
            path=f"{path}.filterType",
        )
    else:
        filter_type = _optional_string(
            mapping.get("filterType"),
            path=f"{path}.filterType",
        ) or ""

    fields: list[tuple[str, DzengiJsonScalar]] = []

    for key, value in mapping.items():
        if key == "filterType":
            continue

        fields.append(
            (
                key,
                _require_json_scalar(
                    value,
                    path=f"{path}.{key}",
                ),
            )
        )

    return DzengiUnknownFilter(
        filter_type=filter_type,
        fields=tuple(fields),
    )


def _optional_string_tuple(
    value: object,
    *,
    path: str,
) -> tuple[str, ...]:
    if value is None:
        return ()

    items = _require_sequence(value, path=path)
    result: list[str] = []

    for index, item in enumerate(items):
        result.append(
            _required_string(
                item,
                path=f"{path}[{index}]",
            )
        )

    return tuple(result)


def _required_string(
    value: object,
    *,
    path: str,
) -> str:
    if not isinstance(value, str):
        raise InstrumentReferenceParseError(
            f"{path} должен быть строкой, "
            f"получен {type(value).__name__}."
        )

    return value


def _optional_string(
    value: object,
    *,
    path: str,
) -> str | None:
    if value is None:
        return None

    return _required_string(value, path=path)


def _required_int(
    value: object,
    *,
    path: str,
) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise InstrumentReferenceParseError(
            f"{path} должен быть целым числом, "
            f"получен {type(value).__name__}."
        )

    return value


def _optional_int(
    value: object,
    *,
    path: str,
) -> int | None:
    if value is None:
        return None

    return _required_int(value, path=path)


def _optional_json_number(
    value: object,
    *,
    path: str,
) -> DzengiJsonNumber | None:
    if value is None:
        return None

    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise InstrumentReferenceParseError(
            f"{path} должен быть JSON-числом, "
            f"получен {type(value).__name__}."
        )

    return value


def _optional_raw_numeric(
    value: object,
    *,
    path: str,
) -> DzengiRawNumeric | None:
    if value is None:
        return None

    if isinstance(value, bool) or not isinstance(value, (str, int, float)):
        raise InstrumentReferenceParseError(
            f"{path} должен быть строкой или JSON-числом, "
            f"получен {type(value).__name__}."
        )

    return value


def _require_json_scalar(
    value: object,
    *,
    path: str,
) -> DzengiJsonScalar:
    if value is None or isinstance(value, (str, bool)):
        return value

    if isinstance(value, (int, float)):
        return value

    raise InstrumentReferenceParseError(
        f"{path} должен быть скалярным JSON-значением, "
        f"получен {type(value).__name__}."
    )


def _require_mapping(
    value: object,
    *,
    path: str,
) -> Mapping[str, object]:
    if not isinstance(value, Mapping):
        raise InstrumentReferenceParseError(
            f"{path} должен быть отображением, "
            f"получен {type(value).__name__}."
        )

    for key in value:
        if not isinstance(key, str):
            raise InstrumentReferenceParseError(
                f"{path} содержит нестроковый ключ "
                f"типа {type(key).__name__}."
            )

    return value


def _require_sequence(
    value: object,
    *,
    path: str,
) -> Sequence[object]:
    if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
        raise InstrumentReferenceParseError(
            f"{path} должен быть последовательностью, "
            f"получен {type(value).__name__}."
        )

    return value

def parse_quote(
    document: ValidatedQuoteDocument,
) -> DzengiTicker24hrResponse:
    """
    Преобразовать структурно проверенный ticker/24hr в raw-модель Dzengi.

    Функция не выполняет schema validation, предметную валидацию
    или mapping во внутреннюю модель Quote.
    """

    payload = document.payload

    return DzengiTicker24hrResponse(
        symbol=_quote_required_string(
            payload.get("symbol"),
            path="$.payload.symbol",
        ),
        last_price=_quote_required_raw_numeric(
            payload.get("lastPrice"),
            path="$.payload.lastPrice",
        ),
        bid_price=_quote_required_raw_numeric(
            payload.get("bidPrice"),
            path="$.payload.bidPrice",
        ),
        ask_price=_quote_required_raw_numeric(
            payload.get("askPrice"),
            path="$.payload.askPrice",
        ),
        close_time=_quote_required_int(
            payload.get("closeTime"),
            path="$.payload.closeTime",
        ),
    )


def _quote_required_string(
    value: object,
    *,
    path: str,
) -> str:
    if not isinstance(value, str):
        raise QuoteParseError(
            f"{path} должен быть строкой, "
            f"получен {type(value).__name__}."
        )

    return value


def _quote_required_raw_numeric(
    value: object,
    *,
    path: str,
) -> DzengiRawNumeric:
    if isinstance(value, bool) or not isinstance(value, (str, int, float)):
        raise QuoteParseError(
            f"{path} должен быть строкой или JSON-числом, "
            f"получен {type(value).__name__}."
        )

    return value


def _quote_required_int(
    value: object,
    *,
    path: str,
) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise QuoteParseError(
            f"{path} должен быть целым числом, "
            f"получен {type(value).__name__}."
        )

    return value


def parse_dzengi_websocket_quote(
    document: ValidatedWebSocketQuoteDocument,
) -> DzengiWebSocketQuoteResponse:
    """Преобразовать проверенное WebSocket-сообщение в raw-модель Dzengi."""

    payload = document.payload
    symbol_value = (
        payload.get("symbolName")
        or payload.get("symbol")
        or document.root_symbol
    )

    symbol = _quote_required_string(
        symbol_value,
        path="$.payload.symbol",
    )

    if "bid" in payload:
        bid_price = _quote_required_raw_numeric(
            payload.get("bid"),
            path="$.payload.bid",
        )
        ask_key = "ofr" if "ofr" in payload else "ask"
        ask_price = _quote_required_raw_numeric(
            payload.get(ask_key),
            path=f"$.payload.{ask_key}",
        )
    else:
        bid_price = _websocket_depth_price(
            payload.get("bids"),
            side="bids",
        )
        ask_price = _websocket_depth_price(
            payload.get("asks"),
            side="asks",
        )

    timestamp = _websocket_optional_timestamp(
        payload.get("timestamp"),
        path="$.payload.timestamp",
    )

    return DzengiWebSocketQuoteResponse(
        symbol=symbol,
        bid_price=bid_price,
        ask_price=ask_price,
        timestamp=timestamp,
    )


def _websocket_depth_price(
    value: object,
    *,
    side: str,
) -> DzengiRawNumeric:
    if not isinstance(value, list) or not value:
        raise QuoteParseError(
            f"$.payload.{side} должен быть непустым списком."
        )

    first = value[0]

    if isinstance(first, list):
        if not first:
            raise QuoteParseError(
                f"$.payload.{side}[0] не должен быть пустым."
            )
        return _quote_required_raw_numeric(
            first[0],
            path=f"$.payload.{side}[0][0]",
        )

    if isinstance(first, Mapping):
        for key in ("price", "p", "bidPrice", "askPrice"):
            if key in first:
                return _quote_required_raw_numeric(
                    first.get(key),
                    path=f"$.payload.{side}[0].{key}",
                )

        raise QuoteParseError(
            f"$.payload.{side}[0] не содержит поле цены."
        )

    raise QuoteParseError(
        f"$.payload.{side}[0] должен быть JSON-массивом или объектом."
    )


def _websocket_optional_timestamp(
    value: object,
    *,
    path: str,
) -> int | None:
    if value is None:
        return None

    return _quote_required_int(value, path=path)


# Преобразовать структурно проверенное событие WebSocket OHLC
# в транспортную модель адаптера Dzengi.
def parse_dzengi_websocket_ohlc(
    document: ValidatedWebSocketOhlcDocument,
) -> DzengiWebSocketOhlcEvent:
    """
    Преобразовать проверенный payload события ``ohlc.event``
    в transport-модель Dzengi.

    Функция не выполняет предметную проверку значений, не проверяет
    допустимость интервала или типа свечи, не преобразует timestamp
    в datetime и не выполняет mapping во внутреннюю модель Dzentra.
    """

    payload = document.payload

    return DzengiWebSocketOhlcEvent(
        symbol=_websocket_ohlc_required_string(
            payload.get("symbol"),
            path="$.payload.symbol",
        ),
        interval=_websocket_ohlc_required_string(
            payload.get("interval"),
            path="$.payload.interval",
        ),
        candle_type=_websocket_ohlc_required_string(
            payload.get("type"),
            path="$.payload.type",
        ),
        open_time=_websocket_ohlc_required_int(
            payload.get("t"),
            path="$.payload.t",
        ),
        open_price=_websocket_ohlc_required_raw_numeric(
            payload.get("o"),
            path="$.payload.o",
        ),
        high_price=_websocket_ohlc_required_raw_numeric(
            payload.get("h"),
            path="$.payload.h",
        ),
        low_price=_websocket_ohlc_required_raw_numeric(
            payload.get("l"),
            path="$.payload.l",
        ),
        close_price=_websocket_ohlc_required_raw_numeric(
            payload.get("c"),
            path="$.payload.c",
        ),
    )


def _websocket_ohlc_required_string(
    value: object,
    *,
    path: str,
) -> str:
    if not isinstance(value, str):
        raise CandleWebSocketParseError(
            f"{path} должен быть строкой, "
            f"получен {type(value).__name__}."
        )

    return value


def _websocket_ohlc_required_int(
    value: object,
    *,
    path: str,
) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise CandleWebSocketParseError(
            f"{path} должен быть целым числом, "
            f"получен {type(value).__name__}."
        )

    return value


def _websocket_ohlc_required_raw_numeric(
    value: object,
    *,
    path: str,
) -> DzengiRawNumeric:
    if isinstance(value, bool) or not isinstance(value, (str, int, float)):
        raise CandleWebSocketParseError(
            f"{path} должен быть строкой или числом, "
            f"получен {type(value).__name__}."
        )

    return value


# Преобразовать структурно проверенный ответ klines в raw-модели Dzengi.
def parse_candles(
    document: ValidatedCandlesDocument,
) -> DzengiKlinesResponse:
    """
    Преобразовать элементы проверенного документа klines в transport-модели.

    Функция не выполняет предметную проверку числовых значений,
    OHLC-инвариантов или mapping во внутреннюю модель Candle.
    """

    items = tuple(
        _parse_candle_item(
            item,
            path=f"$.candles[{index}]",
        )
        for index, item in enumerate(document.items)
    )

    return DzengiKlinesResponse(items=items)


def _parse_candle_item(
    item: object,
    *,
    path: str,
) -> DzengiKline:
    if isinstance(item, Mapping):
        return _parse_candle_mapping(item, path=path)

    if isinstance(item, tuple):
        return _parse_candle_sequence(item, path=path)

    raise CandleParseError(
        f"{path} должен быть проверенным JSON-объектом или массивом."
    )


def _parse_candle_mapping(
    item: Mapping[str, object],
    *,
    path: str,
) -> DzengiKline:
    open_time_key = _first_present_key(
        item,
        keys=("openTime", "open_time", "time", "timestamp"),
    )

    if open_time_key is None:
        raise CandleParseError(
            f"{path} не содержит openTime, open_time, time или timestamp."
        )

    return DzengiKline(
        open_time=_candle_required_int(
            item.get(open_time_key),
            path=f"{path}.{open_time_key}",
        ),
        open_price=_candle_required_raw_numeric(
            _required_candle_field(item, key="open", path=path),
            path=f"{path}.open",
        ),
        high_price=_candle_required_raw_numeric(
            _required_candle_field(item, key="high", path=path),
            path=f"{path}.high",
        ),
        low_price=_candle_required_raw_numeric(
            _required_candle_field(item, key="low", path=path),
            path=f"{path}.low",
        ),
        close_price=_candle_required_raw_numeric(
            _required_candle_field(item, key="close", path=path),
            path=f"{path}.close",
        ),
        volume=_candle_required_raw_numeric(
            _required_candle_field(item, key="volume", path=path),
            path=f"{path}.volume",
        ),
    )


def _parse_candle_sequence(
    item: tuple[object, ...],
    *,
    path: str,
) -> DzengiKline:
    if len(item) < 6:
        raise CandleParseError(
            f"{path} должен содержать минимум 6 элементов."
        )

    return DzengiKline(
        open_time=_candle_required_int(item[0], path=f"{path}[0]"),
        open_price=_candle_required_raw_numeric(item[1], path=f"{path}[1]"),
        high_price=_candle_required_raw_numeric(item[2], path=f"{path}[2]"),
        low_price=_candle_required_raw_numeric(item[3], path=f"{path}[3]"),
        close_price=_candle_required_raw_numeric(item[4], path=f"{path}[4]"),
        volume=_candle_required_raw_numeric(item[5], path=f"{path}[5]"),
    )


def _first_present_key(
    mapping: Mapping[str, object],
    *,
    keys: tuple[str, ...],
) -> str | None:
    for key in keys:
        if key in mapping:
            return key
    return None


def _required_candle_field(
    mapping: Mapping[str, object],
    *,
    key: str,
    path: str,
) -> object:
    if key not in mapping:
        raise CandleParseError(
            f"{path}.{key} отсутствует в элементе klines."
        )
    return mapping.get(key)


def _candle_required_int(
    value: object,
    *,
    path: str,
) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise CandleParseError(
            f"{path} должен быть целым числом, "
            f"получен {type(value).__name__}."
        )
    return value


def _candle_required_raw_numeric(
    value: object,
    *,
    path: str,
) -> DzengiRawNumeric:
    if isinstance(value, bool) or not isinstance(value, (str, int, float)):
        raise CandleParseError(
            f"{path} должен быть строкой или числом, "
            f"получен {type(value).__name__}."
        )
    return value
