from __future__ import annotations

from datetime import datetime, timezone
from math import exp
from typing import Any

import polars as pl

from uforia.config import AppConfig, get_config
from uforia.ingest.deribit.client import DeribitPublicClient


def fetch_option_chain_payload(
    underlying: str = "BTC",
    venue: str = "deribit",
    client: DeribitPublicClient | None = None,
    config: AppConfig | None = None,
) -> dict[str, Any]:
    cfg = config or get_config()
    deribit = client or DeribitPublicClient(cfg)
    observed_at = deribit.now().astimezone(timezone.utc)
    summaries = deribit.get_book_summaries(underlying, kind="option")
    instruments = deribit.get_instruments(underlying, kind="option", expired=False)
    ticker_by_name = {
        row["instrument_name"]: row
        for row in deribit.get_tickers(
        [str(row["instrument_name"]) for row in instruments if row.get("instrument_name")]
        )
        if row.get("instrument_name")
    }
    books = []
    for summary in summaries:
        instrument_name = summary.get("instrument_name")
        merged = dict(summary)
        ticker = ticker_by_name.get(str(instrument_name))
        if ticker:
            merged.update(ticker)
        books.append(merged)
    return {
        "observed_at": observed_at,
        "venue": venue,
        "underlying": underlying,
        "books": books,
        "instruments": instruments,
    }


def normalize_option_chain_payload(
    payload: dict[str, Any],
    *,
    logical_ts: datetime | None = None,
    config: AppConfig | None = None,
) -> pl.DataFrame:
    cfg = config or get_config()
    observed_at = _coerce_dt(payload["observed_at"])
    snapshot_ts = (
        (logical_ts or observed_at)
        .astimezone(timezone.utc)
        .replace(minute=0, second=0, microsecond=0)
    )
    venue = str(payload.get("venue", "deribit"))
    underlying = str(payload.get("underlying", "BTC"))
    instrument_by_name = {
        row["instrument_name"]: row
        for row in payload.get("instruments", [])
        if row.get("instrument_name")
    }
    rows: list[dict[str, Any]] = []

    for book in payload.get("books", []):
        instrument = instrument_by_name.get(book.get("instrument_name"))
        if instrument is None:
            continue
        bid = _as_float(book.get("bid_price", book.get("best_bid_price")))
        ask = _as_float(book.get("ask_price", book.get("best_ask_price")))
        mid = _compute_mid(bid, ask, _as_float(book.get("mid_price")))
        quote_age = max(
            0,
            int(
                (observed_at.timestamp() * 1000)
                - book.get("creation_timestamp", book.get("timestamp", 0))
            ),
        )
        quality_flag = "ok"
        if mid is None or instrument.get("strike") is None:
            quality_flag = "malformed"
        elif quote_age > cfg.ingestion.stale_quote_threshold_ms:
            quality_flag = "stale"
        elif (
            bid is not None
            and ask is not None
            and ask > 0
            and (ask - bid) > cfg.ingestion.max_absolute_spread
        ):
            quality_flag = "wide_spread"

        expiry = datetime.fromtimestamp(instrument["expiration_timestamp"] / 1000, tz=timezone.utc)
        time_to_expiry_years = max(
            (expiry - snapshot_ts).total_seconds() / (365.0 * 24.0 * 3600.0),
            0.0,
        )
        underlying_price = _as_float(book.get("underlying_price"))
        interest_rate = _as_float(book.get("interest_rate"), default=0.0) or 0.0
        rows.append(
            {
                "ts": snapshot_ts,
                "date": snapshot_ts.date(),
                "venue": venue,
                "underlying": underlying,
                "expiry": expiry,
                "instrument_name": book["instrument_name"],
                "option_type": instrument["option_type"],
                "strike": _as_float(instrument.get("strike")),
                "bid": bid,
                "ask": ask,
                "mark": _as_float(book.get("mark_price")),
                "mid": mid,
                "mark_iv": _scaled_iv(book.get("mark_iv")),
                "delta": _greek(book, "delta"),
                "gamma": _greek(book, "gamma"),
                "vega": _greek(book, "vega"),
                "index_price": underlying_price,
                "underlying_price": underlying_price,
                # Deribit book summaries expose the index price; approximate forward via carry.
                "forward": (
                    None
                    if underlying_price is None
                    else float(underlying_price * exp(interest_rate * time_to_expiry_years))
                ),
                "interest_rate": interest_rate,
                "open_interest": _as_float(book.get("open_interest"), default=0.0),
                "volume": _as_float(
                    book.get("volume", (book.get("stats") or {}).get("volume")),
                    default=0.0,
                ),
                "quote_age_ms": quote_age,
                "quality_flag": quality_flag,
            }
        )

    frame = pl.DataFrame(rows)
    if frame.is_empty():
        return frame
    return frame.filter(pl.col("quality_flag") != "malformed")


def fetch_option_chain(
    start: datetime | None = None,
    end: datetime | None = None,
    underlying: str = "BTC",
    venue: str = "deribit",
    client: DeribitPublicClient | None = None,
    config: AppConfig | None = None,
) -> pl.DataFrame:
    payload = fetch_option_chain_payload(
        underlying=underlying,
        venue=venue,
        client=client,
        config=config,
    )
    observed_hour = _coerce_dt(payload["observed_at"]).replace(minute=0, second=0, microsecond=0)
    if start is not None and end is not None and (end < observed_hour or start > observed_hour):
        raise ValueError(
            "Deribit public API adapter only supports current option-chain snapshots; "
            "historical book snapshot backfill requires a separate historical market-data source."
        )
    return normalize_option_chain_payload(payload, logical_ts=observed_hour, config=config)


def _coerce_dt(value: Any) -> datetime:
    if isinstance(value, datetime):
        return value.astimezone(timezone.utc)
    if isinstance(value, str):
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        return parsed.astimezone(timezone.utc)
    raise TypeError(f"Unsupported datetime value: {type(value)!r}")


def _as_float(value: Any, default: float | None = None) -> float | None:
    if value is None:
        return default
    return float(value)


def _compute_mid(bid: float | None, ask: float | None, existing_mid: float | None) -> float | None:
    if bid is not None and ask is not None and bid <= ask:
        return (bid + ask) / 2.0
    return existing_mid


def _scaled_iv(value: Any) -> float | None:
    raw = _as_float(value)
    if raw is None:
        return None
    return raw / 100.0


def _greek(book: dict[str, Any], name: str) -> float | None:
    greeks = book.get("greeks") or {}
    return _as_float(greeks.get(name))
