"""Compute + record the B2a strategy-level allocation from A's records + B1's deploy set. Runs in the nightly
AFTER B1's promotion (so the deploy-tier set is fresh). Pure allocation math (allocation_engine) + a recorded
snapshot; no capital is moved (that is sub-project C).

B3 — funding revocation on sustained decay: before `compute_allocation` runs, each effective-deploy edge's
funding status is recomputed from its immutable forward record (pure replay, no persisted mutable state — see
`funding_status.compute_funding`). A de-funded edge is filtered out of the allocation input, so it is simply
absent from the returned dollars (B2a's engine itself is untouched). The `strategy_funding` snapshot is
recorded for ALL effective-deploy edges (funded and de-funded) for display/audit."""
from __future__ import annotations

import datetime as dt
import functools
import logging

from fxhnt.adapters.persistence.cockpit_models import StrategyAllocationRow, StrategyFundingRow
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.allocation_engine import apply_venue_caps, compute_allocation, target_capital
from fxhnt.application.allocation_honest_inputs import honest_allocation_inputs
from fxhnt.application.allocation_policy import (
    ALLOCATION_POLICY_VERSION,
    allocation_policy_hash,
    resolve_allocation_policy,
)
from fxhnt.application.bybit_forward_track import POSITIONING_COIN_GROSS_CAP
from fxhnt.application.forward_ingest import CockpitBacktestRefProvider
from fxhnt.application.funding_status import FundingStatus, compute_funding
from fxhnt.application.gate_policy import POLICY, POLICY_VERSION, policy_hash
from fxhnt.application.gate_policy_resolve import resolve_policy
from fxhnt.config import get_settings
from fxhnt.registry import STRATEGY_REGISTRY

_log = logging.getLogger(__name__)


def _effective_policy(policy: object | None):
    """`policy=None` resolves to TODAY's exact default (`resolve_allocation_policy(get_settings())`) — the
    single choke point `record_allocation` must call through so `policy=None` stays byte-identical."""
    return resolve_allocation_policy(get_settings()) if policy is None else policy


def record_allocation(dsn: str, equity: float, *, store=None, spread_bps=None,
                      unlock_events=None, policy=None) -> dict[str, float]:
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    at = dt.datetime.now(dt.UTC).replace(tzinfo=None)
    as_of = at.strftime("%Y-%m-%d")

    # `promoted_strategy_ids()` is a persisted, ONE-WAY, never-deleted set. If a promoted sid's registry
    # entry later drops `promote_on_pass` (it became a book CONSTITUENT rather than a standalone deploy edge —
    # e.g. xsfunding/unlock folded into bybit_4edge), the stale promotion row must NOT re-enter deploy_sids:
    # doing so would double-count that edge's risk (once via the book, once via the stale promotion).
    deploy_sids = {sid for sid, m in STRATEGY_REGISTRY.items() if m.get("tier") == "deploy"} \
        | {sid for sid in repo.promoted_strategy_ids()
           if STRATEGY_REGISTRY.get(sid, {}).get("promote_on_pass")}
    nav_rows = {sid: repo.nav_history(sid) for sid in deploy_sids}

    # B3 — funding revocation on sustained decay: recompute each edge's funding from its immutable record.
    provider = CockpitBacktestRefProvider(repo)
    gphash = policy_hash(POLICY, POLICY_VERSION)
    funding = {}
    for sid in deploy_sids:
        gate_spec = STRATEGY_REGISTRY.get(sid, {}).get("gate_spec", {})
        if gate_spec.get("gate_type") == "reconciliation":
            rp = resolve_policy(POLICY, gate_spec)
            ref_for_days = functools.partial(provider.reference, sid)
            funding[sid] = compute_funding(sid, nav_rows[sid], ref_for_days, rp,
                                           POLICY.defund_window, POLICY.refund_window)
        else:
            # a non-reconciliation edge has no divergence signal to decay against -> never de-funded.
            funding[sid] = FundingStatus("funded", "funded", 0, 0, None)
    funded_sids = {sid for sid in deploy_sids if funding[sid].state != "de-funded"}
    defunded = sorted(deploy_sids - funded_sids)
    if defunded:
        _log.warning("record_allocation: de-funded (sustained decay): %s", defunded)

    pol = _effective_policy(policy)
    if store is not None:
        # Honest-reference sizing: stable, capacity-aware backtest series + per-strategy dollar ceilings for
        # the crypto deploy set (equity sids get no series -> caller's nav fallback + the equity ceiling).
        # Capacity-curve AUM grid. The BOOK deploy target (`bybit_4edge`) probes its 3 constituent sleeves'
        # honest series across this grid; the load-once/reprice provider (honest_allocation_inputs loads each
        # sleeve's AUM-independent panels ONCE and reprices per AUM) makes grid LENGTH cheap — it is repricing,
        # not reloading, so a wide grid no longer OOM-kills the nightly (the earlier 3-point clamp was the
        # pre-refactor OOM band-aid). Restored to span the book's ~$350k break WITH headroom above it, so
        # growth past the break is detectable. Equity sids don't use this grid (they get equity_default_ceiling,
        # no crypto curve).
        honest_returns, ceilings = honest_allocation_inputs(
            store, spread_bps or {}, unlock_events or [],
            deploy_sids=funded_sids, target_aum=equity,
            aums=[35_000, 100_000, 350_000, 1_000_000, 10_000_000],
            min_sharpe=getattr(pol, "capacity_min_sharpe", 1.0),
            equity_default_ceiling=getattr(pol, "equity_default_ceiling", 10_000_000.0),
            positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP)

        # Equity leg (sub-project: equity leg deployable, Task 3): the ibkr-equity deploy sids (today only
        # `multistrat`) get their OWN cost-netted sizing series + real-ADV capacity ceiling here, merged on
        # top of the crypto honest_returns/ceilings above. An empty eq_sids (no ibkr-equity sid funded) is a
        # complete no-op — this block must not run at all, so `store=None`/no-equity-sid stays byte-identical.
        from fxhnt.application.equity_allocation_inputs import equity_allocation_inputs
        eq_sids = {sid for sid in funded_sids
                   if STRATEGY_REGISTRY.get(sid, {}).get("venue") == "ibkr-equity"}
        if eq_sids:
            eq_returns, eq_ceilings = equity_allocation_inputs(
                repo, eq_sids, target_aum=equity,
                aums=[100_000, 350_000, 1_000_000, 3_000_000, 10_000_000],
                min_sharpe=getattr(pol, "capacity_min_sharpe", 1.0),
                # regime-aware ADV/spread sourcing (TASK 2, 2026-07-16 UCITS re-base plan): "ucits" (retail
                # EU, default) | "us" (post-MiFID-II professional) — NEVER auto-switched on AUM.
                regime=get_settings().allocation.equity_execution_regime,
                # which STORE the ucits regime's ADV reads from (2026-07-16 IBKR UCITS volume source plan):
                # "yahoo" (default, byte-identical) | "ibkr" (real on-exchange volume, once the
                # `backfill-ucits-volume-ibkr` CronJob is populating `ibkr_pit_volume`). No effect in "us".
                volume_source=get_settings().allocation.ucits_volume_source)
            honest_returns = {**honest_returns, **eq_returns}
            ceilings = {**ceilings, **eq_ceilings}
    else:
        honest_returns, ceilings = {}, {}
    # honest backtest series where available (stable, capacity-aware), else the forward nav record (fallback)
    returns = {sid: (honest_returns.get(sid) or {r.date: r.ret for r in nav_rows[sid]}) for sid in funded_sids}
    # The drawdown killswitch is LIVE protection, so it runs on the FORWARD nav record — never the (possibly
    # deep-drawdown) honest backtest sizing series, which would permanently disable an edge that once drew
    # down >kill_dd in history. When store is None there is no honest series, so returns already IS the nav
    # record -> pass None (byte-identical to before this fix).
    killswitch_returns = (
        {sid: {r.date: r.ret for r in nav_rows[sid]} for sid in funded_sids}
        if store is not None else None
    )
    weights = compute_allocation(returns, pol, killswitch_returns=killswitch_returns)
    dollars = target_capital(weights, equity, pol, capacity_ceiling=ceilings)
    venue_by_sid = {sid: STRATEGY_REGISTRY.get(sid, {}).get("venue", "") for sid in dollars}
    dollars = apply_venue_caps(dollars, venue_by_sid, getattr(pol, "max_venue_weight", 1.0), equity)

    phash = allocation_policy_hash(pol, ALLOCATION_POLICY_VERSION)
    alloc_rows = [StrategyAllocationRow(strategy_id=sid, target_weight=weights[sid],
                                        target_dollars=dollars.get(sid, 0.0),
                                        policy_version=ALLOCATION_POLICY_VERSION, policy_hash=phash,
                                        as_of=as_of, computed_at=at)
                  for sid in weights]
    repo.replace_allocations(alloc_rows, at)

    fund_rows = [StrategyFundingRow(strategy_id=sid, state=f.state, label=f.label, decay_run=f.decay_run,
                                    recovery_run=f.recovery_run, first_pass_date=f.first_pass_date,
                                    policy_version=POLICY_VERSION, policy_hash=gphash,
                                    as_of=as_of, computed_at=at)
                 for sid, f in funding.items()]
    repo.replace_funding(fund_rows, at)

    _log.info("record_allocation: %d/%d strategies funded+allocated (gross $%.0f, ceiling $%.0f)",
              len(weights), len(deploy_sids), sum(abs(v) for v in dollars.values()),
              pol.capital_ceiling)
    return dollars
