"""
================================================================================
ACCOUNT MANAGER — Real Account State & Persistent P&L Tracking
================================================================================
Quantum Terminal | Layer 3 — Account & Risk Management

Bridges the signal engine to real account data. Reads MT5 account balance/equity,
tracks daily and weekly P&L with persistence across restarts, and provides
configurable trading rules for prop firm compliance.

Architecture:
    MT5 Account ──► AccountManager ──► PortfolioState (signal engine)
                         │
                         ▼
                  account_state.json (persistent)
                         │
                         ▼
                  RiskGovernor (pre-trade checks)

Settings are stored in account_settings.json (user-editable from terminal UI).
Account state (daily P&L, peak equity, etc.) is stored in account_state.json.

Usage:
    from account_manager import AccountManager
    mgr = AccountManager()
    mgr.sync_from_mt5()
    portfolio = mgr.get_portfolio_state()  # feeds into signal engine
    mgr.record_trade_result(pnl_usd=150.0)

Dependencies: MetaTrader5 (optional), json, pathlib
================================================================================
"""

import json
import logging
import os
from dataclasses import dataclass, field, asdict
from typing import Dict, List, Optional
from datetime import datetime, date, timedelta
from pathlib import Path

# version: v4 (broker-TZ-aware period-pnl bucketing)
# v4 — get_period_pnl now corrects for the broker TZ before bucketing
#      deals into today/yesterday/this_week/etc. The MetaTrader5 library
#      returns deal.time as broker server time treated as a Unix epoch
#      (i.e. broker_local seconds, not real UTC). Our boundaries
#      (datetime.now() + datetime(year, month, day)) are in real UTC
#      via .timestamp(). Without correction, the comparison was off by
#      the broker's TZ offset — for an operator + broker both at GMT+3,
#      ~3h of deals spilled across day boundaries, e.g. yesterday-late
#      trades counted as today and vice versa. v4 detects the broker
#      offset from a recent tick (round to whole hours) and converts
#      d.time → real-UTC before comparison.
log = logging.getLogger("mk.account_manager")

PROJECT_ROOT = Path(__file__).resolve().parent

# v3: AppData dir name from env var so v1 / v2 builds get separate dirs.
APP_DIR = os.environ.get("MK_APP_DIR_NAME", "QuantumTerminal")


def _get_user_data_dir() -> Path:
    """Per-user writable dir for account_settings.json + account_state.json.
    Fixes Errno 13 Permission denied under Program Files on installed consumer.
    v3: AppData dir name comes from MK_APP_DIR_NAME (default "QuantumTerminal")."""
    import platform as _platform
    if _platform.system() == "Windows":
        appdata = os.environ.get("APPDATA", "")
        base = Path(appdata) / APP_DIR if appdata else \
               Path.home() / "AppData" / "Roaming" / APP_DIR
    else:
        base = Path.home() / f".{APP_DIR.lower()}"
    base.mkdir(parents=True, exist_ok=True)
    return base


def _resolve_default_settings_path() -> Path:
    """v2: Prefer the AppData path. If a legacy file exists next to the module
    (dev-mode / old install), migrate it on first use."""
    user_dir = _get_user_data_dir()
    user_path = user_dir / "account_settings.json"
    legacy = PROJECT_ROOT / "account_settings.json"
    if legacy.exists() and not user_path.exists():
        try:
            user_path.write_bytes(legacy.read_bytes())
            log.info(f"Migrated {legacy} → {user_path}")
        except Exception as e:
            log.warning(f"Settings migration failed: {e}")
    return user_path


def _resolve_default_state_path() -> Path:
    user_dir = _get_user_data_dir()
    user_path = user_dir / "account_state.json"
    legacy = PROJECT_ROOT / "account_state.json"
    if legacy.exists() and not user_path.exists():
        try:
            user_path.write_bytes(legacy.read_bytes())
            log.info(f"Migrated {legacy} → {user_path}")
        except Exception as e:
            log.warning(f"State migration failed: {e}")
    return user_path


# ============================================================
# 1. TRADING SETTINGS (user-configurable)
# ============================================================

@dataclass
class TradingSettings:
    """
    User-configurable trading rules. Saved to account_settings.json.
    Editable from the terminal settings panel.
    """
    # -- Account --
    account_size: float = 100_000.0        # Initial account balance (or prop firm start)
    currency: str = "USD"

    # -- Prop Firm Rules --
    max_daily_loss_pct: float = 2.0        # Max daily loss as % of initial balance
    max_daily_profit_pct: float = 0.0      # Max daily profit lock (0 = disabled)
    max_total_drawdown_pct: float = 10.0   # Max drawdown from initial balance
    max_trailing_drawdown_pct: float = 0.0 # Max drawdown from peak equity (0 = disabled)

    # -- Position Limits --
    max_open_positions: int = 4
    max_correlated_positions: int = 2      # Max same-direction in correlated group
    risk_per_trade_pct: float = 0.5        # Fixed fractional risk per trade

    # -- Kelly --
    use_kelly: bool = True
    kelly_fraction: float = 0.25           # Quarter-Kelly

    # -- Calibration Gating --
    min_calibration_grade: str = "C"       # Filter signals below this grade
    require_calibration: bool = False      # If True, only trade calibrated setups

    # -- Session Rules --
    trading_enabled: bool = True           # Master switch
    auto_sync_mt5: bool = True             # Auto-sync equity from MT5 on startup

    # -- Auto Trading (Phase 3G) --
    auto_trading_enabled: bool = False     # Auto-execute signals (separate from live_trading)
    auto_min_confidence: float = 0.60      # Min signal confidence to auto-trade
    auto_min_grade: str = "C"              # Min calibration grade to auto-trade
    auto_max_daily_trades: int = 5         # Max auto-trades per day
    auto_allowed_types: list = field(default_factory=lambda: [
        "cone_boundary_fade", "drift_momentum", "institutional_anchor",
        "cone_convergence", "regime_transition", "cone_breakout",
    ])
    auto_allowed_tickers: list = field(default_factory=list)  # Empty = all universe
    auto_scan_interval: float = 30.0       # Seconds between scans
    auto_log_only: bool = True             # True = dry run (log but don't execute)

    # -- Scheduler (Phase 3G) --
    scheduler_enabled: bool = True
    scheduler_weekly_enabled: bool = True
    scheduler_weekly_day: int = 6              # 0=Mon, 6=Sun
    scheduler_weekly_time: str = "21:30"       # HH:MM UTC
    scheduler_daily_enabled: bool = True
    scheduler_daily_time: str = "21:30"        # HH:MM UTC
    scheduler_daily_skip_weekends: bool = True
    scheduler_calc_before_weekly: bool = True   # Run cones before weekly orch
    # Calibration
    calibration_workers: int = 1               # CPU threads for ATR/management sweep
    scheduler_calc_modules: str = "anchors,cones"

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict) -> 'TradingSettings':
        # Only take keys that exist in the dataclass
        valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in d.items() if k in valid_keys}
        return cls(**filtered)


# ============================================================
# 2. ACCOUNT STATE (persistent)
# ============================================================

@dataclass
class AccountState:
    """
    Persistent account state — survives server restarts.
    Saved to account_state.json.
    """
    # -- Equity tracking --
    initial_balance: float = 100_000.0     # Set once at start (prop firm funded amount)
    current_equity: float = 100_000.0      # Last known equity
    peak_equity: float = 100_000.0         # Highest equity ever (for trailing DD)

    # -- Daily tracking --
    daily_start_equity: float = 100_000.0  # Equity at start of current day
    daily_pnl: float = 0.0                 # P&L since daily reset
    daily_trades: int = 0                  # Trade count today
    current_date: str = ""                 # ISO date of current tracking day

    # -- Weekly tracking --
    weekly_start_equity: float = 100_000.0
    weekly_pnl: float = 0.0
    weekly_trades: int = 0
    current_week: str = ""                 # ISO week string (e.g., "2026-W12")

    # -- Circuit breaker states --
    daily_loss_halt: bool = False           # True = daily loss limit hit
    daily_profit_lock: bool = False         # True = daily profit target hit
    total_drawdown_halt: bool = False       # True = max drawdown breached
    trailing_drawdown_halt: bool = False    # True = trailing DD breached

    # -- Trade log (lightweight) --
    recent_trades: List[Dict] = field(default_factory=list)  # Last 50 trades

    # -- Timestamps --
    last_mt5_sync: str = ""
    last_updated: str = ""

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict) -> 'AccountState':
        valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in d.items() if k in valid_keys}
        # Handle list fields that may come as None
        if 'recent_trades' not in filtered or filtered['recent_trades'] is None:
            filtered['recent_trades'] = []
        return cls(**filtered)


# ============================================================
# 3. ACCOUNT MANAGER
# ============================================================

class AccountManager:
    """
    Manages account state, MT5 sync, and trading settings.
    """

    def __init__(self, settings_path: Optional[Path] = None,
                 state_path: Optional[Path] = None):
        # v2: default to %APPDATA%\QuantumTerminal\ (writable) instead of the module
        # directory, which lives under Program Files and is read-only for
        # non-admin users. Legacy files are migrated on first use.
        self._settings_path = settings_path or _resolve_default_settings_path()
        self._state_path = state_path or _resolve_default_state_path()

        self.settings = self._load_settings()
        self.state = self._load_state()

        # Check for day/week rollover
        self._check_daily_reset()
        self._check_weekly_reset()

    # ────────────────────────────────────────────────────────
    # PERSISTENCE
    # ────────────────────────────────────────────────────────

    def _load_settings(self) -> TradingSettings:
        """Load or create settings file."""
        if self._settings_path.exists():
            try:
                with open(self._settings_path) as f:
                    data = json.load(f)
                log.info(f"Loaded trading settings from {self._settings_path}")
                return TradingSettings.from_dict(data)
            except Exception as e:
                log.warning(f"Failed to load settings: {e} — using defaults")

        settings = TradingSettings()
        self._save_settings(settings)
        return settings

    def _save_settings(self, settings: TradingSettings = None):
        """Save settings to JSON."""
        if settings is None:
            settings = self.settings
        try:
            with open(self._settings_path, "w") as f:
                json.dump(settings.to_dict(), f, indent=2)
        except Exception as e:
            log.warning(f"Failed to save settings: {e}")

    def _load_state(self) -> AccountState:
        """Load or create state file."""
        if self._state_path.exists():
            try:
                with open(self._state_path) as f:
                    data = json.load(f)
                log.info(f"Loaded account state from {self._state_path}")
                return AccountState.from_dict(data)
            except Exception as e:
                log.warning(f"Failed to load state: {e} — using defaults")

        state = AccountState(
            initial_balance=self.settings.account_size,
            current_equity=self.settings.account_size,
            peak_equity=self.settings.account_size,
            daily_start_equity=self.settings.account_size,
            weekly_start_equity=self.settings.account_size,
        )
        self._save_state(state)
        return state

    def _save_state(self, state: AccountState = None):
        """Save state to JSON."""
        if state is None:
            state = self.state
        state.last_updated = datetime.now().isoformat()
        try:
            with open(self._state_path, "w") as f:
                json.dump(state.to_dict(), f, indent=2, default=str)
        except Exception as e:
            log.warning(f"Failed to save state: {e}")

    def update_settings(self, new_settings: dict):
        """Update settings from terminal UI (partial update)."""
        old_account_size = self.settings.account_size
        current = self.settings.to_dict()
        current.update(new_settings)
        self.settings = TradingSettings.from_dict(current)
        self._save_settings()
        log.info(f"Trading settings updated: {list(new_settings.keys())}")

        # Auto-reset baseline when account_size changes
        if "account_size" in new_settings and new_settings["account_size"] != old_account_size:
            self.reset_baseline(self.settings.account_size)

    def reset_baseline(self, new_balance: float = None):
        """
        Reset account state baseline. Call when:
        - Setting up a new funded account
        - Account size changes in settings
        - Starting fresh after a reset

        Sets initial_balance, peak_equity, daily/weekly start to the new value.
        Clears all circuit breaker halts. Preserves current_equity if MT5-synced.
        """
        bal = new_balance or self.settings.account_size
        equity = self.state.current_equity if self.state.current_equity > 0 else bal

        self.state.initial_balance = bal
        self.state.peak_equity = max(bal, equity)
        self.state.daily_start_equity = equity
        self.state.weekly_start_equity = equity
        self.state.daily_pnl = 0.0
        self.state.weekly_pnl = 0.0
        self.state.daily_trades = 0
        self.state.weekly_trades = 0

        # Clear all circuit breaker halts
        self.state.daily_loss_halt = False
        self.state.daily_profit_lock = False
        self.state.total_drawdown_halt = False
        self.state.trailing_drawdown_halt = False

        self._save_state()
        log.info(f"Account baseline reset: initial_balance=${bal:,.2f}, "
                 f"equity=${equity:,.2f}, all circuit breakers cleared")

    # ────────────────────────────────────────────────────────
    # MT5 SYNC
    # ────────────────────────────────────────────────────────

    def sync_from_mt5(self) -> bool:
        """
        Read real account equity from MT5.
        Returns True if sync successful.
        """
        try:
            import MetaTrader5 as mt5

            if not mt5.terminal_info():
                log.warning("MT5 not connected — cannot sync account")
                return False

            account = mt5.account_info()
            if account is None:
                log.warning("MT5 account_info() returned None")
                return False

            equity = account.equity
            balance = account.balance

            self.state.current_equity = equity
            self.state.peak_equity = max(self.state.peak_equity, equity)
            self.state.last_mt5_sync = datetime.now().isoformat()

            # Update daily P&L
            self.state.daily_pnl = equity - self.state.daily_start_equity
            self.state.weekly_pnl = equity - self.state.weekly_start_equity

            # Check circuit breakers
            self._check_circuit_breakers()

            self._save_state()
            log.info(f"MT5 sync: equity=${equity:,.2f}, balance=${balance:,.2f}, "
                     f"daily P&L=${self.state.daily_pnl:+,.2f}")
            return True

        except ImportError:
            log.info("MetaTrader5 package not available — manual equity mode")
            return False
        except Exception as e:
            log.warning(f"MT5 sync failed: {e}")
            return False

    def sync_positions_from_mt5(self) -> List[Dict]:
        """Read open positions from MT5."""
        try:
            import MetaTrader5 as mt5

            if not mt5.terminal_info():
                return []

            positions = mt5.positions_get()
            if positions is None:
                return []

            result = []
            for pos in positions:
                result.append({
                    'ticket': pos.ticket,
                    'ticker': pos.symbol,
                    'direction': 'long' if pos.type == 0 else 'short',
                    'size': pos.volume,
                    'entry': pos.price_open,
                    'current_price': pos.price_current,
                    'current_pnl': pos.profit,
                    'sl': pos.sl,
                    'tp': pos.tp,
                    'magic': pos.magic,
                    'comment': pos.comment,
                })

            return result

        except (ImportError, Exception):
            return []

    # ────────────────────────────────────────────────────────
    # DAILY/WEEKLY RESETS
    # ────────────────────────────────────────────────────────

    def _check_daily_reset(self):
        """Reset daily tracking if it's a new day."""
        today = date.today().isoformat()
        if self.state.current_date != today:
            if self.state.current_date:
                log.info(f"Daily reset: {self.state.current_date} → {today} "
                         f"(yesterday P&L: ${self.state.daily_pnl:+,.2f})")
            self.state.daily_start_equity = self.state.current_equity
            self.state.daily_pnl = 0.0
            self.state.daily_trades = 0
            self.state.daily_loss_halt = False
            self.state.daily_profit_lock = False
            self.state.current_date = today
            self._save_state()

    def _check_weekly_reset(self):
        """Reset weekly tracking if it's a new week."""
        current_week = date.today().isocalendar()
        week_str = f"{current_week[0]}-W{current_week[1]:02d}"
        if self.state.current_week != week_str:
            if self.state.current_week:
                log.info(f"Weekly reset: {self.state.current_week} → {week_str} "
                         f"(last week P&L: ${self.state.weekly_pnl:+,.2f})")
            self.state.weekly_start_equity = self.state.current_equity
            self.state.weekly_pnl = 0.0
            self.state.weekly_trades = 0
            self.state.current_week = week_str
            self._save_state()

    # ────────────────────────────────────────────────────────
    # CIRCUIT BREAKERS
    # ────────────────────────────────────────────────────────

    def _check_circuit_breakers(self):
        """Check all prop firm circuit breakers."""
        s = self.settings
        st = self.state

        # Daily loss limit
        if s.max_daily_loss_pct > 0:
            daily_loss_limit = st.initial_balance * (s.max_daily_loss_pct / 100)
            if st.daily_pnl <= -daily_loss_limit:
                if not st.daily_loss_halt:
                    log.warning(f"🛑 DAILY LOSS LIMIT HIT: ${st.daily_pnl:+,.2f} "
                                f"exceeds -{s.max_daily_loss_pct}% of ${st.initial_balance:,.0f}")
                st.daily_loss_halt = True

        # Daily profit lock
        if s.max_daily_profit_pct > 0:
            daily_profit_limit = st.initial_balance * (s.max_daily_profit_pct / 100)
            if st.daily_pnl >= daily_profit_limit:
                if not st.daily_profit_lock:
                    log.info(f"🔒 DAILY PROFIT LOCK: ${st.daily_pnl:+,.2f} "
                             f"exceeds +{s.max_daily_profit_pct}% target")
                st.daily_profit_lock = True

        # Total drawdown from initial
        if s.max_total_drawdown_pct > 0:
            total_dd = (st.initial_balance - st.current_equity) / st.initial_balance * 100
            if total_dd >= s.max_total_drawdown_pct:
                if not st.total_drawdown_halt:
                    log.warning(f"🛑 MAX DRAWDOWN BREACHED: {total_dd:.1f}% from initial "
                                f"(limit: {s.max_total_drawdown_pct}%)")
                st.total_drawdown_halt = True

        # Trailing drawdown from peak
        if s.max_trailing_drawdown_pct > 0 and st.peak_equity > 0:
            trailing_dd = (st.peak_equity - st.current_equity) / st.peak_equity * 100
            if trailing_dd >= s.max_trailing_drawdown_pct:
                if not st.trailing_drawdown_halt:
                    log.warning(f"🛑 TRAILING DRAWDOWN BREACHED: {trailing_dd:.1f}% from peak "
                                f"${st.peak_equity:,.2f} (limit: {s.max_trailing_drawdown_pct}%)")
                st.trailing_drawdown_halt = True

    @property
    def is_trading_allowed(self) -> bool:
        """Check if trading is allowed based on all circuit breakers."""
        if not self.settings.trading_enabled:
            return False
        st = self.state
        return not (st.daily_loss_halt or st.daily_profit_lock or
                    st.total_drawdown_halt or st.trailing_drawdown_halt)

    @property
    def halt_reason(self) -> str:
        """Get the reason trading is halted, or empty string."""
        if not self.settings.trading_enabled:
            return "Trading disabled in settings"
        st = self.state
        reasons = []
        if st.daily_loss_halt:
            reasons.append(f"Daily loss limit ({self.settings.max_daily_loss_pct}%)")
        if st.daily_profit_lock:
            reasons.append(f"Daily profit locked ({self.settings.max_daily_profit_pct}%)")
        if st.total_drawdown_halt:
            reasons.append(f"Max drawdown ({self.settings.max_total_drawdown_pct}%)")
        if st.trailing_drawdown_halt:
            reasons.append(f"Trailing drawdown ({self.settings.max_trailing_drawdown_pct}%)")
        return " | ".join(reasons)

    # ────────────────────────────────────────────────────────
    # TRADE RECORDING
    # ────────────────────────────────────────────────────────

    def record_trade_result(self, pnl_usd: float, ticker: str = "",
                            direction: str = "", signal_type: str = "",
                            lots: float = 0.0, magic: int = 0):
        """
        Record a trade result and update P&L tracking.
        Called when a trade closes (from lifecycle manager or MT5 bridge).
        """
        self.state.daily_pnl += pnl_usd
        self.state.weekly_pnl += pnl_usd
        self.state.daily_trades += 1
        self.state.weekly_trades += 1
        self.state.current_equity += pnl_usd
        self.state.peak_equity = max(self.state.peak_equity, self.state.current_equity)

        # Append to recent trades (keep last 50)
        self.state.recent_trades.append({
            'timestamp': datetime.now().isoformat(),
            'ticker': ticker,
            'direction': direction,
            'signal_type': signal_type,
            'lots': lots,
            'pnl_usd': round(pnl_usd, 2),
            'magic': magic,
            'equity_after': round(self.state.current_equity, 2),
        })
        if len(self.state.recent_trades) > 50:
            self.state.recent_trades = self.state.recent_trades[-50:]

        self._check_circuit_breakers()
        self._save_state()

        log.info(f"Trade recorded: {ticker} {direction} {signal_type} → "
                 f"PnL=${pnl_usd:+,.2f} | Daily=${self.state.daily_pnl:+,.2f} | "
                 f"Equity=${self.state.current_equity:,.2f}")

    # ────────────────────────────────────────────────────────
    # BRIDGE TO SIGNAL ENGINE
    # ────────────────────────────────────────────────────────

    def get_portfolio_state(self):
        """
        Build a PortfolioState for the signal engine from current account data.
        """
        # Import here to avoid circular dependency
        from signal_engine_v2 import PortfolioState

        positions = self.sync_positions_from_mt5()

        return PortfolioState(
            equity=self.state.current_equity,
            open_positions=[
                {
                    'ticker': p['ticker'],
                    'direction': p['direction'],
                    'size': p['size'],
                    'entry': p['entry'],
                    'current_pnl': p['current_pnl'],
                    'risk_usd': abs(p['entry'] - p.get('sl', p['entry'])) * p['size'],
                }
                for p in positions
            ],
            daily_pnl=self.state.daily_pnl,
            weekly_pnl=self.state.weekly_pnl,
            peak_equity=self.state.peak_equity,
        )

    # ────────────────────────────────────────────────────────
    # REST API HELPERS (for data_server.py endpoints)
    # ────────────────────────────────────────────────────────

    def get_status_dict(self) -> dict:
        """Get full account status for the terminal dashboard."""
        s = self.settings
        st = self.state

        daily_loss_limit = st.initial_balance * (s.max_daily_loss_pct / 100) if s.max_daily_loss_pct > 0 else 0
        daily_profit_limit = st.initial_balance * (s.max_daily_profit_pct / 100) if s.max_daily_profit_pct > 0 else 0
        total_dd = (st.initial_balance - st.current_equity) / st.initial_balance * 100 if st.initial_balance > 0 else 0
        trailing_dd = (st.peak_equity - st.current_equity) / st.peak_equity * 100 if st.peak_equity > 0 else 0

        return {
            "trading_allowed": self.is_trading_allowed,
            "halt_reason": self.halt_reason,
            "equity": round(st.current_equity, 2),
            "initial_balance": round(st.initial_balance, 2),
            "peak_equity": round(st.peak_equity, 2),
            "daily_pnl": round(st.daily_pnl, 2),
            "daily_pnl_pct": round(st.daily_pnl / st.initial_balance * 100, 2) if st.initial_balance > 0 else 0,
            "daily_loss_limit": round(daily_loss_limit, 2),
            "daily_profit_limit": round(daily_profit_limit, 2),
            "daily_loss_halt": st.daily_loss_halt,
            "daily_profit_lock": st.daily_profit_lock,
            "weekly_pnl": round(st.weekly_pnl, 2),
            "total_drawdown_pct": round(total_dd, 2),
            "trailing_drawdown_pct": round(trailing_dd, 2),
            "total_drawdown_halt": st.total_drawdown_halt,
            "trailing_drawdown_halt": st.trailing_drawdown_halt,
            "daily_trades": st.daily_trades,
            "weekly_trades": st.weekly_trades,
            "open_positions": len(self.sync_positions_from_mt5()),
            "max_positions": s.max_open_positions,
            "last_mt5_sync": st.last_mt5_sync,
            "last_updated": st.last_updated,
        }

    def get_settings_dict(self) -> dict:
        """Get current settings for the terminal UI."""
        return self.settings.to_dict()

    # ────────────────────────────────────────────────────────
    # MT5 DEAL HISTORY — REAL PERIOD P&L
    # ────────────────────────────────────────────────────────

    def get_period_pnl(self) -> dict:
        """
        Query MT5 closed deal history for accurate period P&L.

        Returns real closed-trade P&L (profit + swap + commission) for:
          - today, yesterday
          - this week, last week
          - this month, last month

        This is the source of truth — NOT the equity-delta tracking
        which drifts when weekly_start_equity is stale.
        """
        try:
            import MetaTrader5 as mt5
            import time as _time

            if not mt5.terminal_info():
                return self._empty_period_pnl("MT5 not connected")

            # v4: detect broker TZ offset (broker_time treated as UTC -
            # real_UTC). Round to whole hours since broker TZs are integer
            # offsets. Falls back to 0 (treat as UTC broker) on failure.
            broker_offset_sec = 0
            try:
                for sym in ("EURUSD", "XAUUSD", "USDJPY", "BTCUSD"):
                    tick = mt5.symbol_info_tick(sym)
                    if tick and getattr(tick, "time", 0):
                        raw = int(tick.time) - int(_time.time())
                        broker_offset_sec = round(raw / 3600) * 3600
                        # Sanity: clamp to ±14h (no real broker outside that)
                        if abs(broker_offset_sec) > 14 * 3600:
                            broker_offset_sec = 0
                        break
            except Exception as e:
                log.debug(f"broker offset detection failed: {e}")

            now = datetime.now()
            today_start = datetime(now.year, now.month, now.day)
            yesterday_start = today_start - timedelta(days=1)

            # Week boundaries (Monday 00:00)
            weekday = now.weekday()  # 0=Mon
            this_week_start = today_start - timedelta(days=weekday)
            last_week_start = this_week_start - timedelta(days=7)
            last_week_end = this_week_start

            # Month boundaries
            this_month_start = datetime(now.year, now.month, 1)
            if now.month == 1:
                last_month_start = datetime(now.year - 1, 12, 1)
            else:
                last_month_start = datetime(now.year, now.month - 1, 1)
            last_month_end = this_month_start

            # v4: fetch a wider window than strictly needed so deals near the
            # boundaries don't get clipped by MT5's own pseudo-UTC interpretation
            # of the input range. We filter precisely in Python below.
            query_from = last_month_start - timedelta(days=2)
            query_to   = now + timedelta(days=2)
            all_deals = mt5.history_deals_get(query_from, query_to)
            if all_deals is None:
                all_deals = ()

            # Filter to closing deals only (entry: 1=out, 2=inout, 3=out_by)
            # Deal type 6 = balance operations — skip those
            close_deals = [
                d for d in all_deals
                if d.entry in (1, 2, 3) and d.type not in (6,)
            ]

            def _sum_pnl(deals, dt_from, dt_to):
                """Sum profit + swap + commission for deals in [dt_from, dt_to).
                v4: convert d.time (broker pseudo-UTC) to real UTC before
                comparing against the boundaries (which are real UTC via
                .timestamp())."""
                ts_from = dt_from.timestamp()
                ts_to = dt_to.timestamp()
                total = 0.0
                count = 0
                for d in deals:
                    d_real_utc = d.time - broker_offset_sec
                    if ts_from <= d_real_utc < ts_to:
                        total += d.profit + d.swap + d.commission
                        count += 1
                return round(total, 2), count

            today_pnl, today_trades = _sum_pnl(close_deals, today_start, now + timedelta(hours=1))
            yesterday_pnl, yesterday_trades = _sum_pnl(close_deals, yesterday_start, today_start)
            this_week_pnl, this_week_trades = _sum_pnl(close_deals, this_week_start, now + timedelta(hours=1))
            last_week_pnl, last_week_trades = _sum_pnl(close_deals, last_week_start, last_week_end)
            this_month_pnl, this_month_trades = _sum_pnl(close_deals, this_month_start, now + timedelta(hours=1))
            last_month_pnl, last_month_trades = _sum_pnl(close_deals, last_month_start, last_month_end)

            equity = self.state.current_equity or self.state.initial_balance

            return {
                "today": {"pnl": today_pnl, "trades": today_trades,
                          "pct": round(today_pnl / equity * 100, 2) if equity > 0 else 0},
                "yesterday": {"pnl": yesterday_pnl, "trades": yesterday_trades,
                              "pct": round(yesterday_pnl / equity * 100, 2) if equity > 0 else 0},
                "this_week": {"pnl": this_week_pnl, "trades": this_week_trades,
                              "pct": round(this_week_pnl / equity * 100, 2) if equity > 0 else 0},
                "last_week": {"pnl": last_week_pnl, "trades": last_week_trades,
                              "pct": round(last_week_pnl / equity * 100, 2) if equity > 0 else 0},
                "this_month": {"pnl": this_month_pnl, "trades": this_month_trades,
                               "pct": round(this_month_pnl / equity * 100, 2) if equity > 0 else 0},
                "last_month": {"pnl": last_month_pnl, "trades": last_month_trades,
                               "pct": round(last_month_pnl / equity * 100, 2) if equity > 0 else 0},
                "source": "mt5_deals",
                "deals_scanned": len(close_deals),
            }

        except ImportError:
            return self._empty_period_pnl("MetaTrader5 not available")
        except Exception as e:
            log.warning(f"Period P&L fetch failed: {e}")
            return self._empty_period_pnl(str(e))

    @staticmethod
    def _empty_period_pnl(reason: str = "") -> dict:
        """Return empty period P&L structure."""
        empty = {"pnl": 0, "trades": 0, "pct": 0}
        return {
            "today": empty.copy(), "yesterday": empty.copy(),
            "this_week": empty.copy(), "last_week": empty.copy(),
            "this_month": empty.copy(), "last_month": empty.copy(),
            "source": "unavailable", "reason": reason,
            "deals_scanned": 0,
        }


# ============================================================
# 4. SINGLETON
# ============================================================

_manager_instance = None

def get_account_manager() -> AccountManager:
    """Get the singleton AccountManager instance."""
    global _manager_instance
    if _manager_instance is None:
        _manager_instance = AccountManager()
    return _manager_instance