import json
import logging
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

_logger = logging.getLogger(__name__)

_FROZEN = getattr(sys, 'frozen', False)


def _resolve_mt5_base() -> Optional[Path]:
    """跨平台解析 MT5 基础目录，优先 config_manager，env 次之，最后才是 Wine 候选"""
    env_dir = os.environ.get("EA_BRIDGE_FILES_DIR", "").strip()
    if env_dir:
        p = Path(env_dir).expanduser()
        if p.exists():
            return p
    if _FROZEN:
        try:
            from config_manager import get_mt5_dir as _get_mt5_dir
            return Path(_get_mt5_dir())
        except Exception:
            pass
    try:
        from config_manager import get_mt5_dir as _get_mt5_dir
        return Path(_get_mt5_dir())
    except Exception:
        pass
    return None


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _symbol_matches_preferred(candidate_symbol: Any, preferred_symbol: str) -> bool:
    candidate = str(candidate_symbol or "").strip().upper()
    preferred = str(preferred_symbol or "").strip().upper()
    # Smart fallback: if no preferred symbol, try to detect from MT5 terminal
    if not preferred:
        try:
            import MetaTrader5 as _mt5
            if _mt5.initialize():
                _chart_id = _mt5.ChartID()
                if _chart_id:
                    _chart_sym = _mt5.ChartSymbol(_chart_id)
                    if _chart_sym:
                        preferred = str(_chart_sym).strip().upper()
                        _logger.info(f"Auto-detected symbol from MT5 chart: {preferred}")
                _mt5.shutdown()
        except Exception:
            pass
    if not preferred:
        try:
            from config_manager import _scan_windows_mt5_dirs
            _scanned = _scan_windows_mt5_dirs()
            for _inst in _scanned.get("instances", []):
                _cfg = Path(_inst) / ".." / ".." / "config"
                if not _cfg.exists():
                    continue
        except Exception:
            pass
    if not candidate:
        return False
    if candidate == preferred:
        return True
    if candidate.startswith(preferred):
        suffix = candidate[len(preferred):]
        if suffix and all(not ch.isalnum() for ch in suffix):
            return True
    return False


def _candidate_files_dirs() -> List[Path]:
    """候选 MT5 Common Files 目录列表（跨平台）。仅在 macOS 上扫描 Wine 前缀"""
    out: List[Path] = []

    # 优先用 config_manager 确认的路径（跨平台，已缓存）
    mt5_base = _resolve_mt5_base()
    if mt5_base is not None and mt5_base.exists():
        out.append(mt5_base)

    # macOS Wine 前缀作为补充候选（仅 macOS）
    if sys.platform == "darwin":
        home = Path.home()
        prefixes = [
            home / "Library/Application Support/net.metaquotes.wine.metatrader5",
            home / "Library/Application Support/net.metaquotes.wine.metatrader5-beta",
            home / ".wine",
        ]
        for prefix in prefixes:
            fixed = [
                prefix / "drive_c/users/user/AppData/Roaming/MetaQuotes/Terminal/Common/Files",
                prefix / f"drive_c/users/{os.getenv('USER', 'user')}/AppData/Roaming/MetaQuotes/Terminal/Common/Files",
                prefix / "drive_c/Program Files/MetaTrader 5/MQL5/Files",
            ]
            for p in fixed:
                if p.exists():
                    out.append(p)
            if prefix.exists():
                for p in prefix.glob("**/MetaQuotes/Terminal/*/Common/Files"):
                    if p.exists():
                        out.append(p)

    # 去重
    uniq: List[Path] = []
    seen = set()
    for p in out:
        s = str(p.resolve())
        if s in seen:
            continue
        seen.add(s)
        uniq.append(p)
    return uniq


def _wine_common_files_dir() -> Optional[Path]:
    env_dir = os.environ.get("EA_BRIDGE_FILES_DIR", "").strip()
    if env_dir:
        p = Path(env_dir).expanduser()
        if p.exists():
            return p
    candidates = _candidate_files_dirs()
    with_snapshot = []
    with_bridge = []
    for files in candidates:
        bridge = files / "gold_autotrade/bridge"
        snap = bridge / "market_snapshot.json"
        try:
            if snap.exists():
                with_snapshot.append((snap.stat().st_mtime, files))
            elif bridge.exists():
                with_bridge.append(files)
        except OSError:
            if bridge.exists():
                with_bridge.append(files)
    if with_snapshot:
        with_snapshot.sort(key=lambda x: x[0], reverse=True)
        return with_snapshot[0][1]
    if with_bridge:
        return with_bridge[0]
    for p in candidates:
        if p.exists():
            return p
    return None


def _candidate_snapshot_paths(project_root: Path) -> List[Path]:
    candidates: List[Path] = []
    common = _wine_common_files_dir()
    if common is not None:
        candidates.extend(
            [
                common / "gold_autotrade" / "bridge" / "terminal_status.json",
                common / "gold_autotrade" / "bridge" / "market_snapshot.json",
                common / "market_snapshot.json",
            ]
        )
    candidates.extend(
        [
            project_root / "bridge" / "terminal_status.json",
            project_root / "bridge" / "market_snapshot.json",
            project_root / "data" / "bridge" / "terminal_status.json",
            project_root / "data" / "bridge" / "market_snapshot.json",
        ]
    )
    uniq: List[Path] = []
    seen = set()
    for path in candidates:
        key = str(path)
        if key in seen:
            continue
        seen.add(key)
        uniq.append(path)
    return uniq


class EABridgeLiveSource:
    def __init__(self, symbol: str, timeframe: str, project_root: Optional[Path] = None) -> None:
        self.symbol = symbol
        self.timeframe = timeframe
        self.project_root = project_root or Path(__file__).resolve().parents[1]
        self.snapshot_path = self.project_root / "data" / "bridge" / "market_snapshot.json"

    def _select_snapshot_path(self) -> Path:
        freshest_path = self.snapshot_path
        freshest_mtime = 0.0
        for path in _candidate_snapshot_paths(self.project_root):
            if not path.exists():
                continue
            try:
                mtime = float(path.stat().st_mtime)
            except Exception:
                continue
            if mtime >= freshest_mtime:
                freshest_mtime = mtime
                freshest_path = path
        return freshest_path

    def connect(self) -> bool:
        self.snapshot_path = self._select_snapshot_path()
        if self.snapshot_path.exists():
            return True
        return self.snapshot_path.parent.exists()

    def close(self) -> None:
        return None

    def _ema(self, values: List[float], period: int) -> float:
        if not values:
            return 0.0
        alpha = 2.0 / (period + 1.0)
        ema = values[0]
        for v in values[1:]:
            ema = alpha * v + (1.0 - alpha) * ema
        return float(ema)

    def _sma(self, values: List[float], period: int) -> float:
        if not values:
            return 0.0
        if len(values) < period:
            return float(sum(values) / len(values))
        x = values[-period:]
        return float(sum(x) / len(x))

    def _atr(self, highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> float:
        if len(closes) < 2:
            return 0.0
        tr = []
        for i in range(1, len(closes)):
            tr.append(max(highs[i] - lows[i], abs(highs[i] - closes[i - 1]), abs(lows[i] - closes[i - 1])))
        if not tr:
            return 0.0
        if len(tr) < period:
            return float(sum(tr) / len(tr))
        return float(sum(tr[-period:]) / period)

    def _percentile(self, values: List[float], p: float) -> float:
        if not values:
            return 0.0
        xs = sorted(values)
        idx = int(round((len(xs) - 1) * p))
        idx = max(0, min(len(xs) - 1, idx))
        return float(xs[idx])

    def _resolve_symbol_market_view(
        self,
        payload: Dict[str, Any],
    ) -> Optional[Dict[str, Any]]:
        preferred_symbol = str(self.symbol or "XAUUSD").strip().upper() or "XAUUSD"
        instruments = (
            payload.get("instruments", {})
            if isinstance(payload.get("instruments", {}), dict)
            else {}
        )
        for key, candidate in instruments.items():
            if not _symbol_matches_preferred(key, preferred_symbol):
                continue
            if not isinstance(candidate, dict):
                continue
            symbol_tick = {
                "bid": candidate.get("bid", 0),
                "ask": candidate.get("ask", 0),
                "time_msc": candidate.get("time_msc", 0),
                "spread_points": candidate.get("spread", 0),
            }
            symbol_bars = candidate.get("bars", [])
            if isinstance(symbol_bars, list) and len(symbol_bars) >= 10:
                return {
                    "symbol": str(key or "").strip().upper() or preferred_symbol,
                    "tick": symbol_tick,
                    "bars": symbol_bars,
                    "quote_origin": f"instrument:{str(key or '').strip().upper() or preferred_symbol}",
                    "min_bars_required": 10,
                }

        root_tick = payload.get("tick", {}) if isinstance(payload.get("tick", {}), dict) else {}
        root_bars = payload.get("bars", []) if isinstance(payload.get("bars", []), list) else []
        root_symbol = str(payload.get("symbol", "") or "").strip().upper()
        # terminal_status.json uses flat bid/ask/spread at top level (no tick sub-object).
        if not root_tick:
            flat_bid = payload.get("bid")
            flat_ask = payload.get("ask")
            if flat_bid is not None and flat_ask is not None:
                root_tick = {
                    "bid": flat_bid,
                    "ask": flat_ask,
                    "time_msc": payload.get("time_msc", 0),
                    "spread_points": payload.get("spread", 0),
                }
        if _symbol_matches_preferred(root_symbol, preferred_symbol) and len(root_bars) >= 40:
            return {
                "symbol": root_symbol or preferred_symbol,
                "tick": root_tick,
                "bars": root_bars,
                "quote_origin": "root",
                "min_bars_required": 40,
            }
        return None

    def _resolve_market_data_delay_ms(
        self,
        *,
        tick: Dict[str, Any],
        bars: List[Dict[str, Any]],
    ) -> Optional[int]:
        """Compute market data delay from tick timestamp, bars, or file mtime.

        Returns delay in milliseconds, or None if no trustworthy source is available.
        The pipeline is responsible for fallback when None is returned.
        """
        now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
        try:
            tick_time_ms = int(tick.get("time_msc", 0) or 0)
        except Exception:
            tick_time_ms = 0

        # Only trust tick_time_ms that looks like a valid Unix epoch in milliseconds,
        # is not in the future (with 24h grace for clock skew), and is not stale.
        _TICK_MAX_AGE_MS = 180_000
        if 1_000_000_000_000 <= tick_time_ms <= (now_ms + 86_400_000) and now_ms - tick_time_ms <= _TICK_MAX_AGE_MS:
            return max(0, now_ms - tick_time_ms)

        # Fallback 1: latest bar close time (seconds → ms)
        fallback_candidates_ms: List[int] = []
        if bars:
            try:
                last_bar_time = int(bars[-1].get("time", 0) or 0)
            except Exception:
                last_bar_time = 0
            if 1_000_000_000 <= last_bar_time <= int(now_ms / 1000) + 86_400:
                fallback_candidates_ms.append(last_bar_time * 1000)
        # Fallback 2: file modification time of the snapshot itself
        try:
            fallback_candidates_ms.append(int(self.snapshot_path.stat().st_mtime * 1000))
        except Exception:
            pass
        freshest_ms = max(fallback_candidates_ms) if fallback_candidates_ms else 0
        if freshest_ms > 0:
            return max(0, now_ms - freshest_ms)
        # No trustworthy source — let the pipeline compute its own delay
        return None

    def snapshot(self) -> Optional[Dict[str, Any]]:
        self.snapshot_path = self._select_snapshot_path()
        if not self.snapshot_path.exists():
            return None
        try:
            payload = json.loads(self.snapshot_path.read_text(encoding="utf-8"))
        except Exception:
            return None
        market_view = self._resolve_symbol_market_view(payload)
        if not isinstance(market_view, dict):
            return None

        # Recover symbol from signal_id when the EA report has empty symbol field
        detected_symbol = market_view.get("symbol", "") or ""
        if not detected_symbol:
            signal_id = str(payload.get("signal_id", "") or "")
            if "ea_live_" in signal_id and "_" in signal_id:
                parts = signal_id.split("_")
                if len(parts) >= 4:
                    detected_symbol = parts[2]  # ea_live_XAUUSD_timestamp...
            if not detected_symbol:
                detected_symbol = self.symbol or "XAUUSD"
            market_view["symbol"] = detected_symbol

        tick = market_view.get("tick", {}) if isinstance(market_view.get("tick", {}), dict) else {}
        bars = market_view.get("bars", []) if isinstance(market_view.get("bars", []), list) else []
        min_bars_required = int(market_view.get("min_bars_required", 40) or 40)
        if len(bars) < min_bars_required:
            return None
        closes = [float(x.get("close", 0)) for x in bars]
        highs = [float(x.get("high", 0)) for x in bars]
        lows = [float(x.get("low", 0)) for x in bars]
        spreads = [float(x.get("spread", 0)) for x in bars]
        bid = float(tick.get("bid", 0))
        ask = float(tick.get("ask", 0))
        if bid <= 0 or ask <= 0:
            return None
        market_data_delay_ms = self._resolve_market_data_delay_ms(tick=tick, bars=bars)
        close_last = closes[-1]
        mid = (bid + ask) / 2.0
        tick_bar_deviation_bps = abs(mid - close_last) / close_last * 10000 if close_last > 0 else 0.0
        ema9 = self._ema(closes[-80:], 9)
        ema21 = self._ema(closes[-120:], 21)
        macd_fast = self._ema(closes[-120:], 12)
        macd_slow = self._ema(closes[-120:], 26)
        # MACD signal line: EMA(9) of MACD values over last 30 positions
        macd_values = []
        for i in range(max(1, len(closes) - 30), len(closes)):
            ema12_val = self._ema(closes[:i+1], 12)
            ema26_val = self._ema(closes[:i+1], 26)
            macd_values.append(ema12_val - ema26_val)
        macd = macd_values[-1] if macd_values else 0.0
        macd_signal = self._ema(macd_values, 9) if len(macd_values) >= 9 else macd
        bb_mid = self._sma(closes, 20)
        atr14 = self._atr(highs, lows, closes, 14)
        # === 重构：让底层只提供指标，不自作主张下结论 ===
        score_buy = 0
        score_sell = 0
        evidence = []
        
        # 多头证据
        if ema9 > ema21:
            score_buy += 1
            evidence.append("ema9_gt_ema21")
        if macd > macd_signal:
            score_buy += 1
            evidence.append("macd_gt_signal")
        if close_last > bb_mid:
            score_buy += 1
            evidence.append("price_gt_bb_mid")
            
        # 空头证据
        if ema9 < ema21:
            score_sell += 1
            evidence.append("ema9_lt_ema21")
        if macd < macd_signal:
            score_sell += 1
            evidence.append("macd_lt_signal")
        if close_last < bb_mid:
            score_sell += 1
            evidence.append("price_lt_bb_mid")

        # Instrument-aware ATR floor: use point size instead of hardcoded 1.0
        try:
            from src.price_digits import get_point
            _p = get_point(preferred_symbol=self.symbol)
            _atr_floor = _p * 100.0  # 100 points minimum SL distance
        except Exception:
            _atr_floor = 1.0  # fallback for gold

        # 让信号具备方向性，而不是硬编码死多头
        if score_buy >= 2 and score_buy > score_sell:
            action = "OPEN"
            side = "BUY"
            confidence = 0.60 + 0.10 * score_buy
            sl = ask - max(atr14 * 1.5, _atr_floor)
            tp = ask + max(atr14 * 2.0, _atr_floor * 3.0)
        elif score_sell >= 2 and score_sell > score_buy:
            action = "OPEN"
            side = "SELL"
            confidence = 0.60 + 0.10 * score_sell
            # SELL: SL above entry (defense), TP below entry (profit target)
            sl = bid + max(atr14 * 1.5, _atr_floor)
            tp = bid - max(atr14 * 2.0, _atr_floor * 3.0)
        else:
            action = "HOLD"
            side = "NONE"
            confidence = 0.45
            sl = None
            tp = None
            if not evidence:
                evidence = ["indicator_signal_weak"]

        plan: Dict[str, Any] = {
            "signal_id": f"ea_live_{self.symbol}_{int(datetime.now(timezone.utc).timestamp())}",
            "symbol": self.symbol,
            "timeframe": self.timeframe,
            "action": action,
            "side": side,
            "regime": "TREND" if action == "OPEN" else "UNCERTAIN",
            "confidence": round(float(confidence), 4),
            "risk_per_trade_pct": 0.6,
            "volume_lots": 0.1,
            "risk_flags": ["EA_LIVE_SOURCE_CONNECTED"],
            "evidence": evidence,
            "invalidations": [],
            "valid_until": _utc_now_iso(),
            "indicators": {
                "ema9": round(float(ema9), 6),
                "ema21": round(float(ema21), 6),
                "macd": round(float(macd), 6),
                "macd_signal": round(float(macd_signal), 6),
                "bb_mid": round(float(bb_mid), 6),
                "atr14": round(float(atr14), 6),
            },
        }
        if sl is not None and tp is not None:
            # 修复：入场价也必须区分多空！BUY 用 ask，SELL 用 bid。
            entry_price = round(float(ask if side == "BUY" else bid), 6)
            
            # 【最后一道防线：倒挂免疫层】
            # 无论前面怎么算，只要在写入 JSON 前发现倒挂，立刻原地拨正！
            sl_val = round(float(sl), 6)
            tp_val = round(float(tp), 6)
            atr_v = round(float(atr14), 6)
            
            if side == "BUY":
                if sl_val >= entry_price:
                    sl_val = round(entry_price - 1.5 * atr_v, 6)
                if tp_val <= entry_price:
                    tp_val = round(entry_price + 2.0 * atr_v, 6)
            elif side == "SELL":
                if sl_val <= entry_price:
                    sl_val = round(entry_price + 1.5 * atr_v, 6)
                if tp_val >= entry_price:
                    tp_val = round(entry_price - 2.0 * atr_v, 6)

            plan["entry_price"] = entry_price
            plan["sl_price"] = sl_val
            plan["tp_price"] = tp_val
        account = payload.get("account", {}) if isinstance(payload.get("account", {}), dict) else {}
        # ── 持仓数据: terminal_status.json 用 "positions" 键名，兼容 "positions_all" ──
        positions_data = (
            payload.get("positions", None)
            or payload.get("positions_all", None)
            or []
        )
        if not isinstance(positions_data, list):
            positions_data = []
        positions_all = positions_data
        positions_count = len(positions_all) if positions_all else int(payload.get("count", payload.get("positions_count", 0)) or 0)
        equity = float(account.get("equity", 0) or 0) if isinstance(account, dict) else 0.0
        # ── 三位小数经纪商: account可能是字符串(login号) → MT5 API直连fallback ──
        if equity <= 0:
            try:
                import MetaTrader5 as mt5
                if mt5.initialize():
                    acct = mt5.account_info()
                    if acct:
                        equity = float(acct.equity)
            except Exception:
                pass
        resolved_symbol = str(market_view.get("symbol", preferred_symbol := self.symbol) or preferred_symbol)
        resolved_timeframe = str(
            payload.get("timeframe")
            or market_view.get("timeframe")
            or plan.get("timeframe")
            or self.timeframe
            or "M5"
        ).upper()
        return {
            "symbol": resolved_symbol,
            "timeframe": resolved_timeframe,
            "tick": dict(tick),
            "bars": list(bars),
            "positions_all": positions_all,
            "positions_count": positions_count,
            "market": {
                "market_data_delay_ms": float(market_data_delay_ms) if market_data_delay_ms is not None else None,
                "missing_bars": 0,
                "tick_bar_deviation_bps": round(float(tick_bar_deviation_bps), 4),
                "spread_points": float(tick.get("spread_points", 0)),
                "spread_p90_30m": self._percentile(spreads[-120:], 0.9) if spreads else float(tick.get("spread_points", 0)),
                "server_time": _utc_now_iso(),
                "symbol": resolved_symbol,
                "timeframe": resolved_timeframe,
                "quote_origin": str(market_view.get("quote_origin", "unknown") or "unknown"),
                "bid": bid,
                "ask": ask,
            },
            "account": {
                "current_open_positions": positions_count,
                "daily_realized_loss_pct": 0,
                "consecutive_losses": 0,
                "equity": equity,
            },
            "agent": {
                "agent_timeout_ms": 0,
                "risk_level": "MEDIUM",
                "side_conflict_with_regime": False,
            },
            "runtime_feedback": {
                "ack_timeout_count_5m": 0,
                "slippage_points_p95_15m": 0,
                "daily_token_usage_pct": 0,
            },
            "ai_trade_plan": plan,
        }
