"""
Data adapter — converts our OHLC DataFrames to Nautilus objects.

Functions
---------
make_instrument   — CurrencyPair from ticker string
make_bar_type     — BarType from ticker + period_seconds
df_to_bars        — OHLC DataFrame → list[Bar]
extras_lookup     — extract OHLC+ extras dict from DataFrame
make_instrument_from_metadata — instrument with best-effort precision
"""

from __future__ import annotations

import logging
from decimal import Decimal
from typing import Optional

import pandas as pd

from nautilus_trader.model.currencies import Currency
from nautilus_trader.model.data import Bar, BarType, BarSpecification
from nautilus_trader.model.enums import BarAggregation, PriceType, AggregationSource
from nautilus_trader.model.identifiers import InstrumentId, Symbol, Venue
from nautilus_trader.model.instruments import CurrencyPair
from nautilus_trader.model.objects import Price, Quantity

from dexorder.symbol_metadata_client import parse_ticker
from dexorder.nautilus.pandas_strategy import (
    bar_type_from_feed_key,
    _PERIOD_TO_AGGREGATION,
)

log = logging.getLogger(__name__)

# Columns in our OHLC+ DataFrames that are extras (not part of Nautilus Bar)
_EXTRA_COLS = ("buy_vol", "sell_vol", "open_interest")


def make_bar_type(ticker: str, period_seconds: int) -> BarType:
    """
    Construct a Nautilus BarType from our ticker and period_seconds.

    Period mapping:
        period_seconds < 60    → SECOND, step = period_seconds
        period_seconds < 3600  → MINUTE, step = period_seconds // 60
        period_seconds < 86400 → HOUR,   step = period_seconds // 3600
        else                   → DAY,    step = period_seconds // 86400

    Price type = MID (standard for crypto OHLC).
    Source = EXTERNAL (we supply pre-aggregated data, not Nautilus aggregation).
    """
    exchange_id, market_id = parse_ticker(ticker)
    instrument_id = InstrumentId(Symbol(market_id), Venue(exchange_id))

    for threshold, agg, divisor in _PERIOD_TO_AGGREGATION:
        if period_seconds < threshold:
            step = max(1, period_seconds // divisor)
            break
    else:
        agg = BarAggregation.DAY
        step = max(1, period_seconds // 86400)

    spec = BarSpecification(step=step, aggregation=agg, price_type=PriceType.MID)
    return BarType(instrument_id=instrument_id, bar_spec=spec,
                   aggregation_source=AggregationSource.EXTERNAL)


def make_instrument(
    ticker: str,
    price_precision: int = 8,
    size_precision: int = 8,
    tick_size: Optional[float] = None,
    lot_size: Optional[float] = None,
    maker_fee: float = 0.0,
    taker_fee: float = 0.0,
    margin_init: float = 0.0,
    margin_maint: float = 0.0,
) -> CurrencyPair:
    """
    Create a minimal CurrencyPair instrument from a Nautilus-format ticker.

    Args:
        ticker: e.g. "BTC/USDT.BINANCE"
        price_precision: decimal places for price (default 8)
        size_precision: decimal places for quantity (default 8)
        tick_size: minimum price increment (defaults to 10^-price_precision)
        lot_size: minimum order size (defaults to 10^-size_precision)
        maker_fee, taker_fee: fee rates as fractions (0.001 = 0.1%)
        margin_init, margin_maint: margin ratios (0.0 = spot/no margin)
    """
    exchange_id, market_id = parse_ticker(ticker)
    base_str, quote_str = market_id.split("/")

    instrument_id = InstrumentId(Symbol(market_id), Venue(exchange_id))

    if tick_size is None:
        tick_size = 10.0 ** (-price_precision)
    if lot_size is None:
        lot_size = 10.0 ** (-size_precision)

    ts_now = 0  # static instrument — timestamp not relevant for backtesting

    return CurrencyPair(
        instrument_id=instrument_id,
        raw_symbol=Symbol(market_id),
        base_currency=Currency.from_str(base_str),
        quote_currency=Currency.from_str(quote_str),
        price_precision=price_precision,
        size_precision=size_precision,
        price_increment=Price(tick_size, price_precision),
        size_increment=Quantity(lot_size, size_precision),
        lot_size=Quantity(lot_size, size_precision),
        max_quantity=None,
        min_quantity=Quantity(lot_size, size_precision),
        max_notional=None,
        min_notional=None,
        max_price=None,
        min_price=None,
        margin_init=margin_init,
        margin_maint=margin_maint,
        maker_fee=Decimal(str(maker_fee)),
        taker_fee=Decimal(str(taker_fee)),
        ts_event=ts_now,
        ts_init=ts_now,
    )


def make_instrument_from_metadata(ticker: str) -> tuple[CurrencyPair, int, int]:
    """
    Create a CurrencyPair using SymbolMetadata when available.

    Returns:
        (instrument, price_precision, size_precision)

    Falls back to (instrument with 8/8 defaults) if metadata is unavailable.
    """
    try:
        from dexorder.symbol_metadata_client import SymbolMetadataClient
        from dexorder.api import get_api
        # DataAPIImpl stores the catalog URI as an attribute on the OHLCClient
        api = get_api()
        ohlc_client = getattr(api.data, '_ohlc_client', None) or getattr(api.data, 'ohlc_client', None)
        iceberg_client = getattr(ohlc_client, 'iceberg', None) if ohlc_client else None
        catalog_uri = getattr(iceberg_client, 'catalog_uri', None) if iceberg_client else None

        if catalog_uri:
            meta_client = SymbolMetadataClient(catalog_uri=catalog_uri)
            meta = meta_client.get_metadata(ticker)
            pp = meta.price_precision or 8
            sp = meta.size_precision or 8
            instrument = make_instrument(
                ticker,
                price_precision=pp,
                size_precision=sp,
                tick_size=meta.tick_size,
                lot_size=meta.lot_size,
                maker_fee=meta.maker_fee or 0.0,
                taker_fee=meta.taker_fee or 0.0,
                margin_init=meta.margin_init or 0.0,
                margin_maint=meta.margin_maint or 0.0,
            )
            return instrument, pp, sp
    except Exception:
        log.debug("make_instrument_from_metadata: metadata unavailable for %s, using defaults", ticker)

    instrument = make_instrument(ticker)
    return instrument, 8, 8


def df_to_bars(
    df: pd.DataFrame,
    bar_type: BarType,
    price_precision: int = 8,
    size_precision: int = 8,
) -> list[Bar]:
    """
    Convert an OHLC DataFrame to a list of Nautilus Bar objects.

    Args:
        df: DataFrame with columns [timestamp (ns), open, high, low, close].
            volume column is optional; defaults to 0.0 if absent.
        bar_type: BarType to tag each bar with.
        price_precision: decimal precision for Price construction.
        size_precision: decimal precision for Quantity construction.

    Returns:
        list[Bar] sorted ascending by timestamp.
    """
    has_volume = "volume" in df.columns
    bars = []

    for row in df.itertuples(index=False):
        ts_ns = int(row.timestamp)
        volume = float(row.volume) if has_volume else 0.0

        bar = Bar(
            bar_type=bar_type,
            open=Price(float(row.open),  price_precision),
            high=Price(float(row.high),  price_precision),
            low=Price(float(row.low),   price_precision),
            close=Price(float(row.close), price_precision),
            volume=Quantity(volume, size_precision),
            ts_event=ts_ns,
            ts_init=ts_ns,
        )
        bars.append(bar)

    return bars


def extras_lookup(df: pd.DataFrame) -> dict[int, dict]:
    """
    Build a {ts_event_ns → {buy_vol, sell_vol, open_interest}} mapping.

    Values are None for columns absent from the DataFrame.

    Used by PandasStrategy._inject_extras() to enrich each bar with
    OHLC+ fields that Nautilus Bar does not carry natively.
    """
    result: dict[int, dict] = {}

    present = {col: col in df.columns for col in _EXTRA_COLS}

    for row in df.itertuples(index=False):
        ts_ns = int(row.timestamp)
        entry: dict = {}
        for col in _EXTRA_COLS:
            if present[col]:
                val = getattr(row, col)
                entry[col] = None if (val is None or (isinstance(val, float) and pd.isna(val))) else float(val)
            else:
                entry[col] = None
        result[ts_ns] = entry

    return result
