"""US->UCITS execution remap for the PRIIPs-legal multistrat leg. The MODELED track is unchanged (it models
US underlyings); this rewrites ONLY the execute path's symbols, and sizes each sleeve from the REAL UCITS
price fetched from IBKR (a UCITS ETF's price != its US cousin's).

Graceful routing (fix/ucits-contract-routing): a sleeve that has no UCITS mapping, or whose contract won't
resolve / has no price, is LOGGED and SKIPPED — never blocks the sleeves that DO route. Survivors keep their
OWN modeled weight (the book is NOT silently rescaled onto the routable names); the skipped dollar fraction
is reported so the operator sees the reduced deployment. A genuine config bug (two sleeves -> one ticker)
still hard-fails: that is a misconfiguration, not a routing gap."""
from __future__ import annotations

import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from fxhnt.config import UcitsListing

log = logging.getLogger(__name__)


@dataclass
class UcitsRoute:
    """The result of mapping US sleeves to UCITS tickers (symbols only; prices come later from IBKR)."""
    weights: dict[str, float] = field(default_factory=dict)              # ucits ticker -> modeled weight
    listings: dict[str, UcitsListing] = field(default_factory=dict)      # ucits ticker -> contract spec
    skipped: dict[str, float] = field(default_factory=dict)             # US ticker -> weight (unmapped)


def apply_ucits_map(weights: dict[str, float], mapping: dict[str, UcitsListing]) -> UcitsRoute:
    """Split `weights` (keyed by US ticker) into routable UCITS sleeves and skipped (unmapped) ones. Unmapped
    sleeves are logged + reported in `skipped`, NOT raised — one unroutable sleeve must not block the rest.
    Raises only on a genuine config bug: a non-injective map (two US sleeves -> one UCITS ticker)."""
    route = UcitsRoute()
    targets: dict[str, list[str]] = {}
    for us_sym, w in weights.items():
        listing = mapping.get(us_sym)
        if listing is None:
            route.skipped[us_sym] = w
            log.warning("UCITS routing: sleeve %s has NO UCITS mapping — SKIPPING it (weight %+.4f); the "
                        "remaining sleeves still route.", us_sym, w)
            continue
        targets.setdefault(listing.ticker, []).append(us_sym)
        route.weights[listing.ticker] = w
        route.listings[listing.ticker] = listing
    colliding = {t: ss for t, ss in targets.items() if len(ss) > 1}
    if colliding:
        raise ValueError(
            f"UCITS map collision(s): multiple sleeves route to the same ticker {colliding} "
            "— refusing to silently drop a sleeve (fix the map)")
    return route


def route_ucits(
    weights: dict[str, float],
    mapping: dict[str, UcitsListing],
    price_fn: Callable[[UcitsListing], float | None],
) -> tuple[dict[str, float], dict[str, float], dict[str, dict[str, str]], dict[str, float]]:
    """Full UCITS route for `execute-multistrat --venue ucits`: map US -> UCITS, then for each mapped sleeve
    fetch its REAL USD price via `price_fn` (IBKR delayed market data). A sleeve whose contract won't resolve
    or has no usable price (`price_fn` -> None/<=0) is LOGGED and SKIPPED; survivors keep their own weight.

    Returns (weights, prices, contract_specs, skipped):
      - weights:        ucits ticker -> modeled weight (routed + priced sleeves only)
      - prices:         ucits ticker -> REAL UCITS USD price (never the US cousin's price)
      - contract_specs: ucits ticker -> {exchange, currency, sec_id_type, sec_id} for place_order
      - skipped:        US ticker -> weight, for every dropped sleeve (unmapped OR unpriced), so the caller
                        can report the un-deployed dollar fraction (the book is NOT rescaled onto survivors)."""
    route = apply_ucits_map(weights, mapping)
    us_of = {listing.ticker: us for us, listing in mapping.items()}   # UCITS ticker -> US ticker (for reports)
    out_w: dict[str, float] = {}
    out_p: dict[str, float] = {}
    specs: dict[str, dict[str, str]] = {}
    skipped = dict(route.skipped)                                     # already US-ticker-keyed (unmapped)
    for ticker, w in route.weights.items():
        listing = route.listings[ticker]
        try:
            px = price_fn(listing)
        except Exception:  # a broker/IB API blip on ONE sleeve must not wedge the whole book — skip just it,
            skipped[us_of.get(ticker, ticker)] = w                   # downgraded to the same per-sleeve skip
            log.warning("UCITS routing: price fetch RAISED for sleeve %s->%s (ISIN %s) — SKIPPING it "
                        "(weight %+.4f); survivors keep their own weight.",
                        us_of.get(ticker, "?"), ticker, listing.isin, w, exc_info=True)
            continue
        if px is None or px <= 0.0:
            skipped[us_of.get(ticker, ticker)] = w                   # key by US ticker, consistent with above
            log.warning("UCITS routing: sleeve %s->%s (ISIN %s) did not resolve/price on IBKR — SKIPPING it "
                        "(weight %+.4f); survivors keep their own weight.",
                        us_of.get(ticker, "?"), ticker, listing.isin, w)
            continue
        out_w[ticker] = w
        out_p[ticker] = float(px)
        specs[ticker] = {"exchange": listing.exchange, "currency": listing.currency,
                         "sec_id_type": "ISIN", "sec_id": listing.isin}
    return out_w, out_p, specs, skipped
