"""
Thin Saxo OpenAPI REST client for options chain snapshots.

Two parts have high confidence (directly verified against Saxo's docs): the OAuth
flow (services/saxo_auth.py) and the general shape of a streaming *subscription*
call (ContextId/ReferenceId/Arguments, POST-then-DELETE for a one-off snapshot).

One part has LOWER confidence and is written defensively on purpose: the exact
field names returned by /ref/v1/instruments and /ref/v1/instruments/contractoptionspaces
(Saxo's Swagger UI didn't expose the full response schema to static fetching). Every
extraction below tries a couple of documented field-name variants and raises a
clear error showing the raw keys received if none match — this is the one piece
worth a quick calibration pass against a real response once SIM is connected.
"""
import logging
import time
import uuid
from datetime import date
from typing import Any, Dict, List, Optional

import httpx

from services.saxo_auth import SAXO_API_BASE_URL, get_valid_access_token

logger = logging.getLogger(__name__)

_OPTION_ASSET_TYPES = "StockOption,StockIndexOption,FuturesOption,ContractFutures"

# symbol -> option root Uic, cheap in-process cache (roots don't change within a session)
_root_uic_cache: Dict[str, int] = {}


class SaxoNotConnected(Exception):
    pass


def _headers() -> Dict[str, str]:
    token = get_valid_access_token()
    if not token:
        raise SaxoNotConnected("Saxo n'est pas connecté — complétez l'OAuth login d'abord.")
    return {"Authorization": f"Bearer {token}"}


def _get(path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    resp = httpx.get(f"{SAXO_API_BASE_URL}{path}", headers=_headers(), params=params or {}, timeout=15)
    resp.raise_for_status()
    return resp.json()


def _first(d: Dict[str, Any], *keys: str) -> Any:
    for k in keys:
        if k in d:
            return d[k]
    return None


def resolve_option_root_uic(symbol: str) -> int:
    if symbol in _root_uic_cache:
        return _root_uic_cache[symbol]

    data = _get("/ref/v1/instruments", {"Keywords": symbol, "AssetTypes": _OPTION_ASSET_TYPES})
    items = data.get("Data") or data.get("data") or []
    if not items:
        raise ValueError(f"Aucun option root Saxo trouvé pour '{symbol}' (réponse: {list(data.keys())})")

    uic = _first(items[0], "Uic", "uic", "Identifier")
    if uic is None:
        raise ValueError(f"Champ Uic introuvable dans la réponse instruments pour '{symbol}': {items[0]}")

    _root_uic_cache[symbol] = int(uic)
    return int(uic)


def get_option_space(root_uic: int) -> Dict[str, Any]:
    return _get(f"/ref/v1/instruments/contractoptionspaces/{root_uic}")


def _extract_option_legs(space: Dict[str, Any]) -> List[Dict[str, Any]]:
    """Flatten the option space response into [{uic, expiry_date, strike, option_type}, ...]."""
    expiries = _first(space, "OptionSpace", "SpecificOptions", "Expiries") or []
    legs: List[Dict[str, Any]] = []

    # Some Saxo response shapes nest strikes/sides under each expiry; others return a flat
    # list of contracts directly. Handle both defensively.
    for entry in expiries:
        expiry_date = _first(entry, "Expiry", "ExpiryDate", "Date")
        strikes = _first(entry, "SpecificOptions", "Strikes", "Options") or []
        for opt in strikes:
            uic = _first(opt, "Uic", "uic")
            strike = _first(opt, "StrikePrice", "Strike")
            side = _first(opt, "PutCall", "OptionType", "Side")
            if uic is None or strike is None or side is None:
                continue
            legs.append({
                "uic": int(uic),
                "expiry_date": expiry_date,
                "strike": float(strike),
                "option_type": "put" if str(side).lower().startswith("p") else "call",
            })

    if not legs:
        raise ValueError(f"Impossible d'extraire des contrats depuis contractoptionspaces (clés reçues: {list(space.keys())})")
    return legs


def _snapshot_via_subscription(uic: int, asset_type: str = "StockOption") -> Dict[str, Any]:
    """POST creates a subscription and returns an initial snapshot in the same response;
    DELETE immediately after — no websocket needs to be kept alive for a one-off read."""
    context_id = f"of-{uuid.uuid4().hex[:12]}"
    reference_id = "chain"
    token_headers = _headers()

    body = {
        "ContextId": context_id,
        "ReferenceId": reference_id,
        "Arguments": {"Uic": uic, "AssetType": asset_type},
        "RefreshRate": 5000,
    }
    resp = httpx.post(
        f"{SAXO_API_BASE_URL}/trade/v1/optionschain/subscriptions",
        headers={**token_headers, "Content-Type": "application/json"},
        json=body, timeout=15,
    )
    resp.raise_for_status()
    snapshot = resp.json()

    try:
        httpx.delete(
            f"{SAXO_API_BASE_URL}/trade/v1/optionschain/subscriptions/{context_id}/{reference_id}",
            headers=token_headers, timeout=10,
        )
    except Exception as e:
        logger.warning(f"[Saxo] Failed to clean up options-chain subscription {context_id}: {e}")

    return snapshot


def snapshot_options_chain(symbol: str, target_days: int = 30) -> List[Dict[str, Any]]:
    """
    Returns normalized rows ready for services/database.save_saxo_snapshot_rows:
    {symbol, snapshot_date, spot, expiry_date, strike, option_type, bid, ask, mid,
     volatility_pct, delta, gamma, theta, vega}
    """
    root_uic = resolve_option_root_uic(symbol)
    space = get_option_space(root_uic)
    legs = _extract_option_legs(space)

    snapshot = _snapshot_via_subscription(root_uic)
    spot = _first(snapshot, "UnderlyingSpotPrice", "Spot", "UnderlyingPrice")
    snapshot_date = date.today().isoformat()

    by_uic = {leg["uic"]: leg for leg in legs}
    rows: List[Dict[str, Any]] = []

    for expiry_block in (_first(snapshot, "Expiries", "OptionsChain") or []):
        for strike_block in (_first(expiry_block, "Strikes") or []):
            for side_key in ("Call", "Put", "call", "put"):
                side = strike_block.get(side_key)
                if not side:
                    continue
                uic = _first(side, "Uic", "ContractId")
                leg = by_uic.get(int(uic)) if uic is not None else None
                rows.append({
                    "symbol": symbol.upper(),
                    "snapshot_date": snapshot_date,
                    "spot": float(spot) if spot is not None else None,
                    "expiry_date": _first(expiry_block, "Expiry", "ExpiryDate") or (leg["expiry_date"] if leg else None),
                    "strike": float(_first(strike_block, "Strike", "StrikePrice") or (leg["strike"] if leg else 0)),
                    "option_type": "put" if side_key.lower() == "put" else "call",
                    "bid": _first(side, "Bid"),
                    "ask": _first(side, "Ask"),
                    "mid": _first(side, "Mid"),
                    "volatility_pct": _first(strike_block, "MidVolatilityPct", "VolatilityPct"),
                    "delta": _first(side, "DeltaPct", "Delta"),
                    "gamma": _first(side, "Gamma"),
                    "theta": _first(side, "Theta"),
                    "vega": _first(side, "Vega"),
                })

    if not rows:
        raise ValueError(f"Snapshot Saxo vide pour '{symbol}' (clés reçues: {list(snapshot.keys())})")
    return rows
