"""Sub-project C1 — record the EXECUTED multistrat track and bridge B2a's dollar allocation to the venue.

`scaled_weights` turns the multistrat book's within-book weights into weights that ExecutionService sizes to
B2a's envelope (not the full account). `compute_exec_return` measures the executed book's daily return on
B2a's ENVELOPE (not the full NLV) and as the P&L of the HELD positions since the last rebalance (a
re-allocation is a capital flow, not a gain). Recording that return as a forward_record is wired in a later
C1 task (Task 3). No new execution engine, no new return store."""
from __future__ import annotations

import datetime as dt
import logging
from typing import Any

log = logging.getLogger(__name__)


def superseded_holdings(*, fills: list[Any], current_targets: set[str],
                        held: dict[str, float]) -> dict[str, float]:
    """multistrat's OWN superseded lines to close: symbols it PREVIOUSLY traded (per its own `fills`) that are
    STILL held but are NO LONGER in the current target set — e.g. IDTM after the IEF->CBU0 remap. Keyed to the
    LAST recorded fill price (a real mark for the liquidating sell). SCOPED to multistrat's own fills, so a
    position another strategy holds on the SHARED IBKR account is NEVER included (a symbol multistrat never
    traded can't be here). Zero-qty holdings are ignored. Empty dict when the account is already clean."""
    last_price: dict[str, float] = {}
    for f in sorted(fills, key=lambda f: f.at):          # ascending -> the LAST fill's price wins
        last_price[f.symbol] = float(f.price)
    return {sym: last_price[sym] for sym, qty in held.items()
            if qty and sym in last_price and sym not in current_targets}


def scaled_weights(within_weights: dict[str, float], envelope: float, nlv: float) -> dict[str, float]:
    if envelope <= 0.0 or nlv <= 0.0:
        return {s: 0.0 for s in within_weights}          # flatten to cash (B2a $0 / no NLV)
    scale = envelope / nlv
    return {s: w * scale for s, w in within_weights.items()}


def build_pos_state(positions: dict[str, float], prices: dict[str, float], envelope: float,
                    venue: str = "") -> dict[str, Any]:
    return {"positions": {s: float(q) for s, q in positions.items()},
            "prices": {s: float(p) for s, p in prices.items()}, "envelope": float(envelope),
            "venue": str(venue)}


def compute_exec_return(pos_prior: dict[str, Any] | None, today_prices: dict[str, float]) -> float | None:
    if not pos_prior:
        return None                                      # first run / no prior book (None or {}) → inception
    envelope = float(pos_prior.get("envelope", 0.0))
    if envelope <= 0.0:
        return None
    positions: dict[str, float] = pos_prior.get("positions", {})
    prior_prices: dict[str, float] = pos_prior.get("prices", {})
    val_prior = sum(q * prior_prices.get(s, 0.0) for s, q in positions.items())
    val_today = sum(q * today_prices.get(s, prior_prices.get(s, 0.0)) for s, q in positions.items())
    reserve = envelope - val_prior                       # un-deployed envelope earns 0
    exec_equity_today = val_today + reserve
    return exec_equity_today / envelope - 1.0


def exec_return_for_run(pos_prior: dict[str, Any] | None, today_prices: dict[str, float],
                        venue: str) -> float | None:
    """The observed return to book for this run, or None (INCEPTION) when there is no prior book OR the prior
    book was on a DIFFERENT execution venue. On a venue switch (e.g. US SPY-keyed prior book -> first UCITS
    CSPX-keyed run) the prior positions/prices are keyed by the OTHER venue's symbols, so `compute_exec_return`
    would compare mismatched keys and book a spurious return — so we treat the first run on a new venue as a
    fresh book (no return), exactly like inception."""
    if pos_prior and str(pos_prior.get("venue", "")) != str(venue):
        log.warning("multistrat exec venue switch %r -> %r — booking this run as INCEPTION (no return) to "
                    "avoid comparing mismatched-venue position keys.", pos_prior.get("venue"), venue)
        return None
    return compute_exec_return(pos_prior, today_prices)


class _ExecBookingStrategy:
    """A trivial observed ForwardStrategy for run_track: books today's pre-computed executed return (or
    nothing on the inception run). The return is computed by the CLI (it has the live prices/positions); this
    just hands it to A's observed engine."""
    def __init__(self, exec_return: float | None) -> None:
        self._r = exec_return

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        if self._r is None:
            return [], extra
        return [(str(extra.get("_today")), float(self._r))], extra


def record_exec_track(repo: Any, exec_return: float | None, new_positions: dict[str, float],
                      today_prices: dict[str, float], envelope: float, today: str, at: dt.datetime,
                      venue: str = "") -> None:
    """Book `exec_return` as an observed forward_record for `multistrat_exec` via A's run_track, then carry
    the next run's position-state (incl. the execution `venue`) under book-state key `multistrat_exec_pos`."""
    from fxhnt.application.forward_engine import run_track
    run_track(repo, "multistrat_exec", _ExecBookingStrategy(exec_return), today=today, at=at)
    repo.set_book_state("multistrat_exec_pos", build_pos_state(new_positions, today_prices, envelope, venue), at=at)


def plan_and_record(*, repo: Any, exec_svc: Any, within_weights: dict[str, float], prices: dict[str, float],
                    nlv: float, today: str, at: dt.datetime, max_gross: float, execute: bool,
                    paper_envelope: float = 0.0, account_is_paper: bool = True,
                    venue: str = "", account_repo: Any = None, broker: Any = None,
                    contract_specs: dict[str, dict[str, str]] | None = None) -> tuple[Any, float]:
    """Compute the B2a envelope, size the book to it (flatten on $0), reconcile via exec_svc, and on
    `execute` (when the plan was NOT blocked by a safety gate) record the observed multistrat_exec return.
    Returns (plan, envelope). The next run's return basis is the TARGET book we rebalance TO
    (weights·nlv/price), computed here — NOT a broker read taken before the rebalance (which would be the
    stale prior book). Recording is skipped when the plan is blocked: a blocked rebalance placed no orders
    (data gap / LIVE-without-allow_live / gross > max / NLV<=0), so the account still holds its prior
    positions — recording would advance the basis to a target book that was never placed and permanently
    diverge the executed track from the real account.

    `paper_envelope > 0` sizes the book to min(paper_envelope, nlv) and does NOT read B2a — a PAPER run that
    bypasses the real-capital gate, hard-refused (no orders) unless `account_is_paper`. `venue` is recorded.

    `account_repo` + `broker` (both optional, default None) additionally capture the REAL DU-account state
    (D2.1): when both are supplied AND `account_is_paper`, the fills actually placed this run + a fresh
    account snapshot are persisted via `exec_capture`. This is ADDITIVE and BEST-EFFORT — it never changes
    `positions_after` (the gate-relevant return basis, unchanged above) and a capture failure only logs."""
    if paper_envelope > 0.0:
        if not account_is_paper:
            raise ValueError("paper-envelope refused: account is not paper (real-capital exposure guard)")
        envelope = min(float(paper_envelope), float(nlv))
    else:
        envelope = min(float(repo.current_allocation().get("multistrat", 0.0)), float(nlv))
    weights = scaled_weights(within_weights, envelope, nlv)
    # SELF-CLEANING: close multistrat's OWN superseded lines (held + previously traded by multistrat + no
    # longer a target — e.g. IDTM after the IEF->CBU0 remap) so a mapping change doesn't strand legacy holdings
    # in the account NLV. Scoped to multistrat's own fills (NEVER another strategy's position on the SHARED
    # account) and best-effort (a scan failure must never block the rebalance). Added at target weight 0, so
    # `_plan` closes the FULL holding (a weight-0 exit bypasses hysteresis). Priced at the last fill; UCITS
    # orphans resolve on LSEETF/USD by symbol (no ISIN needed to SELL a line already held).
    prices = dict(prices)
    specs = dict(contract_specs or {})
    if execute and broker is not None and account_repo is not None and account_is_paper:
        try:
            held = broker.account_state().positions
            fills = account_repo.fills_for("multistrat", limit=10000)
            superseded = superseded_holdings(fills=fills, current_targets=set(weights), held=held)
            for sym, px in superseded.items():
                weights[sym] = 0.0
                prices.setdefault(sym, px)
                if "ucits" in str(venue).lower():
                    specs.setdefault(sym, {"exchange": "LSEETF", "currency": "USD"})
            if superseded:
                log.info("multistrat self-clean: closing superseded line(s) %s", sorted(superseded))
        except Exception:  # noqa: BLE001 — the cleanup scan is best-effort; never block a real rebalance over it
            log.warning("multistrat superseded-line scan failed (skipped)", exc_info=True)
    plan = exec_svc.rebalance_weights("multistrat", weights, prices, max_gross=max_gross, execute=execute,
                                      contract_specs=specs, sizing_capital=envelope)
    # A DEGRADED run (orders attempted but a leg did not fill — Error 478/10349 live 2026-07-20) is NOT
    # blocked, but recording it would advance the basis to `positions_after` (the INTENDED target book) that
    # the account does NOT actually hold (e.g. the un-filled CBU0 leg), permanently diverging the executed
    # track — the SAME failure the blocked-skip guards against, just partial. So skip recording here too and
    # let the next CLEAN run re-establish the basis (err false-WAIT: a skipped record keeps the gate WAITing;
    # a wrong record silently corrupts it). `degraded` = orders were placed but the plan is not executed.
    degraded = bool(plan.orders) and not plan.executed
    if execute and not plan.blocked and not degraded:
        pos_prior = repo.get_book_state("multistrat_exec_pos")
        exec_return = exec_return_for_run(pos_prior, prices, venue)
        # positions_after is the INTENDED target book (weights·nlv/price) used as the next-run return basis —
        # NOT a post-fill account read. Accepted because the venue is commission-free fractional (spec); a
        # noted-but-not-blocked NLV band or an in-band skip can make the true placed book differ slightly.
        positions_after = {s: (w * float(nlv)) / prices[s]                     # the target book just placed
                           for s, w in weights.items() if prices.get(s, 0.0) > 0.0}
        record_exec_track(repo, exec_return, positions_after, prices, envelope, today, at, venue)
        if account_is_paper and account_repo is not None and broker is not None:
            try:
                from fxhnt.application.exec_capture import capture_fills, persist_execution
                persist_execution(account_repo, strategy_id="multistrat",
                                  rebalance_id=(plan.rebalance_id or today), fills=capture_fills(plan.trades),
                                  snapshot=broker.account_snapshot(), at=at)
            except Exception:  # capture is ADDITIVE — never let it fail an already-placed rebalance
                log.warning("multistrat exec capture failed (best-effort, non-blocking)", exc_info=True)
    return plan, envelope
