"""Fund module: autonomous multi-strategy fund management.

Provides the FundManager orchestrator that ties together:
- NAV computation and tracking
- Capital allocation across strategies
- Strategy lifecycle management (deploy/stop/pause/resume)
- Fund-level risk controls (drawdown circuit breaker)
- Market settlement detection
- Fee accounting and P&L attribution
- Market fitness scoring
- Market universe management and discovery
- Persistent strategy memory (SQLite)
- Cross-strategy correlation monitoring
- VaR-based risk budgeting
- Fund-level stress testing
- Execution quality tracking
- Adaptive execution parameter tuning
- Cross-exchange smart order routing
- Research pipeline (opportunity scanning)
- Autopilot (backtest-to-deploy pipeline)
- Decision framework (governance, audit, rate limits)
- Autonomous agent (LLM-facing interface)
- Alerting and notifications (webhook, callback)
- Double-entry accounting ledger
- Paper-to-live strategy promotion
- Self-healing and automatic recovery
- Adaptive confidence thresholds
- Built-in backtesting for autopilot
- Fund state explainability
- Multi-fund cluster management
"""

from __future__ import annotations

import logging
import threading
import time
from typing import Any

from horizon.fund._accounting import FundAccounting
from horizon.fund._adaptive import AdaptiveConfig, AdaptiveExecutionTuner, ParameterAdjustment
from horizon.fund._adaptive_thresholds import OutcomeRecord, ThresholdTuner
from horizon.fund._agent import AutonomousAgent
from horizon.fund._alerts import Alert, AlertCategory, AlertChannel, AlertLevel, AlertManager, CallbackChannel, WebhookChannel
from horizon.fund._allocator import CapitalAllocator
from horizon.fund._alpha_decay import AlphaDecayTracker, DecayAlert, DecayProfile
from horizon.fund._alpha_model import AlphaEstimate, AlphaModel, FactorIC, FactorValue
from horizon.fund._bias_guard import BiasAlert, BiasGuard, BiasGuardConfig
from horizon.fund._autopilot import Autopilot, DeployCandidate, DeployCriteria
from horizon.fund._backtest_runner import BacktestRunner
from horizon.fund._controller import StrategyController
from horizon.fund._correlation import ConcentrationAlert, CorrelationMonitor, CorrelationPair
from horizon.fund._decisions import (
    ActionType,
    ConfidenceThresholds,
    Decision,
    DecisionFramework,
    OperatingMode,
    RateLimits,
)
from horizon.fund._exec_quality import ExecutionQualityTracker, ExecutionSnapshot, FundExecutionReport
from horizon.fund._execution_intelligence import ExecutionIntelligence, ExecutionSchedule, InventoryRiskReport, ToxicityAlert
from horizon.fund._explainability import Explainer
from horizon.fund._fund_risk import FundRiskMonitor, RiskAlert
from horizon.fund._hypothesis import Hypothesis, HypothesisManager, HypothesisState
from horizon.fund._ledger import Account, FundLedger, JournalEntry
from horizon.fund._multi_fund import FundCluster
from horizon.fund._fund_stress import (
    FundScenario,
    FundScenarioResult,
    FundStressResult,
    FundStressTester,
)
from horizon.fund._memory import MemoryEntry, StrategyMemory
from horizon.fund._nav import NAVEngine
from horizon.fund._performance_attribution import (
    AlphaBetaDecomposition,
    CostAttribution,
    PerformanceAttribution,
    PerformanceSnapshot,
    StrategyAttribution,
)
from horizon.fund._portfolio_optimizer import OptimizationConstraints, OptimizationResult, PortfolioOptimizer
from horizon.fund._promotion import PromotionCriteria, PromotionManager, PromotionRecord, PromotionStage
from horizon.fund._recovery import RecoveryAction, RecoveryEvent, RecoveryManager
from horizon.fund._regime import RegimeDetector, RegimeSnapshot
from horizon.fund._research import Opportunity, ResearchPipeline, ResearchScan
from horizon.fund._research_intel import CrossMarketOpportunity, ResearchIntelligence, ResearchTrigger
from horizon.fund._risk_analytics import DynamicLimits, PortfolioGreeksReport, RiskAnalytics, TailRiskReport
from horizon.fund._signal_ensemble import EnsembleOutput, Signal, SignalEnsemble, SignalQuality
from horizon.fund._routing import ExchangeHealth, RouteDecision, RouteScore, SmartRouter
from horizon.fund._scorer import MarketScorer
from horizon.fund._settlement import SettlementMonitor
from horizon.fund._types import (
    AllocationMethod,
    FeeType,
    FundConfig,
    FundReport,
    MarketScore,
    NAVSnapshot,
    SettlementEvent,
    StrategyConfig,
    StrategyState,
    StrategyStatus,
)
from horizon.fund._universe import MarketUniverse, UniverseConfig, UniverseEntry
from horizon.fund._var_budget import VaRAllocation, VaRBudgetManager, VaRBudgetSnapshot

logger = logging.getLogger("horizon.fund")

__all__ = [
    # Orchestrator
    "FundManager",
    # Config
    "FundConfig",
    "StrategyConfig",
    # Enums
    "AllocationMethod",
    "FeeType",
    "StrategyState",
    # Data types
    "FundReport",
    "MarketScore",
    "NAVSnapshot",
    "SettlementEvent",
    "StrategyStatus",
    "RiskAlert",
    # Sub-components (for direct access)
    "NAVEngine",
    "CapitalAllocator",
    "StrategyController",
    "FundRiskMonitor",
    "SettlementMonitor",
    "FundAccounting",
    "MarketScorer",
    # Universe
    "MarketUniverse",
    "UniverseConfig",
    "UniverseEntry",
    # Memory
    "StrategyMemory",
    "MemoryEntry",
    # Correlation
    "CorrelationMonitor",
    "CorrelationPair",
    "ConcentrationAlert",
    # VaR budgeting
    "VaRBudgetManager",
    "VaRBudgetSnapshot",
    "VaRAllocation",
    # Fund stress testing
    "FundStressTester",
    "FundScenario",
    "FundScenarioResult",
    "FundStressResult",
    # Execution quality
    "ExecutionQualityTracker",
    "ExecutionSnapshot",
    "FundExecutionReport",
    # Adaptive execution
    "AdaptiveExecutionTuner",
    "AdaptiveConfig",
    "ParameterAdjustment",
    # Smart routing
    "SmartRouter",
    "RouteDecision",
    "RouteScore",
    "ExchangeHealth",
    # Research pipeline
    "ResearchPipeline",
    "Opportunity",
    "ResearchScan",
    # Autopilot
    "Autopilot",
    "DeployCandidate",
    "DeployCriteria",
    # Decision framework
    "DecisionFramework",
    "Decision",
    "ActionType",
    "OperatingMode",
    "ConfidenceThresholds",
    "RateLimits",
    # Autonomous agent
    "AutonomousAgent",
    # Bias guard
    "BiasGuard",
    "BiasGuardConfig",
    "BiasAlert",
    # Regime detection
    "RegimeDetector",
    "RegimeSnapshot",
    # Alpha decay
    "AlphaDecayTracker",
    "DecayProfile",
    "DecayAlert",
    # Alpha model
    "AlphaModel",
    "AlphaEstimate",
    "FactorValue",
    "FactorIC",
    # Hypothesis framework
    "HypothesisManager",
    "Hypothesis",
    "HypothesisState",
    # Signal ensemble
    "SignalEnsemble",
    "Signal",
    "EnsembleOutput",
    "SignalQuality",
    # Research intelligence
    "ResearchIntelligence",
    "ResearchTrigger",
    "CrossMarketOpportunity",
    # Portfolio optimizer
    "PortfolioOptimizer",
    "OptimizationConstraints",
    "OptimizationResult",
    # Risk analytics
    "RiskAnalytics",
    "DynamicLimits",
    "TailRiskReport",
    "PortfolioGreeksReport",
    # Performance attribution
    "PerformanceAttribution",
    "PerformanceSnapshot",
    "AlphaBetaDecomposition",
    "StrategyAttribution",
    "CostAttribution",
    # Execution intelligence
    "ExecutionIntelligence",
    "ToxicityAlert",
    "InventoryRiskReport",
    "ExecutionSchedule",
    # Alerts
    "AlertManager",
    "Alert",
    "AlertLevel",
    "AlertCategory",
    "AlertChannel",
    "WebhookChannel",
    "CallbackChannel",
    # Ledger
    "FundLedger",
    "JournalEntry",
    "Account",
    # Promotion
    "PromotionManager",
    "PromotionStage",
    "PromotionCriteria",
    "PromotionRecord",
    # Recovery
    "RecoveryManager",
    "RecoveryAction",
    "RecoveryEvent",
    # Adaptive thresholds
    "ThresholdTuner",
    "OutcomeRecord",
    # Backtest runner
    "BacktestRunner",
    # Explainability
    "Explainer",
    # Multi-fund
    "FundCluster",
]


class FundManager:
    """Orchestrates a multi-strategy fund.

    The FundManager ties all fund components together and runs an
    oversight loop that periodically:
    1. Computes NAV across all strategy engines
    2. Checks fund-level risk (drawdown, correlations)
    3. Monitors cross-strategy correlations and VaR budgets
    4. Detects market settlements
    5. Accrues fees
    6. Tracks execution quality and applies adaptive tuning
    7. Records events to persistent memory

    Usage::

        from horizon.fund import FundManager, FundConfig, StrategyConfig

        fund = FundManager(FundConfig(total_capital=100_000))
        fund.add_strategy(StrategyConfig(
            name="mm_political",
            pipeline=[signal, quote],
            markets=["will-x-win"],
        ))
        fund.start()  # Starts all strategies + oversight loop
        # ... later ...
        fund.stop()

    For autonomous operation::

        from horizon.fund import AutonomousAgent, OperatingMode

        agent = AutonomousAgent(fund, mode=OperatingMode.SUPERVISED)
        decision = agent.deploy_strategy(config, reasoning="...", confidence=0.8)

    """

    def __init__(self, config: FundConfig | None = None) -> None:
        from horizon._horizon import auth_require_ultra
        auth_require_ultra()

        self._config = config or FundConfig()

        # Core components (Phase 1-2)
        self._nav = NAVEngine(self._config.total_capital)
        self._allocator = CapitalAllocator(self._config)
        self._controller = StrategyController(self._config)
        self._risk = FundRiskMonitor(self._config)
        self._settlement = SettlementMonitor()
        self._accounting = FundAccounting(self._config)
        self._scorer = MarketScorer()

        # Universe management (Phase 3)
        self._universe = MarketUniverse()
        self._memory = StrategyMemory()

        # Risk analytics (Phase 4)
        self._correlation = CorrelationMonitor()
        self._var_budget = VaRBudgetManager(
            total_var_budget=self._config.total_capital * 0.10,
        )
        self._stress_tester = FundStressTester(
            max_fund_drawdown_pct=self._config.max_fund_drawdown_pct,
            max_strategy_drawdown_pct=self._config.max_strategy_drawdown_pct,
        )

        # Execution intelligence (Phase 5)
        self._exec_quality = ExecutionQualityTracker()
        self._adaptive = AdaptiveExecutionTuner()
        self._router = SmartRouter()

        # Bias prevention (Phase 7)
        self._bias_guard = BiasGuard()

        # Wire adaptive tuner to controller for param injection
        self._controller._adaptive_tuner = self._adaptive

        # Wire exec quality tracker to controller for fill recording
        self._controller._exec_quality = self._exec_quality

        # Quant flow modules (Phase 8)
        self._regime_detector = RegimeDetector()
        self._alpha_decay = AlphaDecayTracker()
        self._alpha_model = AlphaModel()
        self._hypothesis_mgr = HypothesisManager()
        self._signal_ensemble = SignalEnsemble()
        self._research_intel = ResearchIntelligence()
        self._portfolio_optimizer = PortfolioOptimizer(
            total_capital=self._config.total_capital,
        )
        self._risk_analytics = RiskAnalytics()
        self._perf_attribution = PerformanceAttribution()
        self._exec_intel = ExecutionIntelligence()

        # Research + autopilot (Phase 6 -- wired to bias guard + decay tracker)
        self._research = ResearchPipeline()
        self._research.set_edge_estimator(self._alpha_model.as_edge_estimator())
        self._autopilot = Autopilot(
            bias_guard=self._bias_guard,
            decay_tracker=self._alpha_decay,
        )

        # Agentic fund modules (Phase 9)
        self._alert_mgr = AlertManager()
        if self._config.alert_webhook_url:
            self._alert_mgr.register_channel(WebhookChannel(self._config.alert_webhook_url))

        self._ledger = FundLedger() if self._config.ledger_enabled else None
        self._promotion = PromotionManager() if self._config.promotion_enabled else None
        self._recovery = RecoveryManager() if self._config.self_healing_enabled else None
        self._threshold_tuner = ThresholdTuner() if self._config.adaptive_thresholds_enabled else None
        self._backtest_runner = BacktestRunner()
        self._explainer = Explainer()

        # Lifecycle
        self._oversight_thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._pending_configs: list[tuple[StrategyConfig, Any]] = []
        self._started = False
        self._oversight_tick_count = 0

    # ------------------------------------------------------------------
    # Properties - core
    # ------------------------------------------------------------------

    @property
    def config(self) -> FundConfig:
        return self._config

    @property
    def nav_engine(self) -> NAVEngine:
        return self._nav

    @property
    def allocator(self) -> CapitalAllocator:
        return self._allocator

    @property
    def controller(self) -> StrategyController:
        return self._controller

    @property
    def risk_monitor(self) -> FundRiskMonitor:
        return self._risk

    @property
    def settlement_monitor(self) -> SettlementMonitor:
        return self._settlement

    @property
    def accounting(self) -> FundAccounting:
        return self._accounting

    @property
    def scorer(self) -> MarketScorer:
        return self._scorer

    # ------------------------------------------------------------------
    # Properties - advanced modules
    # ------------------------------------------------------------------

    @property
    def universe(self) -> MarketUniverse:
        return self._universe

    @property
    def memory(self) -> StrategyMemory:
        return self._memory

    @property
    def correlation_monitor(self) -> CorrelationMonitor:
        return self._correlation

    @property
    def var_budget(self) -> VaRBudgetManager:
        return self._var_budget

    @property
    def stress_tester(self) -> FundStressTester:
        return self._stress_tester

    @property
    def exec_quality(self) -> ExecutionQualityTracker:
        return self._exec_quality

    @property
    def adaptive_tuner(self) -> AdaptiveExecutionTuner:
        return self._adaptive

    @property
    def router(self) -> SmartRouter:
        return self._router

    @property
    def research(self) -> ResearchPipeline:
        return self._research

    @property
    def autopilot(self) -> Autopilot:
        return self._autopilot

    @property
    def bias_guard(self) -> BiasGuard:
        return self._bias_guard

    @property
    def regime_detector(self) -> RegimeDetector:
        return self._regime_detector

    @property
    def alpha_decay(self) -> AlphaDecayTracker:
        return self._alpha_decay

    @property
    def alpha_model(self) -> AlphaModel:
        return self._alpha_model

    @property
    def hypothesis_manager(self) -> HypothesisManager:
        return self._hypothesis_mgr

    @property
    def signal_ensemble(self) -> SignalEnsemble:
        return self._signal_ensemble

    @property
    def research_intel(self) -> ResearchIntelligence:
        return self._research_intel

    @property
    def portfolio_optimizer(self) -> PortfolioOptimizer:
        return self._portfolio_optimizer

    @property
    def risk_analytics(self) -> RiskAnalytics:
        return self._risk_analytics

    @property
    def performance_attribution(self) -> PerformanceAttribution:
        return self._perf_attribution

    @property
    def execution_intelligence(self) -> ExecutionIntelligence:
        return self._exec_intel

    # ------------------------------------------------------------------
    # Properties - agentic modules
    # ------------------------------------------------------------------

    @property
    def alert_manager(self) -> AlertManager:
        return self._alert_mgr

    @property
    def ledger(self) -> FundLedger | None:
        return self._ledger

    @property
    def promotion_manager(self) -> PromotionManager | None:
        return self._promotion

    @property
    def recovery_manager(self) -> RecoveryManager | None:
        return self._recovery

    @property
    def threshold_tuner(self) -> ThresholdTuner | None:
        return self._threshold_tuner

    @property
    def backtest_runner(self) -> BacktestRunner:
        return self._backtest_runner

    @property
    def explainer(self) -> Explainer:
        return self._explainer

    # ------------------------------------------------------------------
    # Strategy management
    # ------------------------------------------------------------------

    def add_strategy(
        self,
        config: StrategyConfig,
        risk_config: Any = None,
    ) -> None:
        """Add a strategy to the fund.

        If the fund is already started, deploys immediately.
        Otherwise, queues for deployment on start().
        """
        if self._started:
            self._deploy_strategy(config, risk_config)
        else:
            self._pending_configs.append((config, risk_config))

    def remove_strategy(self, name: str) -> None:
        """Stop and remove a strategy from the fund."""
        try:
            self._controller.retire(name)
        except ValueError:
            pass
        self._nav.remove_strategy(name)
        self._controller.remove(name)
        self._correlation.remove_strategy(name)
        self._var_budget.remove_strategy(name)
        self._exec_quality.remove_strategy(name)

    def pause_strategy(self, name: str) -> None:
        """Pause a running strategy."""
        self._controller.pause(name)

    def resume_strategy(self, name: str) -> None:
        """Resume a paused strategy."""
        self._controller.resume(name)

    def scale_strategy(self, name: str, new_capital: float) -> None:
        """Update capital allocation for a strategy.

        Updates both the NAV allocation and the controller handle's
        allocated_capital so drawdown calculations use the correct base.
        Note: Engine risk config limits are not updated (requires stop + redeploy).
        """
        status = self._controller.status(name)
        old_capital = status.allocated_capital
        self._nav.set_allocation(name, new_capital)
        # Update the handle's capital so drawdown calculations are correct
        with self._controller._lock:
            handle = self._controller._strategies.get(name)
            if handle is not None:
                handle.allocated_capital = new_capital
        logger.info(
            "Scaled '%s' allocation: %.2f -> %.2f",
            name, old_capital, new_capital,
        )

    # ------------------------------------------------------------------
    # Fund lifecycle
    # ------------------------------------------------------------------

    def start(self) -> None:
        """Start the fund: deploy all queued strategies and begin oversight."""
        if self._started:
            logger.warning("Fund already started")
            return

        self._started = True
        self._stop_event.clear()

        # Deploy queued strategies
        for config, risk_config in self._pending_configs:
            try:
                self._deploy_strategy(config, risk_config)
            except Exception as e:
                logger.error("Failed to deploy '%s': %s", config.name, e)

        self._pending_configs.clear()

        # Start oversight loop
        self._oversight_thread = threading.Thread(
            target=self._oversight_loop,
            name="fund-oversight",
            daemon=True,
        )
        self._oversight_thread.start()
        logger.info(
            "Fund started: %d strategies, %.2f capital",
            self._controller.running_count(),
            self._config.total_capital,
        )

    def stop(self, timeout: float = 30.0) -> None:
        """Stop the fund: stop oversight and all strategies."""
        self._stop_event.set()

        if self._oversight_thread is not None and self._oversight_thread.is_alive():
            self._oversight_thread.join(timeout=5.0)

        self._controller.stop_all(timeout=timeout)
        self._started = False
        logger.info("Fund stopped")

    def close(self) -> None:
        """Stop the fund and release all resources (SQLite connections, etc.)."""
        self.stop()
        self._memory.close()
        self._hypothesis_mgr.close()
        if self._ledger is not None:
            self._ledger.close()
        if self._promotion is not None:
            self._promotion.close()
        self._alert_mgr.shutdown()

    @property
    def is_running(self) -> bool:
        return self._started and not self._stop_event.is_set()

    # ------------------------------------------------------------------
    # Status and reporting
    # ------------------------------------------------------------------

    def status(self) -> dict[str, Any]:
        """Get full fund status."""
        nav = self._nav.latest()
        strategies = self._controller.all_statuses()

        return {
            "running": self.is_running,
            "total_capital": self._config.total_capital,
            "nav": nav.total_nav if nav else self._config.total_capital,
            "cash": nav.cash if nav else self._config.total_capital,
            "drawdown_pct": nav.drawdown_pct if nav else 0.0,
            "high_water_mark": nav.high_water_mark if nav else self._config.total_capital,
            "total_strategies": len(strategies),
            "running_strategies": sum(1 for s in strategies if s.state == StrategyState.RUNNING),
            "total_fees": self._accounting.total_fees(),
            "settlements": self._settlement.total_settlements,
            "kill_switch": self._risk.fund_kill_switch_active,
            "universe_size": self._universe.size,
            "correlation_alerts": len(self._correlation.recent_alerts()),
        }

    def report(self) -> FundReport:
        """Generate a full fund report."""
        engines = self._controller.engines
        nav = self._nav.compute_nav(engines)
        strategies = self._controller.all_statuses()

        allocation = {}
        for s in strategies:
            allocation[s.name] = s.allocated_capital

        return FundReport(
            timestamp=time.time(),
            nav=nav,
            strategies=strategies,
            total_strategies=len(strategies),
            running_strategies=sum(1 for s in strategies if s.state == StrategyState.RUNNING),
            recent_settlements=self._settlement.recent_events(10),
            allocation=allocation,
        )

    def nav_history(self, limit: int = 100) -> list[NAVSnapshot]:
        """Get recent NAV history."""
        return self._nav.history(limit)

    def pnl_attribution(self) -> dict[str, dict[str, float]]:
        """Get P&L attribution per strategy."""
        return self._accounting.pnl_attribution(self._controller.engines)

    def risk_dashboard(self) -> dict[str, Any]:
        """Get a consolidated risk dashboard."""
        nav = self._nav.latest()
        var_snap = self._var_budget.snapshot()
        corr_pairs = self._correlation.pairs()
        exec_report = self._exec_quality.snapshot()

        return {
            "nav": nav.total_nav if nav else self._config.total_capital,
            "drawdown_pct": nav.drawdown_pct if nav else 0.0,
            "var_budget": {
                "total": var_snap.total_budget,
                "used": var_snap.used_budget,
                "usage_pct": var_snap.usage_pct,
                "fund_var_95": var_snap.fund_var_95,
            },
            "correlations": [
                {"a": p.strategy_a, "b": p.strategy_b, "corr": round(p.correlation, 3)}
                for p in corr_pairs[:10]
            ],
            "execution": exec_report.summary(),
            "kill_switch": self._risk.fund_kill_switch_active,
        }

    def stress_test(self, scenarios: list[FundScenario] | None = None) -> FundStressResult:
        """Run fund-level stress scenarios against current state."""
        engines = self._controller.engines
        nav = self._nav.latest()
        strategy_capitals = {
            s.name: s.allocated_capital
            for s in self._controller.all_statuses()
        }
        return self._stress_tester.run(
            engines=engines,
            strategy_capitals=strategy_capitals,
            current_nav=nav.total_nav if nav else self._config.total_capital,
            hwm=nav.high_water_mark if nav else self._config.total_capital,
            scenarios=scenarios,
        )

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _deploy_strategy(
        self,
        config: StrategyConfig,
        risk_config: Any = None,
    ) -> None:
        """Deploy a strategy and set up fund tracking."""
        # VaR budget check before deploying
        if not self._var_budget.can_increase_exposure(config.name):
            logger.warning(
                "VaR budget exhausted for '%s' - deploying with caution",
                config.name,
            )

        # Smart router recommendation (log only)
        try:
            route = self._router.route(
                market_id=config.markets[0] if config.markets else config.name,
                side="buy",
                size=0.0,
                venue_prices={"paper": 0.5},
            )
            logger.debug("Router recommendation for '%s': %s", config.name, route.selected_exchange)
        except Exception:
            pass

        # Compute allocation
        all_names = self._controller.strategy_names + [config.name]
        allocations = self._allocator.allocate(all_names, self._controller.engines)
        capital = allocations.get(config.name, 0.0)

        # Scale risk config to allocation
        scaled_risk = self._scale_risk_config(risk_config, capital)

        # Deploy via controller
        self._controller.deploy(config, capital, scaled_risk)

        # Track in NAV engine
        self._nav.set_allocation(config.name, capital)

        # Set VaR budget allocation
        n_strategies = len(all_names)
        if n_strategies > 0:
            self._var_budget.set_allocation(config.name, 1.0 / n_strategies)

        logger.info("Deployed '%s' with %.2f capital", config.name, capital)

    def _scale_risk_config(
        self, risk_config: Any, capital: float
    ) -> Any:
        """Scale a RiskConfig to match allocated capital."""
        if risk_config is None:
            from horizon._horizon import RiskConfig as RC
            return RC(
                max_order_size=capital * 0.1,  # 10% per position
                max_portfolio_notional=capital,
                max_daily_drawdown_pct=self._config.max_strategy_drawdown_pct,
            )

        # If it's a Risk helper, extract the RiskConfig
        from horizon.risk import Risk
        if isinstance(risk_config, Risk):
            risk_config = risk_config.build()

        # Scale notional limits to allocation
        from horizon._horizon import RiskConfig as RC
        return RC(
            max_order_size=min(
                getattr(risk_config, "max_order_size", capital * 0.1),
                capital * 0.5,
            ),
            max_portfolio_notional=min(
                getattr(risk_config, "max_portfolio_notional", capital),
                capital,
            ),
            max_daily_drawdown_pct=min(
                getattr(risk_config, "max_daily_drawdown_pct", 25.0),
                self._config.max_strategy_drawdown_pct,
            ),
        )

    def _oversight_loop(self) -> None:
        """Background loop that monitors fund health."""
        interval = self._config.rebalance_interval_secs

        while not self._stop_event.is_set():
            try:
                self._oversight_tick()
            except Exception as e:
                logger.error("Oversight loop error: %s", e)

            self._stop_event.wait(timeout=interval)

    def _oversight_tick(self) -> None:
        """Single tick of the oversight loop."""
        engines = self._controller.engines
        if not engines:
            return

        self._oversight_tick_count += 1

        # 1. Compute NAV
        nav = self._nav.compute_nav(engines)

        # 2. Check fund risk (includes auto-unpause and kill switch recovery)
        strategy_dd = self._controller.strategy_drawdowns()
        failed = self._controller.failed_strategies()
        alerts = self._risk.check(nav, strategy_dd, failed)

        # 2b. Fire alerts for risk events
        for alert_obj in alerts:
            level = AlertLevel.CRITICAL if getattr(alert_obj, "level", "") == "critical" else AlertLevel.WARNING
            category = AlertCategory.KILL_SWITCH if "kill" in getattr(alert_obj, "category", "") else AlertCategory.DRAWDOWN_BREACH
            self._alert_mgr.fire(Alert(
                level=level,
                category=category,
                message=alert_obj.message,
                timestamp=time.time(),
            ))

        # 3. Handle kill switch (risk.check may auto-reset it via recovery)
        if self._risk.fund_kill_switch_active:
            logger.critical("Fund kill switch active - stopping all strategies")
            self._alert_mgr.fire(Alert(
                level=AlertLevel.CRITICAL,
                category=AlertCategory.KILL_SWITCH,
                message="Fund kill switch activated - all strategies stopped",
                timestamp=time.time(),
            ))
            self._controller.stop_all()
            return

        # 3b. Resume strategies after kill switch recovery
        if self._risk.kill_switch_just_recovered:
            logger.info("Kill switch recovered - resuming paused strategies")
            for name in self._controller.strategy_names:
                try:
                    status = self._controller.status(name)
                    if status.state == StrategyState.PAUSED:
                        self._controller.resume(name)
                except Exception:
                    pass

        # 4. Pause strategies flagged by risk
        for name in self._risk.paused_strategies:
            try:
                status = self._controller.status(name)
                if status.state == StrategyState.RUNNING:
                    self._controller.pause(name)
            except Exception:
                pass

        # 4b. Resume strategies that were auto-unpaused by risk monitor
        for name in self._risk.last_recovered_strategies:
            try:
                status = self._controller.status(name)
                if status.state == StrategyState.PAUSED:
                    self._controller.resume(name)
            except Exception:
                pass

        # 5. Check settlements and clean up resolved positions
        for name, engine in engines.items():
            events = self._settlement.check_settlements(name, engine)
            for event in events:
                try:
                    self._memory.record_strategy_outcome(
                        strategy_name=event.strategy_name,
                        market_type=event.market_id,
                        pnl=event.pnl,
                        sharpe=0.0,
                        duration_days=0.0,
                        notes=f"Settlement: outcome={event.outcome} pnl={event.pnl:.2f}",
                    )
                except Exception:
                    pass
                # Clean up open orders for the settled market
                try:
                    engine.cancel_for_market(event.market_id)
                    logger.info(
                        "Settlement cleanup: canceled orders for %s/%s (pnl=%.2f)",
                        event.strategy_name, event.market_id, event.pnl,
                    )
                except Exception:
                    pass

                # Alert on settlement
                self._alert_mgr.fire(Alert(
                    level=AlertLevel.INFO,
                    category=AlertCategory.SETTLEMENT,
                    message=f"Settlement: {event.market_id} outcome={event.outcome} pnl={event.pnl:.2f}",
                    timestamp=time.time(),
                    strategy_name=event.strategy_name,
                ))

                # Ledger entry for settlement PnL
                if self._ledger is not None and abs(event.pnl) > 1e-6:
                    try:
                        self._ledger.record_trade_pnl(
                            event.strategy_name, event.pnl,
                            reference=f"settlement_{event.market_id}",
                        )
                    except Exception:
                        pass

        # 6. Accrue fees periodically (batched via min_fee_interval_secs)
        mgmt_fee = self._accounting.compute_management_fee(nav)
        perf_fee = self._accounting.compute_performance_fee(nav)

        # 6b. Ledger entries for fees
        if self._ledger is not None:
            if mgmt_fee > 0:
                try:
                    self._ledger.accrue_fee("management", mgmt_fee, f"tick_{self._oversight_tick_count}")
                except Exception:
                    pass
            if perf_fee > 0:
                try:
                    self._ledger.accrue_fee("performance", perf_fee, f"tick_{self._oversight_tick_count}")
                except Exception:
                    pass

        # 7. Update correlation monitor with strategy NAVs (from NAV snapshot)
        strategy_navs = nav.strategy_navs

        if strategy_navs:
            corr_alerts = self._correlation.update(strategy_navs)
            if corr_alerts:
                for ca in corr_alerts:
                    logger.warning("Correlation alert: %s", ca.message)

            # 7b. Regime detection from NAV returns
            try:
                nav_returns = []
                hist = self._nav.history(22)
                for i in range(1, len(hist)):
                    prev_nav = hist[i - 1].total_nav
                    if prev_nav > 0:
                        nav_returns.append((hist[i].total_nav - prev_nav) / prev_nav)
                if nav_returns:
                    prev_regime = self._regime_detector.current_regime()
                    regime_snap = self._regime_detector.update(nav_returns)
                    # Detect regime transition -> fire research trigger
                    if prev_regime is not None and regime_snap.regime != prev_regime.regime:
                        self._research_intel.on_regime_change(prev_regime.regime, regime_snap.regime)
                        self._memory.record_regime_mapping(
                            regime=regime_snap.regime,
                            strategy_type="all",
                            effectiveness=regime_snap.confidence,
                            notes=f"Transition from {prev_regime.regime}",
                        )
            except Exception as e:
                logger.debug("Regime detection error: %s", e)

            # 8. Update VaR budget tracking
            self._var_budget.update_navs(strategy_navs)

        # 8c. Regime-conditional risk limits
        if self._config.regime_risk_enabled:
            try:
                regime_snap = self._regime_detector.current_regime()
                if regime_snap is not None:
                    limits = self._risk_analytics.dynamic_limits(regime_snap.regime)
                    logger.debug(
                        "Dynamic limits: regime=%s scale=%.2f spread=%.2f",
                        limits.regime, limits.position_scale, limits.spread_multiplier,
                    )
            except Exception:
                pass

        # 8d. Portfolio Greeks (every 5 ticks)
        if self._oversight_tick_count % 5 == 0:
            try:
                positions = []
                for name, engine in engines.items():
                    for pos in engine.positions():
                        positions.append({
                            "strategy_name": name,
                            "price": pos.avg_price,
                            "size": pos.net_size,
                            "is_yes": pos.side_str == "yes" if hasattr(pos, "side_str") else True,
                        })
                if positions:
                    self._risk_analytics.portfolio_greeks(positions)
            except Exception:
                pass

        # 8b. Update smart router exchange health
        for name, engine in engines.items():
            try:
                es = engine.status()
                for ex_name in engine.exchange_names():
                    self._router.update_health(
                        exchange=ex_name,
                        latency_ms=50.0,  # default estimate
                        error_rate=0.0 if es.running else 0.5,
                        fill_rate=1.0 if es.running else 0.0,
                        last_fill_age_secs=es.uptime_secs,
                    )
            except Exception:
                pass

        # 9. Adaptive execution tuning (every 5 ticks to avoid overhead)
        if self._oversight_tick_count % 5 == 0:
            for name in engines:
                metrics = self._exec_quality.strategy_metrics(name)
                if metrics.get("total_fills", 0) > 0:
                    self._adaptive.tune(
                        strategy=name,
                        metrics=metrics,
                        engine=engines.get(name),
                    )

        # 9b. Execution intelligence: toxicity monitoring
        try:
            for name, engine in engines.items():
                for market_slug in getattr(
                    self._controller._strategies.get(name, None), "config", type("", (), {"markets": []})
                ).markets:
                    snap = engine.feed_snapshot(market_slug)
                    if snap is not None and snap.price > 0:
                        alert = self._exec_intel.update_toxicity(
                            market_id=market_slug,
                            price=snap.price,
                            volume=getattr(snap, "volume", 1.0),
                            is_buy=True,
                        )
                        if alert is not None:
                            logger.warning("Toxicity alert: %s vpin=%.3f -> %s",
                                           market_slug, alert.vpin, alert.recommended_action)
        except Exception:
            pass

        # 10. Auto-rebalance (every N ticks)
        if self._oversight_tick_count % self._config.auto_rebalance_interval_ticks == 0:
            try:
                names = list(engines.keys())
                if self._config.optimization_enabled:
                    allocations = self._portfolio_optimizer.allocate(names, engines)
                else:
                    allocations = self._allocator.allocate(names, engines)
                for name, capital in allocations.items():
                    self.scale_strategy(name, capital)
                # Update VaR budgets proportionally to new allocations
                total_alloc = sum(allocations.values())
                if total_alloc > 0:
                    var_allocs = {
                        n: c / total_alloc for n, c in allocations.items()
                    }
                    self._var_budget.set_allocations(var_allocs)
            except Exception as e:
                logger.error("Auto-rebalance error: %s", e)

        # 11. Universe refresh (every N ticks)
        if self._oversight_tick_count % self._config.universe_refresh_interval_ticks == 0:
            try:
                self._universe.refresh(scorer=self._scorer)
            except Exception as e:
                logger.debug("Universe refresh error: %s", e)

        # 12. Memory auto-prune (every 100 ticks)
        if self._oversight_tick_count % 100 == 0:
            try:
                self._memory.auto_prune()
            except Exception:
                pass

        # 13. Research-to-deploy loop (every universe_refresh_interval_ticks)
        if self._oversight_tick_count % self._config.universe_refresh_interval_ticks == 0:
            try:
                universe_entries = self._universe.active_markets()
                if universe_entries:
                    # Pass current regime to research pipeline
                    regime_snap = self._regime_detector.current_regime()
                    scan = self._research.scan(
                        universe_entries, scorer=self._scorer, regime=regime_snap,
                    )
                    for opp in scan.opportunities:
                        # Form hypothesis for each opportunity
                        hyp = None
                        try:
                            hyp = self._hypothesis_mgr.form(
                                market_id=opp.market_id,
                                statement=opp.rationale,
                                direction="yes" if opp.edge_estimate > 0 else "no",
                                edge=abs(opp.edge_estimate),
                                confidence=opp.confidence,
                                source="research_pipeline",
                            )
                        except Exception:
                            pass

                        candidate = DeployCandidate(
                            market_id=opp.market_id,
                            exchange=opp.exchange,
                            strategy_type=opp.strategy_type,
                            edge_estimate=opp.edge_estimate,
                            confidence=opp.confidence,
                            hypothesis_id=hyp.hypothesis_id if hyp else None,
                        )
                        self._autopilot.evaluate(
                            candidate,
                            backtest_fn=self._backtest_runner.as_backtest_fn(),
                            robustness_fn=self._backtest_runner.as_robustness_fn(),
                        )

                        # Track edge for decay monitoring
                        self._alpha_decay.record_edge(
                            f"{opp.strategy_type}_{opp.market_id[:20]}",
                            opp.edge_estimate,
                        )

                    passing = self._autopilot.passing_candidates()
                    if passing:
                        logger.info(
                            "Research pipeline: %d passing candidates from %d opportunities",
                            len(passing), scan.opportunities_found,
                        )
            except Exception as e:
                logger.debug("Research-to-deploy loop error: %s", e)

        # 14. Autopilot auto-retire check (every 60 ticks)
        if self._oversight_tick_count % 60 == 0:
            try:
                to_retire = self._autopilot.check_deployed(
                    self._controller.all_statuses()
                )
                for name in to_retire:
                    logger.warning("Autopilot recommends retiring '%s'", name)
            except Exception:
                pass

            # 14b. Alpha decay alerts + hypothesis decay checks
            try:
                decay_alerts = self._alpha_decay.check_alerts()
                for da in decay_alerts:
                    logger.info("Alpha decay: %s - %s", da.strategy_name, da.message)
            except Exception:
                pass

            try:
                for hyp in self._hypothesis_mgr.active_hypotheses():
                    if hyp.state.value == "monitoring":
                        profile = self._alpha_decay.fit_decay(
                            f"{hyp.market_id[:20]}"
                        )
                        if profile is not None:
                            self._hypothesis_mgr.check_decay(
                                hyp.hypothesis_id, profile.current_edge,
                            )
            except Exception:
                pass

        # 15. NAV persistence (every 10 ticks)
        if self._oversight_tick_count % 10 == 0:
            try:
                import os
                nav_dir = os.path.expanduser("~/.horizon")
                os.makedirs(nav_dir, exist_ok=True)
                self._nav.persist_snapshot(os.path.join(nav_dir, "nav_history.jsonl"))
            except Exception:
                pass

        # 16. Feed health monitoring
        for name, engine in engines.items():
            try:
                status_obj = self._controller.status(name)
                if status_obj.state != StrategyState.RUNNING:
                    continue
                handle = self._controller._strategies.get(name)
                if handle is None:
                    continue
                for market_slug in handle.config.markets:
                    snap = engine.feed_snapshot(market_slug)
                    if snap is not None and snap.timestamp > 0:
                        age = time.time() - snap.timestamp
                        if age > 300:  # 5 minutes stale
                            logger.warning(
                                "Stale feed for %s/%s: %.0fs old",
                                name, market_slug, age,
                            )
            except Exception:
                pass

        # 17. Exchange outage detection via router health
        for ex_name, health in (
            (h.exchange, h) for h in self._router.exchange_health_report()
        ):
            if not health.is_healthy:
                logger.warning(
                    "Exchange '%s' unhealthy: error_rate=%.2f latency=%.0fms",
                    ex_name, health.error_rate, health.avg_latency_ms,
                )

        # 18. Event-driven research triggers
        try:
            triggers = self._research_intel.consume_triggers(max_count=5)
            for trigger in triggers:
                logger.debug(
                    "Research trigger: type=%s market=%s priority=%d",
                    trigger.trigger_type, trigger.market_id, trigger.priority,
                )
        except Exception:
            pass

        # 19. Performance attribution (every attribution_interval_ticks)
        if self._oversight_tick_count % self._config.attribution_interval_ticks == 0:
            try:
                # Compute portfolio return from NAV
                hist = self._nav.history(3)
                if len(hist) >= 2:
                    prev_nav_val = hist[-2].total_nav
                    curr_nav_val = hist[-1].total_nav
                    if prev_nav_val > 0:
                        port_ret = (curr_nav_val - prev_nav_val) / prev_nav_val
                        self._perf_attribution.update(port_ret, 0.0)
                        snap = self._perf_attribution.compute()
                        if snap.alpha_beta is not None:
                            logger.debug(
                                "Attribution: alpha=%.4f beta=%.2f IR=%.2f",
                                snap.alpha_beta.alpha,
                                snap.alpha_beta.beta,
                                snap.alpha_beta.information_ratio,
                            )
            except Exception:
                pass

        # 20. Self-healing: recovery checks (every 10 ticks)
        if self._recovery is not None and self._oversight_tick_count % 10 == 0:
            try:
                recovery_events = self._recovery.check_strategies(self._controller)
                for ev in recovery_events:
                    self._alert_mgr.fire(Alert(
                        level=AlertLevel.WARNING if not ev.success else AlertLevel.INFO,
                        category=AlertCategory.RECOVERY,
                        message=f"Recovery {ev.action.value} for {ev.target}: {'OK' if ev.success else ev.error}",
                        timestamp=time.time(),
                        strategy_name=ev.target,
                    ))
                # Also check feeds
                feed_events = self._recovery.check_feeds(engines, self._controller)
                for ev in feed_events:
                    self._alert_mgr.fire(Alert(
                        level=AlertLevel.WARNING,
                        category=AlertCategory.EXCHANGE_OUTAGE,
                        message=ev.error or f"Stale feed: {ev.target}",
                        timestamp=time.time(),
                    ))
            except Exception:
                pass

        # 21. Promotion checks (every 60 ticks)
        if self._promotion is not None and self._oversight_tick_count % 60 == 0:
            try:
                for status_obj in self._controller.all_statuses():
                    if status_obj.state != StrategyState.RUNNING:
                        continue
                    new_stage = self._promotion.check_promotion(status_obj.name, status_obj)
                    if new_stage is not None:
                        self._promotion.promote(status_obj.name, new_stage)
                        self._alert_mgr.fire(Alert(
                            level=AlertLevel.INFO,
                            category=AlertCategory.PROMOTION_READY,
                            message=f"Strategy '{status_obj.name}' promoted to {new_stage.value}",
                            timestamp=time.time(),
                            strategy_name=status_obj.name,
                        ))
            except Exception:
                pass

        # 22. Adaptive threshold tuning (every 120 ticks)
        if self._threshold_tuner is not None and self._oversight_tick_count % 120 == 0:
            try:
                new_thresholds = self._threshold_tuner.tune()
                # Apply to decision framework if agent exists
                if hasattr(self, '_agent') and self._agent is not None:
                    self._agent._decisions.set_thresholds(new_thresholds)
            except Exception:
                pass

        # Log summary
        if alerts:
            for alert in alerts:
                logger.warning("Risk alert: %s", alert.message)
        else:
            logger.debug(
                "Oversight: NAV=%.2f DD=%.1f%% strategies=%d",
                nav.total_nav, nav.drawdown_pct,
                self._controller.running_count(),
            )
