"""Regime-conditional execution (factory layer 5) — the payoff that closes the loop.

Reads the deployed regime-specialists from the lifecycle store, detects TODAY's regime for each of their
markets, and activates only the specialists whose regime is currently live. Their latest positions net
into one set of per-market target weights — a regime-adaptive multi-strategy book that runs trend
specialists when markets trend calmly and mean-reversion specialists when they panic, automatically.

Sizing policy (HRP-first, equal-weight fallback):
  When the FactoryStore has a recorded HRP allocation for the deployed book, each active leg is sized
  by its HRP weight (not equal-weight). Legs without an HRP weight (e.g. just promoted, cycle not yet
  run) share the unallocated gross budget equally. If no allocation is recorded at all, falls back to
  the prior equal-weight behaviour — existing tests continue to pass unchanged.
"""
from __future__ import annotations

from pydantic import BaseModel, Field

from fxhnt.config import Settings
from fxhnt.domain.factory import StrategyStatus
from fxhnt.domain.models import Market
from fxhnt.domain.regime import VolTrendClassifier
from fxhnt.domain.strategies import get_strategy
from fxhnt.ports.data import DataProvider
from fxhnt.ports.repository import AnalyticalStore, StrategyStore

_MIN_HISTORY = 300


class ActiveLeg(BaseModel):
    strategy_id: str
    market: str
    spec: str
    current_regime: str
    position: float          # the strategy's latest signal (-1..+1)


class DormantLeg(BaseModel):
    strategy_id: str
    market: str
    current_regime: str
    works_in: list[str]      # the regimes it IS a specialist in (none matches today)


class RegimeBook(BaseModel):
    as_of: str
    active: list[ActiveLeg] = Field(default_factory=list)
    dormant: list[DormantLeg] = Field(default_factory=list)
    target_weights: dict[str, float] = Field(default_factory=dict)


class RegimeConditionalExecutor:
    def __init__(self, data: DataProvider, analytical: AnalyticalStore, store: StrategyStore,
                 settings: Settings, *, classifier: VolTrendClassifier | None = None,
                 hrp_weights: dict[str, float] | None = None) -> None:
        self._data = data
        self._an = analytical
        self._store = store
        self._s = settings
        self._classifier = classifier or VolTrendClassifier()
        # Optional HRP weight lookup: strategy_id -> weight from latest factory cycle.
        # None means "not provided" (equal-weight fallback); {} means "cycle ran but no sleeves".
        self._hrp_weights: dict[str, float] | None = hrp_weights

    def _prices(self, market: Market):  # type: ignore[no-untyped-def]
        cached = self._an.load_prices(market)
        if cached is not None and len(cached) >= _MIN_HISTORY:
            return cached
        prices = self._data.fetch(market)
        self._an.save_prices(prices)
        return prices

    def assemble(
        self, *, status: StrategyStatus = StrategyStatus.DEPLOYED, max_gross_leverage: float = 1.0
    ) -> RegimeBook:
        specialists = self._store.list(status=status)
        active: list[ActiveLeg] = []
        dormant: list[DormantLeg] = []
        as_of = ""

        for rec in specialists:
            works_in = rec.specialist_regimes()
            for sym in rec.markets:
                market = Market(symbol=sym, asset_class=rec.asset_class)
                prices = self._prices(market)
                as_of = prices.dates[-1]
                current = self._classifier.classify(prices)[-1]  # today's regime for this market
                if current in works_in:
                    pos = float(get_strategy(rec.spec.kind).positions(prices, rec.spec.params)[-1])
                    active.append(ActiveLeg(strategy_id=rec.strategy_id, market=sym, spec=rec.spec.key(),
                                            current_regime=current, position=pos))
                else:
                    dormant.append(DormantLeg(strategy_id=rec.strategy_id, market=sym,
                                              current_regime=current, works_in=works_in))

        weights: dict[str, float] = {}
        if active:
            weights = self._size_active(active, max_gross_leverage)

        return RegimeBook(as_of=as_of, active=active, dormant=dormant, target_weights=weights)

    def _size_active(self, active: list[ActiveLeg], max_gross_leverage: float) -> dict[str, float]:
        """Size each active leg.

        HRP path (when self._hrp_weights is a dict, possibly empty):
          - Legs with a recorded HRP weight are sized proportionally by that weight, scaled so that
            allocated legs fill n_allocated/n_total of the gross budget (renormalised to that share).
          - Legs WITHOUT a recorded HRP weight (newly promoted, not yet in the allocation) receive an
            equal share of the remaining gross budget (n_unallocated/n_total).
          - The whole thing is gross-capped at the end so the gross budget is always honoured.

        Equal-weight fallback (self._hrp_weights is None — no allocation provided at all):
          - Every leg gets max_gross_leverage / n_active (original behaviour).
        """
        hrp = self._hrp_weights

        # leg_sizes: strategy_id -> gross budget allocation for that leg
        leg_sizes: dict[str, float]

        if hrp is None:
            # No allocation provided at all → equal-weight (preserves pre-existing behaviour)
            per_leg = max_gross_leverage / len(active)
            leg_sizes = {leg.strategy_id: per_leg for leg in active}
        else:
            # HRP path: split leg IDs into allocated vs unallocated
            allocated_ids = {leg.strategy_id for leg in active if leg.strategy_id in hrp}
            unallocated_ids = [leg.strategy_id for leg in active if leg.strategy_id not in hrp]

            hrp_sum = sum(hrp[sid] for sid in allocated_ids) or 0.0
            n_total = len(active)
            n_allocated = len(allocated_ids)
            n_unallocated = len(unallocated_ids)

            if n_allocated == 0:
                # All legs unallocated (empty allocation or brand-new book): equal-weight on full budget
                per_leg = max_gross_leverage / n_total
                leg_sizes = {leg.strategy_id: per_leg for leg in active}
            elif n_unallocated == 0:
                # All legs have HRP weights: normalise to gross budget.
                # Guard: if all recorded weights are 0 fall back to equal-weight so we never divide by 0.
                if hrp_sum <= 0:
                    per_leg = max_gross_leverage / n_total
                    leg_sizes = {leg.strategy_id: per_leg for leg in active}
                else:
                    scale = max_gross_leverage / hrp_sum
                    leg_sizes = {sid: hrp[sid] * scale for sid in allocated_ids}
            else:
                # Mixed: allocated legs fill their proportional share; unallocated get the rest equally.
                # Guard: if all allocated weights are 0 treat them as unallocated (equal-weight on full budget).
                hrp_budget = max_gross_leverage * n_allocated / n_total
                ew_budget = max_gross_leverage * n_unallocated / n_total
                if hrp_sum <= 0:
                    ew_per = max_gross_leverage / n_total
                    leg_sizes = {leg.strategy_id: ew_per for leg in active}
                else:
                    scale = hrp_budget / hrp_sum
                    leg_sizes = {sid: hrp[sid] * scale for sid in allocated_ids}
                    ew_per = ew_budget / n_unallocated
                    for sid in unallocated_ids:
                        leg_sizes[sid] = ew_per

        raw: dict[str, float] = {}
        for leg in active:
            sz = leg_sizes[leg.strategy_id]
            raw[leg.market] = raw.get(leg.market, 0.0) + sz * leg.position

        gross = sum(abs(w) for w in raw.values())
        if gross > max_gross_leverage and gross > 0:
            scale_g = max_gross_leverage / gross
            return {k: round(v * scale_g, 4) for k, v in raw.items()}
        return {k: round(v, 4) for k, v in raw.items()}
