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

from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation

from src.market_data.acquisition.adapters.dzengi.models import (
    DzengiExchangeInfoResponse,
    DzengiExchangeInfoSymbol,
    DzengiInstrumentFilter,
    DzengiKlinesResponse,
    DzengiLotSizeFilter,
    DzengiMinNotionalFilter,
    DzengiRawNumeric,
    DzengiTicker24hrResponse,
    DzengiWebSocketQuoteResponse,
)
from src.market_data.acquisition.exceptions import (
    CandleMappingError,
    InstrumentReferenceMappingError,
    QuoteMappingError,
)
from src.market_data.acquisition.models.candle import Candle
from src.market_data.acquisition.models.instrument import Instrument
from src.market_data.acquisition.models.quote import Quote


_DZENGI_SOURCE_NAME = "dzengi"


def map_dzengi_symbol_to_instrument(
    symbol: DzengiExchangeInfoSymbol,
) -> Instrument:
    """
    Преобразовать проверенную raw-модель инструмента Dzengi
    во внутреннюю source-independent модель Instrument.

    Функция предполагает, что до mapper уже были выполнены:
    schema validation, parsing и value validation.
    """

    lot_size = _find_single_filter(
        symbol.filters,
        DzengiLotSizeFilter,
        filter_name="LOT_SIZE",
        symbol=symbol.symbol,
    )
    min_notional = _find_single_filter(
        symbol.filters,
        DzengiMinNotionalFilter,
        filter_name="MIN_NOTIONAL",
        symbol=symbol.symbol,
    )

    return Instrument(
        symbol=symbol.symbol,
        name=symbol.name,
        status=symbol.status,
        base_asset=symbol.base_asset,
        quote_asset=symbol.quote_asset,
        asset_type=_optional_text(symbol.asset_type),
        market_type=symbol.market_type,
        market_modes=symbol.market_modes,
        order_types=symbol.order_types,
        base_asset_precision=symbol.base_asset_precision,
        quote_asset_precision=symbol.quote_precision,
        tick_size=_optional_decimal(
            symbol.tick_size,
            field_name="tickSize",
            symbol=symbol.symbol,
        ),
        tick_value=_optional_decimal(
            symbol.tick_value,
            field_name="tickValue",
            symbol=symbol.symbol,
        ),
        step_size=_optional_decimal(
            lot_size.step_size if lot_size is not None else None,
            field_name="stepSize",
            symbol=symbol.symbol,
        ),
        min_qty=_optional_decimal(
            lot_size.min_qty if lot_size is not None else None,
            field_name="minQty",
            symbol=symbol.symbol,
        ),
        max_qty=_optional_decimal(
            lot_size.max_qty if lot_size is not None else None,
            field_name="maxQty",
            symbol=symbol.symbol,
        ),
        min_notional=_optional_decimal(
            min_notional.min_notional
            if min_notional is not None
            else None,
            field_name="minNotional",
            symbol=symbol.symbol,
        ),
        country=_optional_text(symbol.country),
        sector=_optional_text(symbol.sector),
        industry=_optional_text(symbol.industry),
        trading_hours=_optional_text(symbol.trading_hours),
    )


def map_dzengi_exchange_info_to_instruments(
    response: DzengiExchangeInfoResponse,
) -> tuple[Instrument, ...]:
    """
    Преобразовать все инструменты exchangeInfo
    во внутренние модели Instrument.
    """

    return tuple(
        map_dzengi_symbol_to_instrument(symbol)
        for symbol in response.payload.symbols
    )


def map_dzengi_ticker_to_quote(
    response: DzengiTicker24hrResponse,
    *,
    received_at: datetime,
) -> Quote:
    """
    Преобразовать проверенную raw-модель Dzengi ticker/24hr
    во внутреннюю source-independent модель Quote.

    Функция предполагает, что до mapper уже были выполнены:
    schema validation, parsing и value validation.
    """

    normalized_received_at = _require_aware_datetime(
        received_at,
        field_name="received_at",
    )

    return Quote(
        symbol=response.symbol.strip(),
        last_price=_required_quote_decimal(
            response.last_price,
            field_name="lastPrice",
        ),
        bid_price=_required_quote_decimal(
            response.bid_price,
            field_name="bidPrice",
        ),
        ask_price=_required_quote_decimal(
            response.ask_price,
            field_name="askPrice",
        ),
        exchange_timestamp=_timestamp_ms_to_utc_datetime(
            response.close_time,
        ),
        received_at=normalized_received_at,
        source=_DZENGI_SOURCE_NAME,
    )


def _timestamp_ms_to_utc_datetime(value: int) -> datetime:
    try:
        return datetime.fromtimestamp(
            value / 1000,
            tz=timezone.utc,
        )
    except (OverflowError, OSError, ValueError) as exc:
        raise QuoteMappingError(
            "Поле closeTime невозможно преобразовать "
            "в UTC datetime."
        ) from exc


def _required_quote_decimal(
    value: DzengiRawNumeric,
    *,
    field_name: str,
) -> Decimal:
    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise QuoteMappingError(
            f"Поле {field_name} котировки невозможно "
            "преобразовать в Decimal."
        ) from exc

    if not result.is_finite():
        raise QuoteMappingError(
            f"Поле {field_name} котировки должно быть "
            "конечным числом."
        )

    return result


def _require_aware_datetime(
    value: datetime,
    *,
    field_name: str,
) -> datetime:
    if value.tzinfo is None or value.utcoffset() is None:
        raise QuoteMappingError(
            f"Поле {field_name} должно содержать timezone-aware datetime."
        )

    return value


def _find_single_filter[
    FilterT: DzengiInstrumentFilter
](
    filters: tuple[DzengiInstrumentFilter, ...],
    filter_type: type[FilterT],
    *,
    filter_name: str,
    symbol: str,
) -> FilterT | None:
    matches = tuple(
        instrument_filter
        for instrument_filter in filters
        if isinstance(instrument_filter, filter_type)
    )

    if len(matches) > 1:
        raise InstrumentReferenceMappingError(
            f"Инструмент '{symbol}' содержит несколько "
            f"фильтров {filter_name}."
        )

    if not matches:
        return None

    return matches[0]


def _optional_decimal(
    value: DzengiRawNumeric | None,
    *,
    field_name: str,
    symbol: str,
) -> Decimal | None:
    if value is None:
        return None

    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise InstrumentReferenceMappingError(
            f"Поле {field_name} инструмента '{symbol}' "
            f"невозможно преобразовать в Decimal."
        ) from exc

    if not result.is_finite():
        raise InstrumentReferenceMappingError(
            f"Поле {field_name} инструмента '{symbol}' "
            f"должно быть конечным числом."
        )

    return result


def _optional_text(value: str | None) -> str | None:
    if value is None:
        return None

    normalized = value.strip()

    if not normalized:
        return None

    return normalized


def map_dzengi_websocket_quote_to_quote(
    response: DzengiWebSocketQuoteResponse,
    *,
    received_at: datetime,
) -> Quote:
    """
    Преобразовать проверенную WebSocket-модель Dzengi в канонический Quote.

    Depth-сообщение не содержит цену последней сделки, поэтому временно
    используется midpoint best bid / best ask — так же, как в legacy runtime.
    """

    normalized_received_at = _require_aware_datetime(
        received_at,
        field_name="received_at",
    )
    bid_price = _required_quote_decimal(
        response.bid_price,
        field_name="bidPrice",
    )
    ask_price = _required_quote_decimal(
        response.ask_price,
        field_name="askPrice",
    )

    exchange_timestamp = None
    if response.timestamp is not None:
        try:
            exchange_timestamp = datetime.fromtimestamp(
                response.timestamp / 1000,
                tz=timezone.utc,
            )
        except (OverflowError, OSError, ValueError) as exc:
            raise QuoteMappingError(
                "Поле timestamp невозможно преобразовать в UTC datetime."
            ) from exc

    return Quote(
        symbol=response.symbol.strip(),
        last_price=(bid_price + ask_price) / Decimal("2"),
        bid_price=bid_price,
        ask_price=ask_price,
        exchange_timestamp=exchange_timestamp,
        received_at=normalized_received_at,
        source=_DZENGI_SOURCE_NAME,
    )

def map_dzengi_klines_to_candles(
    response: DzengiKlinesResponse,
    *,
    symbol: str,
    interval: str,
    source: str,
) -> tuple[Candle, ...]:
    """
    Преобразовать проверенные raw-модели Dzengi klines
    в канонический immutable-набор Candle.

    Функция предполагает, что до mapper уже были выполнены:
    schema validation, parsing и value validation.
    """

    candles = tuple(
        Candle(
            symbol=symbol.strip(),
            interval=interval.strip(),
            open_time=_candle_timestamp_ms_to_utc_datetime(
                item.open_time,
            ),
            open_price=_required_candle_decimal(
                item.open_price,
                field_name="open",
            ),
            high_price=_required_candle_decimal(
                item.high_price,
                field_name="high",
            ),
            low_price=_required_candle_decimal(
                item.low_price,
                field_name="low",
            ),
            close_price=_required_candle_decimal(
                item.close_price,
                field_name="close",
            ),
            volume=_required_candle_decimal(
                item.volume,
                field_name="volume",
            ),
            source=source.strip(),
        )
        for item in response.items
    )

    return tuple(
        sorted(
            candles,
            key=lambda candle: candle.open_time,
        )
    )


def _candle_timestamp_ms_to_utc_datetime(value: int) -> datetime:
    try:
        return datetime.fromtimestamp(
            value / 1000,
            tz=timezone.utc,
        )
    except (OverflowError, OSError, ValueError) as exc:
        raise CandleMappingError(
            "Поле openTime свечи невозможно преобразовать "
            "в UTC datetime."
        ) from exc


def _required_candle_decimal(
    value: DzengiRawNumeric,
    *,
    field_name: str,
) -> Decimal:
    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise CandleMappingError(
            f"Поле {field_name} свечи невозможно "
            "преобразовать в Decimal."
        ) from exc

    if not result.is_finite():
        raise CandleMappingError(
            f"Поле {field_name} свечи должно быть конечным числом."
        )

    return result