#!/usr/bin/env python3
"""
XAUUSD 交易策略 - 趋势 + 震荡双模式
regime 检测 → 模式选择 → 信号生成 → SL/TP 计算
"""
import json, math, os, time, pathlib, urllib.request
import pandas as pd

try:
    from src.market_context import normalize_regime_label
except ModuleNotFoundError:
    from market_context import normalize_regime_label

# ============ 配置 ============
import sys
import pathlib

# 确保可以导入项目根目录下的 config_manager
ROOT_DIR = pathlib.Path(__file__).parent.parent.absolute()
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))
import config_manager

# EA Bridge 路径——动态获取，不使用模块级缓存
def _get_mt5_dir():
    d = config_manager.get_mt5_dir()
    if not d or not d.exists():
        d = ROOT_DIR / "test_mt5_files"
    return d

def _bridge_to_ea():
    return _get_mt5_dir() / "gold_autotrade/bridge/to_ea"

def _bridge_from_ea():
    return _get_mt5_dir() / "gold_autotrade/bridge/from_ea"

# ============ 历史K线数据获取（仅 MT5 EA Bridge）============
import pathlib as _pathlib, time as _time, json as _json

# 进程内内存缓存（EA请求结果，5分钟有效）
_memory_cache = {"bars": [], "fetched_at": 0.0}


def _timeframe_to_seconds(timeframe: str) -> int:
    tf = str(timeframe or "M5").strip().upper()
    mapping = {
        "M1": 60,
        "M5": 300,
        "M15": 900,
        "M30": 1800,
        "H1": 3600,
        "H4": 14400,
    }
    return mapping.get(tf, 300)


def _normalize_snapshot_bars(raw_bars) -> list[dict]:
    if not isinstance(raw_bars, list):
        return []
    normalized: list[dict] = []
    for row in raw_bars:
        if not isinstance(row, dict):
            continue
        try:
            time_value = int(float(row.get("time", 0) or 0))
            open_value = float(row.get("open", 0.0) or 0.0)
            high_value = float(row.get("high", 0.0) or 0.0)
            low_value = float(row.get("low", 0.0) or 0.0)
            close_value = float(row.get("close", 0.0) or 0.0)
            volume_value = float(
                row.get("tick_volume", row.get("volume", row.get("real_volume", 0.0))) or 0.0
            )
        except Exception:
            continue
        if time_value <= 0 or high_value <= 0 or low_value <= 0 or close_value <= 0:
            continue
        normalized.append(
            {
                "time": time_value,
                "open": open_value,
                "high": high_value,
                "low": low_value,
                "close": close_value,
                "tick_volume": volume_value,
            }
        )
    normalized.sort(key=lambda item: int(item.get("time", 0) or 0))
    return _normalize_bar_timestamps(normalized)


def _aggregate_bars_to_timeframe(bars: list[dict], timeframe: str) -> list[dict]:
    target_seconds = _timeframe_to_seconds(timeframe)
    if target_seconds <= 300:
        return bars
    source_seconds = 300
    if target_seconds % source_seconds != 0:
        return []
    aggregated: list[dict] = []
    current_bucket = None
    bucket_rows: list[dict] = []
    for row in bars:
        row_time = int(row.get("time", 0) or 0)
        if row_time <= 0:
            continue
        bucket = row_time - (row_time % target_seconds)
        if current_bucket is None or bucket == current_bucket:
            current_bucket = bucket
            bucket_rows.append(row)
            continue
        if bucket_rows:
            aggregated.append(
                {
                    "time": current_bucket,
                    "open": float(bucket_rows[0].get("open", 0.0) or 0.0),
                    "high": max(float(item.get("high", 0.0) or 0.0) for item in bucket_rows),
                    "low": min(float(item.get("low", 0.0) or 0.0) for item in bucket_rows),
                    "close": float(bucket_rows[-1].get("close", 0.0) or 0.0),
                    "tick_volume": round(
                        sum(float(item.get("tick_volume", item.get("volume", 0.0)) or 0.0) for item in bucket_rows),
                        2,
                    ),
                }
            )
        current_bucket = bucket
        bucket_rows = [row]
    if bucket_rows and current_bucket is not None:
        aggregated.append(
            {
                "time": current_bucket,
                "open": float(bucket_rows[0].get("open", 0.0) or 0.0),
                "high": max(float(item.get("high", 0.0) or 0.0) for item in bucket_rows),
                "low": min(float(item.get("low", 0.0) or 0.0) for item in bucket_rows),
                "close": float(bucket_rows[-1].get("close", 0.0) or 0.0),
                "tick_volume": round(
                    sum(float(item.get("tick_volume", item.get("volume", 0.0)) or 0.0) for item in bucket_rows),
                    2,
                ),
            }
        )
    return aggregated


def _snapshot_bar_candidates(symbol: str | None = None) -> list[pathlib.Path]:
    candidates = [
        _bridge_from_ea() / "terminal_status.json",
        _get_mt5_dir() / "gold_autotrade" / "bridge" / "terminal_status.json",
        config_manager.get_data_dir() / "bridge" / "terminal_status.json",
        _bridge_from_ea() / "market_snapshot.json",
        _get_mt5_dir() / "gold_autotrade" / "bridge" / "market_snapshot.json",
        config_manager.get_data_dir() / "bridge" / "market_snapshot.json",
        ROOT_DIR / "bridge" / "market_snapshot.json",
    ]
    deduped: list[pathlib.Path] = []
    seen: set[str] = set()
    for path in candidates:
        normalized = str(path)
        if normalized in seen:
            continue
        seen.add(normalized)
        deduped.append(path)
    return deduped


def _read_bars_from_snapshot_cache(
    count=200,
    timeframe="M5",
    symbol: str = None,
    max_age_sec: float | None = None,
) -> list[dict]:
    target_symbol = str(symbol or "").strip().upper()
    tf = str(timeframe or "M5").strip().upper()
    tf_seconds = _timeframe_to_seconds(tf)
    # Snapshot cache only contains M5 bars.  Sub-M5 timeframes (M1) cannot
    # be derived from M5 — return empty to force a live EA fetch instead.
    if tf_seconds < 300:
        return []
    freshness_limit = (
        float(max_age_sec)
        if max_age_sec is not None
        else max(1800.0, _timeframe_to_seconds(tf) * 6.0)
    )
    best_bars: list[dict] = []
    best_last_time = 0
    for candidate in _snapshot_bar_candidates(target_symbol):
        if not candidate.exists():
            continue
        try:
            payload = _json.loads(candidate.read_text(encoding="utf-8", errors="ignore"))
        except Exception:
            continue
        if not isinstance(payload, dict):
            continue
        payload_symbol = str(payload.get("symbol", "") or "").strip().upper()
        if target_symbol and payload_symbol and payload_symbol != target_symbol:
            continue
        bars = _normalize_snapshot_bars(payload.get("bars", []))
        if not bars:
            continue
        if tf != "M5":
            bars = _aggregate_bars_to_timeframe(bars, tf)
        if not bars:
            continue
        last_bar_time = int(bars[-1].get("time", 0) or 0)
        if last_bar_time <= 0:
            continue
        age_sec = max(0.0, _time.time() - last_bar_time)
        if age_sec > freshness_limit:
            continue
        if last_bar_time > best_last_time:
            best_last_time = last_bar_time
            best_bars = bars
    if not best_bars:
        return []
    return best_bars[-count:] if len(best_bars) >= count else best_bars


def _write_request_atomic(target_path: pathlib.Path, payload: dict):
    tmp_path = target_path.with_suffix(target_path.suffix + ".tmp")
    tmp_path.write_text(_json.dumps(payload))
    os.replace(tmp_path, target_path)


def _read_bars_from_ea(count=200, timeout=15, timeframe="M5", symbol: str = None):
    """
    通过 MT5 EA Bridge 获取历史K线。
    symbol=None 时自动跟随 EA 图表品种。
    优化策略：
    1. 先检查 from_ea 是否有现成的 bars 文件（EA 已写入的响应）
    2. 没有则发请求，最多等 timeout 秒
    3. 超时则返回空
    """
    if not symbol:
        from bridge.gold_trader import _detect_active_symbol
        symbol = _detect_active_symbol()
    snapshot_bars = _read_bars_from_snapshot_cache(count=count, timeframe=timeframe, symbol=symbol,
                                                    max_age_sec=600 if str(timeframe or "").strip().upper() == "M5" else None)
    if snapshot_bars:
        return snapshot_bars
    sig_id = f"bars_{timeframe}_{int(_time.time() * 1000)}"
    req_file = _bridge_to_ea() / f"{sig_id}.request.json"
    resp_file = _bridge_from_ea() / f"{sig_id}.bars.json"

    # 步骤1：快速清理旧的 bars 文件，防止读取到很久以前超时的残留响应
    try:
        _bridge_from_ea().mkdir(parents=True, exist_ok=True)
        now_time = _time.time()
        for f in _bridge_from_ea().glob(f"bars_{timeframe}_*.bars.json"):
            # 如果文件是最近 15 秒内生成的，勉强可以接受（可能是刚刚超时的请求返回的）
            if now_time - f.stat().st_mtime < 15:
                try:
                    raw = f.read_text(encoding="utf-8")
                    import re as _re
                    raw_fixed = _re.sub(r'"time":(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2})', r'"time":"\1"', raw)
                    data = _json.loads(raw_fixed)
                    bars = data.get("bars", [])
                    if bars and bars[-1].get("time"):
                        f.unlink(missing_ok=True)
                        return _normalize_bar_timestamps(bars)[-count:] if len(bars) >= count else _normalize_bar_timestamps(bars)
                except Exception:
                    f.unlink(missing_ok=True)
            else:
                # 过期残留文件，直接清理
                f.unlink(missing_ok=True)
    except Exception:
        pass

    # 步骤2：发请求等响应
    try:
        _bridge_to_ea().mkdir(parents=True, exist_ok=True)
        req = {
            "signal_id": sig_id,
            "action": "GET_BARS",
            "symbol": symbol,
            "timeframe": timeframe,
            "count": count
        }
        _write_request_atomic(req_file, req)
    except Exception:
        return []

    # 轮询等待响应（最多 timeout 秒）
    for _ in range(timeout * 10):
        try:
            if resp_file.exists():
                raw = resp_file.read_text(encoding="utf-8")
                import re as _re
                raw_fixed = _re.sub(r'"time":(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2})', r'"time":"\1"', raw)
                data = _json.loads(raw_fixed)
                bars = data.get("bars", [])
                resp_file.unlink(missing_ok=True)
                req_file.unlink(missing_ok=True)
                if bars:
                    return _normalize_bar_timestamps(bars)
                elif data.get("status") == "ERROR":
                    # 如果 EA 明确返回了 ERROR（例如 K 线数据不足），直接退出轮询，不再傻等
                    return []
        except Exception:
            pass
        _time.sleep(0.1)

    # 超时：清理
    try: req_file.unlink(missing_ok=True)
    except Exception: pass
    try: resp_file.unlink(missing_ok=True)
    except Exception: pass
    return []


def _normalize_bar_timestamps(bars: list[dict]) -> list[dict]:
    """Detect and fix clock skew in bar timestamps.

    EA/MT5 may write bars with timestamps in server time (UTC+0 / UTC+2)
    while Python's time.time() is UTC epoch. If the latest bar timestamp
    differs from now by more than 30 minutes, apply an offset correction.

    Only modifies timestamps if an offset is detected; returns bars as-is
    if they appear correctly synchronized.
    """
    if not bars or len(bars) < 2:
        return bars

    now_utc = _time.time()
    last_bar_ts = bars[-1].get("time", 0)
    if not isinstance(last_bar_ts, (int, float)) or last_bar_ts <= 0:
        return bars

    offset = now_utc - last_bar_ts
    # If bars are within +/- 30 min of UTC epoch, no correction needed
    if abs(offset) < 1800:
        return bars

    # Clock skew detected: normalize all timestamps by applying the offset
    # Cap correction to avoid over-correcting if data is truly stale
    correction = max(-86400, min(86400, offset))
    normalized = []
    for b in bars:
        nb = dict(b)
        if isinstance(nb.get("time"), (int, float)) and nb["time"] > 0:
            nb["time"] = int(nb["time"] + correction)
        normalized.append(nb)
    return normalized


def _read_bars(count=200, timeframe="M5", symbol: str = None):
    """
    主入口：通过 MT5 EA Bridge 获取历史K线。
    优先返回内存缓存（5分钟内有效），无缓存则从 EA 实时读取。
    EA 读取失败时回退到磁盘持久化缓存。
    symbol=None 时自动跟随 EA 图表品种。
    """
    if not symbol:
        from bridge.gold_trader import _detect_active_symbol
        symbol = _detect_active_symbol()
    global _memory_cache
    now = _time.time()
    cache_key = f"bars_{symbol}_{timeframe}"

    # ── 持久化缓存路径 ──
    _bars_cache_dir = config_manager.get_data_dir() / "bars_cache"
    _disk_cache_file = _bars_cache_dir / f"{symbol}_{timeframe}.json"

    # 内存缓存有效期内直接返回（TTL 按时帧区分：M1=60s，其他=300s）
    cache_ttl = 60 if str(timeframe or "").strip().upper() == "M1" else 300
    if _memory_cache.get(cache_key) and (now - _memory_cache.get(f"{cache_key}_fetched_at", 0)) < cache_ttl:
        bars = _memory_cache[cache_key]
        return bars[-count:] if len(bars) >= count else bars

    snapshot_bars = _read_bars_from_snapshot_cache(count=count, timeframe=timeframe, symbol=symbol)
    if snapshot_bars:
        _memory_cache[cache_key] = snapshot_bars
        _memory_cache[f"{cache_key}_fetched_at"] = now
        return snapshot_bars[-count:] if len(snapshot_bars) >= count else snapshot_bars

    # 无内存缓存或已过期，从 EA 读取
    ea_bars = _read_bars_from_ea(count, timeframe=timeframe, symbol=symbol)
    if ea_bars:
        ea_bars = _normalize_bar_timestamps(ea_bars)
        _memory_cache[cache_key] = ea_bars
        _memory_cache[f"{cache_key}_fetched_at"] = now
        # ── 写入持久化磁盘缓存 ──
        try:
            if len(ea_bars) >= 50:
                _bars_cache_dir.mkdir(parents=True, exist_ok=True)
                _disk_cache_file.write_text(_json.dumps({"bars": ea_bars, "ts": now}), encoding="utf-8")
        except Exception:
            pass
        return ea_bars[-count:] if len(ea_bars) >= count else ea_bars

    # EA 读取失败：尝试从内存过期缓存恢复（加新鲜度门禁）
    # 过期过久的缓存宁可返回空，也不能喂给系统旧数据
    # 双重校验：缓存写入时间 + 最后一根 K 线时间，防止最近写入的旧行情被当成新数据
    fallback_max_age = 120 if str(timeframe or "").strip().upper() == "M1" else 600
    if _memory_cache.get(cache_key):
        cached_bars = _memory_cache[cache_key]
        cached_at = _memory_cache.get(f"{cache_key}_fetched_at", 0)
        last_bar_time = int(cached_bars[-1].get("time", 0) or 0) if cached_bars else 0
        if (now - cached_at) < fallback_max_age and last_bar_time > 0 and (now - last_bar_time) < fallback_max_age:
            return cached_bars[-count:]

    # ── 最后回退：持久化磁盘缓存 ──
    try:
        if _disk_cache_file.exists():
            _disk_data = _json.loads(_disk_cache_file.read_text(encoding="utf-8"))
            _disk_bars = _disk_data.get("bars", [])
            _disk_ts = _disk_data.get("ts", 0)
            _disk_last_bar = int(_disk_bars[-1].get("time", 0) or 0) if _disk_bars else 0
            if (_disk_bars and len(_disk_bars) >= 50
                    and (now - _disk_ts) < fallback_max_age
                    and _disk_last_bar > 0
                    and (now - _disk_last_bar) < fallback_max_age):
                # 恢复内存缓存
                _memory_cache[cache_key] = _disk_bars
                _memory_cache[f"{cache_key}_fetched_at"] = _disk_ts
                return _disk_bars[-count:] if len(_disk_bars) >= count else _disk_bars
    except Exception:
        pass

    return []

# ============ 技术指标计算 ============
def _ema(data, period):
    """指数移动平均"""
    if len(data) < period:
        return [None] * len(data)
    k = 2.0 / (period + 1)
    ema = [None] * (period - 1)
    ema.append(sum(data[:period]) / period)
    for v in data[period:]:
        ema.append(v * k + ema[-1] * (1 - k))
    return ema

def _sma(data, period):
    """简单移动平均"""
    result = []
    for i in range(len(data)):
        if i < period - 1:
            result.append(None)
        else:
            result.append(sum(data[i - period + 1:i + 1]) / period)
    return result

def _rsi(closes, period=14):
    """RSI 指标"""
    if len(closes) < period + 1:
        return [None] * len(closes)
    gains, losses = [], []
    for i in range(1, len(closes)):
        delta = closes[i] - closes[i - 1]
        gains.append(max(delta, 0))
        losses.append(max(-delta, 0))
    result = [None] * (period + 1)
    avg_gain = sum(gains[:period]) / period
    avg_loss = sum(losses[:period]) / period
    if avg_loss == 0:
        result.append(100)
    else:
        rs = avg_gain / avg_loss
        result.append(100 - 100 / (1 + rs))
    for i in range(period, len(gains)):
        avg_gain = (avg_gain * (period - 1) + gains[i]) / period
        avg_loss = (avg_loss * (period - 1) + losses[i]) / period
        if avg_loss == 0:
            result.append(100)
        else:
            rs = avg_gain / avg_loss
            result.append(100 - 100 / (1 + rs))
    return result

def _macd(closes, fast=12, slow=26, signal=9):
    """MACD: (ema12 - ema26, signal=ema9 of MACD)"""
    ema_fast = _ema(closes, fast)
    ema_slow = _ema(closes, slow)
    macd_line = [None] * len(closes)
    for i in range(len(closes)):
        if ema_fast[i] is not None and ema_slow[i] is not None:
            macd_line[i] = ema_fast[i] - ema_slow[i]
    signal_line = _ema([v if v is not None else 0 for v in macd_line], signal)
    histogram = [None] * len(closes)
    for i in range(len(closes)):
        if macd_line[i] is not None and signal_line[i] is not None:
            histogram[i] = macd_line[i] - signal_line[i]
    return macd_line, signal_line, histogram

def _atr(bars, period=14):
    """Average True Range"""
    trs = []
    for i, b in enumerate(bars):
        if i == 0:
            trs.append(b["high"] - b["low"])
        else:
            hl = b["high"] - b["low"]
            hc = abs(b["high"] - bars[i-1]["close"])
            lc = abs(b["low"]  - bars[i-1]["close"])
            trs.append(max(hl, hc, lc))
    return _ema(trs, period)

def _bollinger_bands(closes, period=20, std_dev=2):
    """Bollinger Bands: mid, upper, lower"""
    mid = _sma(closes, period)
    std = []
    for i in range(len(closes)):
        if i < period - 1:
            std.append(None)
        else:
            vals = closes[i - period + 1:i + 1]
            m = sum(vals) / period
            variance = sum((v - m) ** 2 for v in vals) / period
            std.append(math.sqrt(variance))
    upper, lower = [], []
    for i in range(len(closes)):
        if mid[i] is None or std[i] is None:
            upper.append(None)
            lower.append(None)
        else:
            upper.append(mid[i] + std_dev * std[i])
            lower.append(mid[i] - std_dev * std[i])
    return mid, upper, lower

def _adx(bars, period=14):
    """
    计算 ADX（趋势强度指标）
    返回: (adx_value, trend_strength_label)
    """
    if len(bars) < period + 15:
        return 25.0, "TRANSITION"

    # 计算 +DI, -DI
    plus_dm, minus_dm, true_range = [], [], []
    for i in range(1, len(bars)):
        b = bars[i]
        pb = bars[i-1]
        hl = b["high"] - pb["high"]
        lh = pb["low"] - b["low"]
        plus_dm.append(max(hl, 0) if hl > lh else 0)
        minus_dm.append(max(lh, 0) if lh > hl else 0)
        tr = max(b["high"] - b["low"], abs(b["high"] - pb["close"]), abs(b["low"] - pb["close"]))
        true_range.append(tr)

    if len(true_range) < period:
        return 25.0, "TRANSITION"

    # 平滑（一次性计算）
    atr_vals = _ema(true_range, period)
    ema_plus_dm = _ema(plus_dm, period)
    ema_minus_dm = _ema(minus_dm, period)

    plus_di = []
    minus_di = []
    for i in range(len(plus_dm)):
        atr_v = atr_vals[i+1] if (i+1) < len(atr_vals) and atr_vals[i+1] is not None else None
        if atr_v and atr_v != 0 and (i+1) < len(ema_plus_dm) and ema_plus_dm[i+1] is not None:
            plus_di.append(100 * ema_plus_dm[i+1] / atr_v)
            minus_di.append(100 * ema_minus_dm[i+1] / atr_v if (i+1) < len(ema_minus_dm) and ema_minus_dm[i+1] is not None else 0)
        else:
            plus_di.append(0)
            minus_di.append(0)

    dx = []
    for i in range(len(plus_di)):
        di_sum = plus_di[i] + minus_di[i]
        if di_sum == 0:
            dx.append(0)
        else:
            dx.append(100 * abs(plus_di[i] - minus_di[i]) / di_sum)

    adx_vals = _ema(dx, period)
    adx = adx_vals[-1] if adx_vals and adx_vals[-1] is not None else 25.0

    if adx >= 25:
        label = "WEAK_TREND"
    elif adx >= 18:
        label = "RANGING"
    else:
        label = "TRANSITION"

    return adx, normalize_regime_label(label)

# ============ 市场快照获取 ============
def get_live_bid_ask():
    """获取实时买卖价（委托给 data_layer）"""
    from src.data_layer import get_live_bid_ask as _dl_get
    return _dl_get()

# ============ 多时段分析集成 ============
def _get_session_analysis():
    """获取多时段分析结果"""
    try:
        import sys
        sys.path.insert(0, str(pathlib.Path(__file__).parent))
        from session_analyzer import analyze_all_sessions, get_strategy_recommendation
        analysis = analyze_all_sessions()
        rec = get_strategy_recommendation(analysis)
        return analysis, rec
    except Exception as e:
        return None, None

# 注意：该文件主要提供 K 线数据获取、多时间周期状态计算以及基础指标函数。
# 旧版 analyze_market 已废弃，现由 signal_generators 中的模块统一生成信号。
