"""
APEX V16 — Runtime configuration.

Single source of truth for paths, modes, account selection, and
runtime defaults. Asset-specific config (tick values, contracts)
lives in config_futures.py.

Environment variables (.env) override defaults where applicable.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional


# ============================================================
# PATHS
# ============================================================

PROJECT_ROOT = Path(__file__).resolve().parent.parent  # ~/apex_v16/
STATE_DIR = PROJECT_ROOT / "state"
LOGS_DIR = PROJECT_ROOT / "logs"


# ============================================================
# MODE / ACCOUNT enums
# ============================================================

class RunMode(str, Enum):
    DRY = "dry"          # connected to broker, NO trades placed
    PAPER = "paper"      # internal simulation, no broker calls
    LIVE = "live"        # real trades on selected account


class AccountKind(str, Enum):
    INELIGIBLE = "ineligible"   # burned $100K combine
    EXPRESS = "express"         # FUNDED $100K express


# Topstep account IDs (from .env, with safe defaults matching
# the user's known accounts; .env always wins).
ACCOUNT_IDS = {
    AccountKind.INELIGIBLE: os.getenv("TOPSTEP_INELIGIBLE_ID",
                                       "100KTC-V2-566414-41410336"),
    AccountKind.EXPRESS:    os.getenv("TOPSTEP_EXPRESS_ID",
                                       "EXPRESS-V2-566414-12462346"),
}


# ============================================================
# RUNTIME DEFAULTS (overridable via .env or CLI)
# ============================================================

@dataclass
class RuntimeConfig:
    """
    Runtime parameters. Built once at startup from CLI + .env.
    Passed by reference to orchestrator and risk_manager.
    """
    # Mode
    mode: RunMode = RunMode.PAPER
    account: AccountKind = AccountKind.INELIGIBLE
    fresh_start: bool = False

    # Asset filter (None = all assets from config_futures.py)
    asset_filter: Optional[list[str]] = None

    # Risk parameters (defaults conservative; tightened for EXPRESS)
    risk_per_trade: float = 0.003        # 0.3% per trade
    daily_profit_target: float = 2000.0   # USD
    daily_loss_hard_stop: float = -1500.0  # USD (negative = loss; auto-halt)
    daily_loss_soft_stop: float = -1000.0  # USD; block entries, NO halt
                                            # V15 ratio was soft/hard = 0.50;
                                            # V16 ratio = 1000/1500 = 0.67.
                                            # DA RIVALIDARE IN CALIBRAZIONE V16.
    max_daily_trades: int = 20           # V16-new: trades executed today
    max_open_trades_total: int = 5       # V15 MAX_OPEN_TRADES_TOTAL parity
    max_risk_vs_daily_budget: float = 0.33

    # Account balance proxy for PAPER/DRY/test paths. LIVE will fetch
    # broker.get_account_balance() in Phase C. V15 paper standard = 100k.
    dry_run_balance: float = 100_000.0

    # Correlation / structural filters
    enable_correlation: bool = False     # OFF for ineligible testing; ON for express
    last_friday_of_month_cutoff_utc_hour: int = 15
    force_flat_utc_hour: int = 21
    force_flat_utc_minute: int = 8

    # DEPRECATED — loop ora disaccoppiato in 3 cadenze separate
    # (scan_loop / manage_loop / maintenance_loop). Mantenuto per
    # backward-compat: alcuni test legacy lo settano in make_config.
    # Non più letto dall'orchestrator.
    loop_sleep_seconds: int = 15

    # Loop architecture (V16, refactor 2026-05-01):
    #   scan_loop:        schedulato sui boundary M5; entry evaluation
    #                     completa per tutti gli asset una volta per candela.
    #                     `scan_loop_phase_offset_seconds` = delay post-close
    #                     (deve coprire candle_close_delay_seconds + margine).
    #   manage_loop:      polling SL/TP/trail su trade attivi. Salta tick
    #                     se nessun trade aperto (zero REST a flat book).
    #   maintenance_loop: balance + watchdog + reconnect. Runs sempre.
    scan_loop_phase_offset_seconds: float = 6.0
    manage_loop_interval_seconds: int = 20
    maintenance_loop_interval_seconds: int = 60

    # Candle-state gating for entry AI calls (V16 patch, 29 apr).
    # Brain.evaluate_entry only invokes AI when:
    #   candle_age_seconds in [candle_close_delay_seconds, candle_max_age_seconds]
    # Combined with same-candle dedup -> exactly 1 AI call per M5 candle
    # in the post-close window. Re-calibrate in CALIBRAZIONE 5-9 may
    # if the SDK consolidation timing or loop cadence changes.
    candle_close_delay_seconds: float = 5.0
    candle_max_age_seconds: float = 60.0

    # Watchdog cadence: now expressed as "every N maintenance_loop iter".
    # Default 1 → watchdog ogni 60s (parità V15 ≈30s rilassata di 30s, ma
    # le posizioni sono comunque coperte dal _check_external_close ogni
    # manage tick = 20s). 0 disabilita.
    reconcile_interval_iterations: int = 1

    # C2b — broker resilience knobs.
    # Mid-loop reconnect uses _connect_with_retry with these parameters:
    #   max_attempts: more than startup (10 vs 3) — Topstep maintenance
    #     windows can last several minutes.
    #   delays_seconds: longer cumulative window (~10min total) than the
    #     startup retry (35s). Mid-loop has open positions to manage,
    #     so it's safer to wait than to fail fast.
    mid_loop_reconnect_max_attempts: int = 10
    mid_loop_reconnect_delays_seconds: tuple = (5, 10, 20, 30, 30, 60, 60, 60, 120, 120)
    # Hard timeout on degraded state. After this, orchestrator exits with
    # EXIT_BROKER_UNRECOVERABLE (4) and cron/systemd recycles the process.
    broker_degraded_max_minutes: int = 15

    # V18 12-mag — anti-freeze envelope timeouts.
    # Coprono ogni async I/O del loop principale (build_tech, balance,
    # watchdog, news, positions_get). Senza timeout, una chiamata SDK
    # WebSocket stalled bloccava il loop > 10 min finché watchdog.sh
    # rilevava system.log stale e mandava SIGTERM, riavviando in ciclo.
    market_data_timeout_seconds: float = 30.0   # build_tech (3 fetch TF)
    h4_fetch_timeout_seconds: float = 15.0
    broker_call_timeout_seconds: float = 10.0   # balance / positions
    watchdog_timeout_seconds: float = 30.0
    news_sync_timeout_seconds: float = 30.0
    # Heartbeat: maintenance_loop scrive una riga in system.log a ogni
    # tick anche in assenza di eventi, così watchdog.sh (stale > 600s)
    # vede liveness pure su book vuoto / mercati chiusi. Set False per
    # silenziare in test (loop_phase_offset==0 implica scan/maintenance
    # rapidi e log molto rumorosi).
    maintenance_heartbeat_enabled: bool = True

    # Caps for sessions
    max_trades_session: int = 999

    # AI — V16 uses Anthropic Claude (V15 used Google Gemini). The
    # AIClient (brain/ai_client.py) is the source of truth: it
    # instantiates anthropic.Anthropic and reads ANTHROPIC_API_KEY.
    # If you re-introduce Gemini, change BOTH layers together.
    ai_model: str = "claude-haiku-4-5-20251001"
    ai_keys_env_prefix: str = "ANTHROPIC_API_KEY"

    # Computed paths (set by post_init)
    state_file: Path = field(init=False)
    log_dir: Path = field(init=False)

    def __post_init__(self) -> None:
        # State file is unique per (mode, account) — never share state across modes
        scope = f"{self.mode.value}_{self.account.value}"
        self.state_file = STATE_DIR / f"state_{scope}.json"
        self.log_dir = LOGS_DIR / scope

        # Ensure directories exist
        STATE_DIR.mkdir(parents=True, exist_ok=True)
        self.log_dir.mkdir(parents=True, exist_ok=True)

    @property
    def is_live(self) -> bool:
        return self.mode == RunMode.LIVE

    @property
    def is_express(self) -> bool:
        return self.account == AccountKind.EXPRESS

    @property
    def account_id(self) -> str:
        return ACCOUNT_IDS[self.account]

    def require_express_confirmation(self) -> None:
        """
        Interactive safety check before live trading on EXPRESS (funded).
        Called by main.py before orchestrator starts.
        """
        if not (self.is_live and self.is_express):
            return
        print("=" * 60)
        print("WARNING: LIVE TRADING ON EXPRESS (FUNDED) ACCOUNT")
        print(f"Account ID: {self.account_id}")
        print(f"Risk per trade: {self.risk_per_trade * 100:.2f}%")
        print(f"Daily loss hard stop: ${self.daily_loss_hard_stop:.2f}")
        print(f"Correlation enabled: {self.enable_correlation}")
        print("=" * 60)
        ans = input("Type EXPRESS to confirm: ").strip()
        if ans != "EXPRESS":
            raise SystemExit("Aborted: EXPRESS confirmation not given.")


# ============================================================
# Tightened profile for EXPRESS (applied automatically when
# account=EXPRESS, unless overridden via CLI).
# ============================================================

def apply_express_profile(cfg: RuntimeConfig) -> RuntimeConfig:
    """
    Returns a tightened copy of cfg suitable for funded trading.
    Caller decides whether to use it (e.g., main.py applies by default
    when --account express is selected).
    """
    cfg.risk_per_trade = 0.0015           # half: 0.15%
    cfg.enable_correlation = True
    cfg.max_risk_vs_daily_budget = 0.33
    cfg.daily_loss_hard_stop = -1500.0
    return cfg
