from datetime import datetime, timezone
from typing import Any, Dict, List, Optional


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


class MT5LiveSource:
    def __init__(self, symbol: str, timeframe: str) -> None:
        self.symbol = symbol
        self.timeframe = timeframe
        self._mt5 = None

    def connect(self) -> bool:
        try:
            import MetaTrader5 as mt5  # type: ignore
        except Exception:
            return False
        if not mt5.initialize():
            return False
        self._mt5 = mt5
        return True

    def close(self) -> None:
        if self._mt5 is not None:
            self._mt5.shutdown()

    def _timeframe_value(self) -> Optional[int]:
        if self._mt5 is None:
            return None
        mapping = {
            "M1": self._mt5.TIMEFRAME_M1,
            "M5": self._mt5.TIMEFRAME_M5,
            "M15": self._mt5.TIMEFRAME_M15,
            "M30": self._mt5.TIMEFRAME_M30,
            "H1": self._mt5.TIMEFRAME_H1,
            "H4": self._mt5.TIMEFRAME_H4,
            "D1": self._mt5.TIMEFRAME_D1,
        }
        return mapping.get(self.timeframe.upper(), self._mt5.TIMEFRAME_M5)

    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 len(values) < period:
            return float(values[-1]) if values else 0.0
        window = values[-period:]
        return float(sum(window) / len(window))

    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 _atr(self, highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> float:
        if len(highs) < 2 or len(lows) < 2 or len(closes) < 2:
            return 0.0
        tr = []
        for i in range(1, len(closes)):
            h = highs[i]
            l = lows[i]
            pc = closes[i - 1]
            tr.append(max(h - l, abs(h - pc), abs(l - pc)))
        if not tr:
            return 0.0
        if len(tr) < period:
            return float(sum(tr) / len(tr))
        return float(sum(tr[-period:]) / period)

    def _build_plan(
        self,
        closes: List[float],
        highs: List[float],
        lows: List[float],
        tick: Any,
        point: float,
    ) -> Dict[str, Any]:
        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 = macd_fast - macd_slow
        macd_signal = self._ema([macd] * max(9, min(40, len(closes))), 9)
        bb_mid = self._sma(closes, 20)
        close_last = float(closes[-1]) if closes else float(tick.bid)
        atr14 = self._atr(highs, lows, closes, 14)

        evidence = []
        # === 修复历史遗留的单向死多头 Bug ===
        score_buy = 0
        score_sell = 0

        # 多头计算
        if ema9 > ema21:
            evidence.append("ema9_gt_ema21")
            score_buy += 1
        if macd > macd_signal:
            evidence.append("macd_gt_signal")
            score_buy += 1
        if close_last > bb_mid:
            evidence.append("price_gt_bb_mid")
            score_buy += 1
            
        # 空头计算
        if ema9 < ema21:
            evidence.append("ema9_lt_ema21")
            score_sell += 1
        if macd < macd_signal:
            evidence.append("macd_lt_signal")
            score_sell += 1
        if close_last < bb_mid:
            evidence.append("price_lt_bb_mid")
            score_sell += 1

        if score_buy >= 2 and score_buy > score_sell:
            side = "BUY"
            action = "OPEN"
            regime = "TREND"
            confidence = 0.60 + 0.10 * score_buy
            entry = float(tick.ask)
            sl = entry - max(atr14 * 1.5, point * 80)
            tp = entry + max(atr14 * 2.0, point * 120)
        elif score_sell >= 2 and score_sell > score_buy:
            side = "SELL"
            action = "OPEN"
            regime = "TREND"
            confidence = 0.60 + 0.10 * score_sell
            entry = float(tick.bid)
            # 修复：做空的止损应该在价格上方，所以是加号；止盈在价格下方，所以是减号
            sl = entry + max(atr14 * 1.5, point * 80)
            tp = entry - max(atr14 * 2.0, point * 120)
        else:
            side = "NONE"
            action = "HOLD"
            regime = "UNCERTAIN"
            confidence = 0.45
            entry = None
            sl = None
            tp = None
            if not evidence:
                evidence = ["indicator_signal_weak"]

        plan: Dict[str, Any] = {
            "signal_id": f"live_{self.symbol}_{int(datetime.now(timezone.utc).timestamp())}",
            "symbol": self.symbol,
            "timeframe": self.timeframe,
            "action": action,
            "side": side,
            "regime": regime,
            "confidence": round(float(confidence), 4),
            "risk_per_trade_pct": 0.6,
            "volume_lots": 0.1,
            "risk_flags": ["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 entry is not None:
            plan["entry_price"] = round(float(entry), 6)
            plan["sl_price"] = round(float(sl), 6)
            plan["tp_price"] = round(float(tp), 6)
        return plan

    def snapshot(self) -> Optional[Dict[str, Any]]:
        if self._mt5 is None:
            return None
        timeframe_value = self._timeframe_value()
        if timeframe_value is None:
            return None
        tick = self._mt5.symbol_info_tick(self.symbol)
        info = self._mt5.symbol_info(self.symbol)
        account = self._mt5.account_info()
        bars = self._mt5.copy_rates_from_pos(self.symbol, timeframe_value, 0, 220)
        if tick is None or info is None or account is None or bars is None or len(bars) < 40:
            return None
        closes = [float(x["close"]) for x in bars]
        highs = [float(x["high"]) for x in bars]
        lows = [float(x["low"]) for x in bars]
        spread_points = 0.0
        if info.point and info.point > 0:
            spread_points = (float(tick.ask) - float(tick.bid)) / float(info.point)
        spread_hist = [float(x["spread"]) for x in bars if "spread" in x.dtype.names]
        spread_p90_30m = self._percentile(spread_hist[-120:], 0.9) if spread_hist else max(round(spread_points, 2), 1.0)
        tick_time_ms = int(getattr(tick, "time_msc", 0))
        now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
        market_data_delay_ms = max(0, now_ms - tick_time_ms) if tick_time_ms > 1_000_000_000_000 else None
        close_last = closes[-1]
        mid = (float(tick.ask) + float(tick.bid)) / 2.0
        tick_bar_deviation_bps = abs(mid - close_last) / close_last * 10000 if close_last > 0 else 0.0
        ai_plan = self._build_plan(closes, highs, lows, tick, float(info.point or 0.01))
        resolved_timeframe = str(
            getattr(self, "timeframe", "")
            or ai_plan.get("timeframe")
            or "M5"
        ).upper()
        return {
            "symbol": str(self.symbol or "XAUUSD").upper(),
            "timeframe": resolved_timeframe,
            "bars": [
                {
                    "time": int(x["time"]),
                    "open": float(x["open"]),
                    "high": float(x["high"]),
                    "low": float(x["low"]),
                    "close": float(x["close"]),
                    "tick_volume": float(x["tick_volume"]) if "tick_volume" in x.dtype.names else 0.0,
                    "spread": float(x["spread"]) if "spread" in x.dtype.names else 0.0,
                }
                for x in bars
            ],
            "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": round(spread_points, 2),
                "spread_p90_30m": round(float(spread_p90_30m), 2),
                "server_time": _utc_now_iso(),
                "symbol": str(self.symbol or "XAUUSD").upper(),
                "timeframe": resolved_timeframe,
                "bid": float(tick.bid),
                "ask": float(tick.ask),
            },
            "account": {
                "daily_realized_loss_pct": 0,
                "consecutive_losses": 0,
                "equity": float(account.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": ai_plan,
        }
