#!/usr/bin/env python3
"""
Gold Trading Agent - Production 24/7
Gold AutoTrade spawns this as a persistent sub-agent.
Architecture:
  1. Fetch live price (Swissquote)
  2. Run strategy logic (SMA/EMA/RSI/MACD)
  3. Risk governor check
  4. Execute via MT5TradeServer EA (mt5_cmd.json)
  5. Log everything, maintain state across cycles
"""

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

# ─── Paths ────────────────────────────────────────────────────────────────────
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if sys.platform == "win32":
    MT5_FILES = Path(os.environ["APPDATA"]) / "MetaQuotes" / "Terminal" / "Common" / "Files"
else:
    MT5_FILES = Path.home() / "Library/Application Support/net.metaquotes.wine.metatrader5/drive_c/Program Files/MetaTrader 5/MQL5/Files"
CMD_FILE = MT5_FILES / "mt5_cmd.json"
RSP_FILE = MT5_FILES / "mt5_rsp.json"
# LOG_DIR now resolved via frozen_path
from frozen_path import LOGS_DIR as LOG_DIR
LOG_FILE = LOG_DIR / f"trading_agent.{datetime.now(timezone.utc).strftime('%Y%m%d')}.log"
import config_manager as _ta_cm
STATE_FILE = _ta_cm.get_data_dir() / "bridge" / "state" / "agent_state.json"

MT5_FILES.mkdir(parents=True, exist_ok=True)
LOG_DIR.mkdir(parents=True, exist_ok=True)


# ─── Logging ─────────────────────────────────────────────────────────────────
def log(level: str, msg: str, **kwargs) -> None:
    ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
    extra = " ".join(f"{k}={v}" for k, v in kwargs.items())
    line = f"[{ts}] [{level}] {msg} {extra}".strip()
    print(line)
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    with LOG_FILE.open("a", encoding="utf-8") as f:
        f.write(line + "\n")


# ─── State ────────────────────────────────────────────────────────────────────
def load_state() -> Dict[str, Any]:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
        except Exception:
            pass
    return {
        "position_open": False, "position_ticket": 0,
        "position_side": None, "position_price": 0,
        "consecutive_losses": 0, "daily_loss": 0,
        "last_signal_id": None, "last_run": None,
        "consecutive_timeouts": 0, "total_cycles": 0,
        "kill_switch": False,
    }


def save_state(state: Dict) -> None:
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")


# ─── MT5 Communication ─────────────────────────────────────────────────────────
def _send_to_mt5(cmd: Dict[str, Any], timeout: float = 20.0) -> Optional[Dict]:
    """Send command to MT5TradeServer EA, read response."""
    from bridge.ea_ipc import send_ea_command
    return send_ea_command(
        cmd, cmd_file=CMD_FILE, rsp_file=RSP_FILE,
        timeout=timeout, poll_interval=0.5,
    )


def mt5_ping() -> bool:
    r = _send_to_mt5({"action": "ping"}, timeout=5)
    return r is not None and r.get("success")


def mt5_get_positions() -> list:
    r = _send_to_mt5({"action": "positions"}, timeout=10)
    if r and r.get("success"):
        return r.get("positions", [])
    return []


def mt5_get_account() -> Dict:
    r = _send_to_mt5({"action": "account"}, timeout=10)
    return r or {}


def mt5_order(side: str, volume: float = 0.01, sl: float = 0, tp: float = 0) -> Dict:
    """Place market order via EA."""
    r = _send_to_mt5({
        "action": side.lower(),
        "symbol": "XAUUSD",
        "volume": volume,
        "sl": sl,
        "tp": tp,
    }, timeout=25)
    return r or {"success": False, "message": "MT5_TIMEOUT"}


def mt5_close(ticket: int) -> Dict:
    r = _send_to_mt5({"action": "close", "ticket": ticket}, timeout=25)
    return r or {"success": False, "message": "MT5_TIMEOUT"}


# ─── Live Price ───────────────────────────────────────────────────────────────
def get_live_price() -> Optional[Dict]:
    try:
        import urllib.request
        url = "https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD"
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
        with urllib.request.urlopen(req, timeout=8) as resp:
            data = json.loads(resp.read())
        entry = data[0]["spreadProfilePrices"][0]
        bid, ask = float(entry["bid"]), float(entry["ask"])
        return {
            "bid": bid, "ask": ask,
            "mid": (bid + ask) / 2,
            "spread": round((ask - bid) * 100, 1),
            "success": True,
        }
    except Exception as e:
        log("WARN", f"Price fetch failed: {e}")
        return None


# ─── Strategy ─────────────────────────────────────────────────────────────────
def compute_indicators(price_mid: float) -> Dict[str, Any]:
    """
    Simplified indicator computation.
    In production, load historical bars from MT5.
    Here using price-based heuristics.
    """
    # RSI estimate (simplified - use mid price variations)
    rsi = 50.0  # neutral
    macd_signal = 0  # neutral
    ema_fast = price_mid
    ema_slow = price_mid

    return {
        "ema_fast": ema_fast,
        "ema_slow": ema_slow,
        "rsi": rsi,
        "macd": macd_signal,
        "regime": "TREND",  # default to trend
        "confidence": 0.50,
    }


def generate_signal(
    price: Dict,
    state: Dict,
    indicators: Dict,
) -> Dict[str, Any]:
    """
    Decision logic:
      - If no position and regime=TREND and confidence>=0.68 → OPEN BUY
      - If position open and (regime changes OR RSI overbought OR TP hit) → CLOSE
      - Otherwise → HOLD
    """
    mid = price["mid"]
    ind = indicators
    sig_id = f"sig_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"

    # ─── Exit logic ──
    if state["position_open"]:
        should_close = False
        reason = ""

        # Check if RSI overbought (>70) or oversold (<30)
        if ind["rsi"] > 70:
            should_close = True
            reason = "RSI_overbought"
        elif ind["rsi"] < 30:
            should_close = True
            reason = "RSI_oversold"

        # Check if TP roughly hit (±0.5%)
        if state["position_side"] == "BUY" and mid >= state.get("position_tp", 0) * 0.995:
            should_close = True
            reason = "TP_hit"
        elif state["position_side"] == "SELL" and mid <= state.get("position_tp", 0) * 1.005:
            should_close = True
            reason = "TP_hit"

        if should_close:
            return {
                "signal_id": sig_id,
                "action": "CLOSE",
                "side": state["position_side"],
                "reason": reason,
                "volume": 0.01,
            }

        return {"signal_id": sig_id, "action": "HOLD", "reason": "position_held"}

    # ─── Entry logic ──
    if ind["confidence"] >= 0.68 and ind["regime"] == "TREND":
        sl_dist = mid * 0.015  # 1.5% stop loss
        sl = round(mid - sl_dist, 3)
        tp = round(mid + sl_dist * 1.5, 3)  # 1.5:1 RR
        return {
            "signal_id": sig_id,
            "action": "OPEN",
            "side": "BUY",
            "volume": 0.01,
            "sl_price": sl,
            "tp_price": tp,
            "entry_price": round(mid, 3),
            "confidence": ind["confidence"],
            "reason": "TREND_confirmed",
        }

    return {"signal_id": sig_id, "action": "HOLD", "reason": f"confidence={ind['confidence']:.2f}"}


# ─── Risk Governor ────────────────────────────────────────────────────────────
def governor_check(signal: Dict, state: Dict, price: Dict) -> Dict[str, Any]:
    """
    Hard rules that cannot be bypassed.
    """
    account = mt5_get_account()
    equity = account.get("equity", 0) or account.get("balance", 10000)

    # Rule 1: Consecutive losses ≥ 4 → cooldown (kill_switch 已降级为置信度惩罚)
    if state["consecutive_losses"] >= 4:
        return {"approved": False, "reason": "COOLDOWN_consecutive_losses"}

    # Rule 3: Daily loss > 2.5%
    if state["daily_loss"] > 2.5:
        return {"approved": False, "reason": "CIRCUIT_BREAKER_daily_loss"}

    # Rule 4: Data quality - spread too wide (>2 USD = 200 points)
    if price.get("spread", 0) > 2.0:
        return {"approved": False, "reason": "LOW_LIQUIDITY_spread"}

    # Rule 5: Confidence < 0.68 → hold
    if signal.get("action") == "OPEN" and signal.get("confidence", 1) < 0.68:
        return {"approved": False, "reason": "LOW_CONFIDENCE"}

    # Rule 6: No SL for new orders
    if signal.get("action") == "OPEN" and signal.get("sl_price", 0) <= 0:
        return {"approved": False, "reason": "NO_STOP_LOSS"}

    # Rule 7: Max 1 position
    positions = mt5_get_positions()
    if signal.get("action") == "OPEN" and len(positions) > 0:
        return {"approved": False, "reason": "POSITION_ALREADY_OPEN"}

    return {"approved": True}


# ─── Execution ────────────────────────────────────────────────────────────────
def execute_signal(signal: Dict) -> Dict[str, Any]:
    action = signal.get("action")
    log("INFO", f"Executing: {action} {signal.get('side','')}", reason=signal.get("reason",""))

    if action == "HOLD":
        return {"status": "RECONCILED", "reason": signal.get("reason", "HOLD")}

    if action == "CLOSE":
        result = mt5_close(signal.get("position_ticket", 0))
        if result.get("success"):
            return {"status": "FILLED", "reason": f"CLOSE {signal.get('reason','')}", "deal_id": result.get("deal_id", 0)}
        return {"status": "REJECTED", "reason": result.get("message", "CLOSE_FAILED")}

    if action == "OPEN":
        result = mt5_order(
            side=signal["side"],
            volume=signal.get("volume", 0.01),
            sl=signal.get("sl_price", 0),
            tp=signal.get("tp_price", 0),
        )
        if result.get("success"):
            return {
                "status": "FILLED",
                "reason": f"OPEN {signal['side']} {signal.get('reason','')}",
                "order_id": result.get("order_id", 0),
                "deal_id": result.get("deal_id", 0),
            }
        return {"status": "REJECTED", "reason": result.get("message", "ORDER_FAILED")}

    return {"status": "REJECTED", "reason": f"UNKNOWN_ACTION: {action}"}


# ─── Main Loop ───────────────────────────────────────────────────────────────
def run_cycle() -> Dict[str, Any]:
    """One trading cycle. Returns summary dict."""
    state = load_state()
    state["total_cycles"] += 1
    state["last_run"] = datetime.now(timezone.utc).isoformat()
    cycle = state["total_cycles"]

    result = {
        "cycle": cycle,
        "timestamp": state["last_run"],
        "position_open": state["position_open"],
        "price_fetch": False,
        "signal": None,
        "governor": None,
        "execution": None,
        "state_updated": False,
    }

    # ── 1. Price ──
    price = get_live_price()
    if not price or not price.get("success"):
        log("WARN", "Cycle skipped: no price data")
        save_state(state)
        return result

    result["price_fetch"] = True
    result["bid"] = price["bid"]
    result["ask"] = price["ask"]
    result["spread"] = price.get("spread", 0)

    # ── 2. Strategy ──
    indicators = compute_indicators(price["mid"])
    signal = generate_signal(price, state, indicators)
    result["signal"] = signal
    log("INFO", f"Signal: {signal['action']} {signal.get('side','')} {signal.get('reason','')}",
        confidence=signal.get("confidence",""))

    # ── 3. Governor ──
    gov = governor_check(signal, state, price)
    result["governor"] = gov

    if not gov["approved"]:
        log("INFO", f"Governor REJECTED: {gov['reason']}", action=signal["action"])
        save_state(state)
        return result

    # ── 4. Execute ──
    exec_result = execute_signal(signal)
    result["execution"] = exec_result
    log("INFO", f"Execution: {exec_result['status']}", reason=exec_result.get("reason",""))

    # ── 5. Update state ──
    if exec_result["status"] == "FILLED":
        if signal["action"] == "OPEN":
            state["position_open"] = True
            state["position_side"] = signal["side"]
            state["position_price"] = price["mid"]
            state["position_ticket"] = exec_result.get("deal_id", 0)
            state["position_sl"] = signal.get("sl_price", 0)
            state["position_tp"] = signal.get("tp_price", 0)
            state["consecutive_timeouts"] = 0
            log("INFO", f"Position OPENED: {signal['side']} @{price['mid']:.3f}",
                SL=signal.get("sl_price"), TP=signal.get("tp_price"))
        elif signal["action"] == "CLOSE":
            pnl = exec_result.get("deal_id", 0)  # simplified
            state["position_open"] = False
            state["position_ticket"] = 0
            state["consecutive_timeouts"] = 0
            if signal.get("reason") in ("RSI_overbought", "TP_hit"):
                state["consecutive_losses"] = 0
            else:
                state["consecutive_losses"] += 1
            log("INFO", f"Position CLOSED: {signal['reason']}")
    elif exec_result["status"] in ("REJECTED", "TIMEOUT"):
        state["consecutive_timeouts"] += 1
        if state["consecutive_timeouts"] >= 3:
            log("WARN", "3 consecutive timeouts - possible MT5 issue")

    state["last_signal_id"] = signal.get("signal_id")
    result["state_updated"] = True
    save_state(state)
    return result


# ─── Entry Point ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--cycles", type=int, default=1, help="Number of cycles to run (0=infinite)")
    parser.add_argument("--interval", type=int, default=30, help="Seconds between cycles")
    args = parser.parse_args()

    log("INFO", "Gold Trading Agent starting", cycles=args.cycles, interval=args.interval)

    # Verify MT5 is alive
    if not mt5_ping():
        log("WARN", "MT5 not responding on startup - will retry each cycle")
    else:
        log("INFO", "MT5 connection verified")

    cycles = 0
    while True:
        cycles += 1
        log("INFO", f"=== Cycle {cycles} ===")

        try:
            r = run_cycle()
            log("INFO", f"Cycle {cycles} done",
                price=r["price_fetch"],
                signal=r["signal"].get("action") if r["signal"] else "NONE",
                exec=r["execution"].get("status") if r["execution"] else "NONE")
        except Exception as e:
            log("ERROR", f"Cycle failed: {e}", exc=str(e)[:200])

        if args.cycles > 0 and cycles >= args.cycles:
            log("INFO", f"Done. Ran {cycles} cycles.")
            break

        log("INFO", f"Sleeping {args.interval}s...")
        time.sleep(args.interval)
