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

from __future__ import annotations

from decimal import Decimal, InvalidOperation

from src.market_data.acquisition.adapters.dzengi.models import (
    DzengiExchangeInfoResponse,
    DzengiExchangeInfoSymbol,
    DzengiInstrumentFilter,
    DzengiLotSizeFilter,
    DzengiMinNotionalFilter,
    DzengiRateLimit,
    DzengiRawNumeric,
    DzengiUnknownFilter,
    DzengiKlinesResponse,
    DzengiTicker24hrResponse,
    DzengiWebSocketQuoteResponse,
)
from src.market_data.acquisition.exceptions import (
    CandleValueError,
    InstrumentReferenceValueError,
    QuoteValueError,
)


def validate_exchange_info_values(
    response: DzengiExchangeInfoResponse,
) -> None:
    """
    Проверить допустимость значений в raw-моделях Dzengi exchangeInfo.

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

    _validate_optional_non_empty_string(
        response.status,
        path="$.status",
    )
    _validate_optional_non_empty_string(
        response.correlation_id,
        path="$.correlationId",
    )

    payload = response.payload

    _validate_optional_non_empty_string(
        payload.timezone,
        path="$.payload.timezone",
    )

    for index, rate_limit in enumerate(payload.rate_limits):
        _validate_rate_limit(
            rate_limit,
            path=f"$.payload.rateLimits[{index}]",
        )

    for index, exchange_filter in enumerate(payload.exchange_filters):
        _validate_unknown_filter(
            exchange_filter,
            path=f"$.payload.exchangeFilters[{index}]",
            allow_empty_filter_type=True,
        )

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


def _validate_symbol(
    symbol: DzengiExchangeInfoSymbol,
    *,
    path: str,
) -> None:
    _validate_required_non_empty_string(
        symbol.symbol,
        path=f"{path}.symbol",
    )
    _validate_required_non_empty_string(
        symbol.name,
        path=f"{path}.name",
    )
    _validate_required_non_empty_string(
        symbol.status,
        path=f"{path}.status",
    )
    _validate_required_non_empty_string(
        symbol.base_asset,
        path=f"{path}.baseAsset",
    )
    _validate_required_non_empty_string(
        symbol.quote_asset,
        path=f"{path}.quoteAsset",
    )
    _validate_required_non_empty_string(
        symbol.market_type,
        path=f"{path}.marketType",
    )

    _validate_optional_non_empty_string(
        symbol.asset_type,
        path=f"{path}.assetType",
    )
    _validate_optional_non_empty_string(
        symbol.quote_asset_id,
        path=f"{path}.quoteAssetId",
    )
    _validate_optional_non_empty_string(
        symbol.trading_hours,
        path=f"{path}.tradingHours",
    )

    # Dzengi может возвращать пустые строки для country, sector и industry.
    # Эти значения сохраняются как часть raw-контракта и не считаются ошибкой.

    _validate_non_empty_string_tuple(
        symbol.order_types,
        path=f"{path}.orderTypes",
    )
    _validate_non_empty_string_tuple(
        symbol.market_modes,
        path=f"{path}.marketModes",
    )

    _validate_optional_non_negative_int(
        symbol.base_asset_precision,
        path=f"{path}.baseAssetPrecision",
    )
    _validate_optional_non_negative_int(
        symbol.quote_precision,
        path=f"{path}.quotePrecision",
    )
    _validate_optional_non_negative_int(
        symbol.swap_charge_interval,
        path=f"{path}.swapChargeInterval",
    )

    _validate_optional_positive_number(
        symbol.tick_size,
        path=f"{path}.tickSize",
    )

    _validate_optional_finite_number(
        symbol.tick_value,
        path=f"{path}.tickValue",
    )
    _validate_optional_finite_number(
        symbol.trading_fee,
        path=f"{path}.tradingFee",
    )
    _validate_optional_finite_number(
        symbol.exchange_fee,
        path=f"{path}.exchangeFee",
    )
    _validate_optional_finite_number(
        symbol.long_rate,
        path=f"{path}.longRate",
    )
    _validate_optional_finite_number(
        symbol.short_rate,
        path=f"{path}.shortRate",
    )
    _validate_optional_finite_number(
        symbol.min_sl_gap,
        path=f"{path}.minSLGap",
    )
    _validate_optional_finite_number(
        symbol.max_sl_gap,
        path=f"{path}.maxSLGap",
    )
    _validate_optional_finite_number(
        symbol.min_tp_gap,
        path=f"{path}.minTPGap",
    )
    _validate_optional_finite_number(
        symbol.max_tp_gap,
        path=f"{path}.maxTPGap",
    )

    for index, instrument_filter in enumerate(symbol.filters):
        _validate_instrument_filter(
            instrument_filter,
            path=f"{path}.filters[{index}]",
        )


def _validate_rate_limit(
    rate_limit: DzengiRateLimit,
    *,
    path: str,
) -> None:
    _validate_required_non_empty_string(
        rate_limit.interval,
        path=f"{path}.interval",
    )
    _validate_required_non_empty_string(
        rate_limit.rate_limit_type,
        path=f"{path}.rateLimitType",
    )
    _validate_positive_int(
        rate_limit.interval_num,
        path=f"{path}.intervalNum",
    )
    _validate_positive_int(
        rate_limit.limit,
        path=f"{path}.limit",
    )


def _validate_instrument_filter(
    instrument_filter: DzengiInstrumentFilter,
    *,
    path: str,
) -> None:
    _validate_required_non_empty_string(
        instrument_filter.filter_type,
        path=f"{path}.filterType",
    )

    if isinstance(instrument_filter, DzengiLotSizeFilter):
        _validate_lot_size_filter(
            instrument_filter,
            path=path,
        )
        return

    if isinstance(instrument_filter, DzengiMinNotionalFilter):
        _validate_min_notional_filter(
            instrument_filter,
            path=path,
        )
        return

    if isinstance(instrument_filter, DzengiUnknownFilter):
        _validate_unknown_filter(
            instrument_filter,
            path=path,
            allow_empty_filter_type=False,
        )


def _validate_lot_size_filter(
    lot_size: DzengiLotSizeFilter,
    *,
    path: str,
) -> None:
    min_qty = _validate_optional_positive_raw_numeric(
        lot_size.min_qty,
        path=f"{path}.minQty",
    )
    max_qty = _validate_optional_positive_raw_numeric(
        lot_size.max_qty,
        path=f"{path}.maxQty",
    )
    _validate_optional_positive_raw_numeric(
        lot_size.step_size,
        path=f"{path}.stepSize",
    )

    if (
        min_qty is not None
        and max_qty is not None
        and min_qty > max_qty
    ):
        raise InstrumentReferenceValueError(
            f"{path}.minQty не должно превышать {path}.maxQty."
        )


def _validate_min_notional_filter(
    min_notional: DzengiMinNotionalFilter,
    *,
    path: str,
) -> None:
    _validate_optional_non_negative_raw_numeric(
        min_notional.min_notional,
        path=f"{path}.minNotional",
    )


def _validate_unknown_filter(
    unknown_filter: DzengiUnknownFilter,
    *,
    path: str,
    allow_empty_filter_type: bool,
) -> None:
    if allow_empty_filter_type:
        if unknown_filter.filter_type and not unknown_filter.filter_type.strip():
            raise InstrumentReferenceValueError(
                f"{path}.filterType не должен состоять только из пробелов."
            )
        return

    _validate_required_non_empty_string(
        unknown_filter.filter_type,
        path=f"{path}.filterType",
    )


def _validate_required_non_empty_string(
    value: str,
    *,
    path: str,
) -> None:
    if not value.strip():
        raise InstrumentReferenceValueError(
            f"{path} не должен быть пустым."
        )


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

    if not value.strip():
        raise InstrumentReferenceValueError(
            f"{path} не должен быть пустым."
        )


def _validate_non_empty_string_tuple(
    values: tuple[str, ...],
    *,
    path: str,
) -> None:
    for index, value in enumerate(values):
        if not value.strip():
            raise InstrumentReferenceValueError(
                f"{path}[{index}] не должен быть пустым."
            )


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

    if value < 0:
        raise InstrumentReferenceValueError(
            f"{path} должно быть больше или равно нулю."
        )


def _validate_positive_int(
    value: int,
    *,
    path: str,
) -> None:
    if value <= 0:
        raise InstrumentReferenceValueError(
            f"{path} должно быть больше нуля."
        )


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

    decimal_value = _to_finite_decimal(
        value,
        path=path,
    )

    if decimal_value <= 0:
        raise InstrumentReferenceValueError(
            f"{path} должно быть больше нуля."
        )


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

    _to_finite_decimal(
        value,
        path=path,
    )


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

    decimal_value = _to_finite_decimal(
        value,
        path=path,
    )

    if decimal_value <= 0:
        raise InstrumentReferenceValueError(
            f"{path} должно быть больше нуля."
        )

    return decimal_value


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

    decimal_value = _to_finite_decimal(
        value,
        path=path,
    )

    if decimal_value < 0:
        raise InstrumentReferenceValueError(
            f"{path} должно быть больше или равно нулю."
        )

    return decimal_value


def _to_finite_decimal(
    value: str | int | float,
    *,
    path: str,
) -> Decimal:
    try:
        decimal_value = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise InstrumentReferenceValueError(
            f"{path} должно быть корректным числом."
        ) from exc

    if not decimal_value.is_finite():
        raise InstrumentReferenceValueError(
            f"{path} должно быть конечным числом."
        )

    return decimal_value


def validate_quote_values(
    response: DzengiTicker24hrResponse,
) -> None:
    """
    Проверить допустимость значений raw-модели Dzengi ticker/24hr.

    Функция не изменяет модель и не выполняет mapping в Quote.
    """

    if not response.symbol.strip():
        raise QuoteValueError(
            "$.payload.symbol не должен быть пустым."
        )

    last_price = _quote_positive_decimal(
        response.last_price,
        path="$.payload.lastPrice",
    )
    bid_price = _quote_positive_decimal(
        response.bid_price,
        path="$.payload.bidPrice",
    )
    ask_price = _quote_positive_decimal(
        response.ask_price,
        path="$.payload.askPrice",
    )

    if response.close_time <= 0:
        raise QuoteValueError(
            "$.payload.closeTime должно быть больше нуля."
        )

    if bid_price > ask_price:
        raise QuoteValueError(
            "$.payload.bidPrice не должно превышать $.payload.askPrice."
        )

    # Явное чтение сохраняет проверку обязательности lastPrice
    # как самостоятельного положительного рыночного значения.
    del last_price


def _quote_positive_decimal(
    value: DzengiRawNumeric,
    *,
    path: str,
) -> Decimal:
    try:
        decimal_value = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise QuoteValueError(
            f"{path} должно быть корректным числом."
        ) from exc

    if not decimal_value.is_finite():
        raise QuoteValueError(
            f"{path} должно быть конечным числом."
        )

    if decimal_value <= 0:
        raise QuoteValueError(
            f"{path} должно быть больше нуля."
        )

    return decimal_value


def validate_dzengi_websocket_quote_values(
    response: DzengiWebSocketQuoteResponse,
) -> None:
    """Проверить значения raw-модели котировки Dzengi WebSocket."""

    if not response.symbol.strip():
        raise QuoteValueError(
            "$.payload.symbol не должен быть пустым."
        )

    bid_price = _quote_positive_decimal(
        response.bid_price,
        path="$.payload.bidPrice",
    )
    ask_price = _quote_positive_decimal(
        response.ask_price,
        path="$.payload.askPrice",
    )

    if bid_price > ask_price:
        raise QuoteValueError(
            "$.payload.bidPrice не должно превышать $.payload.askPrice."
        )

    if response.timestamp is not None and response.timestamp <= 0:
        raise QuoteValueError(
            "$.payload.timestamp должно быть больше нуля."
        )

def validate_candles_values(
    response: DzengiKlinesResponse,
) -> None:
    """
    Проверить допустимость значений raw-моделей Dzengi klines.

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

    for index, candle in enumerate(response.items):
        path = f"$.candles[{index}]"

        if isinstance(candle.open_time, bool) or candle.open_time <= 0:
            raise CandleValueError(
                f"{path}.openTime должно быть целым числом больше нуля."
            )

        open_price = _candle_positive_decimal(
            candle.open_price,
            path=f"{path}.open",
        )
        high_price = _candle_positive_decimal(
            candle.high_price,
            path=f"{path}.high",
        )
        low_price = _candle_positive_decimal(
            candle.low_price,
            path=f"{path}.low",
        )
        close_price = _candle_positive_decimal(
            candle.close_price,
            path=f"{path}.close",
        )
        _candle_non_negative_decimal(
            candle.volume,
            path=f"{path}.volume",
        )

        if high_price < low_price:
            raise CandleValueError(
                f"{path}.high не должно быть меньше {path}.low."
            )

        if high_price < open_price:
            raise CandleValueError(
                f"{path}.high не должно быть меньше {path}.open."
            )

        if high_price < close_price:
            raise CandleValueError(
                f"{path}.high не должно быть меньше {path}.close."
            )

        if low_price > open_price:
            raise CandleValueError(
                f"{path}.low не должно превышать {path}.open."
            )

        if low_price > close_price:
            raise CandleValueError(
                f"{path}.low не должно превышать {path}.close."
            )


def _candle_positive_decimal(
    value: object,
    *,
    path: str,
) -> Decimal:
    decimal_value = _candle_decimal(
        value,
        path=path,
    )

    if decimal_value <= 0:
        raise CandleValueError(
            f"{path} должно быть больше нуля."
        )

    return decimal_value


def _candle_non_negative_decimal(
    value: object,
    *,
    path: str,
) -> Decimal:
    decimal_value = _candle_decimal(
        value,
        path=path,
    )

    if decimal_value < 0:
        raise CandleValueError(
            f"{path} должно быть больше или равно нулю."
        )

    return decimal_value


def _candle_decimal(
    value: object,
    *,
    path: str,
) -> Decimal:
    if isinstance(value, bool) or not isinstance(value, (str, int, float)):
        raise CandleValueError(
            f"{path} должно быть числом или числовой строкой."
        )

    try:
        decimal_value = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise CandleValueError(
            f"{path} должно быть корректным числом."
        ) from exc

    if not decimal_value.is_finite():
        raise CandleValueError(
            f"{path} должно быть конечным числом."
        )

    return decimal_value