# app/src/market_data/acquisition/validation/schema.py

from __future__ import annotations

from dataclasses import dataclass
from types import MappingProxyType
from typing import Mapping

from src.market_data.acquisition.exceptions import (
    CandleSchemaError,
    CandleWebSocketSchemaError,
    InstrumentReferenceSchemaError,
    QuoteSchemaError,
)


# Проверенное структурное представление ответа exchangeInfo.
@dataclass(frozen=True, slots=True)
class ValidatedExchangeInfoDocument:
    payload: Mapping[str, object]
    is_wrapped: bool
    status: object | None
    correlation_id: object | None


def validate_exchange_info_schema(
    document: object,
) -> ValidatedExchangeInfoDocument:
    """
    Проверить структуру ответа exchangeInfo без разбора предметных значений.

    Поддерживаются:

    1. Unwrapped-формат:

       {
           "symbols": [...]
       }

    2. Wrapped-формат:

       {
           "status": "OK",
           "correlationId": "2",
           "payload": {
               "symbols": [...]
           }
       }
    """

    root = _require_mapping(
        document,
        path="$",
    )

    is_wrapped = "payload" in root

    if is_wrapped:
        payload = _require_mapping(
            root.get("payload"),
            path="$.payload",
        )
    else:
        payload = root

    _validate_exchange_info_payload(payload)

    return ValidatedExchangeInfoDocument(
        payload=MappingProxyType(dict(payload)),
        is_wrapped=is_wrapped,
        status=root.get("status") if is_wrapped else None,
        correlation_id=(
            root.get("correlationId")
            if is_wrapped
            else None
        ),
    )


def _validate_exchange_info_payload(
    payload: Mapping[str, object],
) -> None:
    symbols = _require_list(
        payload.get("symbols"),
        path="$.payload.symbols",
    )

    for index, symbol in enumerate(symbols):
        symbol_path = f"$.payload.symbols[{index}]"

        symbol_mapping = _require_mapping(
            symbol,
            path=symbol_path,
        )

        _validate_optional_mapping_list(
            symbol_mapping,
            key="filters",
            path=f"{symbol_path}.filters",
        )

        _validate_optional_string_list(
            symbol_mapping,
            key="marketModes",
            path=f"{symbol_path}.marketModes",
        )

        _validate_optional_string_list(
            symbol_mapping,
            key="orderTypes",
            path=f"{symbol_path}.orderTypes",
        )

    _validate_optional_mapping_list(
        payload,
        key="rateLimits",
        path="$.payload.rateLimits",
    )

    _validate_optional_mapping_list(
        payload,
        key="exchangeFilters",
        path="$.payload.exchangeFilters",
    )


def _validate_optional_mapping_list(
    mapping: Mapping[str, object],
    *,
    key: str,
    path: str,
) -> None:
    if key not in mapping:
        return

    items = _require_list(
        mapping.get(key),
        path=path,
    )

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


def _validate_optional_string_list(
    mapping: Mapping[str, object],
    *,
    key: str,
    path: str,
) -> None:
    if key not in mapping:
        return

    items = _require_list(
        mapping.get(key),
        path=path,
    )

    for index, item in enumerate(items):
        if not isinstance(item, str):
            raise InstrumentReferenceSchemaError(
                f"{path}[{index}] должен быть строкой, "
                f"получен {type(item).__name__}."
            )


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

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

    return value


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

    return value


# Структурно проверенное представление ответа ticker/24hr.
@dataclass(frozen=True, slots=True)
class ValidatedQuoteDocument:
    payload: Mapping[str, object]
    is_wrapped: bool
    status: object | None
    correlation_id: object | None


def validate_quote_schema(
    document: object,
) -> ValidatedQuoteDocument:
    """
    Проверить структуру ответа Dzengi ticker/24hr без проверки значений.

    Поддерживаются прямой JSON-объект котировки и wrapped-формат
    с объектом котировки в поле payload.
    """

    root = _require_quote_mapping(
        document,
        path="$",
    )

    is_wrapped = "payload" in root

    if is_wrapped:
        payload = _require_quote_mapping(
            root.get("payload"),
            path="$.payload",
        )
    else:
        payload = root

    _validate_quote_payload(payload)

    return ValidatedQuoteDocument(
        payload=MappingProxyType(dict(payload)),
        is_wrapped=is_wrapped,
        status=root.get("status") if is_wrapped else None,
        correlation_id=(
            root.get("correlationId")
            if is_wrapped
            else None
        ),
    )


def _validate_quote_payload(
    payload: Mapping[str, object],
) -> None:
    _require_quote_key(payload, key="symbol", path="$.payload.symbol")
    _require_quote_key(payload, key="lastPrice", path="$.payload.lastPrice")
    _require_quote_key(payload, key="bidPrice", path="$.payload.bidPrice")
    _require_quote_key(payload, key="askPrice", path="$.payload.askPrice")
    _require_quote_key(payload, key="closeTime", path="$.payload.closeTime")


def _require_quote_key(
    mapping: Mapping[str, object],
    *,
    key: str,
    path: str,
) -> None:
    if key not in mapping:
        raise QuoteSchemaError(
            f"{path} отсутствует в документе ticker/24hr."
        )


def _require_quote_mapping(
    value: object,
    *,
    path: str,
) -> Mapping[str, object]:
    if not isinstance(value, dict):
        raise QuoteSchemaError(
            f"{path} должен быть JSON-объектом, "
            f"получен {type(value).__name__}."
        )

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

    return value


# Структурно проверенное представление сообщения котировки Dzengi WebSocket.
@dataclass(frozen=True, slots=True)
class ValidatedWebSocketQuoteDocument:
    payload: Mapping[str, object]
    root_symbol: object | None


def validate_dzengi_websocket_quote_schema(
    document: object,
) -> ValidatedWebSocketQuoteDocument:
    """
    Проверить структуру одного декодированного сообщения Dzengi WebSocket.

    Поддерживаются сообщения без оболочки и до двух известных оболочек
    ``payload`` / ``Payload``. Проверка не преобразует цены и не выполняет
    предметную валидацию.
    """

    root = _require_quote_mapping(document, path="$")
    root_symbol = root.get("symbol")
    payload = _unwrap_websocket_quote_payload(root)

    _validate_websocket_quote_payload(
        payload,
        root_symbol=root_symbol,
    )

    return ValidatedWebSocketQuoteDocument(
        payload=MappingProxyType(dict(payload)),
        root_symbol=root_symbol,
    )


def _unwrap_websocket_quote_payload(
    root: Mapping[str, object],
) -> Mapping[str, object]:
    payload = root

    for level in range(2):
        nested: object | None = None
        nested_path = "$.payload" if level == 0 else "$.payload.payload"

        for key in ("payload", "Payload"):
            candidate = payload.get(key)
            if candidate is not None:
                nested = candidate
                break

        if nested is None:
            break

        payload = _require_quote_mapping(
            nested,
            path=nested_path,
        )

    return payload


def _validate_websocket_quote_payload(
    payload: Mapping[str, object],
    *,
    root_symbol: object | None,
) -> None:
    if (
        "symbolName" not in payload
        and "symbol" not in payload
        and root_symbol is None
    ):
        raise QuoteSchemaError(
            "$.payload не содержит symbolName или symbol."
        )

    has_direct_bid = "bid" in payload
    has_direct_ask = "ask" in payload or "ofr" in payload
    has_depth_bid = "bids" in payload
    has_depth_ask = "asks" in payload

    if has_direct_bid or has_direct_ask:
        if not has_direct_bid or not has_direct_ask:
            raise QuoteSchemaError(
                "WebSocket quote должна содержать полный набор bid и ask/ofr."
            )
        return

    if has_depth_bid or has_depth_ask:
        if not has_depth_bid or not has_depth_ask:
            raise QuoteSchemaError(
                "WebSocket depth quote должна содержать bids и asks."
            )

        bids = payload.get("bids")
        asks = payload.get("asks")

        if not isinstance(bids, list) or not bids:
            raise QuoteSchemaError(
                "$.payload.bids должен быть непустым JSON-массивом."
            )

        if not isinstance(asks, list) or not asks:
            raise QuoteSchemaError(
                "$.payload.asks должен быть непустым JSON-массивом."
            )
        return

    raise QuoteSchemaError(
        "WebSocket quote не содержит bid/ask либо bids/asks."
    )


# Структурно проверенное представление ответа Dzengi /api/v1/klines.
@dataclass(frozen=True, slots=True)
class ValidatedCandlesDocument:
    items: tuple[object, ...]


def validate_candles_schema(
    document: object,
) -> ValidatedCandlesDocument:
    """
    Проверить структуру ответа Dzengi klines без проверки значений.

    Поддерживаются:

    1. Корневой JSON-массив:

       [
           [...],
           {...}
       ]

    2. Корневой JSON-объект с массивом в одном из полей:

       {
           "klines": [...]
       }

       {
           "candles": [...]
       }

       {
           "data": [...]
       }

       {
           "result": [...]
       }

    3. Wrapped-формат:

       {
           "payload": [...]
       }

       {
           "payload": {
               "klines": [...]
           }
       }

       Внутри payload также поддерживаются поля candles и data.
    """

    raw_items = _extract_candle_items(document)

    validated_items: list[object] = []

    for index, item in enumerate(raw_items):
        path = f"$.candles[{index}]"
        validated_items.append(
            _validate_and_freeze_candle_item(
                item,
                path=path,
            )
        )

    return ValidatedCandlesDocument(
        items=tuple(validated_items),
    )


def _extract_candle_items(
    document: object,
) -> list[object]:
    if isinstance(document, list):
        return document

    root = _require_candle_mapping(
        document,
        path="$",
    )

    direct_items = _find_candle_list(
        root,
        keys=("klines", "candles", "data", "result"),
        path="$",
    )

    if direct_items is not None:
        return direct_items

    if "payload" not in root:
        raise CandleSchemaError(
            "$ не содержит поддерживаемый массив klines."
        )

    payload = root.get("payload")

    if isinstance(payload, list):
        return payload

    payload_mapping = _require_candle_mapping(
        payload,
        path="$.payload",
    )

    wrapped_items = _find_candle_list(
        payload_mapping,
        keys=("klines", "candles", "data"),
        path="$.payload",
    )

    if wrapped_items is not None:
        return wrapped_items

    raise CandleSchemaError(
        "$.payload не содержит поддерживаемый массив klines."
    )


def _find_candle_list(
    mapping: Mapping[str, object],
    *,
    keys: tuple[str, ...],
    path: str,
) -> list[object] | None:
    for key in keys:
        if key not in mapping:
            continue

        value = mapping.get(key)

        if not isinstance(value, list):
            raise CandleSchemaError(
                f"{path}.{key} должен быть JSON-массивом, "
                f"получен {type(value).__name__}."
            )

        return value

    return None


def _validate_and_freeze_candle_item(
    item: object,
    *,
    path: str,
) -> object:
    if isinstance(item, dict):
        mapping = _require_candle_mapping(
            item,
            path=path,
        )
        return MappingProxyType(dict(mapping))

    if isinstance(item, list):
        if len(item) < 6:
            raise CandleSchemaError(
                f"{path} должен содержать минимум 6 элементов, "
                f"получено {len(item)}."
            )

        return tuple(item)

    raise CandleSchemaError(
        f"{path} должен быть JSON-объектом или JSON-массивом, "
        f"получен {type(item).__name__}."
    )


def _require_candle_mapping(
    value: object,
    *,
    path: str,
) -> Mapping[str, object]:
    if not isinstance(value, dict):
        raise CandleSchemaError(
            f"{path} должен быть JSON-объектом, "
            f"получен {type(value).__name__}."
        )

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

    return value


# Структурно проверенное представление события Dzengi WebSocket ohlc.event.
@dataclass(frozen=True, slots=True)
class ValidatedWebSocketOhlcDocument:
    payload: Mapping[str, object]
    status: object
    destination: object
    correlation_id: object | None


def validate_dzengi_websocket_ohlc_schema(
    document: object,
) -> ValidatedWebSocketOhlcDocument:
    """
    Проверить структуру события Dzengi WebSocket OHLC без проверки значений.

    Ожидаемый runtime-контракт:

    {
        "status": "OK",
        "destination": "ohlc.event",
        "payload": {
            "symbol": "BTC/USD_LEVERAGE",
            "interval": "1m",
            "type": "classic",
            "t": 1784224740000,
            "o": 63992.0,
            "h": 64032.55,
            "l": 63984.0,
            "c": 64032.55
        }
    }

    Функция проверяет только структуру сообщения:

    - корневой JSON-объект;
    - наличие status;
    - наличие destination;
    - наличие payload;
    - обязательный набор полей OHLC;
    - строковые ключи объектов.

    Функция не проверяет предметные значения, не преобразует timestamp,
    не преобразует цены и не создаёт transport-модель адаптера.
    """

    root = _require_websocket_ohlc_mapping(
        document,
        path="$",
    )

    if "status" not in root:
        raise CandleWebSocketSchemaError(
            "$.status отсутствует в событии WebSocket OHLC."
        )

    if "destination" not in root:
        raise CandleWebSocketSchemaError(
            "$.destination отсутствует в событии WebSocket OHLC."
        )

    if "payload" not in root:
        raise CandleWebSocketSchemaError(
            "$.payload отсутствует в событии WebSocket OHLC."
        )

    payload = _require_websocket_ohlc_mapping(
        root.get("payload"),
        path="$.payload",
    )

    _validate_websocket_ohlc_payload(payload)

    return ValidatedWebSocketOhlcDocument(
        payload=MappingProxyType(dict(payload)),
        status=root.get("status"),
        destination=root.get("destination"),
        correlation_id=root.get("correlationId"),
    )


def _validate_websocket_ohlc_payload(
    payload: Mapping[str, object],
) -> None:
    required_fields = (
        "symbol",
        "interval",
        "type",
        "t",
        "o",
        "h",
        "l",
        "c",
    )

    missing_fields = tuple(
        field_name
        for field_name in required_fields
        if field_name not in payload
    )

    if missing_fields:
        formatted_fields = ", ".join(missing_fields)

        raise CandleWebSocketSchemaError(
            "$.payload не содержит обязательные поля WebSocket OHLC: "
            f"{formatted_fields}."
        )


def _require_websocket_ohlc_mapping(
    value: object,
    *,
    path: str,
) -> Mapping[str, object]:
    if not isinstance(value, dict):
        raise CandleWebSocketSchemaError(
            f"{path} должен быть JSON-объектом, "
            f"получен {type(value).__name__}."
        )

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

    return value